From becbcd05f1e633c087ea33ac285ec086e37b9ec0 Mon Sep 17 00:00:00 2001 From: Qyriad Date: Tue, 2 Apr 2024 17:01:53 -0600 Subject: [PATCH] docs: replace sed invocation with an mdbook preprocessor for @docroot@ We're not entirely clear on why the links preprocessor has to be done *before* rather than after, but we assume it is probably that as a builtin preprocessor it does some processing on the raw book source, and not just the JSON data. Also a real use for Python pattern matching? I know I was surprised too. Change-Id: Ibe8b59e7b5bd5f357a655d8b4c5f0b0f58a67d6b --- doc/manual/book.toml | 9 +++++ doc/manual/docroot.py | 84 +++++++++++++++++++++++++++++++++++++++++++ doc/manual/local.mk | 6 +--- 3 files changed, 94 insertions(+), 5 deletions(-) create mode 100755 doc/manual/docroot.py diff --git a/doc/manual/book.toml b/doc/manual/book.toml index 73fb7e75e..1d14347a4 100644 --- a/doc/manual/book.toml +++ b/doc/manual/book.toml @@ -7,6 +7,15 @@ additional-js = ["redirects.js"] edit-url-template = "https://github.com/NixOS/nix/tree/master/doc/manual/{path}" git-repository-url = "https://github.com/NixOS/nix" +# Handles replacing @docroot@ with a path to ./src relative to that markdown file. +[preprocessor.docroot] +renderers = ["html", "linkcheck"] +command = "python3 doc/manual/docroot.py" +# I would have thought that @docroot@ replacement had to be done *before* +# the link preprocessor gets its hands on this book, but nope it's actually +# the opposite. +after = ["links"] + [preprocessor.anchors] renderers = ["html"] command = "jq --from-file doc/manual/anchors.jq" diff --git a/doc/manual/docroot.py b/doc/manual/docroot.py new file mode 100755 index 000000000..e95f8abbf --- /dev/null +++ b/doc/manual/docroot.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +from pathlib import Path +import json +import os, os.path +import sys + +name = 'process-docroot.py' + +def log(*args, **kwargs): + kwargs['file'] = sys.stderr + return print(f'{name}:', *args, **kwargs) + +def replace_docroot(relative_md_path: Path, content: str, book_root: Path): + assert not relative_md_path.is_absolute(), f'{relative_md_path=} from mdbook should be relative' + + md_path_abs = book_root / relative_md_path + docroot_abs = md_path_abs.parent + assert docroot_abs.is_dir(), f'supposed docroot {docroot_abs} is not a directory (cwd={os.getcwd()})' + + # The paths mdbook gives us are relative to the directory with book.toml. + # @docroot@ wants to be replaced with the path relative to `src/`. + docroot_rel = os.path.relpath(book_root / 'src', start=docroot_abs) + + return content.replace('@docroot@', docroot_rel) + +def recursive_replace(data, book_root): + match data: + case {'sections': sections}: + return data | dict( + sections = [recursive_replace(section, book_root) for section in sections], + ) + case {'Chapter': chapter}: + # Path to the .md file for this chapter, relative to book_root. + path_to_chapter = Path('src') / chapter['path'] + chapter_content = chapter['content'] + + return data | dict( + Chapter = chapter | dict( + content = replace_docroot(path_to_chapter, chapter_content, book_root), + sub_items = [recursive_replace(sub_item, book_root) for sub_item in chapter['sub_items']], + ), + ) + + case rest: + assert False, f'should have been called on a dict, not {type(rest)=}\n\t{rest=}' + +def main(): + + if len(sys.argv) > 1 and sys.argv[1] == 'supports': + log('confirming to mdbook that we support their stuff') + return 0 + + # mdbook communicates with us over stdin and stdout. + # It splorks us a JSON array, the first element describing the context, + # the second element describing the book itself, + # and then expects us to send it the modified book JSON over stdout. + + context, book = json.load(sys.stdin) + + # book_root is *not* @docroot@. @docroot@ gets replaced with a relative path to `./src/`. + # book_root is the directory where book.toml, aka `src`'s parent. + book_root = Path(context['root']) + assert book_root.exists(), f'{book_root=} does not exist' + assert book_root.joinpath('book.toml').is_file(), f'{book_root / "book.toml"} is not a file' + + log('replacing all occurrences of @docroot@ with a relative path') + + # Find @docroot@ in all parts of our recursive book structure. + replaced_content = recursive_replace(book, book_root) + + replaced_content_str = json.dumps(replaced_content) + + # Give mdbook our changes. + print(replaced_content_str) + + log('done!') + +try: + sys.exit(main()) +except AssertionError as e: + print(f'{name}: INTERNAL ERROR in mdbook preprocessor', file=sys.stderr) + print(f'this is a bug in {name}') + raise diff --git a/doc/manual/local.mk b/doc/manual/local.mk index 955357607..e7c029727 100644 --- a/doc/manual/local.mk +++ b/doc/manual/local.mk @@ -159,17 +159,13 @@ doc/manual/generated/man1/nix3-manpages: $(d)/src/command-ref/new-cli done @touch $@ -$(docdir)/manual/index.html: $(MANUAL_SRCS) $(d)/book.toml $(d)/anchors.jq $(d)/custom.css $(d)/src/SUMMARY.md $(d)/src/command-ref/new-cli $(d)/src/contributing/experimental-feature-descriptions.md $(d)/src/command-ref/conf-file.md $(d)/src/language/builtins.md $(d)/src/language/builtin-constants.md $(d)/src/release-notes/rl-next.md +$(docdir)/manual/index.html: $(MANUAL_SRCS) $(d)/book.toml $(d)/anchors.jq $(d)/custom.css $(d)/src/SUMMARY.md $(d)/src/command-ref/new-cli $(d)/src/contributing/experimental-feature-descriptions.md $(d)/src/command-ref/conf-file.md $(d)/src/language/builtins.md $(d)/src/language/builtin-constants.md $(d)/src/release-notes/rl-next.md $(d)/docroot.py $(trace-gen) \ tmp="$$(mktemp -d)"; \ cp -r doc/manual "$$tmp"; \ find "$$tmp" -name '*.md' | while read -r file; do \ doc/manual/process-includes.sh $$file $$file; \ done; \ - find "$$tmp" -name '*.md' | while read -r file; do \ - docroot="$$(realpath --relative-to="$$(dirname "$$file")" $$tmp/manual/src)"; \ - sed -i "s,@docroot@,$$docroot,g" "$$file"; \ - done; \ set -euo pipefail; \ RUST_LOG=warn mdbook build "$$tmp/manual" -d $(DESTDIR)$(docdir)/manual.tmp 2>&1 \ | { grep -Fv "because fragment resolution isn't implemented" || :; }; \