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