lol

nixos-render-docs: add footnote support

this is only used in the stdenv chapter, but footnotes could be useful
in other places as well. since markdown-it has a plugin to parse
footnote syntax we may as well just support them even if they're rare.

pennae 538b3d1b ac7be1f1

+132
+29
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py
··· 298 298 return f'<td align="{cast(str, token.attrs.get("style", "left")).removeprefix("text-align:")}">' 299 299 def td_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 300 300 return "</td>" 301 + def footnote_ref(self, token: Token, tokens: Sequence[Token], i: int) -> str: 302 + href = self._xref_targets[token.meta['target']].href() 303 + id = escape(cast(str, token.attrs["id"]), True) 304 + return ( 305 + f'<a href="{href}" class="footnote" id="{id}">' 306 + f'<sup class="footnote">[{token.meta["id"] + 1}]</sup>' 307 + '</a>' 308 + ) 309 + def footnote_block_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 310 + return ( 311 + '<div class="footnotes">' 312 + '<br />' 313 + '<hr style="width:100; text-align:left;margin-left: 0" />' 314 + ) 315 + def footnote_block_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 316 + return "</div>" 317 + def footnote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 318 + # meta id,label 319 + id = escape(self._xref_targets[token.meta["label"]].id, True) 320 + return f'<div id="{id}" class="footnote">' 321 + def footnote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 322 + return "</div>" 323 + def footnote_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str: 324 + href = self._xref_targets[token.meta['target']].href() 325 + return ( 326 + f'<a href="{href}" class="para">' 327 + f'<sup class="para">[{token.meta["id"] + 1}]</sup>' 328 + '</a>' 329 + ) 301 330 302 331 def _make_hN(self, level: int) -> tuple[str, str]: 303 332 return f"h{min(6, max(1, level + self._hlevel_offset))}", ""
+4
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py
··· 618 618 result.append((id, 'example', tokens[i + 2], target_file, False)) 619 619 elif bt.type == 'figure_open' and (id := cast(str, bt.attrs.get('id', ''))): 620 620 result.append((id, 'figure', tokens[i + 2], target_file, False)) 621 + elif bt.type == 'footnote_open' and (id := cast(str, bt.attrs.get('id', ''))): 622 + result.append(XrefTarget(id, "???", None, None, target_file)) 623 + elif bt.type == 'footnote_ref' and (id := cast(str, bt.attrs.get('id', ''))): 624 + result.append(XrefTarget(id, "???", None, None, target_file)) 621 625 elif bt.type == 'inline': 622 626 assert bt.children 623 627 result += self._collect_ids(bt.children, target_file, typ, False)
+47
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py
··· 12 12 from markdown_it.utils import OptionsDict 13 13 from mdit_py_plugins.container import container_plugin # type: ignore[attr-defined] 14 14 from mdit_py_plugins.deflist import deflist_plugin # type: ignore[attr-defined] 15 + from mdit_py_plugins.footnote import footnote_plugin # type: ignore[attr-defined] 15 16 from mdit_py_plugins.myst_role import myst_role_plugin # type: ignore[attr-defined] 16 17 17 18 _md_escape_table = { ··· 107 108 "tbody_close": self.tbody_close, 108 109 "td_open": self.td_open, 109 110 "td_close": self.td_close, 111 + "footnote_ref": self.footnote_ref, 112 + "footnote_block_open": self.footnote_block_open, 113 + "footnote_block_close": self.footnote_block_close, 114 + "footnote_open": self.footnote_open, 115 + "footnote_close": self.footnote_close, 116 + "footnote_anchor": self.footnote_anchor, 110 117 } 111 118 112 119 self._admonitions = { ··· 276 283 raise RuntimeError("md token not supported", token) 277 284 def td_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 278 285 raise RuntimeError("md token not supported", token) 286 + def footnote_ref(self, token: Token, tokens: Sequence[Token], i: int) -> str: 287 + raise RuntimeError("md token not supported", token) 288 + def footnote_block_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 289 + raise RuntimeError("md token not supported", token) 290 + def footnote_block_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 291 + raise RuntimeError("md token not supported", token) 292 + def footnote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 293 + raise RuntimeError("md token not supported", token) 294 + def footnote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 295 + raise RuntimeError("md token not supported", token) 296 + def footnote_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str: 297 + raise RuntimeError("md token not supported", token) 279 298 280 299 def _is_escaped(src: str, pos: int) -> bool: 281 300 found = 0 ··· 421 440 422 441 md.core.ruler.before("replacements", "heading_ids", heading_ids) 423 442 443 + def _footnote_ids(md: markdown_it.MarkdownIt) -> None: 444 + """generate ids for footnotes, their refs, and their backlinks. the ids we 445 + generate here are derived from the footnote label, making numeric footnote 446 + labels invalid. 447 + """ 448 + def generate_ids(tokens: Sequence[Token]) -> None: 449 + for token in tokens: 450 + if token.type == 'footnote_open': 451 + if token.meta["label"][:1].isdigit(): 452 + assert token.map 453 + raise RuntimeError(f"invalid footnote label in line {token.map[0] + 1}") 454 + token.attrs['id'] = token.meta["label"] 455 + elif token.type == 'footnote_anchor': 456 + token.meta['target'] = f'{token.meta["label"]}.__back.{token.meta["subId"]}' 457 + elif token.type == 'footnote_ref': 458 + token.attrs['id'] = f'{token.meta["label"]}.__back.{token.meta["subId"]}' 459 + token.meta['target'] = token.meta["label"] 460 + elif token.type == 'inline': 461 + assert token.children 462 + generate_ids(token.children) 463 + 464 + def footnote_ids(state: markdown_it.rules_core.StateCore) -> None: 465 + generate_ids(state.tokens) 466 + 467 + md.core.ruler.after("footnote_tail", "footnote_ids", footnote_ids) 468 + 424 469 def _compact_list_attr(md: markdown_it.MarkdownIt) -> None: 425 470 @dataclasses.dataclass 426 471 class Entry: ··· 549 594 validate=lambda name, *args: _parse_blockattrs(name), 550 595 ) 551 596 self._md.use(deflist_plugin) 597 + self._md.use(footnote_plugin) 552 598 self._md.use(myst_role_plugin) 553 599 self._md.use(_attr_span_plugin) 554 600 self._md.use(_inline_comment_plugin) 555 601 self._md.use(_block_comment_plugin) 556 602 self._md.use(_heading_ids) 603 + self._md.use(_footnote_ids) 557 604 self._md.use(_compact_list_attr) 558 605 self._md.use(_block_attr) 559 606 self._md.use(_block_titles("example"))
+27
pkgs/tools/nix/nixos-render-docs/src/tests/test_html.py
··· 119 119 </div> 120 120 """) 121 121 122 + def test_footnotes() -> None: 123 + c = Converter({}, { 124 + "bar": nrd.manual_structure.XrefTarget("bar", "", None, None, ""), 125 + "bar.__back.0": nrd.manual_structure.XrefTarget("bar.__back.0", "", None, None, ""), 126 + "bar.__back.1": nrd.manual_structure.XrefTarget("bar.__back.1", "", None, None, ""), 127 + }) 128 + assert c._render(textwrap.dedent(""" 129 + foo [^bar] baz [^bar] 130 + 131 + [^bar]: note 132 + """)) == unpretty(""" 133 + <p> 134 + foo <a href="#bar" class="footnote" id="bar.__back.0"><sup class="footnote">[1]</sup></a>␣ 135 + baz <a href="#bar" class="footnote" id="bar.__back.1"><sup class="footnote">[1]</sup></a> 136 + </p> 137 + <div class="footnotes"> 138 + <br /> 139 + <hr style="width:100; text-align:left;margin-left: 0" /> 140 + <div id="bar" class="footnote"> 141 + <p> 142 + note<a href="#bar.__back.0" class="para"><sup class="para">[1]</sup></a> 143 + <a href="#bar.__back.1" class="para"><sup class="para">[1]</sup></a> 144 + </p> 145 + </div> 146 + </div> 147 + """) 148 + 122 149 def test_full() -> None: 123 150 c = Converter({ 'man(1)': 'http://example.org' }, {}) 124 151 assert c._render(sample1) == unpretty("""
+25
pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py
··· 501 501 with pytest.raises(RuntimeError) as exc: 502 502 c._parse("::: {.example}\n### foo\n### bar\n:::") 503 503 assert exc.value.args[0] == 'unexpected non-title heading in example in line 3' 504 + 505 + def test_footnotes() -> None: 506 + c = Converter({}) 507 + assert c._parse("text [^foo]\n\n[^foo]: bar") == [ 508 + Token(type='paragraph_open', tag='p', nesting=1, map=[0, 1], block=True), 509 + Token(type='inline', tag='', nesting=0, map=[0, 1], level=1, content='text [^foo]', block=True, 510 + children=[ 511 + Token(type='text', tag='', nesting=0, content='text '), 512 + Token(type='footnote_ref', tag='', nesting=0, attrs={'id': 'foo.__back.0'}, 513 + meta={'id': 0, 'subId': 0, 'label': 'foo', 'target': 'foo'}) 514 + ]), 515 + Token(type='paragraph_close', tag='p', nesting=-1, block=True), 516 + Token(type='footnote_block_open', tag='', nesting=1), 517 + Token(type='footnote_open', tag='', nesting=1, attrs={'id': 'foo'}, meta={'id': 0, 'label': 'foo'}), 518 + Token(type='paragraph_open', tag='p', nesting=1, map=[2, 3], level=1, block=True, hidden=False), 519 + Token(type='inline', tag='', nesting=0, map=[2, 3], level=2, content='bar', block=True, 520 + children=[ 521 + Token(type='text', tag='', nesting=0, content='bar') 522 + ]), 523 + Token(type='footnote_anchor', tag='', nesting=0, 524 + meta={'id': 0, 'label': 'foo', 'subId': 0, 'target': 'foo.__back.0'}), 525 + Token(type='paragraph_close', tag='p', nesting=-1, level=1, block=True), 526 + Token(type='footnote_close', tag='', nesting=-1), 527 + Token(type='footnote_block_close', tag='', nesting=-1), 528 + ]