Scripts

The pile of scripts used to build the garden. Not claiming it’s any good.

b.nu

def ifdo [asd] {
    if $in {
        do $asd
    }
    return $in
}

# ensure there's an enclosing directory for "p"
def ensdir [p] {
    $p | path dirname | mkdir $in
}

# goofy hack: files like garden/listing.md get deleted on clean and might not exist when i run ls,
# so this puts fake entries into the ls table just to make sure out/listing.html is generated
def ensure_names [...ns] {
    let tbl = $in
    $ns | reduce --fold $tbl {|name, tbl|
        if ($tbl | where name == $name | is-empty) {
            $tbl | append {name: $name}
        } else {
            $tbl
        }
    } 
}

# "true" if "target" is older than anything in "srcs"
def older-than [target, ...srcs] {
    if not ($target | path exists) {
        print $"($target): doesn't exist"
        return true
    }
    let mtime = ls $target | get 0.modified
    $srcs
        | where { path exists }
        | ls ...$in
        | where { $in.modified > $mtime }
        | is-not-empty
        | ifdo { print $"($target): outdated" }
}

# pipe something in and it will get saved to "stampfile"; returns "true" if the
# contents of stampfile were different
def stamp [stampfile] {
    let new = $in | to nuon
    if (not ($stampfile | path exists)) or ($new != (open --raw $stampfile)) {
        $new | save --force $stampfile
        return true
    }
    return false
}

##########################

let tool_out = "tmp/tool"

let garden_specials = [garden/nu.md garden/listing.md garden/blog/index.md]
let garden = ls garden/**/*.md
    | ensure_names ...$garden_specials
    | rename -c { name: in-name }
    | insert out-name { $in.in-name | path split | update 0 out | str replace .md .html | path join }

let blog = ls blog/**/*.md
    | rename -c { name: in-name }
    # slightly different path math 
    | insert out-name { $in.in-name | path split | update 0 out/blog | str replace .md "" | append "index.html" | path join }

let statics = ls static/**/*
    | where type == file
    | rename -c { name: in-name }
    | insert out-name { $in.in-name | path split | update 0 out | path join }

##########################

# get pandoc to cough up highlighting styles
older-than "tmp/dollar-highlighting-css-dollar.html" | ifdo {
    mkdir tmp
    "$highlighting-css$\n" | save --force "tmp/dollar-highlighting-css-dollar.html"
}
[kate, zenburn] | par-each { |style|
    let outcss = $"out/highlighting-($style).css"
    older-than $outcss | ifdo {
        ensdir $outcss
        "`a`{.c}" # just a markdown string which has syntax highlighting in it
            | pandoc --highlight-style=($style) --template=tmp/dollar-highlighting-css-dollar.html --metadata title="dummy"
            | save --force $outcss
    }
}

# quine
older-than garden/nu.md b.nu Tool.java deploy.sh | ifdo {
    "# Scripts\n\n" | save --force garden/nu.md
    "The pile of scripts used to build the [garden](index). Not claiming it's any good.\n\n" | save --append garden/nu.md
    
    def putscript [file, lang] {
        $"## `($file)`\n\n```($lang)\n(open --raw $file)```\n\n" | save --append garden/nu.md
    }
    putscript b.nu sh
    putscript Tool.java java
    putscript deploy.sh sh
}

# java tool
# TODO: now that im using a real scripting language the java tool might not be necessary
older-than $"($tool_out)/Tool.class" Tool.java | ifdo {
    mkdir $tool_out
    javac Tool.java -d $tool_out
}

# special listing pages
$garden | get in-name | sort | stamp tmp/listing-stampfile | ifdo {
    print "regenerating listing"
    java -cp $tool_out Tool gardenListing garden/ garden/listing.md
}
$blog | get in-name | sort | stamp tmp/blog-stampfile | ifdo {
    print "regenerating blog listing"
    java -cp $tool_out Tool blogListing blog/ garden/blog/index.md
}

def git-time [md] {
    git log --pretty='%as' -n 1 $md
}

