From 10a4f0daca909e580df687426ced8e0d39056297 Mon Sep 17 00:00:00 2001 From: pennae Date: Tue, 31 Jan 2023 04:19:28 +0100 Subject: nixos-render-docs: add options manpage converter mdoc is just too slow to render on groff, and semantic markup doesn't help us any for generated pages. this produces a lot of changes to configuration.nix.5, but only few rendering changes. most of those seem to be place losing a space where docbook emitted roff code that did not faithfully represent the input text, though a few places also gained space where docbook dropped them. notably we also don't need the compatibility code docbook-xsl emitted because that problem was fixed over a decade ago. this will handle block quotes, which the docbook stylesheets turned into a mess of roff requests that ended up showing up in the output instead of being processed. --- nixos/doc/manual/default.nix | 35 ++- .../src/nixos_render_docs/manpage.py | 316 +++++++++++++++++++++ .../nixos-render-docs/src/nixos_render_docs/md.py | 4 +- .../src/nixos_render_docs/options.py | 141 ++++++++- .../nixos-render-docs/src/tests/test_manpage.py | 29 ++ .../nixos-render-docs/src/tests/test_options.py | 2 +- 6 files changed, 505 insertions(+), 22 deletions(-) create mode 100644 pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manpage.py create mode 100644 pkgs/tools/nix/nixos-render-docs/src/tests/test_manpage.py diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix index 342834e257a2a..9dab1738abedd 100644 --- a/nixos/doc/manual/default.nix +++ b/nixos/doc/manual/default.nix @@ -21,6 +21,8 @@ let withManOptDedupPatch = true; }; + manpageUrls = pkgs.path + "/doc/manpage-urls.json"; + # We need to strip references to /nix/store/* from options, # including any `extraSources` if some modules came from elsewhere, # or else the build will fail. @@ -72,7 +74,7 @@ let nativeBuildInputs = [ pkgs.nixos-render-docs ]; } '' nixos-render-docs manual docbook \ - --manpage-urls ${pkgs.path + "/doc/manpage-urls.json"} \ + --manpage-urls ${manpageUrls} \ "$out" \ --section \ --section-id modules \ @@ -255,9 +257,12 @@ in rec { manpages = runCommand "nixos-manpages" { inherit sources; nativeBuildInputs = [ + buildPackages.installShellFiles + ] ++ lib.optionals allowDocBook [ buildPackages.libxml2.bin buildPackages.libxslt.bin - buildPackages.installShellFiles + ] ++ lib.optionals (! allowDocBook) [ + buildPackages.nixos-render-docs ]; allowedReferences = ["out"]; } @@ -265,14 +270,24 @@ in rec { # Generate manpages. mkdir -p $out/share/man/man8 installManPage ${./manpages}/* - xsltproc --nonet \ - --maxdepth 6000 \ - --param man.output.in.separate.dir 1 \ - --param man.output.base.dir "'$out/share/man/'" \ - --param man.endnotes.are.numbered 0 \ - --param man.break.after.slash 1 \ - ${docbook_xsl_ns}/xml/xsl/docbook/manpages/docbook.xsl \ - ${manual-combined}/man-pages-combined.xml + ${if allowDocBook + then '' + xsltproc --nonet \ + --maxdepth 6000 \ + --param man.output.in.separate.dir 1 \ + --param man.output.base.dir "'$out/share/man/'" \ + --param man.endnotes.are.numbered 0 \ + --param man.break.after.slash 1 \ + ${docbook_xsl_ns}/xml/xsl/docbook/manpages/docbook.xsl \ + ${manual-combined}/man-pages-combined.xml + '' + else '' + mkdir -p $out/share/man/man5 + nixos-render-docs options manpage \ + --revision ${lib.escapeShellArg revision} \ + ${optionsJSON}/share/doc/nixos/options.json \ + $out/share/man/man5/configuration.nix.5 + ''} ''; } diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manpage.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manpage.py new file mode 100644 index 0000000000000..8188cfb9871b2 --- /dev/null +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manpage.py @@ -0,0 +1,316 @@ +from collections.abc import Mapping, MutableMapping, Sequence +from dataclasses import dataclass +from typing import Any, cast, Iterable, Optional + +import re + +import markdown_it +from markdown_it.token import Token +from markdown_it.utils import OptionsDict + +from .md import Renderer + +# roff(7) says: +# +# > roff documents may contain only graphable 7-bit ASCII characters, the space character, +# > and, in certain circumstances, the tab character. The backslash character ‘\’ indicates +# > the start of an escape sequence […] +# +# mandoc_char(7) says about the `'~^ characters: +# +# > In prose, this automatic substitution is often desirable; but when these characters have +# > to be displayed as plain ASCII characters, for example in source code samples, they require +# > escaping to render as follows: +# +# since we don't want these to be touched anywhere (because markdown will do all substituations +# we want to have) we'll escape those as well. we also escape " (macro metacharacter), - (might +# turn into a typographic hyphen), and . (roff request marker at SOL, changes spacing semantics +# at EOL). groff additionally does not allow unicode escapes for codepoints below U+0080, so +# those need "proper" roff escapes/replacements instead. +_roff_unicode = re.compile(r'''[^\n !#$%&()*+,\-./0-9:;<=>?@A-Z[\\\]_a-z{|}]''', re.ASCII) +_roff_escapes = { + ord('"'): "\\(dq", + ord("'"): "\\(aq", + ord('-'): "\\-", + ord('.'): "\\&.", + ord('\\'): "\\e", + ord('^'): "\\(ha", + ord('`'): "\\(ga", + ord('~'): "\\(ti", + ord('…'): "...", # TODO docbook compat, remove later +} +def man_escape(s: str) -> str: + s = s.translate(_roff_escapes) + return _roff_unicode.sub(lambda m: f"\\[u{ord(m[0]):04X}]", s) + +# remove leading and trailing spaces from links and condense multiple consecutive spaces +# into a single space for presentation parity with html. this is currently easiest with +# regex postprocessing and some marker characters. since we don't want to drop spaces +# from code blocks we will have to specially protect *inline* code (luckily not block code) +# so normalization can turn the spaces inside it into regular spaces again. +_normalize_space_re = re.compile(r'''\u0000 < *| *>\u0000 |(?<= ) +''') +def _normalize_space(s: str) -> str: + return _normalize_space_re.sub("", s).replace("\0p", " ") + +def _protect_spaces(s: str) -> str: + return s.replace(" ", "\0p") + +@dataclass(kw_only=True) +class List: + width: int + next_idx: Optional[int] = None + compact: bool + first_item_seen: bool = False + +# this renderer assumed that it produces a set of lines as output, and that those lines will +# be pasted as-is into a larger output. no prefixing or suffixing is allowed for correctness. +# +# NOTE that we output exclusively physical markup. this is because we have to use the older +# mandoc(7) format instead of the newer mdoc(7) format due to limitations in groff: while +# using mdoc in groff works fine it is not a native format and thus very slow to render on +# manpages as large as configuration.nix.5. mandoc(1) renders both really quickly, but with +# groff being our predominant manpage viewer we have to optimize for groff instead. +# +# while we do use only physical markup (adjusting indentation with .RS and .RE, adding +# vertical spacing with .sp, \f[BIRP] escapes for bold/italic/roman/$previous font, \h for +# horizontal motion in a line) we do attempt to copy the style of mdoc(7) semantic requests +# as appropriate for each markup element. +class ManpageRenderer(Renderer): + __output__ = "man" + + _href_targets: dict[str, str] + + _do_parbreak_stack: list[bool] + _list_stack: list[List] + _font_stack: list[str] + + def __init__(self, manpage_urls: Mapping[str, str], href_targets: dict[str, str], + parser: Optional[markdown_it.MarkdownIt] = None): + super().__init__(manpage_urls, parser) + self._href_targets = href_targets + self._do_parbreak_stack = [] + self._list_stack = [] + self._font_stack = [] + + def _join_block(self, ls: Iterable[str]) -> str: + return "\n".join([ l for l in ls if len(l) ]) + def _join_inline(self, ls: Iterable[str]) -> str: + return _normalize_space(super()._join_inline(ls)) + + def _enter_block(self) -> None: + self._do_parbreak_stack.append(False) + def _leave_block(self) -> None: + self._do_parbreak_stack.pop() + self._do_parbreak_stack[-1] = True + def _maybe_parbreak(self, suffix: str = "") -> str: + result = f".sp{suffix}" if self._do_parbreak_stack[-1] else "" + self._do_parbreak_stack[-1] = True + return result + + def _admonition_open(self, kind: str) -> str: + self._enter_block() + return ( + '.sp\n' + '.RS 4\n' + f'\\fB{kind}\\fP\n' + '.br' + ) + def _admonition_close(self) -> str: + self._leave_block() + return ".RE" + + def render(self, tokens: Sequence[Token], options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._do_parbreak_stack = [ False ] + self._font_stack = [ "\\fR" ] + return super().render(tokens, options, env) + + def text(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return man_escape(token.content) + def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return self._maybe_parbreak() + def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return "" + def hardbreak(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return ".br" + def softbreak(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return " " + def code_inline(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return _protect_spaces(man_escape(token.content)) + def code_block(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return self.fence(token, tokens, i, options, env) + def link_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + href = cast(str, token.attrs['href']) + (text, font) = ("", "\\fB") + if tokens[i + 1].type == 'link_close' and href in self._href_targets: + # TODO error or warning if the target can't be resolved + text = self._href_targets[href] + elif href in self._href_targets: + font = "\\fR" # TODO docbook renders these links differently for some reason + self._font_stack.append(font) + return f"{font}{text}\0 <" + def link_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._font_stack.pop() + return f">\0 {self._font_stack[-1]}" + def list_item_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._enter_block() + lst = self._list_stack[-1] + maybe_space = '' if not lst.first_item_seen else '.sp\n' + lst.first_item_seen = True + head = "•" + if lst.next_idx is not None: + head = f" {lst.next_idx}." + lst.next_idx += 1 + return ( + f'{maybe_space}' + f'.RS {lst.width}\n' + f"\\h'-{lst.width}'{man_escape(head)}\\h'{lst.width - len(head)}'\\c" + ) + def list_item_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._leave_block() + return ".RE" + def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._list_stack.append(List(width=4, compact=False)) + return self._maybe_parbreak() + def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._list_stack.pop() + return "" + def em_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._font_stack.append("\\fI") + return "\\fI" + def em_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._font_stack.pop() + return self._font_stack[-1] + def strong_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._font_stack.append("\\fB") + return "\\fB" + def strong_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._font_stack.pop() + return self._font_stack[-1] + def fence(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + s = man_escape(token.content).rstrip('\n') + return ( + '.sp\n' + '.RS 4\n' + '.nf\n' + f'{s}\n' + '.fi\n' + '.RE' + ) + def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + maybe_par = self._maybe_parbreak("\n") + self._enter_block() + return ( + f"{maybe_par}" + ".RS 4\n" + f"\\h'-3'\\fI\\(lq\\(rq\\fP\\h'1'\\c" + ) + def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._leave_block() + return ".RE" + def note_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return self._admonition_open("Note") + def note_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return self._admonition_close() + def caution_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return self._admonition_open( "Caution") + def caution_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return self._admonition_close() + def important_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return self._admonition_open( "Important") + def important_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return self._admonition_close() + def tip_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return self._admonition_open( "Tip") + def tip_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return self._admonition_close() + def warning_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return self._admonition_open( "Warning") + def warning_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return self._admonition_close() + def dl_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return "" + def dl_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return "" + def dt_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return ".PP" + def dt_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return "" + def dd_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._enter_block() + return ".RS 4" + def dd_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._leave_block() + return ".RE" + def myst_role(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + if token.meta['name'] in [ 'command', 'env', 'option' ]: + return f'\\fB{man_escape(token.content)}\\fP' + elif token.meta['name'] == 'file': + return f'{man_escape(token.content)}' + elif token.meta['name'] == 'var': + return f'\\fI{man_escape(token.content)}\\fP' + elif token.meta['name'] == 'manpage': + [page, section] = [ s.strip() for s in token.content.rsplit('(', 1) ] + section = section[:-1] + return f'\\fB{man_escape(page)}\\fP\\fR({man_escape(section)})\\fP' + else: + raise NotImplementedError("md node not supported yet", token) + def inline_anchor(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + return "" # mdoc knows no anchors + def heading_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + raise RuntimeError("md token not supported in manpages", token) + def heading_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + raise RuntimeError("md token not supported in manpages", token) + def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + # max item head width for a number, a dot, and one leading space and one trailing space + width = 3 + len(str(cast(int, token.meta['end']))) + self._list_stack.append( + List(width = width, + next_idx = cast(int, token.attrs.get('start', 1)), + compact = bool(token.meta['compact']))) + return self._maybe_parbreak() + def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, + env: MutableMapping[str, Any]) -> str: + self._list_stack.pop() + return "" diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py index 16e473e97adeb..5bc16e65933c8 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py @@ -391,7 +391,7 @@ class Converter(ABC): tokens = self._md.parse(src, env if env is not None else {}) return self._post_parse(tokens) - def _render(self, src: str) -> str: - env: dict[str, Any] = {} + def _render(self, src: str, env: Optional[MutableMapping[str, Any]] = None) -> str: + env = {} if env is None else env tokens = self._parse(src, env) return self._md.renderer.render(tokens, self._md.options, env) # type: ignore[no-any-return] diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/options.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/options.py index 9603b5726897b..364fb6dc2c3a8 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/options.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/options.py @@ -2,13 +2,16 @@ import argparse import json from abc import abstractmethod -from collections.abc import MutableMapping, Sequence +from collections.abc import Mapping, MutableMapping, Sequence from markdown_it.utils import OptionsDict from markdown_it.token import Token from typing import Any, Optional from xml.sax.saxutils import escape, quoteattr +import markdown_it + from .docbook import DocBookRenderer, make_xml_id +from .manpage import ManpageRenderer, man_escape from .md import Converter, md_escape from .types import OptionLoc, Option, RenderedOption @@ -28,16 +31,10 @@ class BaseConverter(Converter): def __init__(self, manpage_urls: dict[str, str], revision: str, - document_type: str, - varlist_id: str, - id_prefix: str, markdown_by_default: bool): super().__init__(manpage_urls) self._options = {} self._revision = revision - self._document_type = document_type - self._varlist_id = varlist_id - self._id_prefix = id_prefix self._markdown_by_default = markdown_by_default def _sorted_options(self) -> list[tuple[str, RenderedOption]]: @@ -183,6 +180,17 @@ class DocBookConverter(BaseConverter): __renderer__ = OptionsDocBookRenderer __option_block_separator__ = "" + def __init__(self, manpage_urls: dict[str, str], + revision: str, + markdown_by_default: bool, + document_type: str, + varlist_id: str, + id_prefix: str): + super().__init__(manpage_urls, revision, markdown_by_default) + self._document_type = document_type + self._varlist_id = varlist_id + self._id_prefix = id_prefix + def _render_code(self, option: dict[str, Any], key: str) -> list[str]: if lit := option_is(option, key, 'literalDocBook'): return [ f"{key.capitalize()}: {lit['text']}" ] @@ -258,6 +266,101 @@ class DocBookConverter(BaseConverter): return "\n".join(result) +class OptionsManpageRenderer(ManpageRenderer): + pass + +class ManpageConverter(BaseConverter): + def __renderer__(self, manpage_urls: Mapping[str, str], + parser: Optional[markdown_it.MarkdownIt] = None) -> OptionsManpageRenderer: + return OptionsManpageRenderer(manpage_urls, self._options_by_id, parser) + + __option_block_separator__ = ".sp" + + _options_by_id: dict[str, str] + + def __init__(self, revision: str, markdown_by_default: bool): + self._options_by_id = {} + super().__init__({}, revision, markdown_by_default) + + def add_options(self, options: dict[str, Any]) -> None: + for (k, v) in options.items(): + self._options_by_id[f'#{make_xml_id(f"opt-{k}")}'] = k + return super().add_options(options) + + def _render_code(self, option: dict[str, Any], key: str) -> list[str]: + if lit := option_is(option, key, 'literalDocBook'): + raise RuntimeError("can't render manpages in the presence of docbook") + else: + return super()._render_code(option, key) + + def _render_description(self, desc: str | dict[str, Any]) -> list[str]: + if isinstance(desc, str) and not self._markdown_by_default: + raise RuntimeError("can't render manpages in the presence of docbook") + else: + return super()._render_description(desc) + + def _related_packages_header(self) -> list[str]: + return [ + '\\fIRelated packages:\\fP', + '.sp', + ] + + def _decl_def_header(self, header: str) -> list[str]: + return [ + f'\\fI{man_escape(header)}:\\fP', + ] + + def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]: + return [ + '.RS 4', + f'\\fB{man_escape(name)}\\fP', + '.RE' + ] + + def _decl_def_footer(self) -> list[str]: + return [] + + def finalize(self) -> str: + result = [] + + result += [ + r'''.TH "CONFIGURATION\&.NIX" "5" "01/01/1980" "NixOS" "NixOS Reference Pages"''', + r'''.\" disable hyphenation''', + r'''.nh''', + r'''.\" disable justification (adjust text to left margin only)''', + r'''.ad l''', + r'''.\" enable line breaks after slashes''', + r'''.cflags 4 /''', + r'''.SH "NAME"''', + self._render('{file}`configuration.nix` - NixOS system configuration specification'), + r'''.SH "DESCRIPTION"''', + r'''.PP''', + self._render('The file {file}`/etc/nixos/configuration.nix` contains the ' + 'declarative specification of your NixOS system configuration. ' + 'The command {command}`nixos-rebuild` takes this file and ' + 'realises the system configuration specified therein.'), + r'''.SH "OPTIONS"''', + r'''.PP''', + self._render('You can use the following options in {file}`configuration.nix`.'), + ] + + for (name, opt) in self._sorted_options(): + result += [ + ".PP", + f"\\fB{man_escape(name)}\\fR", + ".RS 4", + ] + result += opt.lines + result.append(".RE") + + result += [ + r'''.SH "AUTHORS"''', + r'''.PP''', + r'''Eelco Dolstra and the Nixpkgs/NixOS contributors''', + ] + + return "\n".join(result) + def _build_cli_db(p: argparse.ArgumentParser) -> None: p.add_argument('--manpage-urls', required=True) p.add_argument('--revision', required=True) @@ -268,27 +371,47 @@ def _build_cli_db(p: argparse.ArgumentParser) -> None: p.add_argument("infile") p.add_argument("outfile") +def _build_cli_manpage(p: argparse.ArgumentParser) -> None: + p.add_argument('--revision', required=True) + p.add_argument("infile") + p.add_argument("outfile") + def _run_cli_db(args: argparse.Namespace) -> None: with open(args.manpage_urls, 'r') as manpage_urls: md = DocBookConverter( json.load(manpage_urls), revision = args.revision, + markdown_by_default = args.markdown_by_default, document_type = args.document_type, varlist_id = args.varlist_id, - id_prefix = args.id_prefix, - markdown_by_default = args.markdown_by_default) + id_prefix = args.id_prefix) with open(args.infile, 'r') as f: md.add_options(json.load(f)) with open(args.outfile, 'w') as f: f.write(md.finalize()) +def _run_cli_manpage(args: argparse.Namespace) -> None: + md = ManpageConverter( + revision = args.revision, + # manpage rendering only works if there's no docbook, so we can + # also set markdown_by_default with no ill effects. + markdown_by_default = True) + + with open(args.infile, 'r') as f: + md.add_options(json.load(f)) + with open(args.outfile, 'w') as f: + f.write(md.finalize()) + def build_cli(p: argparse.ArgumentParser) -> None: formats = p.add_subparsers(dest='format', required=True) _build_cli_db(formats.add_parser('docbook')) + _build_cli_manpage(formats.add_parser('manpage')) def run_cli(args: argparse.Namespace) -> None: if args.format == 'docbook': _run_cli_db(args) + elif args.format == 'manpage': + _run_cli_manpage(args) else: raise RuntimeError('format not hooked up', args) diff --git a/pkgs/tools/nix/nixos-render-docs/src/tests/test_manpage.py b/pkgs/tools/nix/nixos-render-docs/src/tests/test_manpage.py new file mode 100644 index 0000000000000..0ccd33f1be418 --- /dev/null +++ b/pkgs/tools/nix/nixos-render-docs/src/tests/test_manpage.py @@ -0,0 +1,29 @@ +import nixos_render_docs + +from typing import Mapping, Optional + +import markdown_it + +class Converter(nixos_render_docs.md.Converter): + def __renderer__(self, manpage_urls: Mapping[str, str], + parser: Optional[markdown_it.MarkdownIt] = None + ) -> nixos_render_docs.manpage.ManpageRenderer: + return nixos_render_docs.manpage.ManpageRenderer(manpage_urls, self.options_by_id, parser) + + def __init__(self, manpage_urls: Mapping[str, str], options_by_id: dict[str, str] = {}): + self.options_by_id = options_by_id + super().__init__(manpage_urls) + +def test_inline_code() -> None: + c = Converter({}) + assert c._render("1 `x a x` 2") == "1 x a x 2" + +def test_fonts() -> None: + c = Converter({}) + assert c._render("*a **b** c*") == "\\fIa \\fBb\\fI c\\fR" + assert c._render("*a [1 `2`](3) c*") == "\\fIa \\fB1 2\\fI c\\fR" + +def test_expand_link_targets() -> None: + c = Converter({}, { '#foo1': "bar", "#foo2": "bar" }) + assert (c._render("[a](#foo1) [](#foo2) [b](#bar1) [](#bar2)") == + "\\fRa\\fR \\fBbar\\fR \\fBb\\fR \\fB\\fR") diff --git a/pkgs/tools/nix/nixos-render-docs/src/tests/test_options.py b/pkgs/tools/nix/nixos-render-docs/src/tests/test_options.py index 5a02fabde0fbc..9608ed6392188 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/tests/test_options.py +++ b/pkgs/tools/nix/nixos-render-docs/src/tests/test_options.py @@ -4,7 +4,7 @@ from markdown_it.token import Token import pytest def test_option_headings() -> None: - c = nixos_render_docs.options.DocBookConverter({}, 'local', 'none', 'vars', 'opt-', False) + c = nixos_render_docs.options.DocBookConverter({}, 'local', False, 'none', 'vars', 'opt-') with pytest.raises(RuntimeError) as exc: c._render("# foo") assert exc.value.args[0] == 'md token not supported in options doc' -- cgit 1.4.1