+++ /dev/null
-zk.zsh -- zettelkästen/wiki/static website helper/generator
-
-* Use plain text files. One file per note/record
-* Use CamelCase names, that are friendly with vi editor word
- boundary determination
-* You can safely place that notes in subdirectories hierarchy
-* Edit and view your notes from their root path (zettelkästen
- root). That way if you press "gf" on "Foo/Bar/Baz" word in
- Vim, then it will open Foo/Bar/Baz file in current window
-* Use expand("%:r") function to get current file path relative
- to the root
-* Place links to another notes inside square brackets. You can
- highlight them with:
- syntax region zkLink start="\[\S" end="\S]"
- highlight link zkLink String
-* Link to the directory's index can be made with [Dir/]
-* Use Vim's filename completion (:help compl-filename) to
- complete directories and filenames in them
-* fzf (https://github.com/junegunn/fzf) related tools can be
- used to navigate among existing notes
-* Ordinary grep, git-jump or similar tools can be used to search
- and quickly open results in the editor
-
-The only thing Vim lacks there is ability to tell who backlinks
-to the specified page. zk.zsh can be used to show what pages
-backlinks to specified page and what pages are referenced by it:
- $ zk.zsh links some/page
- Another/Page
- SomePage
- $ zk.zsh backs some/page
- [...]
-That can be used to make categories and tags on notes. If note
-contains a link to category/tag (even an empty file), then it
-will be backlinked.
-
- $ zk.zsh htmls path/to/dir
-Will convert all your notes to HTMLs with properly created links
-to other pages. It also will include all backlinks in them. Each
-directory will also contain index page with links to all
-existing pages in current directory and to subdirectories.
-
-If ZK_CACHE environment variable contains path to some
-directory, then it will keep caching information for speeding up
-the processes in it. Look for CACHE file for more information.
--- /dev/null
+#!/usr/bin/env perl
+# zk -- zettelkästen/wiki/static website helper/generator
+# Copyright (C) 2022-2025 Sergey Matveev <stargrave@stargrave.org>
+
+=pod
+
+=encoding utf-8
+
+=head1 SYNOPSIS
+
+zk -- zettelkästen/wiki/static website helper/generator
+
+=head1 USAGE
+
+=over
+
+=item *
+
+Use plain text files. One file per note/record.
+
+=item *
+
+Use CamelCase names, that are friendly with vi editor word boundary
+determination.
+
+=item *
+
+You can safely place that notes in subdirectories hierarchy.
+
+=item *
+
+Edit and view your notes from their root path (zettelkästen root). That
+way if you press C<gf> on F<Foo/Bar/Baz> word in Vim, then it will open
+F<Foo/Bar/Baz> file in current window.
+
+=item *
+
+Use C<expand("%:r")> function to get current file path relative to the root.
+
+=item *
+
+Place links to another notes inside square brackets.
+You can highlight them with:
+
+ syntax region zkLink start="\[\S" end="\S]"
+ highlight link zkLink String
+
+=item *
+
+Link to the directory's index can be made with C<[Dir/]>.
+
+=item *
+
+Use Vim's filename completion (C<:help compl-filename>) to complete
+directories and filenames in them.
+
+=item *
+
+L<fzf|https://github.com/junegunn/fzf> related tools can be used to
+navigate among existing notes.
+
+=item *
+
+Ordinary C<grep>, C<git-jump> or similar tools can be used to search and
+quickly open results in the editor.
+
+=back
+
+The only thing Vim lacks there is ability to tell who backlinks
+to the specified page. zk can be used to show what pages backlinks to
+specified page and what pages are referenced by it:
+
+ $ zk links some/page
+ Another/Page
+ SomePage
+ $ zk backs some/page
+ [...]
+
+That can be used to make categories and tags on notes. If note contains
+a link to category/tag (even an empty file), then it will be backlinked.
+
+ $ zk htmls path/to/dir
+
+Will convert all your notes to HTMLs with properly created links
+to other pages. It also will include all backlinks in them. Each
+directory will also contain index page with links to all
+existing pages in current directory and to subdirectories.
+
+Older version of that script, written on Z shell, can be found in Git history.
+
+=cut
+
+use strict;
+use warnings;
+
+sub usage {
+ print STDERR "Usage:
+\t$0 links PAGE
+\t$0 backs PAGE
+\t$0 htmls DIR\n";
+ exit 1;
+}
+
+usage if $#ARGV == -1;
+
+my %mtimes;
+my %sizes;
+my %cats;
+
+{
+ use File::Find;
+ use POSIX qw(strftime);
+ sub wanted {
+ my $fn = $_;
+ my $pth = $File::Find::name;
+ $pth =~ s/^\.\/?//;
+ if (-d $fn) {
+ opendir(my $dh, $fn) or die "$!";
+ my @entries;
+ while (readdir $dh) {
+ next if /^\./;
+ if (-d "$fn/$_") {
+ $_ .= "/";
+ }
+ push @entries, $_;
+ }
+ closedir $dh;
+ $cats{$pth} = \@entries;
+ } else {
+ die "unacceptable filename: $pth" if $_ eq "index";
+ my @s = stat($fn) or die "$!";
+ $sizes{$pth} = $s[7];
+ $mtimes{$pth} = strftime "%Y-%m-%d %H:%M:%S", gmtime $s[9];
+ }
+ }
+ find(\&wanted, ".");
+}
+
+my %links;
+my %backs;
+for my $pth (keys %mtimes) {
+ my @ws;
+ open(my $fh, "<", $pth) or die "$!";
+ while (<$fh>) {
+ foreach my $w (split /\s+/) {
+ next unless $w =~ /\[([^]]+)\]/;
+ $w = $1;
+ if ($w =~ /\/$/) {
+ my $w = substr $w, 0, -1;
+ next unless exists $cats{$w};
+ } else {
+ next unless exists $mtimes{$w};
+ }
+ push @ws, $w;
+ }
+ }
+ close $fh;
+ @ws = sort @ws;
+ next if $#ws == -1;
+ $links{$pth} = \@ws;
+ foreach (@ws) {
+ if (not defined $backs{$_}) {
+ my %h;
+ $backs{$_} = \%h;
+ }
+ $backs{$_}{$pth} = 1;
+ }
+}
+
+sub startBody {
+ my $out = shift;
+ my $title = shift;
+ print $out "<!DOCTYPE html>
+<html><head>
+<title>$title</title>
+<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">
+</head><body>";
+}
+
+use File::Spec;
+
+sub genHTML {
+ my $out = shift;
+ my $page = shift;
+ my @lnks = defined $links{$page} ? @{$links{$page}} : ();
+ my @rels;
+ my $rel;
+ foreach (@lnks) {
+ $rel = File::Spec->abs2rel($_, $page);
+ $rel = substr $rel, 3;
+ if (-d $rel) {
+ $rel .= "/index";
+ }
+ push @rels, $rel;
+ }
+ startBody $out, $page;
+ print $out "<pre>\n";
+ open(my $fh, "<", $page) or die "$!";
+ while (<$fh>) {
+ s/&/\&/g;
+ s/</\</g;
+ s/>/\>/g;
+ my $rel;
+ while (my ($i, $l) = each @lnks) {
+ s/\[$l\]/<a href="$rels[$i].html">[$l]<\/a>/g;
+ }
+ print $out $_;
+ }
+ close $fh;
+ print $out "</pre>\n";
+ if ($#lnks != -1) {
+ print $out "<table border=1><caption>Links</caption>\n";
+ my $mtime;
+ while (my ($i, $l) = each @lnks) {
+ $mtime = (defined $mtimes{$l}) ? defined $mtimes{$l} : "";
+ print $out "<tr><td><a href=\"$rels[$i].html\">$l</a></td>
+ <td><tt>$mtime</tt></td></tr>\n";
+ }
+ print $out "</table>\n";
+ }
+ @lnks = sort keys %{$backs{$page}};
+ if ($#lnks != -1) {
+ print $out "<table border=1><caption>Backlinks</caption>\n";
+ foreach my $l (@lnks) {
+ $rel = File::Spec->abs2rel($l, $page);
+ $rel = substr $rel, 3;
+ print $out "<tr><td><a href=\"$rel.html\">$l</a></td>
+ <td><tt>$mtimes{$l}</tt></td></tr>\n";
+ }
+ print $out "</table>\n";
+ }
+ print $out "</body></html>\n";
+}
+
+sub genIndex {
+ my $out = shift;
+ my $page = shift;
+ startBody $out, "$page/";
+ print $out "<table border=1>\n";
+ my @lnks = sort @{$cats{$page}};
+ foreach my $l (@lnks) {
+ next if $l =~ /\/$/;
+ my $pth = ($page eq "") ? $l : "$page/$l";
+ print $out "<tr><td><a href=\"$l.html\">$pth</a></td>
+ <td><tt>$mtimes{$pth}</tt></td><td>$sizes{$pth} B</td></tr>\n";
+ }
+ print $out "</table>\n";
+ @lnks = grep { /\/$/ } @lnks;
+ if ($#lnks != -1) {
+ print $out "<table border=1><caption>Subcategories</caption>\n";
+ foreach my $l (@lnks) {
+ $l = substr $l, 0, -1;
+ my $pth = ($page eq "") ? $l : "$page/$l";
+ my @entries = @{$cats{$pth}};
+ my $ctr = 1 + $#entries;
+ print $out "<tr><td><a href=\"$l/index.html\">$pth</a></td>
+ <td>$ctr</td></tr>\n"
+ }
+ print $out "</table>\n";
+ }
+ @lnks = sort keys %{$backs{"$page/"}};
+ if ($#lnks != -1) {
+ print $out "<table border=1><caption>Backlinks</caption>\n";
+ my $rel;
+ foreach my $l (@lnks) {
+ $rel = File::Spec->abs2rel($l, $page);
+ print $out "<tr><td><a href=\"$rel.html\">$l</a></td>
+ <td><tt>$mtimes{$l}</tt></td><td>$sizes{$l} B</td></tr>\n";
+ }
+ print $out "</table>\n";
+ }
+ print $out "</body></html>\n"
+}
+
+if ($ARGV[0] eq "dump") {
+ use Data::Dumper;
+ print Data::Dumper->Dump([\%links, \%backs, \%cats], [qw(*links *backs *cats)]);
+} elsif ($ARGV[0] eq "links") {
+ map { print "$_\n" } @{$links{$ARGV[1]}};
+} elsif ($ARGV[0] eq "backs") {
+ map { print "$_\n" } sort keys %{$backs{$ARGV[1]}};
+} elsif ($ARGV[0] eq "html") {
+ genHTML \*STDOUT, $ARGV[1];
+} elsif ($ARGV[0] eq "html-index") {
+ genIndex \*STDOUT, $ARGV[1];
+} elsif ($ARGV[0] eq "htmls") {
+ my $now = time;
+ use File::Path qw(make_path);
+ foreach my $cat (keys %cats) {
+ make_path "$ARGV[1]/$cat";
+ my $fn = "$ARGV[1]/$cat/index.html";
+ open(my $fh, ">", $fn) or die "$!";
+ genIndex $fh, $cat;
+ close $fh;
+ utime $now, $now, $fn;
+ }
+ my @s;
+ foreach my $pth (keys %mtimes) {
+ open(my $fh, ">", "$ARGV[1]/$pth.html") or die "$!";
+ genHTML $fh, $pth;
+ close $fh;
+ @s = stat($pth) or die "$!";
+ utime $s[9], $s[9], "$ARGV[1]/$pth.html";
+ }
+} else {
+ usage;
+}
+++ /dev/null
-#!/usr/bin/env zsh
-# zk.zsh -- zettelkästen/wiki/static website helper/generator
-# Copyright (C) 2022-2025 Sergey Matveev <stargrave@stargrave.org>
-
-setopt ERR_EXIT
-ZK_VERSION=ZKZSH1
-
-usage() {
- >&2 <<EOF
-Usage:
- \$ $0:t links PAGE
- Print PAGE's links
- \$ $0:t backs PAGE
- Print PAGE's backlinks
- \$ $0:t htmls DIR
- Generate HTMLs in DIR
-EOF
- exit 1
-}
-
-[[ $# -eq 2 ]] || usage
-
-setopt GLOB_STAR_SHORT
-zmodload -F zsh/stat b:zstat
-typeset -A mtimes
-typeset -A sizes
-for p (**(.)) {
- [[ $p:t == "index" ]] && {
- echo unacceptable filename: $p >&2
- exit 1
- }
- zstat -A mtime -F "%F %T" +mtime $p
- zstat -A size +size $p
- mtimes[$p]=${mtime[1]}
- sizes[$p]=${size[1]}
-}
-typeset -A cats
-for p (**(/)) {
- local files=($p/*(N))
- cats[$p]=$#files
-}
-
-zmodload zsh/mapfile
-zmodload -F zsh/files b:zf_mkdir
-typeset -A links
-typeset -A backs
-typeset -A cached
-typeset -aU ws
-for p (${(k)mtimes}) {
- [[ $ZK_CACHE ]] && {
- zstat -A inode +inode $p
- zstat -A ctime +ctime $p
- cache=(${(f)mapfile[$ZK_CACHE/$p]})
- if [[ ( ${cache[1]} = $ZK_VERSION ) &&
- ( ${cache[2]} = ${inode[1]} ) &&
- ( ${cache[3]} = ${ctime[1]} ) ]]; then
- ws=(${cache[4,-1]})
- [[ $ws ]] && links[$p]=${(j: :)ws}
- cached[$p]=1
- continue
- fi
- }
- ws=()
- for w (${=mapfile[$p]}) {
- [[ $w =~ "\[([^] ]+)\]" ]] || continue
- w=${match[1]}
- [[ ( $w =~ "/$" ) && ( ${cats[$w[1,-2]]} ) ]] && {
- ws=($ws $w)
- continue
- }
- [[ ${mtimes[$w]} ]] || {
- [[ $ZK_SHOW_MISSING ]] && print "missing $w"
- continue
- }
- ws=($ws $w)
- }
- [[ $ZK_CACHE ]] && {
- zf_mkdir -p $ZK_CACHE/$p:h
- ws=($ZK_VERSION ${inode[1]} ${ctime[1]} $ws)
- print -l $ws >$ZK_CACHE/$p
- ws=(${ws[4,-1]})
- }
- [[ $ws ]] && links[$p]=${(j: :)ws}
-}
-unset cache ws
-for p ws (${(kv)links}) {
- for w (${=ws}) backs[$w]="$p ${backs[$w]}"
-}
-for p ws (${(kv)backs}) backs[$p]=${(j: :)${(u)=ws}}
-
-getrel() {
- # nearly the copy-paste of Functions/Misc/relative
- local dst=$2:a
- local src=$1:h:a
- local -a cur abs
- cur=(${(s:/:)src})
- abs=(${(s:/:)dst:h} $dst:t)
- integer i=1
- while [[ i -le $#abs && $abs[i] == $cur[i] ]] ; do
- ((++i > $#cur)) && {
- REPLY=${(j:/:)abs[i,-1]}
- return
- }
- done
- src=${(j:/:)cur[i,-1]/*/..}
- dst=${(j:/:)abs[i,-1]}
- REPLY=$src${dst:+/$dst}
-}
-
-genHTML() {
- local page=$1
- local data p
- [[ $# -eq 1 ]] && data=${mapfile[$page]} || data=$2
- local _links=(${(oi)=links[$page]})
- if [[ ( ${cached[$page]} ) && ( -s $ZK_CACHE/${page}.html ) ]]; then
- <$ZK_CACHE/${page}.html
- else
- data=${data//&/&}
- data=${data//</<}
- data=${data//>/>}
- for p ($_links) {
- getrel $page $p
- [[ -d $p ]] && REPLY=$REPLY/index
- data="${data//\[${p}\]/<a href=\"${REPLY}.html\">[$p]</a>}"
- }
- data="<!DOCTYPE html>
-<html><head>
-<title>$page</title>
-<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">
-</head><body><pre>
-$data</pre>"
- if [[ $ZK_CACHE ]]; then
- print -r "$data" >$ZK_CACHE/${page}.html
- <$ZK_CACHE/${page}.html
- else
- print -r "$data"
- fi
- fi
- if [[ $_links ]]; then
- print "<table border=1><caption>Links</caption>"
- for p ($_links) {
- getrel $page $p
- [[ -d $p ]] && REPLY=$REPLY/index
- print "<tr><td><a href=\"${REPLY}.html\">$p</a></td><td><tt>${mtimes[$p]}</tt></td></tr>"
- }
- print "</table>"
- fi
- local bs=(${(oi)=${backs[$page]}})
- if [[ $bs ]]; then
- print "<table border=1><caption>Backlinks</caption>"
- for p ($bs) {
- getrel $page $p
- print "<tr><td><a href=\"${REPLY}.html\">$p</a></td><td><tt>${mtimes[$p]}</tt></td></tr>"
- }
- print "</table>"
- fi
- print "</body></html>"
-}
-
-zmodload -F zsh/datetime b:strftime
-strftime -s now "%F %T"
-
-genIndex() {
- local p
- local entries=()
- local _links=()
- typeset -aU _cats=()
- local curdepth=${#${(s:/:)1}}
- (( curdepth = curdepth + 1 ))
- for p (${(k)mtimes[(I)$1*]}) {
- case ${#${(As:/:)p}} in
- ($curdepth) _links=($p $_links) ;;
- ( $(( $curdepth + 1 )) ) _cats=(${1}${${p#$1}%%/*} $_cats) ;;
- (*) continue ;;
- esac
- }
- local page=${1}index
- print "<!DOCTYPE html>
-<html><head>
-<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">
-<title>$page</title>
-</head><body><table border=1>"
- for p (${(oi)_links}) {
- getrel $page $p
- print "<tr><td><a href=\"${REPLY}.html\">$p</a></td><td><tt>${mtimes[$p]}</tt></td><td>${sizes[$p]} B</td></tr>"
- }
- print "</table>"
- if [[ $_cats ]]; then
- print "<table border=1><caption>Subcategories</caption>"
- for p (${(oi)_cats}) {
- getrel $page $p/index
- print "<tr><td><a href=\"${REPLY}.html\">$p</a></td><td>${cats[$p]}</td></tr>"
- }
- print "</table>"
- fi
- local bs=(${(oi)=${backs[$1]}})
- if [[ $bs ]]; then
- print "<table border=1><caption>Backlinks</caption>"
- for p ($bs) {
- getrel $page $p
- print "<tr><td><a href=\"${REPLY}.html\">$p</a></td><td><tt>${mtimes[$p]}</tt></td></tr>"
- }
- print "</table>"
- fi
- print "</body></html>"
-}
-
-case $1 in
-(links) for w (${(oi)=${links[$2]}}) print $w ;;
-(backs) for w (${(oi)=${backs[$2]}}) print $w ;;
-(html) genHTML $2 ;;
-(html-index) genIndex $2 ;;
-(htmls)
- for p (${(k)mtimes}) {
- zf_mkdir -p $2/$p:h
- genHTML $p >$2/$p.html
- touch -r $p $2/$p.html
- }
- for p (${(k)cats}) genIndex $p/ >$2/$p/index.html
- genIndex "" >$2/index.html
- for p ("" ${(k)cats}) touch -d ${now/ /T} $2/$p/index.html
- ;;
-(*) usage ;;
-esac