forked from mirrors/nixpkgs
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:
parent
56f1d99b16
commit
10a4f0daca
|
@ -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
|
||||
''}
|
||||
'';
|
||||
|
||||
}
|
||||
|
|
|
@ -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 ""
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
29
pkgs/tools/nix/nixos-render-docs/src/tests/test_manpage.py
Normal file
29
pkgs/tools/nix/nixos-render-docs/src/tests/test_manpage.py
Normal 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")
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue