"
+
def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return "
"
+
def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return "/doc/README.md (with reverse reference)
- if token.meta['name'] == 'command':
- return f'{escape(token.content)}'
- if token.meta['name'] == 'file':
+ if token.meta["name"] == "command":
+ return (
+ f'{escape(token.content)}'
+ )
+ if token.meta["name"] == "file":
return f'{escape(token.content)}'
- if token.meta['name'] == 'var':
+ if token.meta["name"] == "var":
return f'{escape(token.content)}'
- if token.meta['name'] == 'env':
+ if token.meta["name"] == "env":
return f'{escape(token.content)}'
- if token.meta['name'] == 'option':
+ if token.meta["name"] == "option":
return f'{escape(token.content)}'
- if token.meta['name'] == 'manpage':
- [page, section] = [ s.strip() for s in token.content.rsplit('(', 1) ]
+ if token.meta["name"] == "manpage":
+ [page, section] = [s.strip() for s in token.content.rsplit("(", 1)]
section = section[:-1]
man = f"{page}({section})"
title = f'{escape(page)}'
@@ -159,14 +205,15 @@ def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str:
else:
return ref
return super().myst_role(token, tokens, i)
+
def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str:
# we currently support *only* inline anchors and the special .keycap class to produce
# keycap-styled spans.
(id_part, class_part) = ("", "")
- if s := token.attrs.get('id'):
+ if s := token.attrs.get("id"):
id_part = f''
- if s := token.attrs.get('class'):
- if s == 'keycap':
+ if s := token.attrs.get("class"):
+ if s == "keycap":
class_part = ''
self._attrspans.append("")
else:
@@ -174,158 +221,179 @@ def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str:
else:
self._attrspans.append("")
return id_part + class_part
+
def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return self._attrspans.pop()
+
def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
hlevel = int(token.tag[1:])
htag, hstyle = self._make_hN(hlevel)
if hstyle:
hstyle = f'style="{escape(hstyle, True)}"'
- if anchor := cast(str, token.attrs.get('id', '')):
+ if anchor := cast(str, token.attrs.get("id", "")):
anchor = f'id="{escape(anchor, True)}"'
result = self._close_headings(hlevel)
tag = self._heading_tag(token, tokens, i)
toc_fragment = self._build_toc(tokens, i)
- self._headings.append(Heading(tag, hlevel, htag, tag != 'part', toc_fragment))
+ self._headings.append(Heading(tag, hlevel, htag, tag != "part", toc_fragment))
return (
- f'{result}'
+ f"{result}"
f''
f'
'
- f'
'
- f'
'
+ f"
"
+ f"
"
f' <{htag} {anchor} class="title" {hstyle}>'
)
+
def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
heading = self._headings[-1]
- result = (
- f' {heading.html_tag}>'
- f'
'
- f'
'
- f'
'
- )
- if heading.container_tag == 'part':
+ result = f" {heading.html_tag}>
"
+ if heading.container_tag == "part":
result += ''
else:
result += heading.toc_fragment
return result
+
def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- extra = 'compact' if token.meta.get('compact', False) else ''
- start = f'start="{token.attrs["start"]}"' if 'start' in token.attrs else ""
- style = _ordered_list_styles[self._ordered_list_nesting % len(_ordered_list_styles)]
+ extra = "compact" if token.meta.get("compact", False) else ""
+ start = f'start="{token.attrs["start"]}"' if "start" in token.attrs else ""
+ style = _ordered_list_styles[
+ self._ordered_list_nesting % len(_ordered_list_styles)
+ ]
self._ordered_list_nesting += 1
return f'
"
+
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- if id := cast(str, token.attrs.get('id', '')):
- id = f'id="{escape(id, True)}"' if id else ''
+ if id := cast(str, token.attrs.get("id", "")):
+ id = f'id="{escape(id, True)}"' if id else ""
return f'
'
+
def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return '
'
+
def example_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ''
+
def example_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ''
+
def image(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- src = self._pull_image(cast(str, token.attrs['src']))
+ src = self._pull_image(cast(str, token.attrs["src"]))
alt = f'alt="{escape(token.content, True)}"' if token.content else ""
- if title := cast(str, token.attrs.get('title', '')):
+ if title := cast(str, token.attrs.get("title", "")):
title = f'title="{escape(title, True)}"'
return (
'
'
+ "
"
)
+
def figure_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- if anchor := cast(str, token.attrs.get('id', '')):
+ if anchor := cast(str, token.attrs.get("id", "")):
anchor = f''
return f'{anchor}'
+
def figure_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- return (
- '
'
- '
'
- )
+ return '
'
+
def figure_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- return (
- ''
- ' '
- )
+ return ' '
+
def figure_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- return (
- ' '
- '
'
- ''
- )
+ return '
'
+
def table_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- return (
- '
"
+
def thead_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
cols = []
for j in range(i + 1, len(tokens)):
- if tokens[j].type == 'thead_close':
+ if tokens[j].type == "thead_close":
break
- elif tokens[j].type == 'th_open':
- cols.append(cast(str, tokens[j].attrs.get('style', 'left')).removeprefix('text-align:'))
- return "".join([
- "
",
- "".join([ f'' for col in cols ]),
- "",
- "
",
- ])
+ elif tokens[j].type == "th_open":
+ cols.append(
+ cast(str, tokens[j].attrs.get("style", "left")).removeprefix(
+ "text-align:"
+ )
+ )
+ return "".join(
+ [
+ "",
+ "".join([f'' for col in cols]),
+ "",
+ "",
+ ]
+ )
+
def thead_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ""
+
def tr_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ""
+
def tr_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return "
"
+
def th_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return f''
+
def th_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return " | "
+
def tbody_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ""
+
def tbody_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ""
+
def td_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return f'
'
+
def td_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return " | "
+
def footnote_ref(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- href = self._xref_targets[token.meta['target']].href()
+ href = self._xref_targets[token.meta["target"]].href()
id = escape(cast(str, token.attrs["id"]), True)
return (
f''
+ ""
)
+
def footnote_block_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return (
'"
+
def footnote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
# meta id,label
id = escape(self._xref_targets[token.meta["label"]].id, True)
return f'"
+
def footnote_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- href = self._xref_targets[token.meta['target']].href()
+ href = self._xref_targets[token.meta["target"]].href()
return (
f'
'
f'[{token.meta["id"] + 1}]'
- ''
+ ""
)
def _make_hN(self, level: int) -> tuple[str, str]:
@@ -334,14 +402,16 @@ def _make_hN(self, level: int) -> tuple[str, str]:
def _maybe_close_partintro(self) -> str:
if self._headings:
heading = self._headings[-1]
- if heading.container_tag == 'part' and not heading.partintro_closed:
+ if heading.container_tag == "part" and not heading.partintro_closed:
self._headings[-1] = heading._replace(partintro_closed=True)
return heading.toc_fragment + "
"
return ""
def _close_headings(self, level: Optional[int]) -> str:
result = []
- while len(self._headings) and (level is None or self._headings[-1].level >= level):
+ while len(self._headings) and (
+ level is None or self._headings[-1].level >= level
+ ):
result.append(self._maybe_close_partintro())
result.append("
")
self._headings.pop()
@@ -349,5 +419,6 @@ def _close_headings(self, level: Optional[int]) -> str:
def _heading_tag(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return "section"
+
def _build_toc(self, tokens: Sequence[Token], i: int) -> str:
return ""
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manpage.py b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manpage.py
index a3d6e791cabdf..9112a4f80b052 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manpage.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manpage.py
@@ -25,33 +25,42 @@
# 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_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("-"): "\\-",
+ ord("."): "\\&.",
+ ord("\\"): "\\e",
+ ord("^"): "\\(ha",
+ ord("`"): "\\(ga",
+ ord("~"): "\\(ti",
}
+
+
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 |(?<= ) +''')
+_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
@@ -59,6 +68,7 @@ class List:
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.
#
@@ -95,15 +105,18 @@ def __init__(self, manpage_urls: Mapping[str, str], href_targets: dict[str, str]
self._font_stack = []
def _join_block(self, ls: Iterable[str]) -> str:
- return "\n".join([ l for l in ls if len(l) ])
+ 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
@@ -111,45 +124,49 @@ def _maybe_parbreak(self, suffix: str = "") -> str:
def _admonition_open(self, kind: str) -> str:
self._enter_block()
- return (
- '.sp\n'
- '.RS 4\n'
- f'\\fB{kind}\\fP\n'
- '.br'
- )
+ return f".sp\n.RS 4\n\\fB{kind}\\fP\n.br"
+
def _admonition_close(self) -> str:
self._leave_block()
return ".RE"
def render(self, tokens: Sequence[Token]) -> str:
- self._do_parbreak_stack = [ False ]
- self._font_stack = [ "\\fR" ]
+ self._do_parbreak_stack = [False]
+ self._font_stack = ["\\fR"]
return super().render(tokens)
def text(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return man_escape(token.content)
+
def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return self._maybe_parbreak()
+
def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ""
+
def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ".br"
+
def softbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return " "
+
def code_inline(self, token: Token, tokens: Sequence[Token], i: int) -> str:
s = _protect_spaces(man_escape(token.content))
return f"\\fR\\(oq{s}\\(cq\\fP" if self.inline_code_is_quoted else s
+
def code_block(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return self.fence(token, tokens, i)
+
def link_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- href = cast(str, token.attrs['href'])
+ href = cast(str, token.attrs["href"])
self._link_stack.append(href)
text = ""
- if tokens[i + 1].type == 'link_close' and href in self._href_targets:
+ 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]
self._font_stack.append("\\fB")
return f"\\fB{text}\0 <"
+
def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
href = self._link_stack.pop()
text = ""
@@ -162,127 +179,153 @@ def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
text = "\\fR" + man_escape(f"[{idx}]")
self._font_stack.pop()
return f">\0 {text}{self._font_stack[-1]}"
+
def list_item_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
self._enter_block()
lst = self._list_stack[-1]
- maybe_space = '' if lst.compact or not lst.first_item_seen else '.sp\n'
+ maybe_space = "" if lst.compact or 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"{maybe_space}"
+ f".RS {lst.width}\n"
f"\\h'-{len(head) + 1}'\\fB{man_escape(head)}\\fP\\h'1'\\c"
)
+
def list_item_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
self._leave_block()
return ".RE"
+
def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- self._list_stack.append(List(width=4, compact=bool(token.meta['compact'])))
+ self._list_stack.append(List(width=4, compact=bool(token.meta["compact"])))
return self._maybe_parbreak()
+
def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
self._list_stack.pop()
return ""
+
def em_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
self._font_stack.append("\\fI")
return "\\fI"
+
def em_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
self._font_stack.pop()
return self._font_stack[-1]
+
def strong_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
self._font_stack.append("\\fB")
return "\\fB"
+
def strong_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
self._font_stack.pop()
return self._font_stack[-1]
+
def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- s = man_escape(token.content).rstrip('\n')
- return (
- '.sp\n'
- '.RS 4\n'
- '.nf\n'
- f'{s}\n'
- '.fi\n'
- '.RE'
- )
+ s = man_escape(token.content).rstrip("\n")
+ return f".sp\n.RS 4\n.nf\n{s}\n.fi\n.RE"
+
def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int) -> 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"
- )
+ return f"{maybe_par}.RS 4\n\\h'-3'\\fI\\(lq\\(rq\\fP\\h'1'\\c"
+
def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
self._leave_block()
return ".RE"
+
def note_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return self._admonition_open("Note")
+
def note_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return self._admonition_close()
+
def caution_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- return self._admonition_open( "Caution")
+ return self._admonition_open("Caution")
+
def caution_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return self._admonition_close()
+
def important_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- return self._admonition_open( "Important")
+ return self._admonition_open("Important")
+
def important_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return self._admonition_close()
+
def tip_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- return self._admonition_open( "Tip")
+ return self._admonition_open("Tip")
+
def tip_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return self._admonition_close()
+
def warning_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- return self._admonition_open( "Warning")
+ return self._admonition_open("Warning")
+
def warning_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return self._admonition_close()
+
def dl_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ".RS 4"
+
def dl_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ".RE"
+
def dt_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ".PP"
+
def dt_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ""
+
def dd_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
self._enter_block()
return ".RS 4"
+
def dd_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
self._leave_block()
return ".RE"
+
def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str:
# NixOS-specific roles are documented at /doc/README.md (with reverse reference)
- if token.meta['name'] in [ 'command', 'env', 'option' ]:
- return f'\\fB{man_escape(token.content)}\\fP'
- elif token.meta['name'] in [ 'file', '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) ]
+ if token.meta["name"] in ["command", "env", "option"]:
+ return f"\\fB{man_escape(token.content)}\\fP"
+ elif token.meta["name"] in ["file", "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'
+ return f"\\fB{man_escape(page)}\\fP\\fR({man_escape(section)})\\fP"
else:
raise NotImplementedError("md node not supported yet", token)
+
def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str:
# mdoc knows no anchors so we can drop those, but classes must be rejected.
- if 'class' in token.attrs:
+ if "class" in token.attrs:
return super().attr_span_begin(token, tokens, i)
return ""
+
def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return ""
+
def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported in manpages", token)
+
def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported in manpages", token)
+
def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> 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'])))
+ 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'])))
+ 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) -> str:
self._list_stack.pop()
return ""
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manual.py b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manual.py
index 7dbb78e075880..5c71542c52b78 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manual.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manual.py
@@ -14,11 +14,20 @@
from . import md, options
from .html import HTMLRenderer, UnresolvedXrefError
-from .manual_structure import check_structure, FragmentType, is_include, make_xml_id, TocEntry, TocEntryType, XrefTarget
+from .manual_structure import (
+ check_structure,
+ FragmentType,
+ is_include,
+ make_xml_id,
+ TocEntry,
+ TocEntryType,
+ XrefTarget,
+)
from .md import Converter, Renderer
from .redirects import Redirects
from .src_error import SrcError
+
class BaseConverter(Converter[md.TR], Generic[md.TR]):
# per-converter configuration for ns:arg=value arguments to include blocks, following
# the include type. html converters need something like this to support chunking, or
@@ -32,8 +41,8 @@ class BaseConverter(Converter[md.TR], Generic[md.TR]):
_current_type: list[TocEntryType]
def convert(self, infile: Path, outfile: Path) -> None:
- self._base_paths = [ infile ]
- self._current_type = ['book']
+ self._base_paths = [infile]
+ self._current_type = ["book"]
try:
tokens = self._parse(infile.read_text())
self._postprocess(infile, outfile, tokens)
@@ -42,10 +51,14 @@ def convert(self, infile: Path, outfile: Path) -> None:
except Exception as e:
raise RuntimeError(f"failed to render manual {infile}") from e
- def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) -> None:
+ def _postprocess(
+ self, infile: Path, outfile: Path, tokens: Sequence[Token]
+ ) -> None:
pass
- def _handle_headings(self, tokens: list[Token], *, src: str, on_heading: Callable[[Token,str],None]) -> None:
+ def _handle_headings(
+ self, tokens: list[Token], *, src: str, on_heading: Callable[[Token, str], None]
+ ) -> None:
# Headings in a globally numbered order
# h1 to h6
curr_heading_pos: list[int] = []
@@ -62,27 +75,26 @@ def _handle_headings(self, tokens: list[Token], *, src: str, on_heading: Callabl
if idx >= len(curr_heading_pos):
# extend the list if necessary
- curr_heading_pos.extend([0 for _i in range(idx+1 - len(curr_heading_pos))])
+ curr_heading_pos.extend(
+ [0 for _i in range(idx + 1 - len(curr_heading_pos))]
+ )
- curr_heading_pos = curr_heading_pos[:idx+1]
+ curr_heading_pos = curr_heading_pos[: idx + 1]
curr_heading_pos[-1] += 1
-
ident = ".".join(f"{a}" for a in curr_heading_pos)
- on_heading(token,ident)
-
-
+ on_heading(token, ident)
def _parse(self, src: str, *, auto_id_prefix: None | str = None) -> list[Token]:
tokens = super()._parse(src)
if auto_id_prefix:
+
def set_token_ident(token: Token, ident: str) -> None:
if "id" not in token.attrs:
token.attrs["id"] = f"{auto_id_prefix}-{ident}"
self._handle_headings(tokens, src=src, on_heading=set_token_ident)
-
check_structure(src, self._current_type[-1], tokens)
for token in tokens:
if not is_include(token):
@@ -90,14 +102,18 @@ def set_token_ident(token: Token, ident: str) -> None:
directive = token.info[12:].split()
if not directive:
continue
- args = { k: v for k, _sep, v in map(lambda s: s.partition('='), directive[1:]) }
+ args = {
+ k: v for k, _sep, v in map(lambda s: s.partition("="), directive[1:])
+ }
typ = directive[0]
- if typ == 'options':
- token.type = 'included_options'
- self._process_include_args(src, token, args, self.INCLUDE_OPTIONS_ALLOWED_ARGS)
+ if typ == "options":
+ token.type = "included_options"
+ self._process_include_args(
+ src, token, args, self.INCLUDE_OPTIONS_ALLOWED_ARGS
+ )
self._parse_options(src, token, args)
else:
- fragment_type = typ.removesuffix('s')
+ fragment_type = typ.removesuffix("s")
if fragment_type not in get_args(FragmentType):
raise SrcError(
src=src,
@@ -105,27 +121,33 @@ def set_token_ident(token: Token, ident: str) -> None:
token=token,
)
self._current_type.append(cast(FragmentType, fragment_type))
- token.type = 'included_' + typ
- self._process_include_args(src, token, args, self.INCLUDE_FRAGMENT_ALLOWED_ARGS)
+ token.type = "included_" + typ
+ self._process_include_args(
+ src, token, args, self.INCLUDE_FRAGMENT_ALLOWED_ARGS
+ )
self._parse_included_blocks(src, token, args)
self._current_type.pop()
return tokens
- def _process_include_args(self, src: str, token: Token, args: dict[str, str], allowed: set[str]) -> None:
+ def _process_include_args(
+ self, src: str, token: Token, args: dict[str, str], allowed: set[str]
+ ) -> None:
ns = self.INCLUDE_ARGS_NS + ":"
- args = { k[len(ns):]: v for k, v in args.items() if k.startswith(ns) }
+ args = {k[len(ns) :]: v for k, v in args.items() if k.startswith(ns)}
if unknown := set(args.keys()) - allowed:
raise SrcError(
src=src,
description=f"unrecognized include argument(s): {unknown}",
token=token,
)
- token.meta['include-args'] = args
+ token.meta["include-args"] = args
- def _parse_included_blocks(self, src: str, token: Token, block_args: dict[str, str]) -> None:
+ def _parse_included_blocks(
+ self, src: str, token: Token, block_args: dict[str, str]
+ ) -> None:
assert token.map
- included = token.meta['included'] = []
- for (lnum, line) in enumerate(token.content.splitlines(), token.map[0] + 1):
+ included = token.meta["included"] = []
+ for lnum, line in enumerate(token.content.splitlines(), token.map[0] + 1):
line = line.strip()
path = self._base_paths[-1].parent / line
if path in self._base_paths:
@@ -136,7 +158,7 @@ def _parse_included_blocks(self, src: str, token: Token, block_args: dict[str, s
)
try:
self._base_paths.append(path)
- with open(path, 'r') as f:
+ with open(path, "r") as f:
prefix = None
if "auto-id-prefix" in block_args:
# include the current file number to prevent duplicate ids within include blocks
@@ -152,11 +174,13 @@ def _parse_included_blocks(self, src: str, token: Token, block_args: dict[str, s
token=lnum,
) from e
- def _parse_options(self, src: str, token: Token, block_args: dict[str, str]) -> None:
+ def _parse_options(
+ self, src: str, token: Token, block_args: dict[str, str]
+ ) -> None:
assert token.map
items = {}
- for (lnum, line) in enumerate(token.content.splitlines(), token.map[0] + 1):
+ for lnum, line in enumerate(token.content.splitlines(), token.map[0] + 1):
if len(args := line.split(":", 1)) != 2:
raise SrcError(
src=src,
@@ -179,9 +203,9 @@ def _parse_options(self, src: str, token: Token, block_args: dict[str, str]) ->
items[k] = v
try:
- id_prefix = items.pop('id-prefix')
- varlist_id = items.pop('list-id')
- source = items.pop('source')
+ id_prefix = items.pop("id-prefix")
+ varlist_id = items.pop("list-id")
+ source = items.pop("source")
except KeyError as e:
raise SrcError(
src=src,
@@ -199,10 +223,10 @@ def _parse_options(self, src: str, token: Token, block_args: dict[str, str]) ->
)
try:
- with open(self._base_paths[-1].parent / source, 'r') as f:
- token.meta['id-prefix'] = id_prefix
- token.meta['list-id'] = varlist_id
- token.meta['source'] = json.load(f)
+ with open(self._base_paths[-1].parent / source, "r") as f:
+ token.meta["id-prefix"] = id_prefix
+ token.meta["list-id"] = varlist_id
+ token.meta["source"] = json.load(f)
except Exception as e:
raise SrcError(
src=src,
@@ -210,6 +234,7 @@ def _parse_options(self, src: str, token: Token, block_args: dict[str, str]) ->
token=token,
) from e
+
class RendererMixin(Renderer):
_toplevel_tag: str
_revision: str
@@ -219,19 +244,19 @@ def __init__(self, toplevel_tag: str, revision: str, *args: Any, **kwargs: Any):
self._toplevel_tag = toplevel_tag
self._revision = revision
self.rules |= {
- 'included_sections': lambda *args: self._included_thing("section", *args),
- 'included_chapters': lambda *args: self._included_thing("chapter", *args),
- 'included_preface': lambda *args: self._included_thing("preface", *args),
- 'included_parts': lambda *args: self._included_thing("part", *args),
- 'included_appendix': lambda *args: self._included_thing("appendix", *args),
- 'included_options': self.included_options,
+ "included_sections": lambda *args: self._included_thing("section", *args),
+ "included_chapters": lambda *args: self._included_thing("chapter", *args),
+ "included_preface": lambda *args: self._included_thing("preface", *args),
+ "included_parts": lambda *args: self._included_thing("part", *args),
+ "included_appendix": lambda *args: self._included_thing("appendix", *args),
+ "included_options": self.included_options,
}
def render(self, tokens: Sequence[Token]) -> str:
# books get special handling because they have *two* title tags. doing this with
# generic code is more complicated than it's worth. the checks above have verified
# that both titles actually exist.
- if self._toplevel_tag == 'book':
+ if self._toplevel_tag == "book":
return self._render_book(tokens)
return super().render(tokens)
@@ -241,7 +266,9 @@ def _render_book(self, tokens: Sequence[Token]) -> str:
raise NotImplementedError()
@abstractmethod
- def _included_thing(self, tag: str, token: Token, tokens: Sequence[Token], i: int) -> str:
+ def _included_thing(
+ self, tag: str, token: Token, tokens: Sequence[Token], i: int
+ ) -> str:
raise NotImplementedError()
@abstractmethod
@@ -261,15 +288,24 @@ class HTMLParameters(NamedTuple):
section_toc_depth: int
media_dir: Path
+
class ManualHTMLRenderer(RendererMixin, HTMLRenderer):
_base_path: Path
_in_dir: Path
_html_params: HTMLParameters
_redirects: Redirects | None
- def __init__(self, toplevel_tag: str, revision: str, html_params: HTMLParameters,
- manpage_urls: Mapping[str, str], xref_targets: dict[str, XrefTarget],
- redirects: Redirects | None, in_dir: Path, base_path: Path):
+ def __init__(
+ self,
+ toplevel_tag: str,
+ revision: str,
+ html_params: HTMLParameters,
+ manpage_urls: Mapping[str, str],
+ xref_targets: dict[str, XrefTarget],
+ redirects: Redirects | None,
+ in_dir: Path,
+ base_path: Path,
+ ):
super().__init__(toplevel_tag, revision, manpage_urls, xref_targets)
self._in_dir = in_dir
self._base_path = base_path.absolute()
@@ -290,37 +326,51 @@ def _pull_image(self, src: str) -> str:
return f"./{self._html_params.media_dir}/{target_name}"
def _push(self, tag: str, hlevel_offset: int) -> Any:
- result = (self._toplevel_tag, self._headings, self._attrspans, self._hlevel_offset, self._in_dir)
+ result = (
+ self._toplevel_tag,
+ self._headings,
+ self._attrspans,
+ self._hlevel_offset,
+ self._in_dir,
+ )
self._hlevel_offset += hlevel_offset
self._toplevel_tag, self._headings, self._attrspans = tag, [], []
return result
def _pop(self, state: Any) -> None:
- (self._toplevel_tag, self._headings, self._attrspans, self._hlevel_offset, self._in_dir) = state
+ (
+ self._toplevel_tag,
+ self._headings,
+ self._attrspans,
+ self._hlevel_offset,
+ self._in_dir,
+ ) = state
def _render_book(self, tokens: Sequence[Token]) -> str:
assert tokens[4].children
- title_id = cast(str, tokens[0].attrs.get('id', ""))
+ title_id = cast(str, tokens[0].attrs.get("id", ""))
title = self._xref_targets[title_id].title
# subtitles don't have IDs, so we can't use xrefs to get them
subtitle = self.renderInline(tokens[4].children)
toc = TocEntry.of(tokens[0])
- return "\n".join([
- self._file_header(toc),
- ' ',
- '
',
- '
',
- f'
',
- f'
{subtitle}
',
- '
',
- "
",
- '
',
- self._build_toc(tokens, 0),
- super(HTMLRenderer, self).render(tokens[6:]),
- '
',
- self._file_footer(toc),
- ])
+ return "\n".join(
+ [
+ self._file_header(toc),
+ ' ',
+ '
',
+ "
",
+ f'
',
+ f'
{subtitle}
',
+ "
",
+ "
",
+ "
",
+ self._build_toc(tokens, 0),
+ super(HTMLRenderer, self).render(tokens[6:]),
+ "
",
+ self._file_footer(toc),
+ ]
+ )
def _file_header(self, toc: TocEntry) -> str:
prev_link, up_link, next_link = "", "", ""
@@ -335,55 +385,69 @@ def _file_header(self, toc: TocEntry) -> str:
f''
)
- if (part := toc.parent) and part.kind != 'book':
+ if (part := toc.parent) and part.kind != "book":
assert part.target.title
parent_title = part.target.title
if toc.next:
next_link = f''
next_a = f'Next'
if toc.prev or toc.parent or toc.next:
- nav_html = "\n".join([
- ' ',
- ])
+ nav_html = "\n".join(
+ [
+ ' ",
+ ]
+ )
scripts = self._html_params.scripts
if self._redirects:
- redirects_name = f'{toc.target.path.split('.html')[0]}-redirects.js'
- with open(self._base_path / redirects_name, 'w') as file:
+ redirects_name = f"{toc.target.path.split('.html')[0]}-redirects.js"
+ with open(self._base_path / redirects_name, "w") as file:
file.write(self._redirects.get_redirect_script(toc.target.path))
- scripts.append(f'./{redirects_name}')
-
- return "\n".join([
- '',
- '',
- '',
- ' ',
- ' ',
- f' {toc.target.title}',
- "".join((f''
- for style in self._html_params.stylesheets)),
- "".join((f''
- for script in scripts)),
- f' ',
- f' ' if home.target.href() else "",
- f' {up_link}{prev_link}{next_link}',
- ' ',
- ' ',
- nav_html,
- ])
+ scripts.append(f"./{redirects_name}")
+
+ return "\n".join(
+ [
+ '',
+ '',
+ '',
+ " ",
+ ' ',
+ f" {toc.target.title}",
+ "".join(
+ (
+ f''
+ for style in self._html_params.stylesheets
+ )
+ ),
+ "".join(
+ (
+ f''
+ for script in scripts
+ )
+ ),
+ f' ',
+ f' '
+ if home.target.href()
+ else "",
+ f" {up_link}{prev_link}{next_link}",
+ " ",
+ " ",
+ nav_html,
+ ]
+ )
def _file_footer(self, toc: TocEntry) -> str:
# prev, next = self._get_prev_and_next()
@@ -404,55 +468,64 @@ def _file_footer(self, toc: TocEntry) -> str:
assert toc.next.target.title
next_text = toc.next.target.title
if toc.prev or toc.parent or toc.next:
- nav_html = "\n".join([
- ' ',
- ])
- return "\n".join([
- nav_html,
- ' ',
- '',
- ])
+ nav_html = "\n".join(
+ [
+ ' ",
+ ]
+ )
+ return "\n".join(
+ [
+ nav_html,
+ " ",
+ "",
+ ]
+ )
def _heading_tag(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- if token.tag == 'h1':
+ if token.tag == "h1":
return self._toplevel_tag
return super()._heading_tag(token, tokens, i)
+
def _build_toc(self, tokens: Sequence[Token], i: int) -> str:
toc = TocEntry.of(tokens[i])
- if toc.kind == 'section' and self._html_params.section_toc_depth < 1:
+ if toc.kind == "section" and self._html_params.section_toc_depth < 1:
return ""
+
def walk_and_emit(toc: TocEntry, depth: int) -> list[str]:
if depth <= 0:
return []
result = []
for child in toc.children:
result.append(
- f''
+ f""
f' '
f' {child.target.toc_html}'
- f' '
- f''
+ f" "
+ f""
)
# we want to look straight through parts because docbook-xsl did too, but it
# also makes for more uesful top-level tocs.
- next_level = walk_and_emit(child, depth - (0 if child.kind == 'part' else 1))
+ next_level = walk_and_emit(
+ child, depth - (0 if child.kind == "part" else 1)
+ )
if next_level:
- result.append(f'{"".join(next_level)}
')
+ result.append(f"{''.join(next_level)}
")
return result
+
def build_list(kind: str, id: str, lst: Sequence[TocEntry]) -> str:
if not lst:
return ""
@@ -462,21 +535,22 @@ def build_list(kind: str, id: str, lst: Sequence[TocEntry]) -> str:
]
return (
f''
- f'
List of {kind}
'
- f'
{"".join(entries)}
'
- '
'
+ f"List of {kind}
"
+ f"{''.join(entries)}
"
+ ""
)
+
# we don't want to generate the "Title of Contents" header for sections,
# docbook didn't and it's only distracting clutter unless it's the main table.
# we also want to generate tocs only for a top-level section (ie, one that is
# not itself contained in another section)
- print_title = toc.kind != 'section'
- if toc.kind == 'section':
- if toc.parent and toc.parent.kind == 'section':
+ print_title = toc.kind != "section"
+ if toc.kind == "section":
+ if toc.parent and toc.parent.kind == "section":
toc_depth = 0
else:
toc_depth = self._html_params.section_toc_depth
- elif toc.starts_new_chunk and toc.kind != 'book':
+ elif toc.starts_new_chunk and toc.kind != "book":
toc_depth = self._html_params.chunk_toc_depth
else:
toc_depth = self._html_params.toc_depth
@@ -484,45 +558,48 @@ def build_list(kind: str, id: str, lst: Sequence[TocEntry]) -> str:
return ""
figures = build_list("Figures", "list-of-figures", toc.figures)
examples = build_list("Examples", "list-of-examples", toc.examples)
- return "".join([
- f'',
- '
Table of Contents
' if print_title else "",
- f'
'
- f' {"".join(items)}'
- f'
'
- f'
'
- f'{figures}'
- f'{examples}'
- ])
+ return "".join(
+ [
+ f'',
+ "
Table of Contents
" if print_title else "",
+ f'
{"".join(items)}
{figures}{examples}',
+ ]
+ )
def _make_hN(self, level: int) -> tuple[str, str]:
# for some reason chapters didn't increase the hN nesting count in docbook xslts.
# originally this was duplicated here for consistency with docbook rendering, but
# it could be reevaluated and changed now that docbook is gone.
- if self._toplevel_tag == 'chapter':
+ if self._toplevel_tag == "chapter":
level -= 1
# this style setting is also for docbook compatibility only and could well go away.
style = ""
- if level + self._hlevel_offset < 3 \
- and (self._toplevel_tag == 'section' or (self._toplevel_tag == 'chapter' and level > 0)):
+ if level + self._hlevel_offset < 3 and (
+ self._toplevel_tag == "section"
+ or (self._toplevel_tag == "chapter" and level > 0)
+ ):
style = "clear: both"
tag, hstyle = super()._make_hN(max(1, level))
return tag, style
- def _included_thing(self, tag: str, token: Token, tokens: Sequence[Token], i: int) -> str:
+ def _included_thing(
+ self, tag: str, token: Token, tokens: Sequence[Token], i: int
+ ) -> str:
outer, inner = [], []
# since books have no non-include content the toplevel book wrapper will not count
# towards nesting depth. other types will have at least a title+id heading which
# *does* count towards the nesting depth. chapters give a -1 to included sections
# mirroring the special handing in _make_hN. sigh.
hoffset = (
- 0 if not self._headings
- else self._headings[-1].level - 1 if self._toplevel_tag == 'chapter'
+ 0
+ if not self._headings
+ else self._headings[-1].level - 1
+ if self._toplevel_tag == "chapter"
else self._headings[-1].level
)
outer.append(self._maybe_close_partintro())
- into = token.meta['include-args'].get('into-file')
- fragments = token.meta['included']
+ into = token.meta["include-args"].get("into-file")
+ fragments = token.meta["included"]
state = self._push(tag, hoffset)
if into:
toc = TocEntry.of(fragments[0][0][0])
@@ -544,18 +621,24 @@ def _included_thing(self, tag: str, token: Token, tokens: Sequence[Token], i: in
return "".join(outer)
def included_options(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- conv = options.HTMLConverter(self._manpage_urls, self._revision,
- token.meta['list-id'], token.meta['id-prefix'],
- self._xref_targets)
- conv.add_options(token.meta['source'])
+ conv = options.HTMLConverter(
+ self._manpage_urls,
+ self._revision,
+ token.meta["list-id"],
+ token.meta["id-prefix"],
+ self._xref_targets,
+ )
+ conv.add_options(token.meta["source"])
return conv.finalize()
+
def _to_base26(n: int) -> str:
return (_to_base26(n // 26) if n > 26 else "") + chr(ord("A") + n % 26)
+
class HTMLConverter(BaseConverter[ManualHTMLRenderer]):
INCLUDE_ARGS_NS = "html"
- INCLUDE_FRAGMENT_ALLOWED_ARGS = { 'into-file' }
+ INCLUDE_FRAGMENT_ALLOWED_ARGS = {"into-file"}
_revision: str
_html_params: HTMLParameters
@@ -569,27 +652,46 @@ def _next_appendix_id(self) -> str:
self._appendix_count += 1
return _to_base26(self._appendix_count - 1)
- def __init__(self, revision: str, html_params: HTMLParameters, manpage_urls: Mapping[str, str], redirects: Redirects | None = None):
+ def __init__(
+ self,
+ revision: str,
+ html_params: HTMLParameters,
+ manpage_urls: Mapping[str, str],
+ redirects: Redirects | None = None,
+ ):
super().__init__()
- self._revision, self._html_params, self._manpage_urls, self._redirects = revision, html_params, manpage_urls, redirects
+ self._revision, self._html_params, self._manpage_urls, self._redirects = (
+ revision,
+ html_params,
+ manpage_urls,
+ redirects,
+ )
self._xref_targets = {}
self._redirection_targets = set()
# renderer not set on purpose since it has a dependency on the output path!
def convert(self, infile: Path, outfile: Path) -> None:
self._renderer = ManualHTMLRenderer(
- 'book', self._revision, self._html_params, self._manpage_urls, self._xref_targets,
- self._redirects, infile.parent, outfile.parent)
+ "book",
+ self._revision,
+ self._html_params,
+ self._manpage_urls,
+ self._xref_targets,
+ self._redirects,
+ infile.parent,
+ outfile.parent,
+ )
super().convert(infile, outfile)
def _parse(self, src: str, *, auto_id_prefix: None | str = None) -> list[Token]:
- tokens = super()._parse(src,auto_id_prefix=auto_id_prefix)
+ tokens = super()._parse(src, auto_id_prefix=auto_id_prefix)
for token in tokens:
- if not token.type.startswith('included_') \
- or not (into := token.meta['include-args'].get('into-file')):
+ if not token.type.startswith("included_") or not (
+ into := token.meta["include-args"].get("into-file")
+ ):
continue
assert token.map
- if len(token.meta['included']) == 0:
+ if len(token.meta["included"]) == 0:
raise SrcError(
src=src,
description=f"redirection target {into!r} is empty!",
@@ -597,13 +699,13 @@ def _parse(self, src: str, *, auto_id_prefix: None | str = None) -> list[Token]:
)
# we use blender-style //path to denote paths relative to the origin file
# (usually index.html). this makes everything a lot easier and clearer.
- if not into.startswith("//") or '/' in into[2:]:
+ if not into.startswith("//") or "/" in into[2:]:
raise SrcError(
src=src,
description=f"html:into-file must be a relative-to-origin //filename: {into}",
token=token,
)
- into = token.meta['include-args']['into-file'] = into[2:]
+ into = token.meta["include-args"]["into-file"] = into[2:]
if into in self._redirection_targets:
raise SrcError(
src=src,
@@ -613,105 +715,145 @@ def _parse(self, src: str, *, auto_id_prefix: None | str = None) -> list[Token]:
self._redirection_targets.add(into)
return tokens
- def _number_block(self, block: str, prefix: str, tokens: Sequence[Token], start: int = 1) -> int:
- title_open, title_close = f'{block}_title_open', f'{block}_title_close'
- for (i, token) in enumerate(tokens):
+ def _number_block(
+ self, block: str, prefix: str, tokens: Sequence[Token], start: int = 1
+ ) -> int:
+ title_open, title_close = f"{block}_title_open", f"{block}_title_close"
+ for i, token in enumerate(tokens):
if token.type == title_open:
title = tokens[i + 1]
- assert title.type == 'inline' and title.children
+ assert title.type == "inline" and title.children
# the prefix is split into two tokens because the xref title_html will want
# only the first of the two, but both must be rendered into the example itself.
- title.children = (
- [
- Token('text', '', 0, content=f'{prefix} {start}'),
- Token('text', '', 0, content='. ')
- ] + title.children
- )
+ title.children = [
+ Token("text", "", 0, content=f"{prefix} {start}"),
+ Token("text", "", 0, content=". "),
+ ] + title.children
start += 1
- elif token.type.startswith('included_') and token.type != 'included_options':
- for sub, _path in token.meta['included']:
+ elif (
+ token.type.startswith("included_") and token.type != "included_options"
+ ):
+ for sub, _path in token.meta["included"]:
start = self._number_block(block, prefix, sub, start)
return start
# xref | (id, type, heading inlines, file, starts new file)
- def _collect_ids(self, tokens: Sequence[Token], target_file: str, typ: str, file_changed: bool
- ) -> list[XrefTarget | tuple[str, str, Token, str, bool]]:
+ def _collect_ids(
+ self, tokens: Sequence[Token], target_file: str, typ: str, file_changed: bool
+ ) -> list[XrefTarget | tuple[str, str, Token, str, bool]]:
result: list[XrefTarget | tuple[str, str, Token, str, bool]] = []
# collect all IDs and their xref substitutions. headings are deferred until everything
# has been parsed so we can resolve links in headings. if that's even used anywhere.
- for (i, bt) in enumerate(tokens):
- if bt.type == 'heading_open' and (id := cast(str, bt.attrs.get('id', ''))):
- result.append((id, typ if bt.tag == 'h1' else 'section', tokens[i + 1], target_file,
- i == 0 and file_changed))
- elif bt.type == 'included_options':
- id_prefix = bt.meta['id-prefix']
- for opt in bt.meta['source'].keys():
+ for i, bt in enumerate(tokens):
+ if bt.type == "heading_open" and (id := cast(str, bt.attrs.get("id", ""))):
+ result.append(
+ (
+ id,
+ typ if bt.tag == "h1" else "section",
+ tokens[i + 1],
+ target_file,
+ i == 0 and file_changed,
+ )
+ )
+ elif bt.type == "included_options":
+ id_prefix = bt.meta["id-prefix"]
+ for opt in bt.meta["source"].keys():
id = make_xml_id(f"{id_prefix}{opt}")
name = html.escape(opt)
- result.append(XrefTarget(id, f'{name}', name, None, target_file))
- elif bt.type.startswith('included_'):
- sub_file = bt.meta['include-args'].get('into-file', target_file)
- subtyp = bt.type.removeprefix('included_').removesuffix('s')
- for si, (sub, _path) in enumerate(bt.meta['included']):
- result += self._collect_ids(sub, sub_file, subtyp, si == 0 and sub_file != target_file)
- elif bt.type == 'example_open' and (id := cast(str, bt.attrs.get('id', ''))):
- result.append((id, 'example', tokens[i + 2], target_file, False))
- elif bt.type == 'figure_open' and (id := cast(str, bt.attrs.get('id', ''))):
- result.append((id, 'figure', tokens[i + 2], target_file, False))
- elif bt.type == 'footnote_open' and (id := cast(str, bt.attrs.get('id', ''))):
+ result.append(
+ XrefTarget(
+ id,
+ f'{name}',
+ name,
+ None,
+ target_file,
+ )
+ )
+ elif bt.type.startswith("included_"):
+ sub_file = bt.meta["include-args"].get("into-file", target_file)
+ subtyp = bt.type.removeprefix("included_").removesuffix("s")
+ for si, (sub, _path) in enumerate(bt.meta["included"]):
+ result += self._collect_ids(
+ sub, sub_file, subtyp, si == 0 and sub_file != target_file
+ )
+ elif bt.type == "example_open" and (
+ id := cast(str, bt.attrs.get("id", ""))
+ ):
+ result.append((id, "example", tokens[i + 2], target_file, False))
+ elif bt.type == "figure_open" and (id := cast(str, bt.attrs.get("id", ""))):
+ result.append((id, "figure", tokens[i + 2], target_file, False))
+ elif bt.type == "footnote_open" and (
+ id := cast(str, bt.attrs.get("id", ""))
+ ):
result.append(XrefTarget(id, "???", None, None, target_file))
- elif bt.type == 'footnote_ref' and (id := cast(str, bt.attrs.get('id', ''))):
+ elif bt.type == "footnote_ref" and (
+ id := cast(str, bt.attrs.get("id", ""))
+ ):
result.append(XrefTarget(id, "???", None, None, target_file))
- elif bt.type == 'inline':
+ elif bt.type == "inline":
assert bt.children is not None
result += self._collect_ids(bt.children, target_file, typ, False)
- elif id := cast(str, bt.attrs.get('id', '')):
+ elif id := cast(str, bt.attrs.get("id", "")):
# anchors and examples have no titles we could use, but we'll have to put
# *something* here to communicate that there's no title.
result.append(XrefTarget(id, "???", None, None, target_file))
return result
- def _render_xref(self, id: str, typ: str, inlines: Token, path: str, drop_fragment: bool) -> XrefTarget:
+ def _render_xref(
+ self, id: str, typ: str, inlines: Token, path: str, drop_fragment: bool
+ ) -> XrefTarget:
assert inlines.children
title_html = self._renderer.renderInline(inlines.children)
- if typ == 'appendix':
+ if typ == "appendix":
# NOTE the docbook compat is strong here
n = self._next_appendix_id()
- prefix = f"Appendix\u00A0{n}.\u00A0"
+ prefix = f"Appendix\u00a0{n}.\u00a0"
# HACK for docbook compat: prefix the title inlines with appendix id if
# necessary. the alternative is to mess with titlepage rendering in headings,
# which seems just a lot worse than this
- prefix_tokens = [Token(type='text', tag='', nesting=0, content=prefix)]
+ prefix_tokens = [Token(type="text", tag="", nesting=0, content=prefix)]
inlines.children = prefix_tokens + list(inlines.children)
title = prefix + title_html
toc_html = f"{n}. {title_html}"
title_html = f"Appendix {n}"
- elif typ in ['example', 'figure']:
+ elif typ in ["example", "figure"]:
# skip the prepended `{Example,Figure} N. ` from numbering
- toc_html, title = self._renderer.renderInline(inlines.children[2:]), title_html
+ toc_html, title = (
+ self._renderer.renderInline(inlines.children[2:]),
+ title_html,
+ )
# xref title wants only the prepended text, sans the trailing colon and space
title_html = self._renderer.renderInline(inlines.children[0:1])
else:
toc_html, title = title_html, title_html
title_html = (
f"{title_html}"
- if typ == 'chapter'
- else title_html if typ in [ 'book', 'part' ]
- else f'the section called “{title_html}”'
+ if typ == "chapter"
+ else title_html
+ if typ in ["book", "part"]
+ else f"the section called “{title_html}”"
)
- return XrefTarget(id, title_html, toc_html, re.sub('<.*?>', '', title), path, drop_fragment)
+ return XrefTarget(
+ id, title_html, toc_html, re.sub("<.*?>", "", title), path, drop_fragment
+ )
- def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) -> None:
- self._number_block('example', "Example", tokens)
- self._number_block('figure', "Figure", tokens)
- xref_queue = self._collect_ids(tokens, outfile.name, 'book', True)
+ def _postprocess(
+ self, infile: Path, outfile: Path, tokens: Sequence[Token]
+ ) -> None:
+ self._number_block("example", "Example", tokens)
+ self._number_block("figure", "Figure", tokens)
+ xref_queue = self._collect_ids(tokens, outfile.name, "book", True)
failed = False
deferred = []
while xref_queue:
for item in xref_queue:
try:
- target = item if isinstance(item, XrefTarget) else self._render_xref(*item)
+ target = (
+ item
+ if isinstance(item, XrefTarget)
+ else self._render_xref(*item)
+ )
except UnresolvedXrefError:
if failed:
raise
@@ -722,7 +864,7 @@ def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) ->
raise RuntimeError(f"found duplicate id #{target.id}")
self._xref_targets[target.id] = target
if len(deferred) == len(xref_queue):
- failed = True # do another round and report the first error
+ failed = True # do another round and report the first error
xref_queue = deferred
paths_seen = set()
@@ -730,7 +872,7 @@ def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) ->
paths_seen.add(t.path)
if len(paths_seen) == 1:
- for (k, t) in self._xref_targets.items():
+ for k, t in self._xref_targets.items():
self._xref_targets[k] = XrefTarget(
t.id,
t.title_html,
@@ -738,14 +880,14 @@ def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) ->
t.title,
t.path,
t.drop_fragment,
- drop_target=True
+ drop_target=True,
)
TocEntry.collect_and_link(self._xref_targets, tokens)
if self._redirects:
self._redirects.validate(self._xref_targets)
server_redirects = self._redirects.get_server_redirects()
- with open(outfile.parent / '_redirects', 'w') as server_redirects_file:
+ with open(outfile.parent / "_redirects", "w") as server_redirects_file:
formatted_server_redirects = []
for from_path, to_path in server_redirects.items():
formatted_server_redirects.append(f"{from_path} {to_path} 301")
@@ -753,21 +895,25 @@ def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) ->
def _build_cli_html(p: argparse.ArgumentParser) -> None:
- p.add_argument('--manpage-urls', required=True)
- p.add_argument('--revision', required=True)
- p.add_argument('--generator', default='nixos-render-docs')
- p.add_argument('--stylesheet', default=[], action='append')
- p.add_argument('--script', default=[], action='append')
- p.add_argument('--toc-depth', default=1, type=int)
- p.add_argument('--chunk-toc-depth', default=1, type=int)
- p.add_argument('--section-toc-depth', default=0, type=int)
- p.add_argument('--media-dir', default="media", type=Path)
- p.add_argument('--redirects', type=Path)
- p.add_argument('infile', type=Path)
- p.add_argument('outfile', type=Path)
+ p.add_argument("--manpage-urls", required=True)
+ p.add_argument("--revision", required=True)
+ p.add_argument("--generator", default="nixos-render-docs")
+ p.add_argument("--stylesheet", default=[], action="append")
+ p.add_argument("--script", default=[], action="append")
+ p.add_argument("--toc-depth", default=1, type=int)
+ p.add_argument("--chunk-toc-depth", default=1, type=int)
+ p.add_argument("--section-toc-depth", default=0, type=int)
+ p.add_argument("--media-dir", default="media", type=Path)
+ p.add_argument("--redirects", type=Path)
+ p.add_argument("infile", type=Path)
+ p.add_argument("outfile", type=Path)
+
def _run_cli_html(args: argparse.Namespace) -> None:
- with open(args.manpage_urls) as manpage_urls, open(Path(__file__).parent / "redirects.js") as redirects_script:
+ with (
+ open(args.manpage_urls) as manpage_urls,
+ open(Path(__file__).parent / "redirects.js") as redirects_script,
+ ):
redirects = None
if args.redirects:
with open(args.redirects) as raw_redirects:
@@ -775,17 +921,28 @@ def _run_cli_html(args: argparse.Namespace) -> None:
md = HTMLConverter(
args.revision,
- HTMLParameters(args.generator, args.stylesheet, args.script, args.toc_depth,
- args.chunk_toc_depth, args.section_toc_depth, args.media_dir),
- json.load(manpage_urls), redirects)
+ HTMLParameters(
+ args.generator,
+ args.stylesheet,
+ args.script,
+ args.toc_depth,
+ args.chunk_toc_depth,
+ args.section_toc_depth,
+ args.media_dir,
+ ),
+ json.load(manpage_urls),
+ redirects,
+ )
md.convert(args.infile, args.outfile)
+
def build_cli(p: argparse.ArgumentParser) -> None:
- formats = p.add_subparsers(dest='format', required=True)
- _build_cli_html(formats.add_parser('html'))
+ formats = p.add_subparsers(dest="format", required=True)
+ _build_cli_html(formats.add_parser("html"))
+
def run_cli(args: argparse.Namespace) -> None:
- if args.format == 'html':
+ if args.format == "html":
_run_cli_html(args)
else:
- raise RuntimeError('format not hooked up', args)
+ raise RuntimeError("format not hooked up", args)
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manual_structure.py b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manual_structure.py
index 64effecb88f51..802833a51f19d 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manual_structure.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/manual_structure.py
@@ -12,14 +12,18 @@
from .src_error import SrcError
# FragmentType is used to restrict structural include blocks.
-FragmentType = Literal['preface', 'part', 'chapter', 'section', 'appendix']
+FragmentType = Literal["preface", "part", "chapter", "section", "appendix"]
# in the TOC all fragments are allowed, plus the all-encompassing book.
-TocEntryType = Literal['book', 'preface', 'part', 'chapter', 'section', 'appendix', 'example', 'figure']
+TocEntryType = Literal[
+ "book", "preface", "part", "chapter", "section", "appendix", "example", "figure"
+]
+
def is_include(token: Token) -> bool:
return token.type == "fence" and token.info.startswith("{=include=} ")
+
# toplevel file must contain only the title headings and includes, anything else
# would cause strange rendering.
def _check_book_structure(src: str, tokens: Sequence[Token]) -> None:
@@ -31,28 +35,32 @@ def _check_book_structure(src: str, tokens: Sequence[Token]) -> None:
token=token,
)
+
# much like books, parts may not contain headings other than their title heading.
# this is a limitation of the current renderers and TOC generators that do not handle
# this case well even though it is supported in docbook (and probably supportable
# anywhere else).
-def _check_part_structure(src: str,tokens: Sequence[Token]) -> None:
+def _check_part_structure(src: str, tokens: Sequence[Token]) -> None:
_check_fragment_structure(src, tokens)
for token in tokens[3:]:
- if token.type == 'heading_open':
+ if token.type == "heading_open":
raise SrcError(
src=src,
description="unexpected heading",
token=token,
)
+
# two include blocks must either be adjacent or separated by a heading, otherwise
# we cannot generate a correct TOC (since there'd be nothing to link to between
# the two includes).
def _check_fragment_structure(src: str, tokens: Sequence[Token]) -> None:
for i, token in enumerate(tokens):
- if is_include(token) \
- and i + 1 < len(tokens) \
- and not (is_include(tokens[i + 1]) or tokens[i + 1].type == 'heading_open'):
+ if (
+ is_include(token)
+ and i + 1 < len(tokens)
+ and not (is_include(tokens[i + 1]) or tokens[i + 1].type == "heading_open")
+ ):
assert token.map
raise SrcError(
src=src,
@@ -60,21 +68,22 @@ def _check_fragment_structure(src: str, tokens: Sequence[Token]) -> None:
token=token,
)
+
def check_structure(src: str, kind: TocEntryType, tokens: Sequence[Token]) -> None:
- wanted = { 'h1': 'title' }
- wanted |= { 'h2': 'subtitle' } if kind == 'book' else {}
- for (i, (tag, role)) in enumerate(wanted.items()):
+ wanted = {"h1": "title"}
+ wanted |= {"h2": "subtitle"} if kind == "book" else {}
+ for i, (tag, role) in enumerate(wanted.items()):
if len(tokens) < 3 * (i + 1):
raise RuntimeError(f"missing {role} ({tag}) heading")
token = tokens[3 * i]
- if token.type != 'heading_open' or token.tag != tag:
+ if token.type != "heading_open" or token.tag != tag:
raise SrcError(
src=src,
description=f"expected {role} ({tag}) heading",
token=token,
)
- for t in tokens[3 * len(wanted):]:
- if t.type != 'heading_open' or not (role := wanted.get(t.tag, '')):
+ for t in tokens[3 * len(wanted) :]:
+ if t.type != "heading_open" or not (role := wanted.get(t.tag, "")):
continue
raise SrcError(
src=src,
@@ -86,36 +95,37 @@ def check_structure(src: str, kind: TocEntryType, tokens: Sequence[Token]) -> No
last_heading_level = 0
for token in tokens:
- if token.type != 'heading_open':
+ if token.type != "heading_open":
continue
# book subtitle headings do not need an id, only book title headings do.
# every other headings needs one too. we need this to build a TOC and to
# provide stable links if the manual changes shape.
- if 'id' not in token.attrs and (kind != 'book' or token.tag != 'h2'):
+ if "id" not in token.attrs and (kind != "book" or token.tag != "h2"):
raise SrcError(
src=src,
description=f"heading does not have an id",
token=token,
)
- level = int(token.tag[1:]) # because tag = h1..h6
+ level = int(token.tag[1:]) # because tag = h1..h6
if level > last_heading_level + 1:
raise SrcError(
src=src,
description=f"heading skips one or more heading levels, "
- "which is currently not allowed",
+ "which is currently not allowed",
token=token,
)
last_heading_level = level
- if kind == 'book':
+ if kind == "book":
_check_book_structure(src, tokens)
- elif kind == 'part':
+ elif kind == "part":
_check_part_structure(src, tokens)
else:
_check_fragment_structure(src, tokens)
+
@dc.dataclass(frozen=True)
class XrefTarget:
id: str
@@ -137,6 +147,7 @@ def href(self) -> str:
path = "" if self.drop_target else html.escape(self.path, True)
return path if self.drop_fragment else f"{path}#{html.escape(self.id, True)}"
+
@dc.dataclass
class TocEntry(Freezeable):
kind: TocEntryType
@@ -155,18 +166,24 @@ def root(self) -> TocEntry:
@classmethod
def of(cls, token: Token) -> TocEntry:
- entry = token.meta.get('TocEntry')
+ entry = token.meta.get("TocEntry")
if not isinstance(entry, TocEntry):
- raise RuntimeError('requested toc entry, none found', token)
+ raise RuntimeError("requested toc entry, none found", token)
return entry
@classmethod
- def collect_and_link(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token]) -> TocEntry:
- entries, examples, figures = cls._collect_entries(xrefs, tokens, 'book')
+ def collect_and_link(
+ cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token]
+ ) -> TocEntry:
+ entries, examples, figures = cls._collect_entries(xrefs, tokens, "book")
- def flatten_with_parent(this: TocEntry, parent: TocEntry | None) -> Iterable[TocEntry]:
+ def flatten_with_parent(
+ this: TocEntry, parent: TocEntry | None
+ ) -> Iterable[TocEntry]:
this.parent = parent
- return itertools.chain([this], *[ flatten_with_parent(c, this) for c in this.children ])
+ return itertools.chain(
+ [this], *[flatten_with_parent(c, this) for c in this.children]
+ )
flat = list(flatten_with_parent(entries, None))
prev = flat[0]
@@ -188,8 +205,9 @@ def flatten_with_parent(this: TocEntry, parent: TocEntry | None) -> Iterable[Toc
return entries
@classmethod
- def _collect_entries(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token],
- kind: TocEntryType) -> tuple[TocEntry, list[TocEntry], list[TocEntry]]:
+ def _collect_entries(
+ cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token], kind: TocEntryType
+ ) -> tuple[TocEntry, list[TocEntry], list[TocEntry]]:
# we assume that check_structure has been run recursively over the entire input.
# list contains (tag, entry) pairs that will collapse to a single entry for
# the full sequence.
@@ -197,40 +215,57 @@ def _collect_entries(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token],
examples: list[TocEntry] = []
figures: list[TocEntry] = []
for token in tokens:
- if token.type.startswith('included_') and (included := token.meta.get('included')):
- fragment_type_str = token.type[9:].removesuffix('s')
+ if token.type.startswith("included_") and (
+ included := token.meta.get("included")
+ ):
+ fragment_type_str = token.type[9:].removesuffix("s")
assert fragment_type_str in get_args(TocEntryType)
fragment_type = cast(TocEntryType, fragment_type_str)
for fragment, _path in included:
- subentries, subexamples, subfigures = cls._collect_entries(xrefs, fragment, fragment_type)
+ subentries, subexamples, subfigures = cls._collect_entries(
+ xrefs, fragment, fragment_type
+ )
entries[-1][1].children.append(subentries)
examples += subexamples
figures += subfigures
- elif token.type == 'heading_open' and (id := cast(str, token.attrs.get('id', ''))):
+ elif token.type == "heading_open" and (
+ id := cast(str, token.attrs.get("id", ""))
+ ):
while len(entries) > 1 and entries[-1][0] >= token.tag:
entries[-2][1].children.append(entries.pop()[1])
- entries.append((token.tag,
- TocEntry(kind if token.tag == 'h1' else 'section', xrefs[id])))
- token.meta['TocEntry'] = entries[-1][1]
- elif token.type == 'example_open' and (id := cast(str, token.attrs.get('id', ''))):
- examples.append(TocEntry('example', xrefs[id]))
- elif token.type == 'figure_open' and (id := cast(str, token.attrs.get('id', ''))):
- figures.append(TocEntry('figure', xrefs[id]))
+ entries.append(
+ (
+ token.tag,
+ TocEntry(kind if token.tag == "h1" else "section", xrefs[id]),
+ )
+ )
+ token.meta["TocEntry"] = entries[-1][1]
+ elif token.type == "example_open" and (
+ id := cast(str, token.attrs.get("id", ""))
+ ):
+ examples.append(TocEntry("example", xrefs[id]))
+ elif token.type == "figure_open" and (
+ id := cast(str, token.attrs.get("id", ""))
+ ):
+ figures.append(TocEntry("figure", xrefs[id]))
while len(entries) > 1:
entries[-2][1].children.append(entries.pop()[1])
return (entries[0][1], examples, figures)
+
_xml_id_translate_table = {
- ord('*'): ord('_'),
- ord('<'): ord('_'),
- ord(' '): ord('_'),
- ord('>'): ord('_'),
- ord('['): ord('_'),
- ord(']'): ord('_'),
- ord(':'): ord('_'),
- ord('"'): ord('_'),
+ ord("*"): ord("_"),
+ ord("<"): ord("_"),
+ ord(" "): ord("_"),
+ ord(">"): ord("_"),
+ ord("["): ord("_"),
+ ord("]"): ord("_"),
+ ord(":"): ord("_"),
+ ord('"'): ord("_"),
}
+
+
# this function is needed to generate option id attributes in the same format as
# the docbook toolchain did to not break existing links. we don't actually use
# xml any more, that's just the legacy we're dealing with and part of our structure
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/md.py b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/md.py
index 237c554075bd3..f6d46338036d3 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/md.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/md.py
@@ -1,6 +1,17 @@
from abc import ABC
from collections.abc import Mapping, MutableMapping, Sequence
-from typing import Any, Callable, cast, Generic, get_args, Iterable, Literal, NoReturn, Optional, TypeVar
+from typing import (
+ Any,
+ Callable,
+ cast,
+ Generic,
+ get_args,
+ Iterable,
+ Literal,
+ NoReturn,
+ Optional,
+ TypeVar,
+)
import dataclasses
import re
@@ -11,41 +22,46 @@
import markdown_it
from markdown_it.token import Token
from markdown_it.utils import OptionsDict
-from mdit_py_plugins.container import container_plugin # type: ignore[attr-defined]
-from mdit_py_plugins.deflist import deflist_plugin # type: ignore[attr-defined]
-from mdit_py_plugins.footnote import footnote_plugin # type: ignore[attr-defined]
-from mdit_py_plugins.myst_role import myst_role_plugin # type: ignore[attr-defined]
+from mdit_py_plugins.container import container_plugin # type: ignore[attr-defined]
+from mdit_py_plugins.deflist import deflist_plugin # type: ignore[attr-defined]
+from mdit_py_plugins.footnote import footnote_plugin # type: ignore[attr-defined]
+from mdit_py_plugins.myst_role import myst_role_plugin # type: ignore[attr-defined]
_md_escape_table = {
- ord('*'): '\\*',
- ord('<'): '\\<',
- ord('['): '\\[',
- ord('`'): '\\`',
- ord('.'): '\\.',
- ord('#'): '\\#',
- ord('&'): '\\&',
- ord('\\'): '\\\\',
+ ord("*"): "\\*",
+ ord("<"): "\\<",
+ ord("["): "\\[",
+ ord("`"): "\\`",
+ ord("."): "\\.",
+ ord("#"): "\\#",
+ ord("&"): "\\&",
+ ord("\\"): "\\\\",
}
+
+
def md_escape(s: str) -> str:
return s.translate(_md_escape_table)
+
def md_make_code(code: str, info: str = "", multiline: Optional[bool] = None) -> str:
# for multi-line code blocks we only have to count ` runs at the beginning
# of a line, but this is much easier.
- multiline = multiline or info != "" or '\n' in code
+ multiline = multiline or info != "" or "\n" in code
longest, current = (0, 0)
for c in code:
- current = current + 1 if c == '`' else 0
+ current = current + 1 if c == "`" else 0
longest = max(current, longest)
# inline literals need a space to separate ticks from content, code blocks
# need newlines. inline literals need one extra tick, code blocks need three.
- ticks, sep = ('`' * (longest + (3 if multiline else 1)), '\n' if multiline else ' ')
+ ticks, sep = ("`" * (longest + (3 if multiline else 1)), "\n" if multiline else " ")
return f"{ticks}{info}{sep}{code}{sep}{ticks}"
-AttrBlockKind = Literal['admonition', 'example', 'figure']
+
+AttrBlockKind = Literal["admonition", "example", "figure"]
AdmonitionKind = Literal["note", "caution", "tip", "important", "warning"]
+
class Renderer:
_admonitions: dict[AdmonitionKind, tuple[RenderFn, RenderFn]]
_admonition_stack: list[AdmonitionKind]
@@ -53,33 +69,33 @@ class Renderer:
def __init__(self, manpage_urls: Mapping[str, str]):
self._manpage_urls = manpage_urls
self.rules = {
- 'text': self.text,
- 'paragraph_open': self.paragraph_open,
- 'paragraph_close': self.paragraph_close,
- 'hardbreak': self.hardbreak,
- 'softbreak': self.softbreak,
- 'code_inline': self.code_inline,
- 'code_block': self.code_block,
- 'link_open': self.link_open,
- 'link_close': self.link_close,
- 'list_item_open': self.list_item_open,
- 'list_item_close': self.list_item_close,
- 'bullet_list_open': self.bullet_list_open,
- 'bullet_list_close': self.bullet_list_close,
- 'em_open': self.em_open,
- 'em_close': self.em_close,
- 'strong_open': self.strong_open,
- 'strong_close': self.strong_close,
- 'fence': self.fence,
- 'blockquote_open': self.blockquote_open,
- 'blockquote_close': self.blockquote_close,
- 'dl_open': self.dl_open,
- 'dl_close': self.dl_close,
- 'dt_open': self.dt_open,
- 'dt_close': self.dt_close,
- 'dd_open': self.dd_open,
- 'dd_close': self.dd_close,
- 'myst_role': self.myst_role,
+ "text": self.text,
+ "paragraph_open": self.paragraph_open,
+ "paragraph_close": self.paragraph_close,
+ "hardbreak": self.hardbreak,
+ "softbreak": self.softbreak,
+ "code_inline": self.code_inline,
+ "code_block": self.code_block,
+ "link_open": self.link_open,
+ "link_close": self.link_close,
+ "list_item_open": self.list_item_open,
+ "list_item_close": self.list_item_close,
+ "bullet_list_open": self.bullet_list_open,
+ "bullet_list_close": self.bullet_list_close,
+ "em_open": self.em_open,
+ "em_close": self.em_close,
+ "strong_open": self.strong_open,
+ "strong_close": self.strong_close,
+ "fence": self.fence,
+ "blockquote_open": self.blockquote_open,
+ "blockquote_close": self.blockquote_close,
+ "dl_open": self.dl_open,
+ "dl_close": self.dl_close,
+ "dt_open": self.dt_open,
+ "dt_close": self.dt_close,
+ "dd_open": self.dd_open,
+ "dd_close": self.dd_close,
+ "myst_role": self.myst_role,
"admonition_open": self.admonition_open,
"admonition_close": self.admonition_close,
"attr_span_begin": self.attr_span_begin,
@@ -119,7 +135,7 @@ def __init__(self, manpage_urls: Mapping[str, str]):
self._admonitions = {
"note": (self.note_open, self.note_close),
- "caution": (self.caution_open,self.caution_close),
+ "caution": (self.caution_open, self.caution_close),
"tip": (self.tip_open, self.tip_close),
"important": (self.important_open, self.important_close),
"warning": (self.warning_open, self.warning_close),
@@ -128,13 +144,15 @@ def __init__(self, manpage_urls: Mapping[str, str]):
def _join_block(self, ls: Iterable[str]) -> str:
return "".join(ls)
+
def _join_inline(self, ls: Iterable[str]) -> str:
return "".join(ls)
def admonition_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- tag = token.meta['kind']
+ tag = token.meta["kind"]
self._admonition_stack.append(tag)
return self._admonitions[tag][0](token, tokens, i)
+
def admonition_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
return self._admonitions[self._admonition_stack.pop()][1](token, tokens, i)
@@ -147,184 +165,264 @@ def do_one(i: int, token: Token) -> str:
return self.rules[token.type](tokens[i], tokens, i)
else:
raise NotImplementedError("md token not supported yet", token)
+
return self._join_block(map(lambda arg: do_one(*arg), enumerate(tokens)))
+
def renderInline(self, tokens: Sequence[Token]) -> str:
def do_one(i: int, token: Token) -> str:
if token.type in self.rules:
return self.rules[token.type](tokens[i], tokens, i)
else:
raise NotImplementedError("md token not supported yet", token)
+
return self._join_inline(map(lambda arg: do_one(*arg), enumerate(tokens)))
def text(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def softbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def code_inline(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def code_block(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def link_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def list_item_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def list_item_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def em_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def em_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def strong_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def strong_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def note_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def note_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def caution_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def caution_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def important_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def important_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def tip_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def tip_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def warning_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def warning_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def dl_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def dl_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def dt_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def dt_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def dd_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def dd_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str:
# NixOS-specific roles are documented at /doc/README.md (with reverse reference)
raise RuntimeError("md token not supported", token)
+
def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def example_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def example_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def image(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def figure_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def figure_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def figure_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def figure_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def table_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def table_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def thead_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def thead_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def tr_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def tr_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def th_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def th_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def tbody_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def tbody_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def td_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def td_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def footnote_ref(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def footnote_block_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
- def footnote_block_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
+
+ def footnote_block_close(
+ self, token: Token, tokens: Sequence[Token], i: int
+ ) -> str:
raise RuntimeError("md token not supported", token)
+
def footnote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def footnote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def footnote_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported", token)
+
def _is_escaped(src: str, pos: int) -> bool:
found = 0
- while pos >= 0 and src[pos] == '\\':
+ while pos >= 0 and src[pos] == "\\":
found += 1
pos -= 1
return found % 2 == 1
+
# the contents won't be split apart in the regex because spacing rules get messy here
_ATTR_SPAN_PATTERN = re.compile(r"\{([^}]*)\}")
# this one is for blocks with attrs. we want to use it with fullmatch() to deconstruct an info.
_ATTR_BLOCK_PATTERN = re.compile(r"\s*\{([^}]*)\}\s*")
+
def _parse_attrs(s: str) -> Optional[tuple[Optional[str], list[str]]]:
(id, classes) = (None, [])
for part in s.split():
- if part.startswith('#'):
+ if part.startswith("#"):
if id is not None:
- return None # just bail on multiple ids instead of trying to recover
+ return None # just bail on multiple ids instead of trying to recover
id = part[1:]
- elif part.startswith('.'):
+ elif part.startswith("."):
classes.append(part[1:])
else:
- return None # no support for key=value attrs like in pandoc
+ return None # no support for key=value attrs like in pandoc
return (id, classes)
-def _parse_blockattrs(info: str) -> Optional[tuple[AttrBlockKind, Optional[str], list[str]]]:
+
+def _parse_blockattrs(
+ info: str,
+) -> Optional[tuple[AttrBlockKind, Optional[str], list[str]]]:
if (m := _ATTR_BLOCK_PATTERN.fullmatch(info)) is None:
return None
if (parsed_attrs := _parse_attrs(m[1])) is None:
@@ -336,16 +434,17 @@ def _parse_blockattrs(info: str) -> Optional[tuple[AttrBlockKind, Optional[str],
# don't want to support ids for admonitions just yet
if id is not None:
return None
- return ('admonition', id, classes)
- if classes == ['example']:
- return ('example', id, classes)
- elif classes == ['figure']:
- return ('figure', id, classes)
+ return ("admonition", id, classes)
+ if classes == ["example"]:
+ return ("example", id, classes)
+ elif classes == ["figure"]:
+ return ("figure", id, classes)
return None
+
def _attr_span_plugin(md: markdown_it.MarkdownIt) -> None:
def attr_span(state: markdown_it.rules_inline.StateInline, silent: bool) -> bool:
- if state.src[state.pos] != '[':
+ if state.src[state.pos] != "[":
return False
if _is_escaped(state.src, state.pos - 1):
return False
@@ -358,7 +457,7 @@ def attr_span(state: markdown_it.rules_inline.StateInline, silent: bool) -> bool
return False
# match id and classes in any combination
- match = _ATTR_SPAN_PATTERN.match(state.src[label_end + 1 : ])
+ match = _ATTR_SPAN_PATTERN.match(state.src[label_end + 1 :])
if not match:
return False
@@ -369,9 +468,9 @@ def attr_span(state: markdown_it.rules_inline.StateInline, silent: bool) -> bool
token = state.push("attr_span_begin", "span", 1)
if id:
- token.attrs['id'] = id
+ token.attrs["id"] = id
if classes:
- token.attrs['class'] = " ".join(classes)
+ token.attrs["class"] = " ".join(classes)
state.pos = label_begin
state.posMax = label_end
@@ -385,14 +484,17 @@ def attr_span(state: markdown_it.rules_inline.StateInline, silent: bool) -> bool
md.inline.ruler.before("link", "attr_span", attr_span)
+
def _inline_comment_plugin(md: markdown_it.MarkdownIt) -> None:
- def inline_comment(state: markdown_it.rules_inline.StateInline, silent: bool) -> bool:
- if state.src[state.pos : state.pos + 4] != '': # -->
+ if state.src[i : i + 3] == "-->": # -->
state.pos = i + 3
return True
@@ -400,13 +502,18 @@ def inline_comment(state: markdown_it.rules_inline.StateInline, silent: bool) ->
md.inline.ruler.after("autolink", "inline_comment", inline_comment)
+
def _block_comment_plugin(md: markdown_it.MarkdownIt) -> None:
- def block_comment(state: markdown_it.rules_block.StateBlock, startLine: int, endLine: int,
- silent: bool) -> bool:
+ def block_comment(
+ state: markdown_it.rules_block.StateBlock,
+ startLine: int,
+ endLine: int,
+ silent: bool,
+ ) -> bool:
pos = state.bMarks[startLine] + state.tShift[startLine]
posMax = state.eMarks[startLine]
- if state.src[pos : pos + 4] != '':
+ if state.src[posMax - 3 : posMax] == "-->":
state.line = nextLine + 1
return True
@@ -424,32 +531,36 @@ def block_comment(state: markdown_it.rules_block.StateBlock, startLine: int, end
md.block.ruler.after("code", "block_comment", block_comment)
+
_HEADER_ID_RE = re.compile(r"\s*\{\s*\#([\w.-]+)\s*\}\s*$")
+
def _heading_ids(md: markdown_it.MarkdownIt) -> None:
def heading_ids(state: markdown_it.rules_core.StateCore) -> None:
tokens = state.tokens
# this is purposely simple and doesn't support classes or other kinds of attributes.
- for (i, token) in enumerate(tokens):
- if token.type == 'heading_open':
+ for i, token in enumerate(tokens):
+ if token.type == "heading_open":
children = tokens[i + 1].children
assert children is not None
- if len(children) == 0 or children[-1].type != 'text':
+ if len(children) == 0 or children[-1].type != "text":
continue
if m := _HEADER_ID_RE.search(children[-1].content):
- tokens[i].attrs['id'] = m[1]
- children[-1].content = children[-1].content[:-len(m[0])].rstrip()
+ tokens[i].attrs["id"] = m[1]
+ children[-1].content = children[-1].content[: -len(m[0])].rstrip()
md.core.ruler.before("replacements", "heading_ids", heading_ids)
+
def _footnote_ids(md: markdown_it.MarkdownIt) -> None:
"""generate ids for footnotes, their refs, and their backlinks. the ids we
- generate here are derived from the footnote label, making numeric footnote
- labels invalid.
+ generate here are derived from the footnote label, making numeric footnote
+ labels invalid.
"""
+
def generate_ids(src: str, tokens: Sequence[Token]) -> None:
for token in tokens:
- if token.type == 'footnote_open':
+ if token.type == "footnote_open":
if token.meta["label"][:1].isdigit():
assert token.map
raise SrcError(
@@ -457,13 +568,17 @@ def generate_ids(src: str, tokens: Sequence[Token]) -> None:
description="invalid footnote label",
token=token,
)
- token.attrs['id'] = token.meta["label"]
- elif token.type == 'footnote_anchor':
- token.meta['target'] = f'{token.meta["label"]}.__back.{token.meta["subId"]}'
- elif token.type == 'footnote_ref':
- token.attrs['id'] = f'{token.meta["label"]}.__back.{token.meta["subId"]}'
- token.meta['target'] = token.meta["label"]
- elif token.type == 'inline':
+ token.attrs["id"] = token.meta["label"]
+ elif token.type == "footnote_anchor":
+ token.meta["target"] = (
+ f"{token.meta['label']}.__back.{token.meta['subId']}"
+ )
+ elif token.type == "footnote_ref":
+ token.attrs["id"] = (
+ f"{token.meta['label']}.__back.{token.meta['subId']}"
+ )
+ token.meta["target"] = token.meta["label"]
+ elif token.type == "inline":
assert token.children is not None
generate_ids(src, token.children)
@@ -472,6 +587,7 @@ def footnote_ids(state: markdown_it.rules_core.StateCore) -> None:
md.core.ruler.after("footnote_tail", "footnote_ids", footnote_ids)
+
def _compact_list_attr(md: markdown_it.MarkdownIt) -> None:
@dataclasses.dataclass
class Entry:
@@ -485,20 +601,21 @@ def compact_list_attr(state: markdown_it.rules_core.StateCore) -> None:
# signify this with a special css class on list elements instead.
stack = []
for token in state.tokens:
- if token.type in [ 'bullet_list_open', 'ordered_list_open' ]:
- stack.append(Entry(token, cast(int, token.attrs.get('start', 1))))
- elif token.type in [ 'bullet_list_close', 'ordered_list_close' ]:
+ if token.type in ["bullet_list_open", "ordered_list_open"]:
+ stack.append(Entry(token, cast(int, token.attrs.get("start", 1))))
+ elif token.type in ["bullet_list_close", "ordered_list_close"]:
lst = stack.pop()
- lst.head.meta['compact'] = lst.compact
- if token.type == 'ordered_list_close':
- lst.head.meta['end'] = lst.end - 1
- elif len(stack) > 0 and token.type == 'paragraph_open' and not token.hidden:
+ lst.head.meta["compact"] = lst.compact
+ if token.type == "ordered_list_close":
+ lst.head.meta["end"] = lst.end - 1
+ elif len(stack) > 0 and token.type == "paragraph_open" and not token.hidden:
stack[-1].compact = False
- elif token.type == 'list_item_open':
+ elif token.type == "list_item_open":
stack[-1].end += 1
md.core.ruler.push("compact_list_attr", compact_list_attr)
+
def _block_attr(md: markdown_it.MarkdownIt) -> None:
def assert_never(value: NoReturn) -> NoReturn:
assert False
@@ -506,47 +623,49 @@ def assert_never(value: NoReturn) -> NoReturn:
def block_attr(state: markdown_it.rules_core.StateCore) -> None:
stack = []
for token in state.tokens:
- if token.type == 'container_blockattr_open':
+ if token.type == "container_blockattr_open":
if (parsed_attrs := _parse_blockattrs(token.info)) is None:
# if we get here we've missed a possible case in the plugin validate function
raise RuntimeError("this should be unreachable")
kind, id, classes = parsed_attrs
- if kind == 'admonition':
- token.type = 'admonition_open'
- token.meta['kind'] = classes[0]
- stack.append('admonition_close')
- elif kind == 'example':
- token.type = 'example_open'
+ if kind == "admonition":
+ token.type = "admonition_open"
+ token.meta["kind"] = classes[0]
+ stack.append("admonition_close")
+ elif kind == "example":
+ token.type = "example_open"
if id is not None:
- token.attrs['id'] = id
- stack.append('example_close')
- elif kind == 'figure':
- token.type = 'figure_open'
+ token.attrs["id"] = id
+ stack.append("example_close")
+ elif kind == "figure":
+ token.type = "figure_open"
if id is not None:
- token.attrs['id'] = id
- stack.append('figure_close')
+ token.attrs["id"] = id
+ stack.append("figure_close")
else:
assert_never(kind)
- elif token.type == 'container_blockattr_close':
+ elif token.type == "container_blockattr_close":
token.type = stack.pop()
md.core.ruler.push("block_attr", block_attr)
+
def _block_titles(block: str) -> Callable[[markdown_it.MarkdownIt], None]:
- open, close = f'{block}_open', f'{block}_close'
- title_open, title_close = f'{block}_title_open', f'{block}_title_close'
+ open, close = f"{block}_open", f"{block}_close"
+ title_open, title_close = f"{block}_title_open", f"{block}_title_close"
"""
find title headings of blocks and stick them into meta for renderers, then
remove them from the token stream. also checks whether any block contains a
non-title heading since those would make toc generation extremely complicated.
"""
+
def block_titles(state: markdown_it.rules_core.StateCore) -> None:
in_example = [None]
for i, token in enumerate(state.tokens):
if token.type == open:
- if state.tokens[i + 1].type == 'heading_open':
- assert state.tokens[i + 3].type == 'heading_close'
+ if state.tokens[i + 1].type == "heading_open":
+ assert state.tokens[i + 3].type == "heading_close"
state.tokens[i + 1].type = title_open
state.tokens[i + 3].type = title_close
else:
@@ -558,7 +677,7 @@ def block_titles(state: markdown_it.rules_core.StateCore) -> None:
in_example.append(token)
elif token.type == close:
in_example.pop()
- elif token.type == 'heading_open' and in_example[-1]:
+ elif token.type == "heading_open" and in_example[-1]:
assert token.map
started_at = in_example[-1]
@@ -566,7 +685,7 @@ def block_titles(state: markdown_it.rules_core.StateCore) -> None:
raise SrcError(
description=f"unexpected non-title heading in `{block_display}`; are you missing a `:::`?\n"
- f"Note: blocks like `{block_display}` are only allowed to contain a single heading in order to simplify TOC generation.",
+ f"Note: blocks like `{block_display}` are only allowed to contain a single heading in order to simplify TOC generation.",
src=state.src,
tokens={
f"`{block_display}` block": started_at,
@@ -579,7 +698,9 @@ def do_add(md: markdown_it.MarkdownIt) -> None:
return do_add
-TR = TypeVar('TR', bound='Renderer')
+
+TR = TypeVar("TR", bound="Renderer")
+
class Converter(ABC, Generic[TR]):
# we explicitly disable markdown-it rendering support and use our own entirely.
@@ -592,9 +713,15 @@ class ForbiddenRenderer(markdown_it.renderer.RendererProtocol):
def __init__(self, parser: Optional[markdown_it.MarkdownIt]):
pass
- def render(self, tokens: Sequence[Token], options: OptionsDict,
- env: MutableMapping[str, Any]) -> str:
- raise NotImplementedError("do not use Converter._md.renderer. 'tis a silly place")
+ def render(
+ self,
+ tokens: Sequence[Token],
+ options: OptionsDict,
+ env: MutableMapping[str, Any],
+ ) -> str:
+ raise NotImplementedError(
+ "do not use Converter._md.renderer. 'tis a silly place"
+ )
_renderer: TR
@@ -602,13 +729,13 @@ def __init__(self) -> None:
self._md = markdown_it.MarkdownIt(
"commonmark",
{
- 'maxNesting': 100, # default is 20
- 'html': False, # not useful since we target many formats
- 'typographer': True, # required for smartquotes
+ "maxNesting": 100, # default is 20
+ "html": False, # not useful since we target many formats
+ "typographer": True, # required for smartquotes
},
- renderer_cls=self.ForbiddenRenderer
+ renderer_cls=self.ForbiddenRenderer,
)
- self._md.enable('table')
+ self._md.enable("table")
self._md.use(
container_plugin,
name="blockattr",
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/options.py b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/options.py
index 75fbeadce1d09..13e4e0f95aed7 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/options.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/options.py
@@ -23,14 +23,16 @@
from .md import Converter, md_escape, md_make_code
from .types import OptionLoc, Option, RenderedOption, AnchorStyle
+
def option_is(option: Option, key: str, typ: str) -> Optional[dict[str, str]]:
if key not in option:
return None
if type(option[key]) != dict:
return None
- if option[key].get('_type') != typ: # type: ignore[union-attr]
+ if option[key].get("_type") != typ: # type: ignore[union-attr]
return None
- return option[key] # type: ignore[return-value]
+ return option[key] # type: ignore[return-value]
+
class BaseConverter(Converter[md.TR], Generic[md.TR]):
__option_block_separator__: str
@@ -44,9 +46,20 @@ def __init__(self, revision: str):
def _sorted_options(self) -> list[tuple[str, RenderedOption]]:
keys = list(self._options.keys())
- keys.sort(key=lambda opt: [ (0 if p.startswith("enable") else 1 if p.startswith("package") else 2, p)
- for p in self._options[opt].loc ])
- return [ (k, self._options[k]) for k in keys ]
+ keys.sort(
+ key=lambda opt: [
+ (
+ 0
+ if p.startswith("enable")
+ else 1
+ if p.startswith("package")
+ else 2,
+ p,
+ )
+ for p in self._options[opt].loc
+ ]
+ )
+ return [(k, self._options[k]) for k in keys]
def _format_decl_def_loc(self, loc: OptionLoc) -> tuple[Optional[str], str]:
# locations can be either plain strings (specific to nixpkgs), or attrsets
@@ -55,34 +68,39 @@ def _format_decl_def_loc(self, loc: OptionLoc) -> tuple[Optional[str], str]:
# Hyperlink the filename either to the NixOS github
# repository (if it’s a module and we have a revision number),
# or to the local filesystem.
- if not loc.startswith('/'):
- if self._revision == 'local':
+ if not loc.startswith("/"):
+ if self._revision == "local":
href = f"https://github.com/NixOS/nixpkgs/blob/master/{loc}"
else:
- href = f"https://github.com/NixOS/nixpkgs/blob/{self._revision}/{loc}"
+ href = (
+ f"https://github.com/NixOS/nixpkgs/blob/{self._revision}/{loc}"
+ )
else:
href = f"file://{loc}"
# Print the filename and make it user-friendly by replacing the
# /nix/store/ prefix by the default location of nixos
# sources.
- if not loc.startswith('/'):
+ if not loc.startswith("/"):
name = f""
- elif 'nixops' in loc and '/nix/' in loc:
- name = f""
+ elif "nixops" in loc and "/nix/" in loc:
+ name = f""
else:
name = loc
return (href, name)
else:
- return (loc['url'] if 'url' in loc else None, loc['name'])
+ return (loc["url"] if "url" in loc else None, loc["name"])
@abstractmethod
- def _decl_def_header(self, header: str) -> list[str]: raise NotImplementedError()
+ def _decl_def_header(self, header: str) -> list[str]:
+ raise NotImplementedError()
@abstractmethod
- def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]: raise NotImplementedError()
+ def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]:
+ raise NotImplementedError()
@abstractmethod
- def _decl_def_footer(self) -> list[str]: raise NotImplementedError()
+ def _decl_def_footer(self) -> list[str]:
+ raise NotImplementedError()
def _render_decl_def(self, header: str, locs: list[OptionLoc]) -> list[str]:
result = []
@@ -94,11 +112,11 @@ def _render_decl_def(self, header: str, locs: list[OptionLoc]) -> list[str]:
return result
def _render_code(self, option: Option, key: str) -> list[str]:
- if lit := option_is(option, key, 'literalMD'):
- return [ self._render(f"*{key.capitalize()}:*\n{lit['text']}") ]
- elif lit := option_is(option, key, 'literalExpression'):
- code = md_make_code(lit['text'])
- return [ self._render(f"*{key.capitalize()}:*\n{code}") ]
+ if lit := option_is(option, key, "literalMD"):
+ return [self._render(f"*{key.capitalize()}:*\n{lit['text']}")]
+ elif lit := option_is(option, key, "literalExpression"):
+ code = md_make_code(lit["text"])
+ return [self._render(f"*{key.capitalize()}:*\n{code}")]
elif key in option:
raise Exception(f"{key} has unrecognized type", option[key])
else:
@@ -106,57 +124,61 @@ def _render_code(self, option: Option, key: str) -> list[str]:
def _render_description(self, desc: str | dict[str, str]) -> list[str]:
if isinstance(desc, str):
- return [ self._render(desc) ] if desc else []
- elif isinstance(desc, dict) and desc.get('_type') == 'mdDoc':
- return [ self._render(desc['text']) ] if desc['text'] else []
+ return [self._render(desc)] if desc else []
+ elif isinstance(desc, dict) and desc.get("_type") == "mdDoc":
+ return [self._render(desc["text"])] if desc["text"] else []
else:
raise Exception("description has unrecognized type", desc)
@abstractmethod
- def _related_packages_header(self) -> list[str]: raise NotImplementedError()
+ def _related_packages_header(self) -> list[str]:
+ raise NotImplementedError()
def _convert_one(self, option: dict[str, Any]) -> list[str]:
blocks: list[list[str]] = []
- if desc := option.get('description'):
+ if desc := option.get("description"):
blocks.append(self._render_description(desc))
- if typ := option.get('type'):
- ro = " *(read only)*" if option.get('readOnly', False) else ""
- blocks.append([ self._render(f"*Type:*\n{md_escape(typ)}{ro}") ])
+ if typ := option.get("type"):
+ ro = " *(read only)*" if option.get("readOnly", False) else ""
+ blocks.append([self._render(f"*Type:*\n{md_escape(typ)}{ro}")])
- if option.get('default'):
- blocks.append(self._render_code(option, 'default'))
- if option.get('example'):
- blocks.append(self._render_code(option, 'example'))
+ if option.get("default"):
+ blocks.append(self._render_code(option, "default"))
+ if option.get("example"):
+ blocks.append(self._render_code(option, "example"))
- if related := option.get('relatedPackages'):
+ if related := option.get("relatedPackages"):
blocks.append(self._related_packages_header())
blocks[-1].append(self._render(related))
- if decl := option.get('declarations'):
+ if decl := option.get("declarations"):
blocks.append(self._render_decl_def("Declared by", decl))
- if defs := option.get('definitions'):
+ if defs := option.get("definitions"):
blocks.append(self._render_decl_def("Defined by", defs))
- for part in [ p for p in blocks[0:-1] if p ]:
+ for part in [p for p in blocks[0:-1] if p]:
part.append(self.__option_block_separator__)
- return [ l for part in blocks for l in part ]
+ return [l for part in blocks for l in part]
# this could return a TState parameter, but that does not allow dependent types and
# will cause headaches when using BaseConverter as a type bound anywhere. Any is the
# next best thing we can use, and since this is internal it will be mostly safe.
@abstractmethod
- def _parallel_render_prepare(self) -> Any: raise NotImplementedError()
+ def _parallel_render_prepare(self) -> Any:
+ raise NotImplementedError()
+
# this should return python 3.11's Self instead to ensure that a prepare+finish
# round-trip ends up with an object of the same type. for now we'll use BaseConverter
# since it's good enough so far.
@classmethod
@abstractmethod
- def _parallel_render_init_worker(cls, a: Any) -> BaseConverter[md.TR]: raise NotImplementedError()
+ def _parallel_render_init_worker(cls, a: Any) -> BaseConverter[md.TR]:
+ raise NotImplementedError()
def _render_option(self, name: str, option: dict[str, Any]) -> RenderedOption:
try:
- return RenderedOption(option['loc'], self._convert_one(option))
+ return RenderedOption(option["loc"], self._convert_one(option))
except Exception as e:
raise Exception(f"Failed to render option {name}") from e
@@ -165,39 +187,54 @@ def _parallel_render_step(cls, s: BaseConverter[md.TR], a: Any) -> RenderedOptio
return s._render_option(*a)
def add_options(self, options: dict[str, Any]) -> None:
- mapped = parallel.map(self._parallel_render_step, options.items(), 100,
- self._parallel_render_init_worker, self._parallel_render_prepare())
- for (name, option) in zip(options.keys(), mapped):
+ mapped = parallel.map(
+ self._parallel_render_step,
+ options.items(),
+ 100,
+ self._parallel_render_init_worker,
+ self._parallel_render_prepare(),
+ )
+ for name, option in zip(options.keys(), mapped):
self._options[name] = option
@abstractmethod
- def finalize(self) -> str: raise NotImplementedError()
+ def finalize(self) -> str:
+ raise NotImplementedError()
+
class OptionDocsRestrictions:
def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported in options doc", token)
+
def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported in options doc", token)
+
def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported in options doc", token)
+
def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
raise RuntimeError("md token not supported in options doc", token)
+
class OptionsManpageRenderer(OptionDocsRestrictions, ManpageRenderer):
pass
+
class ManpageConverter(BaseConverter[OptionsManpageRenderer]):
__option_block_separator__ = ".sp"
_options_by_id: dict[str, str]
_links_in_last_description: Optional[list[str]] = None
- def __init__(self, revision: str,
- header: list[str] | None,
- footer: list[str] | None,
- *,
- # only for parallel rendering
- _options_by_id: Optional[dict[str, str]] = None):
+ def __init__(
+ self,
+ revision: str,
+ header: list[str] | None,
+ footer: list[str] | None,
+ *,
+ # only for parallel rendering
+ _options_by_id: Optional[dict[str, str]] = None,
+ ):
super().__init__(revision)
self._options_by_id = _options_by_id or {}
self._renderer = OptionsManpageRenderer({}, self._options_by_id)
@@ -209,8 +246,9 @@ def _parallel_render_prepare(self) -> Any:
self._revision,
self._header,
self._footer,
- { '_options_by_id': self._options_by_id },
+ {"_options_by_id": self._options_by_id},
)
+
@classmethod
def _parallel_render_init_worker(cls, a: Any) -> ManpageConverter:
return cls(a[0], a[1], a[2], **a[3])
@@ -222,8 +260,8 @@ def _render_option(self, name: str, option: dict[str, Any]) -> RenderedOption:
return result._replace(links=links)
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
+ 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]:
@@ -235,21 +273,17 @@ def _render_code(self, option: dict[str, Any], key: str) -> list[str]:
def _related_packages_header(self) -> list[str]:
return [
- '\\fIRelated packages:\\fP',
- '.sp',
+ "\\fIRelated packages:\\fP",
+ ".sp",
]
def _decl_def_header(self, header: str) -> list[str]:
return [
- f'\\fI{man_escape(header)}:\\fP',
+ 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'
- ]
+ return [".RS 4", f"\\fB{man_escape(name)}\\fP", ".RE"]
def _decl_def_footer(self) -> list[str]:
return []
@@ -262,26 +296,32 @@ def finalize(self) -> str:
else:
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""".\" 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'),
+ 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""".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`.'),
+ r""".PP""",
+ self._render(
+ "You can use the following options in {file}`configuration.nix`."
+ ),
]
- for (name, opt) in self._sorted_options():
+ for name, opt in self._sorted_options():
result += [
".PP",
f"\\fB{man_escape(name)}\\fR",
@@ -293,10 +333,10 @@ def finalize(self) -> str:
md_links = ""
for i in range(0, len(links)):
md_links += "\n" if i > 0 else ""
- if links[i].startswith('#opt-'):
- md_links += f"{i+1}. see the {{option}}`{self._options_by_id[links[i]]}` option"
+ if links[i].startswith("#opt-"):
+ md_links += f"{i + 1}. see the {{option}}`{self._options_by_id[links[i]]}` option"
else:
- md_links += f"{i+1}. " + md_escape(links[i])
+ md_links += f"{i + 1}. " + md_escape(links[i])
result.append(self._render(md_links))
result.append(".RE")
@@ -306,22 +346,29 @@ def finalize(self) -> str:
else:
result += [
r'''.SH "AUTHORS"''',
- r'''.PP''',
- r'''Eelco Dolstra and the Nixpkgs/NixOS contributors''',
+ r""".PP""",
+ r"""Eelco Dolstra and the Nixpkgs/NixOS contributors""",
]
return "\n".join(result)
+
class OptionsCommonMarkRenderer(OptionDocsRestrictions, CommonMarkRenderer):
pass
+
class CommonMarkConverter(BaseConverter[OptionsCommonMarkRenderer]):
__option_block_separator__ = ""
_anchor_style: AnchorStyle
_anchor_prefix: str
-
- def __init__(self, manpage_urls: Mapping[str, str], revision: str, anchor_style: AnchorStyle = AnchorStyle.NONE, anchor_prefix: str = ""):
+ def __init__(
+ self,
+ manpage_urls: Mapping[str, str],
+ revision: str,
+ anchor_style: AnchorStyle = AnchorStyle.NONE,
+ anchor_prefix: str = "",
+ ):
super().__init__(revision)
self._renderer = OptionsCommonMarkRenderer(manpage_urls)
self._anchor_style = anchor_style
@@ -329,20 +376,21 @@ def __init__(self, manpage_urls: Mapping[str, str], revision: str, anchor_style:
def _parallel_render_prepare(self) -> Any:
return (self._renderer._manpage_urls, self._revision)
+
@classmethod
def _parallel_render_init_worker(cls, a: Any) -> CommonMarkConverter:
return cls(*a)
def _related_packages_header(self) -> list[str]:
- return [ "*Related packages:*" ]
+ return ["*Related packages:*"]
def _decl_def_header(self, header: str) -> list[str]:
- return [ f"*{header}:*" ]
+ return [f"*{header}:*"]
def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]:
if href is not None:
- return [ f" - [{md_escape(name)}]({href})" ]
- return [ f" - {md_escape(name)}" ]
+ return [f" - [{md_escape(name)}]({href})"]
+ return [f" - {md_escape(name)}"]
def _decl_def_footer(self) -> list[str]:
return []
@@ -359,7 +407,7 @@ def _make_anchor_suffix(self, loc: list[str]) -> str:
def finalize(self) -> str:
result = []
- for (name, opt) in self._sorted_options():
+ for name, opt in self._sorted_options():
anchor_suffix = self._make_anchor_suffix(opt.loc)
result.append(f"## {md_escape(name)}{anchor_suffix}\n")
result += opt.lines
@@ -367,9 +415,11 @@ def finalize(self) -> str:
return "\n".join(result)
+
class OptionsAsciiDocRenderer(OptionDocsRestrictions, AsciiDocRenderer):
pass
+
class AsciiDocConverter(BaseConverter[OptionsAsciiDocRenderer]):
__option_block_separator__ = ""
@@ -379,20 +429,21 @@ def __init__(self, manpage_urls: Mapping[str, str], revision: str):
def _parallel_render_prepare(self) -> Any:
return (self._renderer._manpage_urls, self._revision)
+
@classmethod
def _parallel_render_init_worker(cls, a: Any) -> AsciiDocConverter:
return cls(*a)
def _related_packages_header(self) -> list[str]:
- return [ "__Related packages:__" ]
+ return ["__Related packages:__"]
def _decl_def_header(self, header: str) -> list[str]:
- return [ f"__{header}:__\n" ]
+ return [f"__{header}:__\n"]
def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]:
if href is not None:
- return [ f"* link:{quote(href, safe='/:')}[{asciidoc_escape(name)}]" ]
- return [ f"* {asciidoc_escape(name)}" ]
+ return [f"* link:{quote(href, safe='/:')}[{asciidoc_escape(name)}]"]
+ return [f"* {asciidoc_escape(name)}"]
def _decl_def_footer(self) -> list[str]:
return []
@@ -400,30 +451,40 @@ def _decl_def_footer(self) -> list[str]:
def finalize(self) -> str:
result = []
- for (name, opt) in self._sorted_options():
+ for name, opt in self._sorted_options():
result.append(f"== {asciidoc_escape(name)}\n")
result += opt.lines
result.append("\n\n")
return "\n".join(result)
+
class OptionsHTMLRenderer(OptionDocsRestrictions, HTMLRenderer):
# TODO docbook compat. must be removed together with the matching docbook handlers.
def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- token.meta['compact'] = False
+ token.meta["compact"] = False
return super().ordered_list_open(token, tokens, i)
+
def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
- token.meta['compact'] = False
+ token.meta["compact"] = False
return super().bullet_list_open(token, tokens, i)
+
def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str:
info = f" {html.escape(token.info, True)}" if token.info != "" else ""
return f'{html.escape(token.content)}
'
+
class HTMLConverter(BaseConverter[OptionsHTMLRenderer]):
__option_block_separator__ = ""
- def __init__(self, manpage_urls: Mapping[str, str], revision: str,
- varlist_id: str, id_prefix: str, xref_targets: Mapping[str, XrefTarget]):
+ def __init__(
+ self,
+ manpage_urls: Mapping[str, str],
+ revision: str,
+ varlist_id: str,
+ id_prefix: str,
+ xref_targets: Mapping[str, XrefTarget],
+ ):
super().__init__(revision)
self._xref_targets = xref_targets
self._varlist_id = varlist_id
@@ -431,8 +492,14 @@ def __init__(self, manpage_urls: Mapping[str, str], revision: str,
self._renderer = OptionsHTMLRenderer(manpage_urls, self._xref_targets)
def _parallel_render_prepare(self) -> Any:
- return (self._renderer._manpage_urls, self._revision,
- self._varlist_id, self._id_prefix, self._xref_targets)
+ return (
+ self._renderer._manpage_urls,
+ self._revision,
+ self._varlist_id,
+ self._id_prefix,
+ self._xref_targets,
+ )
+
@classmethod
def _parallel_render_init_worker(cls, a: Any) -> HTMLConverter:
return cls(*a)
@@ -445,7 +512,7 @@ def _related_packages_header(self) -> list[str]:
def _decl_def_header(self, header: str) -> list[str]:
return [
f'{header}:
',
- ''
+ '',
]
def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]:
@@ -454,13 +521,13 @@ def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]:
return [
"",
f'',
- f'{html.escape(name)}',
- '',
- " |
"
+ f"{html.escape(name)}",
+ "",
+ "",
]
def _decl_def_footer(self) -> list[str]:
- return [ "
" ]
+ return ["
"]
def finalize(self) -> str:
result = []
@@ -471,73 +538,78 @@ def finalize(self) -> str:
' ',
]
- for (name, opt) in self._sorted_options():
+ for name, opt in self._sorted_options():
id = make_xml_id(self._id_prefix + name)
target = self._xref_targets[id]
result += [
- '- ',
+ "
- ",
' ',
# docbook compat, these could be one tag
f' '
# no spaces here (and string merging) for docbook output compat
f'
{html.escape(name)}',
- ' ',
- ' ',
- ' ',
- ' - ',
+ " ",
+ " ",
+ "",
+ "
- ",
]
result += opt.lines
result += [
"
",
]
- result += [
- "
",
- ""
- ]
+ result += [" ", ""]
return "\n".join(result)
+
def _build_cli_manpage(p: argparse.ArgumentParser) -> None:
- p.add_argument('--revision', required=True)
+ p.add_argument("--revision", required=True)
p.add_argument("--header", type=Path)
p.add_argument("--footer", type=Path)
p.add_argument("infile")
p.add_argument("outfile")
-def parse_anchor_style(value: str|AnchorStyle) -> AnchorStyle:
+
+def parse_anchor_style(value: str | AnchorStyle) -> AnchorStyle:
if isinstance(value, AnchorStyle):
# Used by `argparse.add_argument`'s `default`
return value
try:
return AnchorStyle(value.lower())
except ValueError:
- raise argparse.ArgumentTypeError(f"Invalid value {value}\nExpected one of {', '.join(style.value for style in AnchorStyle)}")
+ raise argparse.ArgumentTypeError(
+ f"Invalid value {value}\nExpected one of {', '.join(style.value for style in AnchorStyle)}"
+ )
+
def _build_cli_commonmark(p: argparse.ArgumentParser) -> None:
- p.add_argument('--manpage-urls', required=True)
- p.add_argument('--revision', required=True)
+ p.add_argument("--manpage-urls", required=True)
+ p.add_argument("--revision", required=True)
p.add_argument(
- '--anchor-style',
+ "--anchor-style",
required=False,
default=AnchorStyle.NONE.value,
- choices = [style.value for style in AnchorStyle],
- help = "(default: %(default)s) Anchor style to use for links to options. \nOnly none is standard CommonMark."
+ choices=[style.value for style in AnchorStyle],
+ help="(default: %(default)s) Anchor style to use for links to options. \nOnly none is standard CommonMark.",
)
- p.add_argument('--anchor-prefix',
+ p.add_argument(
+ "--anchor-prefix",
required=False,
default="",
- help="(default: no prefix) String to prepend to anchor ids. Not used when anchor style is none."
+ help="(default: no prefix) String to prepend to anchor ids. Not used when anchor style is none.",
)
p.add_argument("infile")
p.add_argument("outfile")
+
def _build_cli_asciidoc(p: argparse.ArgumentParser) -> None:
- p.add_argument('--manpage-urls', required=True)
- p.add_argument('--revision', required=True)
+ p.add_argument("--manpage-urls", required=True)
+ p.add_argument("--revision", required=True)
p.add_argument("infile")
p.add_argument("outfile")
+
def _run_cli_manpage(args: argparse.Namespace) -> None:
header = None
footer = None
@@ -551,49 +623,55 @@ def _run_cli_manpage(args: argparse.Namespace) -> None:
footer = f.read().splitlines()
md = ManpageConverter(
- revision = args.revision,
- header = header,
- footer = footer,
+ revision=args.revision,
+ header=header,
+ footer=footer,
)
- with open(args.infile, 'r') as f:
+ with open(args.infile, "r") as f:
md.add_options(json.load(f))
- with open(args.outfile, 'w') as f:
+ with open(args.outfile, "w") as f:
f.write(md.finalize())
+
def _run_cli_commonmark(args: argparse.Namespace) -> None:
- with open(args.manpage_urls, 'r') as manpage_urls:
- md = CommonMarkConverter(json.load(manpage_urls),
- revision = args.revision,
- anchor_style = parse_anchor_style(args.anchor_style),
- anchor_prefix = args.anchor_prefix)
+ with open(args.manpage_urls, "r") as manpage_urls:
+ md = CommonMarkConverter(
+ json.load(manpage_urls),
+ revision=args.revision,
+ anchor_style=parse_anchor_style(args.anchor_style),
+ anchor_prefix=args.anchor_prefix,
+ )
- with open(args.infile, 'r') as f:
+ with open(args.infile, "r") as f:
md.add_options(json.load(f))
- with open(args.outfile, 'w') as f:
+ with open(args.outfile, "w") as f:
f.write(md.finalize())
+
def _run_cli_asciidoc(args: argparse.Namespace) -> None:
- with open(args.manpage_urls, 'r') as manpage_urls:
- md = AsciiDocConverter(json.load(manpage_urls), revision = args.revision)
+ with open(args.manpage_urls, "r") as manpage_urls:
+ md = AsciiDocConverter(json.load(manpage_urls), revision=args.revision)
- with open(args.infile, 'r') as f:
+ with open(args.infile, "r") as f:
md.add_options(json.load(f))
- with open(args.outfile, 'w') as 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_manpage(formats.add_parser('manpage'))
- _build_cli_commonmark(formats.add_parser('commonmark'))
- _build_cli_asciidoc(formats.add_parser('asciidoc'))
+ formats = p.add_subparsers(dest="format", required=True)
+ _build_cli_manpage(formats.add_parser("manpage"))
+ _build_cli_commonmark(formats.add_parser("commonmark"))
+ _build_cli_asciidoc(formats.add_parser("asciidoc"))
+
def run_cli(args: argparse.Namespace) -> None:
- if args.format == 'manpage':
+ if args.format == "manpage":
_run_cli_manpage(args)
- elif args.format == 'commonmark':
+ elif args.format == "commonmark":
_run_cli_commonmark(args)
- elif args.format == 'asciidoc':
+ elif args.format == "asciidoc":
_run_cli_asciidoc(args)
else:
- raise RuntimeError('format not hooked up', args)
+ raise RuntimeError("format not hooked up", args)
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/parallel.py b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/parallel.py
index ad58bf0264067..ee4ba3c095449 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/parallel.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/parallel.py
@@ -6,10 +6,10 @@
from typing import Any, Callable, Iterable, Optional, TypeVar
-R = TypeVar('R')
-S = TypeVar('S')
-T = TypeVar('T')
-A = TypeVar('A')
+R = TypeVar("R")
+S = TypeVar("S")
+T = TypeVar("T")
+A = TypeVar("A")
pool_processes: Optional[int] = None
@@ -21,10 +21,12 @@
_map_worker_state_fn: Any = None
_map_worker_state_arg: Any = None
+
def _map_worker_init(*args: Any) -> None:
global _map_worker_fn, _map_worker_state_fn, _map_worker_state_arg
(_map_worker_fn, _map_worker_state_fn, _map_worker_state_arg) = args
+
# NOTE: the state argument is never passed by any caller, we only use it as a localized
# cache for the created state in lieu of another global. it is effectively a global though.
def _map_worker_step(arg: Any, state: Any = []) -> Any:
@@ -35,8 +37,14 @@ def _map_worker_step(arg: Any, state: Any = []) -> Any:
state.append(_map_worker_state_fn(_map_worker_state_arg))
return _map_worker_fn(state[0], arg)
-def map(fn: Callable[[S, T], R], d: Iterable[T], chunk_size: int,
- state_fn: Callable[[A], S], state_arg: A) -> list[R]:
+
+def map(
+ fn: Callable[[S, T], R],
+ d: Iterable[T],
+ chunk_size: int,
+ state_fn: Callable[[A], S],
+ state_arg: A,
+) -> list[R]:
"""
`[ fn(state, i) for i in d ]` where `state = state_fn(state_arg)`, but using multiprocessing
if `pool_processes` is not `None`. when using multiprocessing is used the state function will
@@ -53,6 +61,8 @@ def map(fn: Callable[[S, T], R], d: Iterable[T], chunk_size: int,
"""
if pool_processes is None:
state = state_fn(state_arg)
- return [ fn(state, i) for i in d ]
- with multiprocessing.Pool(pool_processes, _map_worker_init, (fn, state_fn, state_arg)) as p:
+ return [fn(state, i) for i in d]
+ with multiprocessing.Pool(
+ pool_processes, _map_worker_init, (fn, state_fn, state_arg)
+ ) as p:
return list(p.imap(_map_worker_step, d, chunk_size))
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py
index e8ddfee895ef1..96b9eccb1e4c8 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py
@@ -12,11 +12,13 @@ def __init__(
divergent_redirects: set[str] = None,
identifiers_missing_current_outpath: set[str] = None,
identifiers_without_redirects: set[str] = None,
- orphan_identifiers: set[str] = None
+ orphan_identifiers: set[str] = None,
):
self.conflicting_anchors = conflicting_anchors or set()
self.divergent_redirects = divergent_redirects or set()
- self.identifiers_missing_current_outpath = identifiers_missing_current_outpath or set()
+ self.identifiers_missing_current_outpath = (
+ identifiers_missing_current_outpath or set()
+ )
self.identifiers_without_redirects = identifiers_without_redirects or set()
self.orphan_identifiers = orphan_identifiers or set()
@@ -53,7 +55,11 @@ def __str__(self):
error_messages.append(f"""
Keys of the redirects mapping must correspond to some identifier in the source.
- {"\n - ".join(self.orphan_identifiers)}""")
- if self.identifiers_without_redirects or self.orphan_identifiers or self.identifiers_missing_current_outpath:
+ if (
+ self.identifiers_without_redirects
+ or self.orphan_identifiers
+ or self.identifiers_missing_current_outpath
+ ):
error_messages.append(f"""
This can happen when an identifier was added, renamed, or removed.
@@ -77,7 +83,9 @@ def __str__(self):
NixOS:
$ nix-shell nixos/doc/manual
""")
- error_messages.append("NOTE: If your build passes locally and you see this message in CI, you probably need a rebase.")
+ error_messages.append(
+ "NOTE: If your build passes locally and you see this message in CI, you probably need a rebase."
+ )
return "\n".join(error_messages)
@@ -100,7 +108,13 @@ def validate(self, initial_xref_targets: dict[str, XrefTarget]):
- The first element of an identifier's redirects list must denote its current location.
"""
xref_targets = {}
- ignored_identifier_patterns = ("opt-", "auto-generated-", "function-library-", "service-opt-", "systemd-service-opt")
+ ignored_identifier_patterns = (
+ "opt-",
+ "auto-generated-",
+ "function-library-",
+ "service-opt-",
+ "systemd-service-opt",
+ )
for id, target in initial_xref_targets.items():
# filter out automatically generated identifiers from module options and library documentation
if id.startswith(ignored_identifier_patterns):
@@ -120,18 +134,23 @@ def validate(self, initial_xref_targets: dict[str, XrefTarget]):
if identifier not in xref_targets:
continue
- if not locations or locations[0] != f"{xref_targets[identifier].path}#{identifier}":
+ if (
+ not locations
+ or locations[0] != f"{xref_targets[identifier].path}#{identifier}"
+ ):
identifiers_missing_current_outpath.add(identifier)
for location in locations[1:]:
- if '#' in location:
- path, anchor = location.split('#')
+ if "#" in location:
+ path, anchor = location.split("#")
if anchor in identifiers_without_redirects:
identifiers_without_redirects.remove(anchor)
if location not in client_side_redirects:
- client_side_redirects[location] = f"{xref_targets[identifier].path}#{identifier}"
+ client_side_redirects[location] = (
+ f"{xref_targets[identifier].path}#{identifier}"
+ )
for identifier, xref_target in xref_targets.items():
if xref_target.path == path and anchor == identifier:
conflicting_anchors.add(anchor)
@@ -143,31 +162,35 @@ def validate(self, initial_xref_targets: dict[str, XrefTarget]):
else:
divergent_redirects.add(location)
- if any([
- conflicting_anchors,
- divergent_redirects,
- identifiers_missing_current_outpath,
- identifiers_without_redirects,
- orphan_identifiers
- ]):
+ if any(
+ [
+ conflicting_anchors,
+ divergent_redirects,
+ identifiers_missing_current_outpath,
+ identifiers_without_redirects,
+ orphan_identifiers,
+ ]
+ ):
raise RedirectsError(
conflicting_anchors=conflicting_anchors,
divergent_redirects=divergent_redirects,
identifiers_missing_current_outpath=identifiers_missing_current_outpath,
identifiers_without_redirects=identifiers_without_redirects,
- orphan_identifiers=orphan_identifiers
+ orphan_identifiers=orphan_identifiers,
)
self._xref_targets = xref_targets
def get_client_redirects(self, target: str):
- paths_to_target = {src for src, dest in self.get_server_redirects().items() if dest == target}
+ paths_to_target = {
+ src for src, dest in self.get_server_redirects().items() if dest == target
+ }
client_redirects = {}
for locations in self._raw_redirects.values():
for location in locations[1:]:
- if '#' not in location:
+ if "#" not in location:
continue
- path, anchor = location.split('#')
+ path, anchor = location.split("#")
if path not in [target, *paths_to_target]:
continue
client_redirects[anchor] = locations[0]
@@ -177,10 +200,12 @@ def get_server_redirects(self):
server_redirects = {}
for identifier, locations in self._raw_redirects.items():
for location in locations[1:]:
- if '#' not in location and location not in server_redirects:
+ if "#" not in location and location not in server_redirects:
server_redirects[location] = self._xref_targets[identifier].path
return server_redirects
def get_redirect_script(self, target: str) -> str:
client_redirects = self.get_client_redirects(target)
- return self._redirects_script.replace('REDIRECTS_PLACEHOLDER', json.dumps(client_redirects))
+ return self._redirects_script.replace(
+ "REDIRECTS_PLACEHOLDER", json.dumps(client_redirects)
+ )
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/types.py b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/types.py
index b5c6e91a9b031..560f84a6cf503 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/types.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/types.py
@@ -7,13 +7,16 @@
OptionLoc = str | dict[str, str]
Option = dict[str, str | dict[str, str] | list[OptionLoc]]
+
class RenderedOption(NamedTuple):
loc: list[str]
lines: list[str]
links: Optional[list[str]] = None
+
RenderFn = Callable[[Token, Sequence[Token], int], str]
+
class AnchorStyle(Enum):
NONE = "none"
LEGACY = "legacy"
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/utils.py b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/utils.py
index 3377d1fa4fe18..aa0d2fe8c5c60 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/utils.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/utils.py
@@ -2,6 +2,7 @@
_frozen_classes: dict[type, type] = {}
+
# make a derived class freezable (ie, disallow modifications).
# we do this by changing the class of an instance at runtime when freeze()
# is called, providing a derived class that is exactly the same except
@@ -12,10 +13,16 @@ class Freezeable:
def freeze(self) -> None:
cls = type(self)
if not (frozen := _frozen_classes.get(cls)):
+
def __setattr__(instance: Any, n: str, v: Any) -> None:
- raise TypeError(f'{cls.__name__} is frozen')
- frozen = type(cls.__name__, (cls,), {
- '__setattr__': __setattr__,
- })
+ raise TypeError(f"{cls.__name__} is frozen")
+
+ frozen = type(
+ cls.__name__,
+ (cls,),
+ {
+ "__setattr__": __setattr__,
+ },
+ )
_frozen_classes[cls] = frozen
self.__class__ = frozen
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/tests/test_asciidoc.py b/pkgs/by-name/ni/nixos-render-docs/src/tests/test_asciidoc.py
index 3cf5b208f3923..46516b28a78cc 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/tests/test_asciidoc.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/tests/test_asciidoc.py
@@ -2,15 +2,18 @@
from sample_md import sample1
+
class Converter(nrd.md.Converter[nrd.asciidoc.AsciiDocRenderer]):
def __init__(self, manpage_urls: dict[str, str]):
super().__init__()
self._renderer = nrd.asciidoc.AsciiDocRenderer(manpage_urls)
+
def test_lists() -> None:
c = Converter({})
# attaching to the nth ancestor list requires n newlines before the +
- assert c._render("""\
+ assert (
+ c._render("""\
- a
b
@@ -21,7 +24,8 @@ def test_lists() -> None:
1
f
-""") == """\
+""")
+ == """\
[]
* {empty}a
+
@@ -41,10 +45,14 @@ def test_lists() -> None:
+
f
"""
+ )
+
def test_full() -> None:
- c = Converter({ 'man(1)': 'http://example.org' })
- assert c._render(sample1) == """\
+ c = Converter({"man(1)": "http://example.org"})
+ assert (
+ c._render(sample1)
+ == """\
[WARNING]
====
foo
@@ -143,3 +151,4 @@ def test_full() -> None:
more stuff in same deflist:: {empty}foo
"""
+ )
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/tests/test_auto_id_prefix.py b/pkgs/by-name/ni/nixos-render-docs/src/tests/test_auto_id_prefix.py
index 6fb706bad5ac1..3581bc3d76b73 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/tests/test_auto_id_prefix.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/tests/test_auto_id_prefix.py
@@ -4,7 +4,9 @@
from nixos_render_docs.manual import HTMLConverter, HTMLParameters
from nixos_render_docs.md import Converter
-auto_id_prefix="TEST_PREFIX"
+auto_id_prefix = "TEST_PREFIX"
+
+
def set_prefix(token: Token, ident: str) -> None:
token.attrs["id"] = f"{auto_id_prefix}-{ident}"
@@ -24,10 +26,7 @@ def test_auto_id_prefix_simple() -> None:
{**token.attrs, "tag": token.tag}
for token in tokens
if token.type == "heading_open"
- ] == [
- {"id": "TEST_PREFIX-1", "tag": "h1"},
- {"id": "TEST_PREFIX-1.1", "tag": "h2"}
- ]
+ ] == [{"id": "TEST_PREFIX-1", "tag": "h1"}, {"id": "TEST_PREFIX-1.1", "tag": "h2"}]
def test_auto_id_prefix_repeated() -> None:
@@ -56,6 +55,7 @@ def test_auto_id_prefix_repeated() -> None:
{"id": "TEST_PREFIX-2.1", "tag": "h2"},
]
+
def test_auto_id_prefix_maximum_nested() -> None:
md = HTMLConverter("1.0.0", HTMLParameters("", [], [], 2, 2, 2, Path("")), {})
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/tests/test_commonmark.py b/pkgs/by-name/ni/nixos-render-docs/src/tests/test_commonmark.py
index 4ff0bc3095c3d..d19bb227c6973 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/tests/test_commonmark.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/tests/test_commonmark.py
@@ -10,9 +10,11 @@ def __init__(self, manpage_urls: Mapping[str, str]):
super().__init__()
self._renderer = nrd.commonmark.CommonMarkRenderer(manpage_urls)
+
# NOTE: in these tests we represent trailing spaces by ` ` and replace them with real space later,
# since a number of editors will strip trailing whitespace on save and that would break the tests.
+
def test_indented_fence() -> None:
c = Converter({})
s = """\
@@ -21,12 +23,15 @@ def test_indented_fence() -> None:
>
> rest
> ```\
-""".replace(' ', ' ')
+""".replace(" ", " ")
assert c._render(s) == s
+
def test_full() -> None:
- c = Converter({ 'man(1)': 'http://example.org' })
- assert c._render(sample1) == """\
+ c = Converter({"man(1)": "http://example.org"})
+ assert (
+ c._render(sample1)
+ == """\
**Warning:** foo
**Note:** nested
@@ -90,10 +95,12 @@ def test_full() -> None:
- *more stuff in same deflist*
- foo""".replace(' ', ' ')
+ foo""".replace(" ", " ")
+ )
+
def test_images() -> None:
c = Converter({})
- assert c._render("") == (
- ""
+ assert c._render('') == (
+ ''
)
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/tests/test_headings.py b/pkgs/by-name/ni/nixos-render-docs/src/tests/test_headings.py
index d2f7c5cbe69ec..0b4dd5c250f84 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/tests/test_headings.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/tests/test_headings.py
@@ -2,103 +2,466 @@
from markdown_it.token import Token
+
class Converter(nrd.md.Converter[nrd.html.HTMLRenderer]):
# actual renderer doesn't matter, we're just parsing.
def __init__(self, manpage_urls: dict[str, str]) -> None:
super().__init__()
self._renderer = nrd.html.HTMLRenderer(manpage_urls, {})
+
def test_heading_id_absent() -> None:
c = Converter({})
assert c._parse("# foo") == [
- Token(type='heading_open', tag='h1', nesting=1, attrs={}, map=[0, 1], level=0, children=None,
- content='', markup='#', info='', meta={}, block=True, hidden=False),
- Token(type='inline', tag='', nesting=0, attrs={}, map=[0, 1], level=1,
- children=[
- Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None,
- content='foo', markup='', info='', meta={}, block=False, hidden=False)
- ],
- content='foo', markup='', info='', meta={}, block=True, hidden=False),
- Token(type='heading_close', tag='h1', nesting=-1, attrs={}, map=None, level=0, children=None,
- content='', markup='#', info='', meta={}, block=True, hidden=False)
+ Token(
+ type="heading_open",
+ tag="h1",
+ nesting=1,
+ attrs={},
+ map=[0, 1],
+ level=0,
+ children=None,
+ content="",
+ markup="#",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
+ Token(
+ type="inline",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=[0, 1],
+ level=1,
+ children=[
+ Token(
+ type="text",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="foo",
+ markup="",
+ info="",
+ meta={},
+ block=False,
+ hidden=False,
+ )
+ ],
+ content="foo",
+ markup="",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
+ Token(
+ type="heading_close",
+ tag="h1",
+ nesting=-1,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="",
+ markup="#",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
]
+
def test_heading_id_present() -> None:
c = Converter({})
assert c._parse("# foo {#foo}\n## bar { #bar}\n### bal { #bal} ") == [
- Token(type='heading_open', tag='h1', nesting=1, attrs={'id': 'foo'}, map=[0, 1], level=0,
- children=None, content='', markup='#', info='', meta={}, block=True, hidden=False),
- Token(type='inline', tag='', nesting=0, attrs={}, map=[0, 1], level=1,
- content='foo {#foo}', markup='', info='', meta={}, block=True, hidden=False,
- children=[
- Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None,
- content='foo', markup='', info='', meta={}, block=False, hidden=False)
- ]),
- Token(type='heading_close', tag='h1', nesting=-1, attrs={}, map=None, level=0, children=None,
- content='', markup='#', info='', meta={}, block=True, hidden=False),
- Token(type='heading_open', tag='h2', nesting=1, attrs={'id': 'bar'}, map=[1, 2], level=0,
- children=None, content='', markup='##', info='', meta={}, block=True, hidden=False),
- Token(type='inline', tag='', nesting=0, attrs={}, map=[1, 2], level=1,
- content='bar { #bar}', markup='', info='', meta={}, block=True, hidden=False,
- children=[
- Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None,
- content='bar', markup='', info='', meta={}, block=False, hidden=False)
- ]),
- Token(type='heading_close', tag='h2', nesting=-1, attrs={}, map=None, level=0, children=None,
- content='', markup='##', info='', meta={}, block=True, hidden=False),
- Token(type='heading_open', tag='h3', nesting=1, attrs={'id': 'bal'}, map=[2, 3], level=0,
- children=None, content='', markup='###', info='', meta={}, block=True, hidden=False),
- Token(type='inline', tag='', nesting=0, attrs={}, map=[2, 3], level=1,
- content='bal { #bal}', markup='', info='', meta={}, block=True, hidden=False,
- children=[
- Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None,
- content='bal', markup='', info='', meta={}, block=False, hidden=False)
- ]),
- Token(type='heading_close', tag='h3', nesting=-1, attrs={}, map=None, level=0, children=None,
- content='', markup='###', info='', meta={}, block=True, hidden=False)
+ Token(
+ type="heading_open",
+ tag="h1",
+ nesting=1,
+ attrs={"id": "foo"},
+ map=[0, 1],
+ level=0,
+ children=None,
+ content="",
+ markup="#",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
+ Token(
+ type="inline",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=[0, 1],
+ level=1,
+ content="foo {#foo}",
+ markup="",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ children=[
+ Token(
+ type="text",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="foo",
+ markup="",
+ info="",
+ meta={},
+ block=False,
+ hidden=False,
+ )
+ ],
+ ),
+ Token(
+ type="heading_close",
+ tag="h1",
+ nesting=-1,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="",
+ markup="#",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
+ Token(
+ type="heading_open",
+ tag="h2",
+ nesting=1,
+ attrs={"id": "bar"},
+ map=[1, 2],
+ level=0,
+ children=None,
+ content="",
+ markup="##",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
+ Token(
+ type="inline",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=[1, 2],
+ level=1,
+ content="bar { #bar}",
+ markup="",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ children=[
+ Token(
+ type="text",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="bar",
+ markup="",
+ info="",
+ meta={},
+ block=False,
+ hidden=False,
+ )
+ ],
+ ),
+ Token(
+ type="heading_close",
+ tag="h2",
+ nesting=-1,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="",
+ markup="##",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
+ Token(
+ type="heading_open",
+ tag="h3",
+ nesting=1,
+ attrs={"id": "bal"},
+ map=[2, 3],
+ level=0,
+ children=None,
+ content="",
+ markup="###",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
+ Token(
+ type="inline",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=[2, 3],
+ level=1,
+ content="bal { #bal}",
+ markup="",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ children=[
+ Token(
+ type="text",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="bal",
+ markup="",
+ info="",
+ meta={},
+ block=False,
+ hidden=False,
+ )
+ ],
+ ),
+ Token(
+ type="heading_close",
+ tag="h3",
+ nesting=-1,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="",
+ markup="###",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
]
+
def test_heading_id_incomplete() -> None:
c = Converter({})
assert c._parse("# foo {#}") == [
- Token(type='heading_open', tag='h1', nesting=1, attrs={}, map=[0, 1], level=0, children=None,
- content='', markup='#', info='', meta={}, block=True, hidden=False),
- Token(type='inline', tag='', nesting=0, attrs={}, map=[0, 1], level=1,
- content='foo {#}', markup='', info='', meta={}, block=True, hidden=False,
- children=[
- Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None,
- content='foo {#}', markup='', info='', meta={}, block=False, hidden=False)
- ]),
- Token(type='heading_close', tag='h1', nesting=-1, attrs={}, map=None, level=0, children=None,
- content='', markup='#', info='', meta={}, block=True, hidden=False)
+ Token(
+ type="heading_open",
+ tag="h1",
+ nesting=1,
+ attrs={},
+ map=[0, 1],
+ level=0,
+ children=None,
+ content="",
+ markup="#",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
+ Token(
+ type="inline",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=[0, 1],
+ level=1,
+ content="foo {#}",
+ markup="",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ children=[
+ Token(
+ type="text",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="foo {#}",
+ markup="",
+ info="",
+ meta={},
+ block=False,
+ hidden=False,
+ )
+ ],
+ ),
+ Token(
+ type="heading_close",
+ tag="h1",
+ nesting=-1,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="",
+ markup="#",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
]
+
def test_heading_id_double() -> None:
c = Converter({})
assert c._parse("# foo {#a} {#b}") == [
- Token(type='heading_open', tag='h1', nesting=1, attrs={'id': 'b'}, map=[0, 1], level=0,
- children=None, content='', markup='#', info='', meta={}, block=True, hidden=False),
- Token(type='inline', tag='', nesting=0, attrs={}, map=[0, 1], level=1,
- content='foo {#a} {#b}', markup='', info='', meta={}, block=True, hidden=False,
- children=[
- Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None,
- content='foo {#a}', markup='', info='', meta={}, block=False, hidden=False)
- ]),
- Token(type='heading_close', tag='h1', nesting=-1, attrs={}, map=None, level=0, children=None,
- content='', markup='#', info='', meta={}, block=True, hidden=False)
+ Token(
+ type="heading_open",
+ tag="h1",
+ nesting=1,
+ attrs={"id": "b"},
+ map=[0, 1],
+ level=0,
+ children=None,
+ content="",
+ markup="#",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
+ Token(
+ type="inline",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=[0, 1],
+ level=1,
+ content="foo {#a} {#b}",
+ markup="",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ children=[
+ Token(
+ type="text",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="foo {#a}",
+ markup="",
+ info="",
+ meta={},
+ block=False,
+ hidden=False,
+ )
+ ],
+ ),
+ Token(
+ type="heading_close",
+ tag="h1",
+ nesting=-1,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="",
+ markup="#",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
]
+
def test_heading_id_suffixed() -> None:
c = Converter({})
assert c._parse("# foo {#a} s") == [
- Token(type='heading_open', tag='h1', nesting=1, attrs={}, map=[0, 1], level=0,
- children=None, content='', markup='#', info='', meta={}, block=True, hidden=False),
- Token(type='inline', tag='', nesting=0, attrs={}, map=[0, 1], level=1,
- content='foo {#a} s', markup='', info='', meta={}, block=True, hidden=False,
- children=[
- Token(type='text', tag='', nesting=0, attrs={}, map=None, level=0, children=None,
- content='foo {#a} s', markup='', info='', meta={}, block=False, hidden=False)
- ]),
- Token(type='heading_close', tag='h1', nesting=-1, attrs={}, map=None, level=0, children=None,
- content='', markup='#', info='', meta={}, block=True, hidden=False)
+ Token(
+ type="heading_open",
+ tag="h1",
+ nesting=1,
+ attrs={},
+ map=[0, 1],
+ level=0,
+ children=None,
+ content="",
+ markup="#",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
+ Token(
+ type="inline",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=[0, 1],
+ level=1,
+ content="foo {#a} s",
+ markup="",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ children=[
+ Token(
+ type="text",
+ tag="",
+ nesting=0,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="foo {#a} s",
+ markup="",
+ info="",
+ meta={},
+ block=False,
+ hidden=False,
+ )
+ ],
+ ),
+ Token(
+ type="heading_close",
+ tag="h1",
+ nesting=-1,
+ attrs={},
+ map=None,
+ level=0,
+ children=None,
+ content="",
+ markup="#",
+ info="",
+ meta={},
+ block=True,
+ hidden=False,
+ ),
]
diff --git a/pkgs/by-name/ni/nixos-render-docs/src/tests/test_html.py b/pkgs/by-name/ni/nixos-render-docs/src/tests/test_html.py
index 9a3e07cb24c7a..5627366419f46 100644
--- a/pkgs/by-name/ni/nixos-render-docs/src/tests/test_html.py
+++ b/pkgs/by-name/ni/nixos-render-docs/src/tests/test_html.py
@@ -4,17 +4,25 @@
from sample_md import sample1
+
class Renderer(nrd.html.HTMLRenderer):
def _pull_image(self, src: str) -> str:
return src
+
class Converter(nrd.md.Converter[nrd.html.HTMLRenderer]):
- def __init__(self, manpage_urls: dict[str, str], xrefs: dict[str, nrd.manual_structure.XrefTarget]):
+ def __init__(
+ self,
+ manpage_urls: dict[str, str],
+ xrefs: dict[str, nrd.manual_structure.XrefTarget],
+ ):
super().__init__()
self._renderer = Renderer(manpage_urls, xrefs)
+
def unpretty(s: str) -> str:
- return "".join(map(str.strip, s.splitlines())).replace('␣', ' ').replace('↵', '\n')
+ return "".join(map(str.strip, s.splitlines())).replace("␣", " ").replace("↵", "\n")
+
def test_lists_styles() -> None:
# nested lists rotate through a number of list style
@@ -62,21 +70,36 @@ def test_lists_styles() -> None:
""")
+
def test_xrefs() -> None:
# nested lists rotate through a number of list style
- c = Converter({}, {
- 'foo': nrd.manual_structure.XrefTarget('foo', '
', 'toc1', 'title1', 'index.html'),
- 'bar': nrd.manual_structure.XrefTarget('bar', '
', 'toc2', 'title2', 'index.html', True),
- })
- assert c._render("[](#foo)") == '
'
- assert c._render("[](#bar)") == '
'
+ c = Converter(
+ {},
+ {
+ "foo": nrd.manual_structure.XrefTarget(
+ "foo", "
", "toc1", "title1", "index.html"
+ ),
+ "bar": nrd.manual_structure.XrefTarget(
+ "bar", "
", "toc2", "title2", "index.html", True
+ ),
+ },
+ )
+ assert (
+ c._render("[](#foo)")
+ == '
'
+ )
+ assert (
+ c._render("[](#bar)")
+ == '
'
+ )
with pytest.raises(nrd.html.UnresolvedXrefError) as exc:
c._render("[](#baz)")
- assert exc.value.args[0] == 'bad local reference, id #baz not known'
+ assert exc.value.args[0] == "bad local reference, id #baz not known"
+
def test_images() -> None:
c = Converter({}, {})
- assert c._render("") == unpretty("""
+ assert c._render('') == unpretty("""