2 # zk -- zettelkästen/wiki/static website helper/generator
3 # Copyright (C) 2022-2025 Sergey Matveev <stargrave@stargrave.org>
11 zk -- zettelkästen/wiki/static website helper/generator
19 Use plain text files. One file per note/record.
23 Use file names that are friendly to vi editor word boundary determination.
27 You can safely place that notes in subdirectories hierarchy.
31 Edit and view your notes from their root path (zettelkästen root). That
32 way if you press C<gf> on F<Foo/Bar/Baz> word in Vim, then it will open
33 F<Foo/Bar/Baz> file in current window.
37 Use C<expand("%:r")> function to get current file path relative to the root.
41 Place links to another notes inside square brackets.
42 You can highlight them with:
44 syntax region zkLink start="\[\S" end="\S]"
45 highlight link zkLink String
49 Link to the directory's index can be made with C<[Dir/]>.
53 Use Vim's filename completion (C<:help compl-filename>) to complete
54 directories and filenames in them.
58 L<fzf|https://github.com/junegunn/fzf> related tools can be used to
59 navigate among existing notes.
63 Ordinary C<grep>, C<git-jump> or similar tools can be used to search and
64 quickly open results in the editor.
68 C<=E<gt> URL[ optional text]\r> on a separate line will add a link to the
69 specified URL (with optional text). Pay attention that line contains
70 carriage-return character at the end.
74 C<I URL[ alt]\r> on a separate line will add an image with
75 optional alternative text similarly as link is specified.
79 The only thing Vim lacks there is ability to tell who backlinks
80 to the specified page. zk can be used to show what pages backlinks to
81 specified page and what pages are referenced by it:
89 That can be used to make categories and tags on notes. If note contains
90 a link to category/tag (even an empty file), then it will be backlinked.
92 $ zk htmls path/to/dir
94 Will convert all your notes to HTMLs with properly created links
95 to other pages. It also will include all backlinks in them. Each
96 directory will also contain index page with links to all
97 existing pages in current directory and to subdirectories.
99 =head1 (BACK)LINKS GENERATION
101 Set C<ZK_DO_LINKS=0> to disable automatic links-table creation in the
102 HTML output. Set C<ZK_DO_BACKS=0> to disable automatic backlinks-table.
103 If C<do-links\r> line is presented, then links-table is forcefully generated.
104 C<do-backs\r> generated backlinks table.
108 You can ignore specified files processing by placing regular expressions
109 on separate lines in .zkignore file. They are applied to full relative
110 path of walked files.
114 Older version of that script, written on Z shell, can be found in Git history.
126 \t$0 dot | dot -Tpng >png
131 usage if $#ARGV == -1;
133 my $doLinks = ((not exists $ENV{ZK_DO_LINKS}) or
134 ($ENV{ZK_DO_LINKS} eq "1")) ? 1 : 0;
135 my $doBacks = ((not exists $ENV{ZK_DO_BACKS}) or
136 ($ENV{ZK_DO_BACKS} eq "1")) ? 1 : 0;
144 if (-e ".zkignore") {
145 open my $fh, "<", ".zkignore" or die "$!";
154 foreach my $re (@ignores) {
155 return 1 if $p =~ m/$re/;
160 use POSIX qw(strftime);
163 return if ($fn =~ /^\./) and ($fn ne ".");
164 my $pth = $File::Find::name;
167 return if isignored "$pth/";
168 opendir(my $dh, $fn) or die "$!";
170 while (readdir $dh) {
175 next if isignored(($fn eq ".") ? $_ : "$fn/$_");
179 $cats{$pth} = \@entries;
181 return if isignored $pth;
182 my @s = stat($fn) or die "$!";
183 $sizes{$pth} = $s[7];
184 $mtimes{$pth} = strftime "%Y-%m-%d %H:%M:%S", gmtime $s[9];
187 my %opts = (wanted => \&wanted, follow => 1);
194 return (defined $1) ? $1 : $_;
199 for my $pth (keys %mtimes) {
201 open(my $fh, "<", $pth) or die "$!";
203 foreach my $w (split /\s+/) {
204 next unless $w =~ /\[([^]]+)\]/;
207 my $w = substr $w, 0, -1;
208 if (not exists $cats{$w}) {
209 print "missing $w\n" if exists $ENV{ZK_PRINT_MISSING};
213 if (not exists $mtimes{$w}) {
214 print "missing $w\n" if exists $ENV{ZK_PRINT_MISSING};
222 my @ws = sort keys %found;
224 $links{indexless $pth} = \@ws;
226 if (not exists $backs{$_}) {
230 $backs{$_}{$pth} = 1;
237 print $out "<!DOCTYPE html>
239 <title>$title</title>
240 <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">
258 my @lnks = exists $links{indexless $page} ? @{$links{indexless $page}} : ();
263 $rel = File::Spec->abs2rel($_, $page);
264 $rel = (length $rel > 2) ? (substr $rel, 3) : "";
274 startHead $out, indexless $page;
276 my $fn = basename $page;
277 print $out "<link rel=\"alternate\" type=\"text/plain\" title=\"src\" href=\"$fn.txt\" />\n";
279 print $out "</head><body><pre>";
280 my $doLinksForced = 0;
281 my $doBacksForced = 0;
282 open(my $fh, "<", $page) or die "$!";
289 my @cols = split /\s+/;
290 if ($cols[0] eq "=>") {
291 my $t = ($#cols > 1) ? (join " ", @cols[2..$#cols]) : $cols[1];
294 $_ = "${head}=> <a href=\"$cols[1]\">$t</a>";
295 } elsif ($cols[0] eq "I") {
297 my $t = htmlescape join " ", @cols[2..$#cols];
299 $_ = "<img src=\"$cols[1]\" alt=\"$t\" />";
301 $_ = "<img src=\"$cols[1]\" />";
303 } elsif ($cols[0] eq "do-links") {
306 } elsif ($cols[0] eq "do-backs") {
310 die "unknown $cols[0] command: $page\n";
315 while (my ($i, $l) = each @lnks) {
316 s/\[\Q$l\E\]/<a href="$rels[$i].html">[$l]<\/a>/g;
323 print $out "</pre>\n";
324 if ($doLinksForced or ($doLinks and $#lnks != -1)) {
325 print $out "<a id=\"links\"></a><table border=1><caption>Links</caption>\n";
327 while (my ($i, $l) = each @lnks) {
328 $mtime = (exists $mtimes{$l}) ? $mtimes{$l} : "";
329 print $out "<tr><td><a href=\"$rels[$i].html\">$l</a></td>
330 <td><tt>$mtime</tt></td></tr>\n";
332 print $out "</table>\n";
334 @lnks = sort keys %{$backs{indexless $page}};
335 if ($doBacksForced or ($doBacks and $#lnks != -1)) {
336 print $out "<a id=\"backs\"></a><table border=1><caption>Backlinks</caption>\n";
338 foreach my $l (@lnks) {
339 $rel = File::Spec->abs2rel($l, $page);
340 $rel = substr $rel, 3;
341 print $out "<tr><td><a href=\"$rel.html\">$l</a></td>
342 <td><tt>$mtimes{$l}</tt></td></tr>\n";
344 print $out "</table>\n";
346 print $out "</body></html>\n";
352 startHead $out, "$page/";
353 print $out "</head><body><table border=1>\n";
354 my @lnks = sort @{$cats{$page}};
355 foreach my $l (@lnks) {
357 my $pth = ($page eq "") ? $l : "$page/$l";
358 print $out "<tr><td><a href=\"$l.html\">$pth</a></td>
359 <td><tt>$mtimes{$pth}</tt></td><td>$sizes{$pth} B</td></tr>\n";
361 print $out "</table>\n";
362 @lnks = grep { /\/$/ } @lnks;
364 print $out "<a id=\"cats\"></a><table border=1><caption>Subcategories</caption>\n";
365 foreach my $l (@lnks) {
366 $l = substr $l, 0, -1;
367 my $pth = ($page eq "") ? $l : "$page/$l";
368 my @entries = @{$cats{$pth}};
369 my $ctr = 1 + $#entries;
370 print $out "<tr><td><a href=\"$l/index.html\">$pth</a></td>
371 <td>$ctr</td></tr>\n"
373 print $out "</table>\n";
375 @lnks = sort keys %{$backs{"$page/"}};
377 print $out "<a id=\"backs\"></a><table border=1><caption>Backlinks</caption>\n";
379 foreach my $l (@lnks) {
380 $rel = File::Spec->abs2rel($l, $page);
381 print $out "<tr><td><a href=\"$rel.html\">$l</a></td>
382 <td><tt>$mtimes{$l}</tt></td><td>$sizes{$l} B</td></tr>\n";
384 print $out "</table>\n";
386 print $out "</body></html>\n"
389 if ($ARGV[0] eq "dump") {
391 print Data::Dumper->Dump([\%links, \%backs, \%cats], [qw(*links *backs *cats)]);
392 } elsif ($ARGV[0] eq "links") {
393 map { print "$_\n" } @{$links{$ARGV[1]}};
394 } elsif ($ARGV[0] eq "backs") {
395 map { print "$_\n" } sort keys %{$backs{$ARGV[1]}};
396 } elsif ($ARGV[0] eq "html") {
397 genHTML \*STDOUT, $ARGV[1];
398 } elsif ($ARGV[0] eq "html-index") {
399 genIndex \*STDOUT, $ARGV[1];
400 } elsif ($ARGV[0] eq "htmls") {
402 use File::Path qw(make_path);
404 foreach my $cat (keys %cats) {
405 make_path "$ARGV[1]/$cat";
406 next if (exists $mtimes{"$cat/index"});
407 my $fn = "$ARGV[1]/$cat/index.html";
408 open(my $fh, ">", $fn) or die "$!";
411 utime $now, $now, $fn;
414 foreach my $pth (keys %mtimes) {
415 open(my $fh, ">", "$ARGV[1]/$pth.html") or die "$!";
418 @s = stat($pth) or die "$!";
419 utime $s[9], $s[9], "$ARGV[1]/$pth.html";
420 copy $pth, "$ARGV[1]/$pth.txt" or die "$!";
421 utime $s[9], $s[9], "$ARGV[1]/$pth.txt";
423 } elsif ($ARGV[0] eq "dot") {
424 print "digraph d {\n";
425 while (my ($from, $v) = each %links) {
427 print "\t\"$from\" -> \"$_\"\n";