SITE_URL ?= https://example.com
.DEFAULT_GOAL := all

# Website .typ files: cv.typ, plus any .typ file that imports it
# directly or indirectly. Other .typ files (src/, projects/, etc.)
# are standalone PDFs and are skipped. Cyclical imports would error
# in typst anyway, so no cycle guard is needed here.
TYP_FILES := $(shell bash -c 'cv=$$(realpath cv.typ); pages="$$cv"; changed=1; while [ $$changed -eq 1 ]; do changed=0; for f in $$(find . -name "*.typ" -not -name "_*"); do f=$$(realpath "$$f"); [[ " $$pages " == *" $$f "* ]] && continue; imports=$$(grep -oP "#import\s+\"\K[^\"]+" "$$f" 2>/dev/null | grep -v "^@"); for imp in $$imports; do target=$$(realpath "$$(dirname "$$f")/$$imp" 2>/dev/null); if [[ " $$pages " == *" $$target "* ]]; then pages="$$pages $$f"; changed=1; fi; done; done; done; for p in $$pages; do realpath --relative-to=. "$$p"; done | sort -u')
# Corresponding page targets. cv.typ is the site root, so its HTML
# output is index.html rather than cv.html.
PAGE_TYP_FILES := $(filter-out cv.typ,$(TYP_FILES))

# Typst writes the exact local dependency set for each compiled HTML/PDF target.
# Those .d files are included below, so changing one page no longer rebuilds
# unrelated pages or PDFs.
# Presentations are PDF-only Typst documents. Their public route is generated
# as a small PDF viewer shell instead of going through Typst's incomplete HTML
# export, which cannot reproduce Touying slides reliably. Recognize both the
# shared template's presentation flag and documents which configure Touying /
# Metropolis directly (as the original lecture does).
# Draft status is read only from the protected prefix before // BEGIN DOCUMENT.
# The editable body is deliberately ignored by build classification.
DRAFT_TYP_FILES := $(shell for f in $(TYP_FILES); do \
  python3 src/draft_boundary.py is-draft "$$f" >/dev/null 2>&1 && echo "$$f"; \
done)
DRAFT_PAGE_TYP_FILES := $(filter $(PAGE_TYP_FILES),$(DRAFT_TYP_FILES))
DRAFT_HTML_FILES := $(DRAFT_PAGE_TYP_FILES:.typ=.html)

# The documentation page contains literal examples of these declarations.
# Drafts are excluded from feature scanning: arbitrary half-written body text
# cannot make a draft become a feed, presentation, or printable PDF target.
FEATURE_SCAN_TYP_FILES := $(filter-out documentation/index.typ $(DRAFT_TYP_FILES),$(TYP_FILES))
PRESENTATION_TYP_FILES := $(shell grep -El -e 'presentation[[:space:]]*:[[:space:]]*true' -e '@preview/touying' -e 'metropolis-theme' -e 'simple-theme' -e 'university-theme' $(FEATURE_SCAN_TYP_FILES) 2>/dev/null)
PRESENTATION_VIEWER_FILES := $(PRESENTATION_TYP_FILES:.typ=.html)
NORMAL_PAGE_TYP_FILES := $(filter-out $(PRESENTATION_TYP_FILES) $(DRAFT_PAGE_TYP_FILES),$(PAGE_TYP_FILES))
NORMAL_HTML_FILES := $(NORMAL_PAGE_TYP_FILES:.typ=.html)
TYPST_HTML_FILES := $(NORMAL_HTML_FILES) $(DRAFT_HTML_FILES) index.html
HTML_FILES := $(TYPST_HTML_FILES) $(PRESENTATION_VIEWER_FILES)

# Any TYP_FILES entry that calls the shared feed helper gets its own
# feed.xml, written alongside it.
FEED_TYP_FILES := $(shell for f in $(FEATURE_SCAN_TYP_FILES); do grep -Eq '#[[:space:]]*feed' "$$f" 2>/dev/null && echo "$$f"; done)
FEED_XML_FILES := $(foreach f,$(FEED_TYP_FILES),$(dir $(f))feed.xml)

# Presentation and research documents are also printable deliverables. Their
# flags live in the source document, so match only explicitly enabled values.
FLAGGED_PDF_TYP_FILES := $(shell for f in $(FEATURE_SCAN_TYP_FILES); do grep -Eq '(^|[^[:alnum:]_-])(presentation|research)[[:space:]]*:[[:space:]]*true([^[:alnum:]_-]|$$)|@preview/touying|(metropolis|simple|university)-theme' "$$f" 2>/dev/null && echo "$$f"; done)
# Drafts must never become automatic PDF targets, even when they also set
# research: true / presentation: true or happen to match another PDF feature.
# The explicit filter is intentionally redundant with FEATURE_SCAN_TYP_FILES.
PDF_TYP_FILES := $(filter-out $(DRAFT_TYP_FILES),$(sort $(FEED_TYP_FILES) $(FLAGGED_PDF_TYP_FILES) digest/index.typ))
PDF_FILES := $(PDF_TYP_FILES:.typ=.pdf)

