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
This commit is contained in:
eldritch horrors 2024-04-08 21:44:40 +02:00
parent e6aab36d60
commit e691498eba
4 changed files with 98 additions and 87 deletions

View file

@ -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.

View file

@ -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

View file

@ -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 \

95
doc/manual/substitute.py Executable file
View file

@ -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