Makefile
The makefile behind the garden. Not claiming it is any good.
garden-special := garden/makefile.md garden/listing.md garden/blog/index.md
garden-sources := $(shell find garden -name "*.md" -type f) $(garden-special)
garden-outs := $(patsubst garden/%.md,out/%.html,$(garden-sources))
blog-sources := $(shell find blog -name "*.md" -type f)
blog-outs := $(patsubst blog/%.md,out/blog/%/index.html,$(blog-sources))
static-special := out/highlighting-kate.css out/highlighting-zenburn.css
static-sources := $(shell find static -type f) $(static-special)
static-outs := $(patsubst static/%,out/%,$(static-sources))
outs := $(garden-outs) $(blog-outs) $(static-outs)
slow-outs := out/pagefind
tool-out := tmp/tool/
PAGEFIND := ./precompiled-pagefind/pagefind
# or npx -y pagefind
$(shell mkdir -p out)
$(shell mkdir -p tmp)
# remove some ancient make cruft by defining this as empty...
.SUFFIXES:
# default target (`make`) excludes pagefind cause it's a little slow. `make all` to get it
.PHONY: no-search all
no-search: $(outs)
all: no-search $(slow-outs)
# a java tool, because sometimes you need a real programming language, not bash >.>
$(tool-out)/Tool.class: Tool.java
javac "$<" -d "$(@D)"
# quine?
garden/makefile.md: Makefile Tool.java deploy.sh
printf '# Makefile\n\nThe makefile behind the [garden](garden). Not claiming it is any good.\n\n```makefile\n' > "$@"
cat Makefile >> "$@"
printf '\n```\n\n## `Tool.java`\n\n```java\n' >> "$@"
cat Tool.java >> "$@"
printf '\n```\n\n## `deploy.sh`\n\n```sh\n' >> "$@"
cat deploy.sh >> "$@"
printf '\n```' >> "$@"
# garden listing, using a trick to make it only outdated when the list of files change, i don't care about the actual contents.
# cf. https://www.cmcrossroads.com/article/rebuilding-when-files-checksum-changes
# the listing itself is generated with the java tool
garden-sources-no-listing := $(filter-out garden/listing.md,$(garden-sources))
tmp/listing-dirty: FORCE
$(if $(filter-out $(shell cat "$@" 2>/dev/null),$(garden-sources-no-listing)),echo "$(garden-sources-no-listing)" > $@)
garden/listing.md: tmp/listing-dirty $(tool-out)/Tool.class
java -cp $(tool-out) Tool gardenListing "garden/" "$@"
# blog listing, using the same trick and the same tool
blog-sources-no-index := $(filter-out blog/index.md,$(blog-sources))
tmp/blog-listing-dirty: FORCE
$(if $(filter-out $(shell cat "$@" 2>/dev/null),$(blog-sources-no-index)),echo "$(blog-sources-no-index)" > $@)
garden/blog/index.md: tmp/blog-listing-dirty $(tool-out)/Tool.class
java -cp $(tool-out) Tool blogListing "blog/" "$@"
# garden files
# break this out into a lazily-substituted variable (= instead of :=)
PANDOC_CMD = pandoc --from=markdown+autolink_bare_uris+raw_attribute $< -o $@ --template=mytemplate.html --lua-filter=filter.lua --mathml --wrap=preserve --highlight-style=kate --variable=quat_filename="$<"
# files which shouldn't have a "last updated" time since they're not in git
out/makefile.html: garden/makefile.md mytemplate.html filter.lua
mkdir -p $(@D) && $(PANDOC_CMD)
out/listing.html: garden/listing.md mytemplate.html filter.lua
mkdir -p $(@D) && $(PANDOC_CMD)
out/blog/index.html: garden/blog/index.md mytemplate.html filter.lua
mkdir -p $(@D) && $(PANDOC_CMD)
# all other garden files
PANDOC_CMD_WITH_LAST_UPDATED = $(PANDOC_CMD) --variable=quat_last_updated="$$(git log --pretty='%as' -n 1 '$<')"
out/%.html: garden/%.md mytemplate.html filter.lua
mkdir -p $(@D) && $(PANDOC_CMD_WITH_LAST_UPDATED)
# pattern rule that just has slightly different path math? eugh
out/blog/%/index.html: blog/%.md mytemplate.html filter.lua
mkdir -p $(@D) && $(PANDOC_CMD_WITH_LAST_UPDATED)
# making pandoc cough up syntax highlighting CSS stylesheets is a bit tricky, but can be done like this
# the `a`{.c} thing is just a random block of markdown that makes pandoc initialize the syntax highlighter
# cf. https://github.com/jgm/pandoc/issues/7860#issuecomment-1018696254
tmp/dollar-highlighting-css-dollar.html:
echo '$$highlighting-css$$' > $@
out/highlighting-%.css: tmp/dollar-highlighting-css-dollar.html
echo '`a`{.c}' | pandoc --highlight-style="$*" --template=tmp/dollar-highlighting-css-dollar.html --metadata title="dummy" > $@
# copy all the other static resources as-is (pattern rule)
out/%: static/%
mkdir -p $(@D)
cp $< $@
# run pagefind only after creating html files
# if i'm running thru termux this might fail since it's not installed -> excuse the failure
out/pagefind: $(outs)
$(PAGEFIND) --site out || command -v "termux-setup-storage"
.PHONY: clean cleanspecial serve open deploy push
cleanspecial:
rm -f $(garden-special) $(static-special)
clean: cleanspecial
rm -rf ./out
rm -rf ./tmp
serve:
#miniserve -v out --index index.html
cd out && python3 -m http.server 8080
open:
start http://[::1]:8080
deploy: all
./deploy.sh
# i have muscle-memory that "make push" deploys the site
push:
git add .
git commit -m "lazy commit"
git push
./deploy.sh
# fake perpetually out-of-date target, used by the listing trick
FORCE: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 + " – " + 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