# Dependency files are generated by Typst itself. External package/cache paths
# are stripped after compilation so these remain portable between machines.
# Normal Typst outputs use compiler-generated dependency files. Draft HTML is
# different: it is compiled from a protected-header shell and therefore uses
# explicit conservative shell dependencies below instead of a .d file that
# could accidentally mention the editable body.
TYPST_DEP_OUTPUTS := $(NORMAL_HTML_FILES) $(PDF_FILES) cv.pdf
TYPST_DEP_FILES := $(addprefix .typst-deps/,$(addsuffix .d,$(TYPST_DEP_OUTPUTS)))
-include $(TYPST_DEP_FILES)

# On a fresh checkout, compile each dependency-tracked Typst output once to seed
# its dependency file. Draft shells do not participate in this mechanism.
.PHONY: FORCE_TYPST_DEPS
FORCE_TYPST_DEPS:
MISSING_TYPST_DEP_OUTPUTS := $(foreach out,$(TYPST_DEP_OUTPUTS),$(if $(wildcard .typst-deps/$(out).d),,$(out)))
$(MISSING_TYPST_DEP_OUTPUTS): FORCE_TYPST_DEPS

# Draft shells are tiny and safe to rebuild conservatively when shared template
# code changes. Crucially, these prerequisites are timestamps only: Typst never
# receives a draft body as compilation input.
DRAFT_SHELL_DEPS := $(sort cv.typ src/draft_boundary.py src/draft-editor.js src/assets/draft-editor.css $(filter-out $(DRAFT_TYP_FILES),$(TYP_FILES)) $(shell find src -name "*.typ"))

all: publish personalwebsite
publish: cv.pdf html pdf $(FEED_XML_FILES)
html: $(HTML_FILES)
pdf: $(PDF_FILES)

# Compile page sources to matching .html paths. The output path determines both
# the relative path back to the site root and the canonical public page path.
# Both are supplied once through Typst's sys.inputs; page sources contain no
# deployment-specific path or hostname plumbing.
$(PRESENTATION_VIEWER_FILES): %.html: %.typ %.pdf src/presentation-viewer.html src/presentation-viewer.js src/assets/tufted.css src/assets/custom.css
	@source="$<"; \
	dir="$$(dirname "$$source")"; \
	if [ "$$dir" = "." ]; then page_root="./"; \
	else page_root="$$(printf '%s/' "$$dir" | sed 's#[^/]*/#../#g')"; fi; \
	pdf_name="$$(basename "$$source" .typ).pdf"; \
	sed -e "s|__ROOT__|$$page_root|g" -e "s|__PDF__|$$pdf_name|g" \
	  src/presentation-viewer.html > "$@"

# Compile a draft page from only its protected prefix. The temporary file is a
# sibling of the real source so all relative imports in the header resolve
# exactly as they do in the original document. Invalid or incomplete Typst after
# // BEGIN DOCUMENT is never parsed and therefore cannot break the site build.
$(DRAFT_HTML_FILES): %.html: %.typ $(DRAFT_SHELL_DEPS)
	@bash src/build/compile-draft.sh "$<" "$@" "$(SITE_URL)"

%.html: %.typ
	@if python3 src/draft_boundary.py is-draft "$<" >/dev/null 2>&1; then \
	  exec bash src/build/compile-draft.sh "$<" "$@" "$(SITE_URL)"; \
	fi; \
	dir="$(@D)"; \
	if [ "$$dir" = "." ]; then page_root="./"; \
	else page_root="$$(printf '%s/' "$$dir" | sed 's#[^/]*/#../#g')"; fi; \
	page_path="$@"; \
	case "$$page_path" in \
	  index.html) page_path="" ;; \
	  */index.html) page_path="$${page_path%index.html}" ;; \
	esac; \
	deps=".typst-deps/$@.d"; \
	mkdir -p "$$(dirname "$$deps")"; \
	err="$$(mktemp)"; \
	if ! typst compile --features html --format html \
	  --deps "$$deps" --deps-format make \
	  --input root="$$page_root" \
	  --input site-url="$(SITE_URL)" \
	  --input page-path="$$page_path" \
	  --input source-path="$<" \
	  $< $@ --root . 2>"$$err"; then \
	  cat "$$err" >&2; rm -f "$$err"; exit 1; \
	fi; \
	rm -f "$$err"; \
	sed -E 's# /[^ ]+##g' "$$deps" > "$$deps.tmp" && mv "$$deps.tmp" "$$deps"

# cv.typ is both the root web page and the printable CV.
index.html: cv.typ
	@deps=".typst-deps/$@.d"; \
	mkdir -p "$$(dirname "$$deps")"; \
	err="$$(mktemp)"; \
	if ! typst compile --features html --format html \
	  --deps "$$deps" --deps-format make \
	  --input root="./" --input site-url="$(SITE_URL)" --input page-path="" \
	  --input source-path="$<" \
	  $< $@ --root . 2>"$$err"; then \
	  cat "$$err" >&2; rm -f "$$err"; exit 1; \
	fi; \
	rm -f "$$err"; \
	sed -E 's# /[^ ]+##g' "$$deps" > "$$deps.tmp" && mv "$$deps.tmp" "$$deps"