def do-pandoc [md, htm, get_date] {
    mut flags = [
        $md,
        "-o",
        $htm,
        "--from=markdown+autolink_bare_uris+raw_attribute",
        "--template=mytemplate.html",
        "--lua-filter=filter.lua",
        "--mathml",
        "--wrap=preserve",
        "--highlight-style=kate",
        $"--variable=quat_filename=($md)"
    ]
    if $get_date {
        $flags = $flags | append $"--variable=quat_last_updated=(git-time $md)"
    }
    
    ensdir $htm
    pandoc ...$flags
}

# markdown parsin
($garden | append $blog) | par-each { |row|
    older-than $row.out-name $row.in-name mytemplate.html filter.lua | ifdo {
        do-pandoc $row.in-name $row.out-name ($row.in-name not-in $garden_specials)
    }
}

# statics
$statics | par-each { |row|
    older-than $row.out-name $row.in-name | ifdo {
        ensdir $row.out-name
        cp $row.in-name $row.out-name
    }
}

# search index (but not on Android)
if (which "termux-setup-storage" | is-empty) {
    ls out/**/*.html | stamp tmp/out-stamp | ifdo {
        ./precompiled-pagefind/pagefind --site out
    }
}

exit

Tool.java

import java.io.*;
import java.util.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.nio.file.attribute.*;

class Tool {
  public static void main(String[] args) throws IOException {
    System.out.println("hello");
    
    if(args[0].equals("gardenListing")) {
      writeGardenListing(get(args[1]), get(args[2]));
    } else if(args[0].equals("blogListing")) {
      writeBlogListing(get(args[1]), get(args[2]));
    } else {
      throw new RuntimeException("unknown op " + args[0]);
    }
    System.out.println("Done");
  }
  
  static Path get(String arg) {
    return Paths.get(arg).toAbsolutePath().normalize();
  }
  
  static void writeGardenListing(Path gardenSrc, Path listingMd) throws IOException {
    System.out.println("gardenSrc: " + gardenSrc);
    System.out.println("listingMd: " + listingMd);
    
    List<Meta> metas = readMetas(gardenSrc).stream()
      .sorted(Meta::compareByTitle)
      .toList();
    
    List<String> out = new ArrayList<>();
    out.add("# Listing");
    out.add("");
    out.add("There are currently <b>" + metas.size() + "</b> files in the garden.");
    out.add("");
    for(Meta m : metas) out.add("* " + m.mdLink());
    
    Files.createDirectories(listingMd.getParent());
    Files.write(listingMd, out, StandardCharsets.UTF_8);
    //System.out.println(String.join("\n", out));
  }
  
  static void writeBlogListing(Path blogSrc, Path blogListingMd) throws IOException {
    System.out.println("blogSrc: " + blogSrc);
    System.out.println("blogListingMd: " + blogListingMd);
    
    List<Meta> metas = readMetas(blogSrc).stream()
      .peek(it -> {
        if(it.date == null) throw new IllegalArgumentException("Post at '" + it.bareUrl + "' has no date");
      })
      .sorted(Meta::compareByDate)
      .toList();
    
    List<String> out = new ArrayList<>();
    out.add("# Blog");
    out.add("");
    out.add("There are " + metas.size() + " posts, but I don't blog as often now that I have the [garden](index).\n\nPlease pardon my dust, still migrating stuff here.");
    out.add("");
    for(Meta m : metas) {
      // \u2b50 -> star
      String pre = m.good ? "\u2b50 **" : m.draft ? "*" : "";
      String post = m.good ? "**" : m.draft ? " (draft)*" : "";
      
      out.add("* " + m.date + " &ndash; " + pre + m.mdLink("blog/", "/") + post);
      if(m.blurb != null) {
        out.add("  ");
        out.add("  > " + m.blurb);
      }
      out.add("");
    }
    Files.createDirectories(blogListingMd.getParent());
    Files.write(blogListingMd, out, StandardCharsets.UTF_8);
    //System.out.println(String.join("\n", out));
  }
  
  static class Meta {
    String bareUrl;
    String title;
    String date;
    String blurb;
    boolean good;
    boolean draft;
    
