diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix index 9f0998089787..8f05ff27749b 100644 --- a/nixos/doc/manual/default.nix +++ b/nixos/doc/manual/default.nix @@ -68,7 +68,30 @@ let optionIdPrefix = "test-opt-"; }; - sources = lib.sourceFilesBySuffices ./. [".xml"]; + sources = runCommand "manual-sources" { + inputs = lib.sourceFilesBySuffices ./. [ ".xml" ".md" ]; + nativeBuildInputs = [ pkgs.nixos-render-docs ]; + } '' + mkdir $out + cd $out + cp -r --no-preserve=all $inputs/* . + rm -rf from_md + + declare -a convert_args + while read -r mf; do + if [[ "$mf" = *.chapter.md ]]; then + convert_args+=("--chapter") + else + convert_args+=("--section") + fi + + convert_args+=("from_md/''${mf%.md}.xml" "$mf") + done < <(find . -type f -name '*.md') + + nixos-render-docs manual docbook-fragment \ + --manpage-urls ${manpageUrls} \ + "''${convert_args[@]}" + ''; modulesDoc = runCommand "modules.xml" { nativeBuildInputs = [ pkgs.nixos-render-docs ]; diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py index 8bb108d8c251..ab61d699d7f5 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py @@ -76,6 +76,10 @@ class ManualDocBookRenderer(DocBookRenderer): return f"\n{escape(token.content)}" def fence(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, env: MutableMapping[str, Any]) -> str: + # HACK for temporarily being able to replace md-to-db.sh. pandoc used this syntax to + # allow md files to inject arbitrary docbook, and manual chapters use it. + if token.info == '{=docbook}': + return token.content info = f" language={quoteattr(token.info)}" if token.info != "" else "" return f"\n{escape(token.content)}" @@ -93,6 +97,29 @@ class DocBookSectionConverter(BaseConverter): return "\n".join(result) +class ManualFragmentDocBookRenderer(ManualDocBookRenderer): + _tag: str = "chapter" + + def _heading_tag(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> tuple[str, dict[str, str]]: + (tag, attrs) = super()._heading_tag(token, tokens, i, options, env) + if token.tag == 'h1': + return (self._tag, attrs | { 'xmlns:xi': "http://www.w3.org/2001/XInclude" }) + return (tag, attrs) + +class DocBookFragmentConverter(Converter): + __renderer__ = ManualFragmentDocBookRenderer + + def convert(self, file: Path, tag: str) -> str: + assert isinstance(self._md.renderer, ManualFragmentDocBookRenderer) + try: + with open(file, 'r') as f: + self._md.renderer._title_seen = False + self._md.renderer._tag = tag + return self._render(f.read()) + except Exception as e: + raise RuntimeError(f"failed to render manual {tag} {file}") from e + class Section: @@ -124,6 +151,14 @@ class ChaptersAction(argparse.Action): if sections is None: raise argparse.ArgumentError(self, "no active section") sections[-1].chapters.extend(map(Path, cast(Sequence[str], values))) +class SingleFileAction(argparse.Action): + def __call__(self, parser: argparse.ArgumentParser, ns: argparse.Namespace, + values: Union[str, Sequence[Any], None], opt_str: Optional[str] = None) -> None: + assert isinstance(values, Sequence) + chapters = getattr(ns, self.dest) or [] + chapters.append((Path(values[0]), Path(values[1]))) + setattr(ns, self.dest, chapters) + def _build_cli_db_section(p: argparse.ArgumentParser) -> None: p.add_argument('--manpage-urls', required=True) p.add_argument("outfile") @@ -131,6 +166,11 @@ def _build_cli_db_section(p: argparse.ArgumentParser) -> None: p.add_argument("--section-id", dest="contents", action=SectionIDAction) p.add_argument("--chapters", dest="contents", action=ChaptersAction, nargs='+') +def _build_cli_db_fragment(p: argparse.ArgumentParser) -> None: + p.add_argument('--manpage-urls', required=True) + p.add_argument("--chapter", action=SingleFileAction, required=True, nargs=2) + p.add_argument("--section", action=SingleFileAction, required=True, nargs=2) + def _run_cli_db_section(args: argparse.Namespace) -> None: with open(args.manpage_urls, 'r') as manpage_urls: md = DocBookSectionConverter(json.load(manpage_urls)) @@ -139,12 +179,24 @@ def _run_cli_db_section(args: argparse.Namespace) -> None: with open(args.outfile, 'w') as f: f.write(md.finalize()) +def _run_cli_db_fragment(args: argparse.Namespace) -> None: + with open(args.manpage_urls, 'r') as manpage_urls: + md = DocBookFragmentConverter(json.load(manpage_urls)) + for kind in [ 'chapter', 'section' ]: + for (target, file) in getattr(args, kind): + converted = md.convert(file, kind) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(converted) + def build_cli(p: argparse.ArgumentParser) -> None: formats = p.add_subparsers(dest='format', required=True) _build_cli_db_section(formats.add_parser('docbook-section')) + _build_cli_db_fragment(formats.add_parser('docbook-fragment')) def run_cli(args: argparse.Namespace) -> None: if args.format == 'docbook-section': _run_cli_db_section(args) + elif args.format == 'docbook-fragment': + _run_cli_db_fragment(args) else: raise RuntimeError('format not hooked up', args)