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.
This commit is contained in:
pennae 2023-01-31 04:19:28 +01:00 committed by pennae
parent 56f1d99b16
commit 10a4f0daca
6 changed files with 505 additions and 22 deletions

View File

@ -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
''}
'';
}

View File

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

View File

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

View File

@ -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"<para><emphasis>{key.capitalize()}:</emphasis> {lit['text']}</para>" ]
@ -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)

View File

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

View File

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