    int compareByTitle(Meta other) {
      String myTitle = title.toLowerCase(Locale.ROOT).replaceAll("[^a-z]", "");
      String theirTitle = other.title.toLowerCase(Locale.ROOT).replaceAll("[^a-z]", "");
      return myTitle.compareTo(theirTitle);
    }
    
    int compareByDate(Meta other) {
      return -date.compareTo(other.date);
    }
    
    String mdLink() {
      return mdLink("", "");
    }
    
    String mdLink(String pre, String post) {
      return "[" + escapeForMdLink(title) + "](" + pre + escapeForMdLink(bareUrl) + post + ")";
    }
  }
  
  static Meta readMeta(Path base, Path md) throws IOException {
    Meta m = new Meta();
    m.bareUrl = fwdString(chopExtension(chopStart(base, md)));
        
    boolean yamlMode = false;
    for(String line : Files.readAllLines(md)) {
      if("---".equals(line)) {
        yamlMode = !yamlMode;
        continue;
      }
      if(yamlMode) {
        if("...".equals(line)) {
          yamlMode = false;
          continue;
        }
        if(line.startsWith("title:")) m.title = line.substring(6).trim();
        if(line.startsWith("date:")) m.date = line.substring(5).trim();
        if(line.startsWith("blurb:")) m.blurb = line.substring(6).trim();
        if(line.startsWith("good:")) m.good = true;
        if(line.startsWith("draft:")) m.draft = true;
      }
      
      //parse titles out of the first heading in the document
      if(m.title == null && !yamlMode && line.startsWith("#")) {
        do { line = line.substring(1); } while(line.startsWith("#"));
        m.title = line.trim();
      }
    }
    
    if(m.title == null) m.title = m.bareUrl;
    
    return m;
  }
  
  static List<Meta> readMetas(Path dir) throws IOException {
    List<Meta> m = new ArrayList<>();
    Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
      @Override
      public FileVisitResult visitFile(Path md, BasicFileAttributes attrs) throws IOException {
        if(!md.toString().endsWith(".md")) return FileVisitResult.CONTINUE;
        m.add(readMeta(dir, md));
        return FileVisitResult.CONTINUE;
      }
    });
    return m;
  }
  
  /// path math ///
  
  // start:  /a/b/c/
  // sub:    /a/b/c/something/foo.txt
  // result: something/foo.txt
  static Path chopStart(Path start, Path sub) {
    return sub.subpath(start.getNameCount(), sub.getNameCount());
  }
  
  static Path chopExtension(Path p) {
    String filename = p.getFileName().toString();
    int dot = filename.indexOf('.');
    if(dot == -1) return p;
    else return p.resolveSibling(filename.substring(0, dot));
  }
  
  //Always use / as the path separator even on windows >.>
  static String fwdString(Path p) {
    StringBuilder b = new StringBuilder(p.getName(0).toString());
    for(int i = 1; i < p.getNameCount(); i++) b.append('/').append(p.getName(i).toString());
    return b.toString();
  }
  
  /// markdown gunk ///
  
  static String escapeForMdLink(String s) {
    return s.replace("(", "\\(").replace(")", "\\)").replace("[", "\\[").replace("]", "\\]");
  }
}

deploy.sh

# sync pagefind indices with --delete so old ones get culled
if [ -f out/pagefind/pagefind.js ]; then
    echo syncing pagefind indices...
    rsync -lpvrtz --delete out/pagefind/ root@door:/opt/notes/pagefind/
else
    echo not syncing pagefind
fi

echo uploading everything else
rsync -lpvrtz out/ root@door:/opt/notes/

# rsync flags!
# -l, copy symlinks as symlinks (not really needed tbh)
# -p, copy permission bits (probably bad on android?)
# -v, be noisier
# -r, recursive
# -t, copy mtimes
# -z, use compression in transit

# Termux has fucked up umasks, rsync will dutifully copy them,
# and then the web server can't read anything anymore
if command -v "termux-setup-storage"
then
    echo fixing perms...
    ssh door "chmod +rX -R /opt/notes/; chown root:root -R /opt/notes/"
fi