From 245d0515454a24e125f4dda190f54bc9c567d08a Mon Sep 17 00:00:00 2001 From: eldritch horrors Date: Mon, 8 Apr 2024 21:44:40 +0200 Subject: [PATCH] docs: generalize replacement script let's have a script that can't replaced only docroot, but also other variables. this could be used to generate manual fragments elsewhere and have mdbook include them from an absolute path, for example. Change-Id: I81aadddfc79462bf057c2de683dd270056a99e90 --- doc/manual/book.toml | 4 +- doc/manual/docroot.py | 84 ----------------------------------- doc/manual/local.mk | 2 +- doc/manual/substitute.py | 95 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 87 deletions(-) delete mode 100755 doc/manual/docroot.py create mode 100755 doc/manual/substitute.py diff --git a/doc/manual/book.toml b/doc/manual/book.toml index e03bed737..78e11be51 100644 --- a/doc/manual/book.toml +++ b/doc/manual/book.toml @@ -9,8 +9,8 @@ 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" +command = "python3 doc/manual/substitute.py docroot" +replace-kind = "relative-to-book-src" # 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. diff --git a/doc/manual/docroot.py b/doc/manual/docroot.py deleted file mode 100755 index e95f8abbf..000000000 --- a/doc/manual/docroot.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/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 13ab6da2f..a664f04d1 100644 --- a/doc/manual/local.mk +++ b/doc/manual/local.mk @@ -147,7 +147,7 @@ doc/manual/generated/man1/nix3-manpages: $(d)/src/command-ref/new-cli done @touch $@ -doc/manual/generated/out: $(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-generated.md $(d)/docroot.py +doc/manual/generated/out: $(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-generated.md $(d)/substitute.py @rm -rf $@ $(trace-gen) \ RUST_LOG=warn mdbook build doc/manual -d generated/out 2>&1 \ diff --git a/doc/manual/substitute.py b/doc/manual/substitute.py new file mode 100755 index 000000000..8945a845f --- /dev/null +++ b/doc/manual/substitute.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +from pathlib import Path +import json +import os, os.path +import sys + +name = 'substitute.py' + +def log(*args, **kwargs): + kwargs['file'] = sys.stderr + return print(f'{name}:', *args, **kwargs) + +def relative_to(var: str, source_root: Path): + def replace(relative_md_path: Path, content: str): + nonlocal source_root + assert not relative_md_path.is_absolute(), f'{relative_md_path=} from mdbook should be relative' + + md_path_abs = source_root / relative_md_path + var_abs = md_path_abs.parent + assert var_abs.is_dir(), f'supposed directory {var_abs} is not a directory (cwd={os.getcwd()})' + + # @var@ wants to be replaced with the path relative to source_root. + var_rel = os.path.relpath(source_root, start=var_abs) + + return content.replace(f'@{var}@', var_rel) + + return replace + +def recursive_replace(data, do_replace): + match data: + case {'sections': sections}: + return data | dict( + sections = [recursive_replace(section, do_replace) for section in sections], + ) + case {'Chapter': chapter}: + path_to_chapter = Path(chapter['path']) + chapter_content = chapter['content'] + + return data | dict( + Chapter = chapter | dict( + content = do_replace(path_to_chapter, chapter_content), + sub_items = [ + recursive_replace(sub_item, do_replace) + 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(): + + var = sys.argv[1] + + if len(sys.argv) > 2 and sys.argv[2] == '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) + config = context['config']['preprocessor'][var] + + if config['replace-kind'] == "relative-to-book-src": + kind = "relative to book sources" + + # book_root is the directory where book contents leave (ie, src/) + book_root = Path(context['root']) / context['config']['book']['src'] + replace = relative_to(var, book_root) + else: + assert False, "need a replace-kind" + + log(f'replacing all occurrences of @{var}@ with a path {kind}') + + # Find @var@ in all parts of our recursive book structure. + replaced_content = recursive_replace(book, replace) + + 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