cv.pdf: cv.typ
	@deps=".typst-deps/$@.d"; \
	mkdir -p "$$(dirname "$$deps")"; \
	typst compile --deps "$$deps" --deps-format make \
	  --input site-url="$(SITE_URL)" $< $@ --root . && \
	sed -E 's# /[^ ]+##g' "$$deps" > "$$deps.tmp" && mv "$$deps.tmp" "$$deps"

# Compile PDFs automatically for RSS, presentation, and research sources.
# Re-check draft status at execution time as a final guard against stale
# classification/dependency metadata. A draft body is never passed to Typst.
$(PDF_FILES): %.pdf: %.typ
	@if python3 src/draft_boundary.py is-draft "$<" >/dev/null 2>&1; then \
	  echo "skipping PDF for draft source: $<"; \
	  exit 0; \
	fi; \
	source="$<"; \
	case "$$source" in \
	  index.typ) page_path="" ;; \
	  */index.typ) page_path="$${source%index.typ}" ;; \
	  *) page_path="$${source%.typ}.html" ;; \
	esac; \
	deps=".typst-deps/$@.d"; \
	mkdir -p "$$(dirname "$$deps")"; \
	typst compile --deps "$$deps" --deps-format make \
	  --input site-url="$(SITE_URL)" \
	  --input page-path="$$page_path" \
	  $< $@ --root . && \
	sed -E 's# /[^ ]+##g' "$$deps" > "$$deps.tmp" && mv "$$deps.tmp" "$$deps"

# Generate one rule per feed source: dir/feed.xml <- dir/source.typ.
# Derive the canonical page path from the source just as the HTML rule does.
# `typst query` only emits JSON, so unwrap the JSON-quoted string back into raw
# XML: strip surrounding quotes, un-escape \" -> ", then let printf %b handle
# \n \t \r \\ etc.
define FEED_RULE
$(dir $(1))feed.xml: $(1) $(patsubst %.typ,%.html,$(1))
	@source="$(1)"; \
	case "$$$$source" in \
	  index.typ) page_path="" ;; \
	  */index.typ) page_path="$$$${source%index.typ}" ;; \
	  *) page_path="$$$${source%.typ}.html" ;; \
	esac; \
	json="$$$$(typst eval 'query(<feed>).first().value' \
	  --root . --input site-url="$(SITE_URL)" --input page-path="$$$$page_path" --in $$<)"; \
	json="$$$${json#\"}"; \
	json="$$$${json%\"}"; \
	json="$$$$(printf '%s' "$$$$json" | sed 's/\\"/"/g')"; \
	printf '%b' "$$$$json" > $$@
endef
$(foreach f,$(FEED_TYP_FILES),$(eval $(call FEED_RULE,$(f))))

# Regression check: make the editable body syntactically invalid, then rebuild
# the draft shell. This must continue to succeed because the body is not parsed.
test-draft-isolation:
	@bash src/build/test-draft-isolation.sh

# Regression check: a draft that also requests research/PDF output must still
# be excluded from every automatic PDF target.
test-draft-pdf-exclusion:
	@python3 src/draft_boundary.py is-draft papers/ideas/index.typ >/dev/null
	@case " $(PDF_TYP_FILES) " in \
	  *" papers/ideas/index.typ "*) \
	    echo "draft source leaked into PDF_TYP_FILES: papers/ideas/index.typ" >&2; \
	    exit 1 ;; \
	esac

draft-local:
	python3 src/draft-local.py

clean:
	rm -f $(HTML_FILES) $(PDF_FILES) cv.pdf $(FEED_XML_FILES)
	rm -rf .typst-deps
.PHONY: all publish html pdf clean test-draft-isolation test-draft-pdf-exclusion draft-local

# --- personal website export ---
# Build a reusable, personal-information-free copy of the full website
# feature set. The exporter creates fresh dummy content instead of copying
# the real CV, bibliography, blog, courses, links, projects, or digest data.
PERSONAL_WEBSITE_ZIP := projects/personalwebsite.zip
PERSONAL_WEBSITE_SHARED := $(shell find src -type f) \
	$(shell find .github/workflows -maxdepth 1 -type f) \
	documentation/index.typ \
	book/index.typ \
	book/book.js \
	noise/index.typ \
	noise/noise.js \
	LICENSE \
	$(shell find licenses -type f) \
	digest/digest.py \
	digest/config.yml \
	digest/requirements.txt \
	Makefile

personalwebsite: $(PERSONAL_WEBSITE_ZIP)

$(PERSONAL_WEBSITE_ZIP): $(PERSONAL_WEBSITE_SHARED)
	bash .github/workflows/export-personalwebsite.sh

.PHONY: personalwebsite
