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/
$(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
printf '# Makefile\n\nThe makefile behind the [garden](garden). Not claiming it is any good.\n\n```makefile\n' > "$@"
"$@"
cat Makefile >> '\n```\n\n## `Tool.java`\n\n```java\n' >> "$@"
printf "$@"
cat Tool.java >> '\n```' >> "$@"
printf
# 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 (pattern rule)
out/%.html: garden/%.md mytemplate.html filter.lua
mkdir -p $(@D)
$< -o $@ --template=mytemplate.html --lua-filter=filter.lua --mathml --wrap=preserve --highlight-style=kate --variable=quat_filename="$<"
pandoc --from=markdown+autolink_bare_uris+raw_attribute
# blog files (pattern rule that just has slightly different path math? eugh)
out/blog/%/index.html: blog/%.md mytemplate.html filter.lua
mkdir -p $(@D)
$< -o $@ --template=mytemplate.html --lua-filter=filter.lua --mathml --wrap=preserve --highlight-style=kate --variable=quat_filename="$<"
pandoc --from=markdown+autolink_bare_uris+raw_attribute
# 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 > $@
# copy all the other static resources as-is (pattern rule)
out/%: static/%
mkdir -p $(@D)
$< $@
cp
# run pagefind nly oafter creating html files
out/pagefind: $(outs)
npx -y pagefind --site out
.PHONY: clean cleanspecial serve open 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
push:
git add .
"lazy commit"
git commit -m
git push
# 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<>();
.add("# Listing");
out.add("");
out.add("There are currently <b>" + metas.size() + "</b> files in the garden.");
out.add("");
outfor(Meta m : metas) out.add("* " + m.mdLink());
.createDirectories(listingMd.getParent());
Files.write(listingMd, out, StandardCharsets.UTF_8);
Files//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<>();
.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("");
outfor(Meta m : metas) {
// \u2b50 -> star
String pre = m.good ? "\u2b50 **" : m.draft ? "*" : "";
String post = m.good ? "**" : m.draft ? " (draft)*" : "";
.add("* " + m.date + " – " + pre + m.mdLink("blog/", "/") + post);
outif(m.blurb != null) {
.add(" ");
out.add(" > " + m.blurb);
out}
.add("");
out}
.createDirectories(blogListingMd.getParent());
Files.write(blogListingMd, out, StandardCharsets.UTF_8);
Files//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) {
return title.toLowerCase(Locale.ROOT).compareTo(other.title.toLowerCase(Locale.ROOT));
}
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 {
= new Meta();
Meta m .bareUrl = fwdString(chopExtension(chopStart(base, md)));
m
boolean yamlMode = false;
for(String line : Files.readAllLines(md)) {
if("---".equals(line)) {
= !yamlMode;
yamlMode continue;
}
if(yamlMode) {
if("...".equals(line)) {
= false;
yamlMode 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("#"));
.title = line.trim();
m}
}
if(m.title == null) m.title = m.bareUrl;
return m;
}
static List<Meta> readMetas(Path dir) throws IOException {
List<Meta> m = new ArrayList<>();
.walkFileTree(dir, new SimpleFileVisitor<Path>() {
Files@Override
public FileVisitResult visitFile(Path md, BasicFileAttributes attrs) throws IOException {
if(!md.toString().endsWith(".md")) return FileVisitResult.CONTINUE;
.add(readMeta(dir, md));
mreturn 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("]", "\\]");
}
}