Merge pull request #217342 from pennae/nrd-html-manual

nixos-render-docs: add manual html renderer, use it for the nixos manual

authored by

Naïm Favier and committed by
GitHub
45e44c56 e3d3bf7f

+1721 -767
+61 -29
nixos/doc/manual/default.nix
··· 135 135 } 136 136 ''; 137 137 138 + prepareManualFromMD = '' 139 + cp -r --no-preserve=all $inputs/* . 140 + 141 + substituteInPlace ./manual.md \ 142 + --replace '@NIXOS_VERSION@' "${version}" 143 + substituteInPlace ./configuration/configuration.md \ 144 + --replace \ 145 + '@MODULE_CHAPTERS@' \ 146 + ${lib.escapeShellArg (lib.concatMapStringsSep "\n" (p: "${p.value}") config.meta.doc)} 147 + substituteInPlace ./nixos-options.md \ 148 + --replace \ 149 + '@NIXOS_OPTIONS_JSON@' \ 150 + ${optionsDoc.optionsJSON}/share/doc/nixos/options.json 151 + substituteInPlace ./development/writing-nixos-tests.section.md \ 152 + --replace \ 153 + '@NIXOS_TEST_OPTIONS_JSON@' \ 154 + ${testOptionsDoc.optionsJSON}/share/doc/nixos/options.json 155 + ''; 156 + 138 157 manual-combined = runCommand "nixos-manual-combined" 139 158 { inputs = lib.sourceFilesBySuffices ./. [ ".xml" ".md" ]; 140 159 nativeBuildInputs = [ pkgs.nixos-render-docs pkgs.libxml2.bin pkgs.libxslt.bin ]; 141 160 meta.description = "The NixOS manual as plain docbook XML"; 142 161 } 143 162 '' 144 - cp -r --no-preserve=all $inputs/* . 145 - 146 - substituteInPlace ./manual.md \ 147 - --replace '@NIXOS_VERSION@' "${version}" 148 - substituteInPlace ./configuration/configuration.md \ 149 - --replace \ 150 - '@MODULE_CHAPTERS@' \ 151 - ${lib.escapeShellArg (lib.concatMapStringsSep "\n" (p: "${p.value}") config.meta.doc)} 152 - substituteInPlace ./nixos-options.md \ 153 - --replace \ 154 - '@NIXOS_OPTIONS_JSON@' \ 155 - ${optionsDoc.optionsJSON}/share/doc/nixos/options.json 156 - substituteInPlace ./development/writing-nixos-tests.section.md \ 157 - --replace \ 158 - '@NIXOS_TEST_OPTIONS_JSON@' \ 159 - ${testOptionsDoc.optionsJSON}/share/doc/nixos/options.json 163 + ${prepareManualFromMD} 160 164 161 165 nixos-render-docs -j $NIX_BUILD_CORES manual docbook \ 162 166 --manpage-urls ${manpageUrls} \ ··· 193 197 194 198 # Generate the NixOS manual. 195 199 manualHTML = runCommand "nixos-manual-html" 196 - { nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.libxslt.bin ]; 200 + { nativeBuildInputs = 201 + if allowDocBook then [ 202 + buildPackages.libxml2.bin 203 + buildPackages.libxslt.bin 204 + ] else [ 205 + buildPackages.nixos-render-docs 206 + ]; 207 + inputs = lib.optionals (! allowDocBook) (lib.sourceFilesBySuffices ./. [ ".md" ]); 197 208 meta.description = "The NixOS manual in HTML format"; 198 209 allowedReferences = ["out"]; 199 210 } ··· 201 212 # Generate the HTML manual. 202 213 dst=$out/share/doc/nixos 203 214 mkdir -p $dst 204 - xsltproc \ 205 - ${manualXsltprocOptions} \ 206 - --stringparam id.warnings "1" \ 207 - --nonet --output $dst/ \ 208 - ${docbook_xsl_ns}/xml/xsl/docbook/xhtml/chunktoc.xsl \ 209 - ${manual-combined}/manual-combined.xml \ 210 - |& tee xsltproc.out 211 - grep "^ID recommended on" xsltproc.out &>/dev/null && echo "error: some IDs are missing" && false 212 - rm xsltproc.out 213 - 214 - mkdir -p $dst/images/callouts 215 - cp ${docbook_xsl_ns}/xml/xsl/docbook/images/callouts/*.svg $dst/images/callouts/ 216 215 217 216 cp ${../../../doc/style.css} $dst/style.css 218 217 cp ${../../../doc/overrides.css} $dst/overrides.css 219 218 cp -r ${pkgs.documentation-highlighter} $dst/highlightjs 219 + 220 + ${if allowDocBook then '' 221 + xsltproc \ 222 + ${manualXsltprocOptions} \ 223 + --stringparam id.warnings "1" \ 224 + --nonet --output $dst/ \ 225 + ${docbook_xsl_ns}/xml/xsl/docbook/xhtml/chunktoc.xsl \ 226 + ${manual-combined}/manual-combined.xml \ 227 + |& tee xsltproc.out 228 + grep "^ID recommended on" xsltproc.out &>/dev/null && echo "error: some IDs are missing" && false 229 + rm xsltproc.out 230 + 231 + mkdir -p $dst/images/callouts 232 + cp ${docbook_xsl_ns}/xml/xsl/docbook/images/callouts/*.svg $dst/images/callouts/ 233 + '' else '' 234 + ${prepareManualFromMD} 235 + 236 + # TODO generator is set like this because the docbook/md manual compare workflow will 237 + # trigger if it's different 238 + nixos-render-docs -j $NIX_BUILD_CORES manual html \ 239 + --manpage-urls ${manpageUrls} \ 240 + --revision ${lib.escapeShellArg revision} \ 241 + --generator "DocBook XSL Stylesheets V${docbook_xsl_ns.version}" \ 242 + --stylesheet style.css \ 243 + --stylesheet overrides.css \ 244 + --stylesheet highlightjs/mono-blue.css \ 245 + --script ./highlightjs/highlight.pack.js \ 246 + --script ./highlightjs/loader.js \ 247 + --toc-depth 1 \ 248 + --chunk-toc-depth 1 \ 249 + ./manual.md \ 250 + $dst/index.html 251 + ''} 220 252 221 253 mkdir -p $out/nix-support 222 254 echo "nix-build out $out" >> $out/nix-support/hydra-build-products
+4 -1
nixos/doc/manual/manual.md
··· 47 47 contributing-to-this-manual.chapter.md 48 48 ``` 49 49 50 - ```{=include=} appendix 50 + ```{=include=} appendix html:into-file=//options.html 51 51 nixos-options.md 52 + ``` 53 + 54 + ```{=include=} appendix html:into-file=//release-notes.html 52 55 release-notes/release-notes.md 53 56 ```
+2 -2
nixos/modules/services/web-apps/akkoma.md
··· 318 318 {option}`services.systemd.akkoma.serviceConfig.BindPaths` and 319 319 {option}`services.systemd.akkoma.serviceConfig.BindReadOnlyPaths` permit access to outside paths 320 320 through bind mounts. Refer to 321 - [{manpage}`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths=) 322 - for details. 321 + [`BindPaths=`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths=) 322 + of {manpage}`systemd.exec(5)` for details. 323 323 324 324 ### Distributed deployment {#modules-services-akkoma-distributed-deployment} 325 325
+1 -1
nixos/modules/system/boot/networkd.nix
··· 1948 1948 Extra command-line arguments to pass to systemd-networkd-wait-online. 1949 1949 These also affect per-interface `systemd-network-wait-online@` services. 1950 1950 1951 - See [{manpage}`systemd-networkd-wait-online.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-networkd-wait-online.service.html) for all available options. 1951 + See {manpage}`systemd-networkd-wait-online.service(8)` for all available options. 1952 1952 ''; 1953 1953 type = with types; listOf str; 1954 1954 default = [];
+49 -95
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/asciidoc.py
··· 1 - from collections.abc import Mapping, MutableMapping, Sequence 1 + from collections.abc import Mapping, Sequence 2 2 from dataclasses import dataclass 3 3 from typing import Any, cast, Optional 4 4 from urllib.parse import quote 5 5 6 6 from .md import Renderer 7 7 8 - import markdown_it 9 8 from markdown_it.token import Token 10 - from markdown_it.utils import OptionsDict 11 9 12 10 _asciidoc_escapes = { 13 11 # escape all dots, just in case one is pasted at SOL ··· 59 57 _list_stack: list[List] 60 58 _attrspans: list[str] 61 59 62 - def __init__(self, manpage_urls: Mapping[str, str], parser: Optional[markdown_it.MarkdownIt] = None): 63 - super().__init__(manpage_urls, parser) 60 + def __init__(self, manpage_urls: Mapping[str, str]): 61 + super().__init__(manpage_urls) 64 62 self._parstack = [ Par("\n\n", "====") ] 65 63 self._list_stack = [] 66 64 self._attrspans = [] ··· 96 94 self._list_stack.pop() 97 95 return "" 98 96 99 - def text(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 100 - env: MutableMapping[str, Any]) -> str: 97 + def text(self, token: Token, tokens: Sequence[Token], i: int) -> str: 101 98 self._parstack[-1].continuing = True 102 99 return asciidoc_escape(token.content) 103 - def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 104 - env: MutableMapping[str, Any]) -> str: 100 + def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 105 101 return self._break() 106 - def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 107 - env: MutableMapping[str, Any]) -> str: 102 + def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 108 103 return "" 109 - def hardbreak(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 110 - env: MutableMapping[str, Any]) -> str: 104 + def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 111 105 return " +\n" 112 - def softbreak(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 113 - env: MutableMapping[str, Any]) -> str: 106 + def softbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 114 107 return f" " 115 - def code_inline(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 116 - env: MutableMapping[str, Any]) -> str: 108 + def code_inline(self, token: Token, tokens: Sequence[Token], i: int) -> str: 117 109 self._parstack[-1].continuing = True 118 110 return f"``{asciidoc_escape(token.content)}``" 119 - def code_block(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 120 - env: MutableMapping[str, Any]) -> str: 121 - return self.fence(token, tokens, i, options, env) 122 - def link_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 123 - env: MutableMapping[str, Any]) -> str: 111 + def code_block(self, token: Token, tokens: Sequence[Token], i: int) -> str: 112 + return self.fence(token, tokens, i) 113 + def link_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 124 114 self._parstack[-1].continuing = True 125 115 return f"link:{quote(cast(str, token.attrs['href']), safe='/:')}[" 126 - def link_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 127 - env: MutableMapping[str, Any]) -> str: 116 + def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 128 117 return "]" 129 - def list_item_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 130 - env: MutableMapping[str, Any]) -> str: 118 + def list_item_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 131 119 self._enter_block(True) 132 120 # allow the next token to be a block or an inline. 133 121 return f'\n{self._list_stack[-1].head} {{empty}}' 134 - def list_item_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 135 - env: MutableMapping[str, Any]) -> str: 122 + def list_item_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 136 123 self._leave_block() 137 124 return "\n" 138 - def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 139 - env: MutableMapping[str, Any]) -> str: 125 + def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 140 126 return self._list_open(token, '*') 141 - def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 142 - env: MutableMapping[str, Any]) -> str: 127 + def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 143 128 return self._list_close() 144 - def em_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 145 - env: MutableMapping[str, Any]) -> str: 129 + def em_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 146 130 return "__" 147 - def em_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 148 - env: MutableMapping[str, Any]) -> str: 131 + def em_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 149 132 return "__" 150 - def strong_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 151 - env: MutableMapping[str, Any]) -> str: 133 + def strong_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 152 134 return "**" 153 - def strong_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 154 - env: MutableMapping[str, Any]) -> str: 135 + def strong_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 155 136 return "**" 156 - def fence(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 157 - env: MutableMapping[str, Any]) -> str: 137 + def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str: 158 138 attrs = f"[source,{token.info}]\n" if token.info else "" 159 139 code = token.content 160 140 if code.endswith('\n'): 161 141 code = code[:-1] 162 142 return f"{self._break(True)}{attrs}----\n{code}\n----" 163 - def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 164 - env: MutableMapping[str, Any]) -> str: 143 + def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 165 144 pbreak = self._break(True) 166 145 self._enter_block(False) 167 146 return f"{pbreak}[quote]\n{self._parstack[-2].block_delim}\n" 168 - def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 169 - env: MutableMapping[str, Any]) -> str: 147 + def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 170 148 self._leave_block() 171 149 return f"\n{self._parstack[-1].block_delim}" 172 - def note_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 173 - env: MutableMapping[str, Any]) -> str: 150 + def note_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 174 151 return self._admonition_open("NOTE") 175 - def note_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 176 - env: MutableMapping[str, Any]) -> str: 152 + def note_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 177 153 return self._admonition_close() 178 - def caution_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 179 - env: MutableMapping[str, Any]) -> str: 154 + def caution_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 180 155 return self._admonition_open("CAUTION") 181 - def caution_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 182 - env: MutableMapping[str, Any]) -> str: 156 + def caution_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 183 157 return self._admonition_close() 184 - def important_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 185 - env: MutableMapping[str, Any]) -> str: 158 + def important_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 186 159 return self._admonition_open("IMPORTANT") 187 - def important_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 188 - env: MutableMapping[str, Any]) -> str: 160 + def important_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 189 161 return self._admonition_close() 190 - def tip_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 191 - env: MutableMapping[str, Any]) -> str: 162 + def tip_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 192 163 return self._admonition_open("TIP") 193 - def tip_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 194 - env: MutableMapping[str, Any]) -> str: 164 + def tip_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 195 165 return self._admonition_close() 196 - def warning_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 197 - env: MutableMapping[str, Any]) -> str: 166 + def warning_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 198 167 return self._admonition_open("WARNING") 199 - def warning_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 200 - env: MutableMapping[str, Any]) -> str: 168 + def warning_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 201 169 return self._admonition_close() 202 - def dl_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 203 - env: MutableMapping[str, Any]) -> str: 170 + def dl_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 204 171 return f"{self._break()}[]" 205 - def dl_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 206 - env: MutableMapping[str, Any]) -> str: 172 + def dl_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 207 173 return "" 208 - def dt_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 209 - env: MutableMapping[str, Any]) -> str: 174 + def dt_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 210 175 return self._break() 211 - def dt_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 212 - env: MutableMapping[str, Any]) -> str: 176 + def dt_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 213 177 self._enter_block(True) 214 178 return ":: {empty}" 215 - def dd_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 216 - env: MutableMapping[str, Any]) -> str: 179 + def dd_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 217 180 return "" 218 - def dd_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 219 - env: MutableMapping[str, Any]) -> str: 181 + def dd_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 220 182 self._leave_block() 221 183 return "\n" 222 - def myst_role(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 223 - env: MutableMapping[str, Any]) -> str: 184 + def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str: 224 185 self._parstack[-1].continuing = True 225 186 content = asciidoc_escape(token.content) 226 187 if token.meta['name'] == 'manpage' and (url := self._manpage_urls.get(token.content)): 227 188 return f"link:{quote(url, safe='/:')}[{content}]" 228 189 return f"[.{token.meta['name']}]``{asciidoc_escape(token.content)}``" 229 - def inline_anchor(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 230 - env: MutableMapping[str, Any]) -> str: 190 + def inline_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str: 231 191 self._parstack[-1].continuing = True 232 192 return f"[[{token.attrs['id']}]]" 233 - def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 234 - env: MutableMapping[str, Any]) -> str: 193 + def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str: 235 194 self._parstack[-1].continuing = True 236 195 (id_part, class_part) = ("", "") 237 196 if id := token.attrs.get('id'): ··· 241 200 class_part = "kbd:[" 242 201 self._attrspans.append("]") 243 202 else: 244 - return super().attr_span_begin(token, tokens, i, options, env) 203 + return super().attr_span_begin(token, tokens, i) 245 204 else: 246 205 self._attrspans.append("") 247 206 return id_part + class_part 248 - def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 249 - env: MutableMapping[str, Any]) -> str: 207 + def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str: 250 208 return self._attrspans.pop() 251 - def heading_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 252 - env: MutableMapping[str, Any]) -> str: 209 + def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 253 210 return token.markup.replace("#", "=") + " " 254 - def heading_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 255 - env: MutableMapping[str, Any]) -> str: 211 + def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 256 212 return "\n" 257 - def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 258 - env: MutableMapping[str, Any]) -> str: 213 + def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 259 214 return self._list_open(token, '.') 260 - def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 261 - env: MutableMapping[str, Any]) -> str: 215 + def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 262 216 return self._list_close()
+47 -92
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/commonmark.py
··· 1 - from collections.abc import Mapping, MutableMapping, Sequence 1 + from collections.abc import Mapping, Sequence 2 2 from dataclasses import dataclass 3 3 from typing import Any, cast, Optional 4 4 5 5 from .md import md_escape, md_make_code, Renderer 6 6 7 - import markdown_it 8 7 from markdown_it.token import Token 9 - from markdown_it.utils import OptionsDict 10 8 11 9 @dataclass(kw_only=True) 12 10 class List: ··· 26 24 _link_stack: list[str] 27 25 _list_stack: list[List] 28 26 29 - def __init__(self, manpage_urls: Mapping[str, str], parser: Optional[markdown_it.MarkdownIt] = None): 30 - super().__init__(manpage_urls, parser) 27 + def __init__(self, manpage_urls: Mapping[str, str]): 28 + super().__init__(manpage_urls) 31 29 self._parstack = [ Par("") ] 32 30 self._link_stack = [] 33 31 self._list_stack = [] ··· 58 56 return s 59 57 return f"\n{self._parstack[-1].indent}".join(s.splitlines()) 60 58 61 - def text(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 62 - env: MutableMapping[str, Any]) -> str: 59 + def text(self, token: Token, tokens: Sequence[Token], i: int) -> str: 63 60 self._parstack[-1].continuing = True 64 61 return self._indent_raw(md_escape(token.content)) 65 - def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 66 - env: MutableMapping[str, Any]) -> str: 62 + def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 67 63 return self._maybe_parbreak() 68 - def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 69 - env: MutableMapping[str, Any]) -> str: 64 + def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 70 65 return "" 71 - def hardbreak(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 72 - env: MutableMapping[str, Any]) -> str: 66 + def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 73 67 return f" {self._break()}" 74 - def softbreak(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 75 - env: MutableMapping[str, Any]) -> str: 68 + def softbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 76 69 return self._break() 77 - def code_inline(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 78 - env: MutableMapping[str, Any]) -> str: 70 + def code_inline(self, token: Token, tokens: Sequence[Token], i: int) -> str: 79 71 self._parstack[-1].continuing = True 80 72 return md_make_code(token.content) 81 - def code_block(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 82 - env: MutableMapping[str, Any]) -> str: 83 - return self.fence(token, tokens, i, options, env) 84 - def link_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 85 - env: MutableMapping[str, Any]) -> str: 73 + def code_block(self, token: Token, tokens: Sequence[Token], i: int) -> str: 74 + return self.fence(token, tokens, i) 75 + def link_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 86 76 self._parstack[-1].continuing = True 87 77 self._link_stack.append(cast(str, token.attrs['href'])) 88 78 return "[" 89 - def link_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 90 - env: MutableMapping[str, Any]) -> str: 79 + def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 91 80 return f"]({md_escape(self._link_stack.pop())})" 92 - def list_item_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 93 - env: MutableMapping[str, Any]) -> str: 81 + def list_item_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 94 82 lst = self._list_stack[-1] 95 83 lbreak = "" if not lst.first_item_seen else self._break() * (1 if lst.compact else 2) 96 84 lst.first_item_seen = True ··· 100 88 lst.next_idx += 1 101 89 self._enter_block(" " * (len(head) + 1)) 102 90 return f'{lbreak}{head} ' 103 - def list_item_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 104 - env: MutableMapping[str, Any]) -> str: 91 + def list_item_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 105 92 self._leave_block() 106 93 return "" 107 - def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 108 - env: MutableMapping[str, Any]) -> str: 94 + def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 109 95 self._list_stack.append(List(compact=bool(token.meta['compact']))) 110 96 return self._maybe_parbreak() 111 - def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 112 - env: MutableMapping[str, Any]) -> str: 97 + def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 113 98 self._list_stack.pop() 114 99 return "" 115 - def em_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 116 - env: MutableMapping[str, Any]) -> str: 100 + def em_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 117 101 return "*" 118 - def em_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 119 - env: MutableMapping[str, Any]) -> str: 102 + def em_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 120 103 return "*" 121 - def strong_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 122 - env: MutableMapping[str, Any]) -> str: 104 + def strong_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 123 105 return "**" 124 - def strong_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 125 - env: MutableMapping[str, Any]) -> str: 106 + def strong_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 126 107 return "**" 127 - def fence(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 128 - env: MutableMapping[str, Any]) -> str: 108 + def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str: 129 109 code = token.content 130 110 if code.endswith('\n'): 131 111 code = code[:-1] 132 112 pbreak = self._maybe_parbreak() 133 113 return pbreak + self._indent_raw(md_make_code(code, info=token.info, multiline=True)) 134 - def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 135 - env: MutableMapping[str, Any]) -> str: 114 + def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 136 115 pbreak = self._maybe_parbreak() 137 116 self._enter_block("> ") 138 117 return pbreak + "> " 139 - def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 140 - env: MutableMapping[str, Any]) -> str: 118 + def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 141 119 self._leave_block() 142 120 return "" 143 - def note_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 144 - env: MutableMapping[str, Any]) -> str: 121 + def note_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 145 122 return self._admonition_open("Note") 146 - def note_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 147 - env: MutableMapping[str, Any]) -> str: 123 + def note_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 148 124 return self._admonition_close() 149 - def caution_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 150 - env: MutableMapping[str, Any]) -> str: 125 + def caution_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 151 126 return self._admonition_open("Caution") 152 - def caution_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 153 - env: MutableMapping[str, Any]) -> str: 127 + def caution_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 154 128 return self._admonition_close() 155 - def important_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 156 - env: MutableMapping[str, Any]) -> str: 129 + def important_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 157 130 return self._admonition_open("Important") 158 - def important_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 159 - env: MutableMapping[str, Any]) -> str: 131 + def important_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 160 132 return self._admonition_close() 161 - def tip_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 162 - env: MutableMapping[str, Any]) -> str: 133 + def tip_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 163 134 return self._admonition_open("Tip") 164 - def tip_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 165 - env: MutableMapping[str, Any]) -> str: 135 + def tip_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 166 136 return self._admonition_close() 167 - def warning_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 168 - env: MutableMapping[str, Any]) -> str: 137 + def warning_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 169 138 return self._admonition_open("Warning") 170 - def warning_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 171 - env: MutableMapping[str, Any]) -> str: 139 + def warning_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 172 140 return self._admonition_close() 173 - def dl_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 174 - env: MutableMapping[str, Any]) -> str: 141 + def dl_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 175 142 self._list_stack.append(List(compact=False)) 176 143 return "" 177 - def dl_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 178 - env: MutableMapping[str, Any]) -> str: 144 + def dl_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 179 145 self._list_stack.pop() 180 146 return "" 181 - def dt_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 182 - env: MutableMapping[str, Any]) -> str: 147 + def dt_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 183 148 pbreak = self._maybe_parbreak() 184 149 self._enter_block(" ") 185 150 # add an opening zero-width non-joiner to separate *our* emphasis from possible 186 151 # emphasis in the provided term 187 152 return f'{pbreak} - *{chr(0x200C)}' 188 - def dt_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 189 - env: MutableMapping[str, Any]) -> str: 153 + def dt_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 190 154 return f"{chr(0x200C)}*" 191 - def dd_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 192 - env: MutableMapping[str, Any]) -> str: 155 + def dd_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 193 156 self._parstack[-1].continuing = True 194 157 return "" 195 - def dd_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 196 - env: MutableMapping[str, Any]) -> str: 158 + def dd_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 197 159 self._leave_block() 198 160 return "" 199 - def myst_role(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 200 - env: MutableMapping[str, Any]) -> str: 161 + def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str: 201 162 self._parstack[-1].continuing = True 202 163 content = md_make_code(token.content) 203 164 if token.meta['name'] == 'manpage' and (url := self._manpage_urls.get(token.content)): 204 165 return f"[{content}]({url})" 205 166 return content # no roles in regular commonmark 206 - def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 207 - env: MutableMapping[str, Any]) -> str: 167 + def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str: 208 168 # there's no way we can emit attrspans correctly in all cases. we could use inline 209 169 # html for ids, but that would not round-trip. same holds for classes. since this 210 170 # renderer is only used for approximate options export and all of these things are 211 171 # not allowed in options we can ignore them for now. 212 172 return "" 213 - def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 214 - env: MutableMapping[str, Any]) -> str: 173 + def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str: 215 174 return "" 216 - def heading_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 217 - env: MutableMapping[str, Any]) -> str: 175 + def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 218 176 return token.markup + " " 219 - def heading_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 220 - env: MutableMapping[str, Any]) -> str: 177 + def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 221 178 return "\n" 222 - def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 223 - env: MutableMapping[str, Any]) -> str: 179 + def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 224 180 self._list_stack.append( 225 181 List(next_idx = cast(int, token.attrs.get('start', 1)), 226 182 compact = bool(token.meta['compact']))) 227 183 return self._maybe_parbreak() 228 - def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 229 - env: MutableMapping[str, Any]) -> str: 184 + def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 230 185 self._list_stack.pop() 231 186 return ""
+58 -108
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/docbook.py
··· 1 - from collections.abc import Mapping, MutableMapping, Sequence 1 + from collections.abc import Mapping, Sequence 2 2 from typing import Any, cast, Optional, NamedTuple 3 3 4 4 import markdown_it 5 5 from markdown_it.token import Token 6 - from markdown_it.utils import OptionsDict 7 6 from xml.sax.saxutils import escape, quoteattr 8 7 9 8 from .md import Renderer ··· 32 31 partintro_closed: bool = False 33 32 34 33 class DocBookRenderer(Renderer): 35 - __output__ = "docbook" 36 34 _link_tags: list[str] 37 35 _deflists: list[Deflist] 38 36 _headings: list[Heading] 39 37 _attrspans: list[str] 40 38 41 - def __init__(self, manpage_urls: Mapping[str, str], parser: Optional[markdown_it.MarkdownIt] = None): 42 - super().__init__(manpage_urls, parser) 39 + def __init__(self, manpage_urls: Mapping[str, str]): 40 + super().__init__(manpage_urls) 43 41 self._link_tags = [] 44 42 self._deflists = [] 45 43 self._headings = [] 46 44 self._attrspans = [] 47 45 48 - def render(self, tokens: Sequence[Token], options: OptionsDict, 49 - env: MutableMapping[str, Any]) -> str: 50 - result = super().render(tokens, options, env) 51 - result += self._close_headings(None, env) 46 + def render(self, tokens: Sequence[Token]) -> str: 47 + result = super().render(tokens) 48 + result += self._close_headings(None) 52 49 return result 53 - def renderInline(self, tokens: Sequence[Token], options: OptionsDict, 54 - env: MutableMapping[str, Any]) -> str: 50 + def renderInline(self, tokens: Sequence[Token]) -> str: 55 51 # HACK to support docbook links and xrefs. link handling is only necessary because the docbook 56 52 # manpage stylesheet converts - in urls to a mathematical minus, which may be somewhat incorrect. 57 53 for i, token in enumerate(tokens): ··· 65 61 if tokens[i + 1].type == 'text' and tokens[i + 1].content == token.attrs['href']: 66 62 tokens[i + 1].content = '' 67 63 68 - return super().renderInline(tokens, options, env) 64 + return super().renderInline(tokens) 69 65 70 - def text(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 71 - env: MutableMapping[str, Any]) -> str: 66 + def text(self, token: Token, tokens: Sequence[Token], i: int) -> str: 72 67 return escape(token.content) 73 - def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 74 - env: MutableMapping[str, Any]) -> str: 68 + def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 75 69 return "<para>" 76 - def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 77 - env: MutableMapping[str, Any]) -> str: 70 + def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 78 71 return "</para>" 79 - def hardbreak(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 80 - env: MutableMapping[str, Any]) -> str: 72 + def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 81 73 return "<literallayout>\n</literallayout>" 82 - def softbreak(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 83 - env: MutableMapping[str, Any]) -> str: 74 + def softbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 84 75 # should check options.breaks() and emit hard break if so 85 76 return "\n" 86 - def code_inline(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 87 - env: MutableMapping[str, Any]) -> str: 77 + def code_inline(self, token: Token, tokens: Sequence[Token], i: int) -> str: 88 78 return f"<literal>{escape(token.content)}</literal>" 89 - def code_block(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 90 - env: MutableMapping[str, Any]) -> str: 79 + def code_block(self, token: Token, tokens: Sequence[Token], i: int) -> str: 91 80 return f"<programlisting>{escape(token.content)}</programlisting>" 92 - def link_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 93 - env: MutableMapping[str, Any]) -> str: 81 + def link_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 94 82 self._link_tags.append(token.tag) 95 83 href = cast(str, token.attrs['href']) 96 84 (attr, start) = ('linkend', 1) if href[0] == '#' else ('xlink:href', 0) 97 85 return f"<{token.tag} {attr}={quoteattr(href[start:])}>" 98 - def link_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 99 - env: MutableMapping[str, Any]) -> str: 86 + def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 100 87 return f"</{self._link_tags.pop()}>" 101 - def list_item_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 102 - env: MutableMapping[str, Any]) -> str: 88 + def list_item_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 103 89 return "<listitem>" 104 - def list_item_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 105 - env: MutableMapping[str, Any]) -> str: 90 + def list_item_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 106 91 return "</listitem>\n" 107 92 # HACK open and close para for docbook change size. remove soon. 108 - def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 109 - env: MutableMapping[str, Any]) -> str: 93 + def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 110 94 spacing = ' spacing="compact"' if token.meta.get('compact', False) else '' 111 95 return f"<para><itemizedlist{spacing}>\n" 112 - def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 113 - env: MutableMapping[str, Any]) -> str: 96 + def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 114 97 return "\n</itemizedlist></para>" 115 - def em_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 116 - env: MutableMapping[str, Any]) -> str: 98 + def em_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 117 99 return "<emphasis>" 118 - def em_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 119 - env: MutableMapping[str, Any]) -> str: 100 + def em_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 120 101 return "</emphasis>" 121 - def strong_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 122 - env: MutableMapping[str, Any]) -> str: 102 + def strong_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 123 103 return "<emphasis role=\"strong\">" 124 - def strong_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 125 - env: MutableMapping[str, Any]) -> str: 104 + def strong_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 126 105 return "</emphasis>" 127 - def fence(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 128 - env: MutableMapping[str, Any]) -> str: 106 + def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str: 129 107 info = f" language={quoteattr(token.info)}" if token.info != "" else "" 130 108 return f"<programlisting{info}>{escape(token.content)}</programlisting>" 131 - def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 132 - env: MutableMapping[str, Any]) -> str: 109 + def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 133 110 return "<para><blockquote>" 134 - def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 135 - env: MutableMapping[str, Any]) -> str: 111 + def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 136 112 return "</blockquote></para>" 137 - def note_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 138 - env: MutableMapping[str, Any]) -> str: 113 + def note_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 139 114 return "<para><note>" 140 - def note_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 141 - env: MutableMapping[str, Any]) -> str: 115 + def note_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 142 116 return "</note></para>" 143 - def caution_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 144 - env: MutableMapping[str, Any]) -> str: 117 + def caution_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 145 118 return "<para><caution>" 146 - def caution_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 147 - env: MutableMapping[str, Any]) -> str: 119 + def caution_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 148 120 return "</caution></para>" 149 - def important_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 150 - env: MutableMapping[str, Any]) -> str: 121 + def important_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 151 122 return "<para><important>" 152 - def important_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 153 - env: MutableMapping[str, Any]) -> str: 123 + def important_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 154 124 return "</important></para>" 155 - def tip_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 156 - env: MutableMapping[str, Any]) -> str: 125 + def tip_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 157 126 return "<para><tip>" 158 - def tip_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 159 - env: MutableMapping[str, Any]) -> str: 127 + def tip_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 160 128 return "</tip></para>" 161 - def warning_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 162 - env: MutableMapping[str, Any]) -> str: 129 + def warning_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 163 130 return "<para><warning>" 164 - def warning_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 165 - env: MutableMapping[str, Any]) -> str: 131 + def warning_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 166 132 return "</warning></para>" 167 133 # markdown-it emits tokens based on the html syntax tree, but docbook is 168 134 # slightly different. html has <dl>{<dt/>{<dd/>}}</dl>, 169 135 # docbook has <variablelist>{<varlistentry><term/><listitem/></varlistentry>}<variablelist> 170 136 # we have to reject multiple definitions for the same term for time being. 171 - def dl_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 172 - env: MutableMapping[str, Any]) -> str: 137 + def dl_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 173 138 self._deflists.append(Deflist()) 174 139 return "<para><variablelist>" 175 - def dl_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 176 - env: MutableMapping[str, Any]) -> str: 140 + def dl_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 177 141 self._deflists.pop() 178 142 return "</variablelist></para>" 179 - def dt_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 180 - env: MutableMapping[str, Any]) -> str: 143 + def dt_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 181 144 self._deflists[-1].has_dd = False 182 145 return "<varlistentry><term>" 183 - def dt_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 184 - env: MutableMapping[str, Any]) -> str: 146 + def dt_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 185 147 return "</term>" 186 - def dd_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 187 - env: MutableMapping[str, Any]) -> str: 148 + def dd_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 188 149 if self._deflists[-1].has_dd: 189 150 raise Exception("multiple definitions per term not supported") 190 151 self._deflists[-1].has_dd = True 191 152 return "<listitem>" 192 - def dd_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 193 - env: MutableMapping[str, Any]) -> str: 153 + def dd_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 194 154 return "</listitem></varlistentry>" 195 - def myst_role(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 196 - env: MutableMapping[str, Any]) -> str: 155 + def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str: 197 156 if token.meta['name'] == 'command': 198 157 return f"<command>{escape(token.content)}</command>" 199 158 if token.meta['name'] == 'file': ··· 216 175 else: 217 176 return ref 218 177 raise NotImplementedError("md node not supported yet", token) 219 - def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 220 - env: MutableMapping[str, Any]) -> str: 178 + def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str: 221 179 # we currently support *only* inline anchors and the special .keycap class to produce 222 180 # <keycap> docbook elements. 223 181 (id_part, class_part) = ("", "") ··· 228 186 class_part = "<keycap>" 229 187 self._attrspans.append("</keycap>") 230 188 else: 231 - return super().attr_span_begin(token, tokens, i, options, env) 189 + return super().attr_span_begin(token, tokens, i) 232 190 else: 233 191 self._attrspans.append("") 234 192 return id_part + class_part 235 - def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 236 - env: MutableMapping[str, Any]) -> str: 193 + def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str: 237 194 return self._attrspans.pop() 238 - def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 239 - env: MutableMapping[str, Any]) -> str: 195 + def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 240 196 start = f' startingnumber="{token.attrs["start"]}"' if 'start' in token.attrs else "" 241 197 spacing = ' spacing="compact"' if token.meta.get('compact', False) else '' 242 198 return f"<orderedlist{start}{spacing}>" 243 - def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 244 - env: MutableMapping[str, Any]) -> str: 199 + def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 245 200 return f"</orderedlist>" 246 - def heading_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 247 - env: MutableMapping[str, Any]) -> str: 201 + def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 248 202 hlevel = int(token.tag[1:]) 249 - result = self._close_headings(hlevel, env) 250 - (tag, attrs) = self._heading_tag(token, tokens, i, options, env) 203 + result = self._close_headings(hlevel) 204 + (tag, attrs) = self._heading_tag(token, tokens, i) 251 205 self._headings.append(Heading(tag, hlevel)) 252 206 attrs_str = "".join([ f" {k}={quoteattr(v)}" for k, v in attrs.items() ]) 253 207 return result + f'<{tag}{attrs_str}>\n<title>' 254 - def heading_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 255 - env: MutableMapping[str, Any]) -> str: 208 + def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 256 209 heading = self._headings[-1] 257 210 result = '</title>' 258 211 if heading.container_tag == 'part': ··· 264 217 maybe_id = " xml:id=" + quoteattr(id + "-intro") 265 218 result += f"<partintro{maybe_id}>" 266 219 return result 267 - def example_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 268 - env: MutableMapping[str, Any]) -> str: 220 + def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 269 221 if id := token.attrs.get('id'): 270 222 return f"<anchor xml:id={quoteattr(cast(str, id))} />" 271 223 return "" 272 - def example_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 273 - env: MutableMapping[str, Any]) -> str: 224 + def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 274 225 return "" 275 226 276 - def _close_headings(self, level: Optional[int], env: MutableMapping[str, Any]) -> str: 227 + def _close_headings(self, level: Optional[int]) -> str: 277 228 # we rely on markdown-it producing h{1..6} tags in token.tag for this to work 278 229 result = [] 279 230 while len(self._headings): ··· 286 237 break 287 238 return "\n".join(result) 288 239 289 - def _heading_tag(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 290 - env: MutableMapping[str, Any]) -> tuple[str, dict[str, str]]: 240 + def _heading_tag(self, token: Token, tokens: Sequence[Token], i: int) -> tuple[str, dict[str, str]]: 291 241 attrs = {} 292 242 if id := token.attrs.get('id'): 293 243 attrs['xml:id'] = cast(str, id)
+245
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py
··· 1 + from collections.abc import Mapping, Sequence 2 + from typing import cast, Optional, NamedTuple 3 + 4 + from html import escape 5 + from markdown_it.token import Token 6 + 7 + from .manual_structure import XrefTarget 8 + from .md import Renderer 9 + 10 + class UnresolvedXrefError(Exception): 11 + pass 12 + 13 + class Heading(NamedTuple): 14 + container_tag: str 15 + level: int 16 + html_tag: str 17 + # special handling for part content: whether partinfo div was already closed from 18 + # elsewhere or still needs closing. 19 + partintro_closed: bool 20 + # tocs are generated when the heading opens, but have to be emitted into the file 21 + # after the heading titlepage (and maybe partinfo) has been closed. 22 + toc_fragment: str 23 + 24 + _bullet_list_styles = [ 'disc', 'circle', 'square' ] 25 + _ordered_list_styles = [ '1', 'a', 'i', 'A', 'I' ] 26 + 27 + class HTMLRenderer(Renderer): 28 + _xref_targets: Mapping[str, XrefTarget] 29 + 30 + _headings: list[Heading] 31 + _attrspans: list[str] 32 + _hlevel_offset: int = 0 33 + _bullet_list_nesting: int = 0 34 + _ordered_list_nesting: int = 0 35 + 36 + def __init__(self, manpage_urls: Mapping[str, str], xref_targets: Mapping[str, XrefTarget]): 37 + super().__init__(manpage_urls) 38 + self._headings = [] 39 + self._attrspans = [] 40 + self._xref_targets = xref_targets 41 + 42 + def render(self, tokens: Sequence[Token]) -> str: 43 + result = super().render(tokens) 44 + result += self._close_headings(None) 45 + return result 46 + 47 + def text(self, token: Token, tokens: Sequence[Token], i: int) -> str: 48 + return escape(token.content) 49 + def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 50 + return "<p>" 51 + def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 52 + return "</p>" 53 + def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 54 + return "<br />" 55 + def softbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 56 + return "\n" 57 + def code_inline(self, token: Token, tokens: Sequence[Token], i: int) -> str: 58 + return f'<code class="literal">{escape(token.content)}</code>' 59 + def code_block(self, token: Token, tokens: Sequence[Token], i: int) -> str: 60 + return self.fence(token, tokens, i) 61 + def link_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 62 + href = escape(cast(str, token.attrs['href']), True) 63 + tag, title, target, text = "link", "", 'target="_top"', "" 64 + if href.startswith('#'): 65 + if not (xref := self._xref_targets.get(href[1:])): 66 + raise UnresolvedXrefError(f"bad local reference, id {href} not known") 67 + if tokens[i + 1].type == 'link_close': 68 + tag, text = "xref", xref.title_html 69 + if xref.title: 70 + title = f'title="{escape(xref.title, True)}"' 71 + target, href = "", xref.href() 72 + return f'<a class="{tag}" href="{href}" {title} {target}>{text}' 73 + def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 74 + return "</a>" 75 + def list_item_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 76 + return '<li class="listitem">' 77 + def list_item_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 78 + return "</li>" 79 + def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 80 + extra = 'compact' if token.meta.get('compact', False) else '' 81 + style = _bullet_list_styles[self._bullet_list_nesting % len(_bullet_list_styles)] 82 + self._bullet_list_nesting += 1 83 + return f'<div class="itemizedlist"><ul class="itemizedlist {extra}" style="list-style-type: {style};">' 84 + def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 85 + self._bullet_list_nesting -= 1 86 + return "</ul></div>" 87 + def em_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 88 + return '<span class="emphasis"><em>' 89 + def em_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 90 + return "</em></span>" 91 + def strong_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 92 + return '<span class="strong"><strong>' 93 + def strong_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 94 + return "</strong></span>" 95 + def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str: 96 + # TODO use token.info. docbook doesn't so we can't yet. 97 + return f'<pre class="programlisting">\n{escape(token.content)}</pre>' 98 + def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 99 + return '<div class="blockquote"><blockquote class="blockquote">' 100 + def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 101 + return "</blockquote></div>" 102 + def note_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 103 + return '<div class="note"><h3 class="title">Note</h3>' 104 + def note_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 105 + return "</div>" 106 + def caution_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 107 + return '<div class="caution"><h3 class="title">Caution</h3>' 108 + def caution_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 109 + return "</div>" 110 + def important_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 111 + return '<div class="important"><h3 class="title">Important</h3>' 112 + def important_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 113 + return "</div>" 114 + def tip_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 115 + return '<div class="tip"><h3 class="title">Tip</h3>' 116 + def tip_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 117 + return "</div>" 118 + def warning_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 119 + return '<div class="warning"><h3 class="title">Warning</h3>' 120 + def warning_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 121 + return "</div>" 122 + def dl_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 123 + return '<div class="variablelist"><dl class="variablelist">' 124 + def dl_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 125 + return "</dl></div>" 126 + def dt_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 127 + return '<dt><span class="term">' 128 + def dt_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 129 + return "</span></dt>" 130 + def dd_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 131 + return "<dd>" 132 + def dd_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 133 + return "</dd>" 134 + def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str: 135 + if token.meta['name'] == 'command': 136 + return f'<span class="command"><strong>{escape(token.content)}</strong></span>' 137 + if token.meta['name'] == 'file': 138 + return f'<code class="filename">{escape(token.content)}</code>' 139 + if token.meta['name'] == 'var': 140 + return f'<code class="varname">{escape(token.content)}</code>' 141 + if token.meta['name'] == 'env': 142 + return f'<code class="envar">{escape(token.content)}</code>' 143 + if token.meta['name'] == 'option': 144 + return f'<code class="option">{escape(token.content)}</code>' 145 + if token.meta['name'] == 'manpage': 146 + [page, section] = [ s.strip() for s in token.content.rsplit('(', 1) ] 147 + section = section[:-1] 148 + man = f"{page}({section})" 149 + title = f'<span class="refentrytitle">{escape(page)}</span>' 150 + vol = f"({escape(section)})" 151 + ref = f'<span class="citerefentry">{title}{vol}</span>' 152 + if man in self._manpage_urls: 153 + return f'<a class="link" href="{escape(self._manpage_urls[man], True)}" target="_top">{ref}</a>' 154 + else: 155 + return ref 156 + return super().myst_role(token, tokens, i) 157 + def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str: 158 + # we currently support *only* inline anchors and the special .keycap class to produce 159 + # keycap-styled spans. 160 + (id_part, class_part) = ("", "") 161 + if s := token.attrs.get('id'): 162 + id_part = f'<a id="{escape(cast(str, s), True)}" />' 163 + if s := token.attrs.get('class'): 164 + if s == 'keycap': 165 + class_part = '<span class="keycap"><strong>' 166 + self._attrspans.append("</strong></span>") 167 + else: 168 + return super().attr_span_begin(token, tokens, i) 169 + else: 170 + self._attrspans.append("") 171 + return id_part + class_part 172 + def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str: 173 + return self._attrspans.pop() 174 + def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 175 + hlevel = int(token.tag[1:]) 176 + htag, hstyle = self._make_hN(hlevel) 177 + if hstyle: 178 + hstyle = f'style="{escape(hstyle, True)}"' 179 + if anchor := cast(str, token.attrs.get('id', '')): 180 + anchor = f'<a id="{escape(anchor, True)}"></a>' 181 + result = self._close_headings(hlevel) 182 + tag = self._heading_tag(token, tokens, i) 183 + toc_fragment = self._build_toc(tokens, i) 184 + self._headings.append(Heading(tag, hlevel, htag, tag != 'part', toc_fragment)) 185 + return ( 186 + f'{result}' 187 + f'<div class="{tag}">' 188 + f' <div class="titlepage">' 189 + f' <div>' 190 + f' <div>' 191 + f' <{htag} class="title" {hstyle}>' 192 + f' {anchor}' 193 + ) 194 + def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 195 + heading = self._headings[-1] 196 + result = ( 197 + f' </{heading.html_tag}>' 198 + f' </div>' 199 + f' </div>' 200 + f'</div>' 201 + ) 202 + if heading.container_tag == 'part': 203 + result += '<div class="partintro">' 204 + else: 205 + result += heading.toc_fragment 206 + return result 207 + def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 208 + extra = 'compact' if token.meta.get('compact', False) else '' 209 + start = f'start="{token.attrs["start"]}"' if 'start' in token.attrs else "" 210 + style = _ordered_list_styles[self._ordered_list_nesting % len(_ordered_list_styles)] 211 + self._ordered_list_nesting += 1 212 + return f'<div class="orderedlist"><ol class="orderedlist {extra}" {start} type="{style}">' 213 + def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 214 + self._ordered_list_nesting -= 1; 215 + return "</ol></div>" 216 + def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 217 + if id := token.attrs.get('id'): 218 + return f'<a id="{escape(cast(str, id), True)}" />' 219 + return "" 220 + def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 221 + return "" 222 + 223 + def _make_hN(self, level: int) -> tuple[str, str]: 224 + return f"h{min(6, max(1, level + self._hlevel_offset))}", "" 225 + 226 + def _maybe_close_partintro(self) -> str: 227 + if self._headings: 228 + heading = self._headings[-1] 229 + if heading.container_tag == 'part' and not heading.partintro_closed: 230 + self._headings[-1] = heading._replace(partintro_closed=True) 231 + return heading.toc_fragment + "</div>" 232 + return "" 233 + 234 + def _close_headings(self, level: Optional[int]) -> str: 235 + result = [] 236 + while len(self._headings) and (level is None or self._headings[-1].level >= level): 237 + result.append(self._maybe_close_partintro()) 238 + result.append("</div>") 239 + self._headings.pop() 240 + return "\n".join(result) 241 + 242 + def _heading_tag(self, token: Token, tokens: Sequence[Token], i: int) -> str: 243 + return "section" 244 + def _build_toc(self, tokens: Sequence[Token], i: int) -> str: 245 + return ""
+50 -98
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manpage.py
··· 1 - from collections.abc import Mapping, MutableMapping, Sequence 1 + from collections.abc import Mapping, Sequence 2 2 from dataclasses import dataclass 3 3 from typing import Any, cast, Iterable, Optional 4 4 ··· 6 6 7 7 import markdown_it 8 8 from markdown_it.token import Token 9 - from markdown_it.utils import OptionsDict 10 9 11 10 from .md import Renderer 12 11 ··· 75 74 # horizontal motion in a line) we do attempt to copy the style of mdoc(7) semantic requests 76 75 # as appropriate for each markup element. 77 76 class ManpageRenderer(Renderer): 78 - __output__ = "man" 79 - 80 77 # whether to emit mdoc .Ql equivalents for inline code or just the contents. this is 81 78 # mainly used by the options manpage converter to not emit extra quotes in defaults 82 79 # and examples where it's already clear from context that the following text is code. ··· 90 87 _list_stack: list[List] 91 88 _font_stack: list[str] 92 89 93 - def __init__(self, manpage_urls: Mapping[str, str], href_targets: dict[str, str], 94 - parser: Optional[markdown_it.MarkdownIt] = None): 95 - super().__init__(manpage_urls, parser) 90 + def __init__(self, manpage_urls: Mapping[str, str], href_targets: dict[str, str]): 91 + super().__init__(manpage_urls) 96 92 self._href_targets = href_targets 97 93 self._link_stack = [] 98 94 self._do_parbreak_stack = [] ··· 126 122 self._leave_block() 127 123 return ".RE" 128 124 129 - def render(self, tokens: Sequence[Token], options: OptionsDict, 130 - env: MutableMapping[str, Any]) -> str: 125 + def render(self, tokens: Sequence[Token]) -> str: 131 126 self._do_parbreak_stack = [ False ] 132 127 self._font_stack = [ "\\fR" ] 133 - return super().render(tokens, options, env) 128 + return super().render(tokens) 134 129 135 - def text(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 136 - env: MutableMapping[str, Any]) -> str: 130 + def text(self, token: Token, tokens: Sequence[Token], i: int) -> str: 137 131 return man_escape(token.content) 138 - def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 139 - env: MutableMapping[str, Any]) -> str: 132 + def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 140 133 return self._maybe_parbreak() 141 - def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 142 - env: MutableMapping[str, Any]) -> str: 134 + def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 143 135 return "" 144 - def hardbreak(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 145 - env: MutableMapping[str, Any]) -> str: 136 + def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 146 137 return ".br" 147 - def softbreak(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 148 - env: MutableMapping[str, Any]) -> str: 138 + def softbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 149 139 return " " 150 - def code_inline(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 151 - env: MutableMapping[str, Any]) -> str: 140 + def code_inline(self, token: Token, tokens: Sequence[Token], i: int) -> str: 152 141 s = _protect_spaces(man_escape(token.content)) 153 142 return f"\\fR\\(oq{s}\\(cq\\fP" if self.inline_code_is_quoted else s 154 - def code_block(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 155 - env: MutableMapping[str, Any]) -> str: 156 - return self.fence(token, tokens, i, options, env) 157 - def link_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 158 - env: MutableMapping[str, Any]) -> str: 143 + def code_block(self, token: Token, tokens: Sequence[Token], i: int) -> str: 144 + return self.fence(token, tokens, i) 145 + def link_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 159 146 href = cast(str, token.attrs['href']) 160 147 self._link_stack.append(href) 161 148 text = "" ··· 164 151 text = self._href_targets[href] 165 152 self._font_stack.append("\\fB") 166 153 return f"\\fB{text}\0 <" 167 - def link_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 168 - env: MutableMapping[str, Any]) -> str: 154 + def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 169 155 href = self._link_stack.pop() 170 156 text = "" 171 157 if self.link_footnotes is not None: ··· 177 163 text = "\\fR" + man_escape(f"[{idx}]") 178 164 self._font_stack.pop() 179 165 return f">\0 {text}{self._font_stack[-1]}" 180 - def list_item_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 181 - env: MutableMapping[str, Any]) -> str: 166 + def list_item_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 182 167 self._enter_block() 183 168 lst = self._list_stack[-1] 184 169 maybe_space = '' if lst.compact or not lst.first_item_seen else '.sp\n' ··· 192 177 f'.RS {lst.width}\n' 193 178 f"\\h'-{len(head) + 1}'\\fB{man_escape(head)}\\fP\\h'1'\\c" 194 179 ) 195 - def list_item_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 196 - env: MutableMapping[str, Any]) -> str: 180 + def list_item_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 197 181 self._leave_block() 198 182 return ".RE" 199 - def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 200 - env: MutableMapping[str, Any]) -> str: 183 + def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 201 184 self._list_stack.append(List(width=4, compact=bool(token.meta['compact']))) 202 185 return self._maybe_parbreak() 203 - def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 204 - env: MutableMapping[str, Any]) -> str: 186 + def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 205 187 self._list_stack.pop() 206 188 return "" 207 - def em_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 208 - env: MutableMapping[str, Any]) -> str: 189 + def em_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 209 190 self._font_stack.append("\\fI") 210 191 return "\\fI" 211 - def em_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 212 - env: MutableMapping[str, Any]) -> str: 192 + def em_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 213 193 self._font_stack.pop() 214 194 return self._font_stack[-1] 215 - def strong_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 216 - env: MutableMapping[str, Any]) -> str: 195 + def strong_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 217 196 self._font_stack.append("\\fB") 218 197 return "\\fB" 219 - def strong_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 220 - env: MutableMapping[str, Any]) -> str: 198 + def strong_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 221 199 self._font_stack.pop() 222 200 return self._font_stack[-1] 223 - def fence(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 224 - env: MutableMapping[str, Any]) -> str: 201 + def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str: 225 202 s = man_escape(token.content).rstrip('\n') 226 203 return ( 227 204 '.sp\n' ··· 231 208 '.fi\n' 232 209 '.RE' 233 210 ) 234 - def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 235 - env: MutableMapping[str, Any]) -> str: 211 + def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 236 212 maybe_par = self._maybe_parbreak("\n") 237 213 self._enter_block() 238 214 return ( ··· 240 216 ".RS 4\n" 241 217 f"\\h'-3'\\fI\\(lq\\(rq\\fP\\h'1'\\c" 242 218 ) 243 - def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 244 - env: MutableMapping[str, Any]) -> str: 219 + def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 245 220 self._leave_block() 246 221 return ".RE" 247 - def note_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 248 - env: MutableMapping[str, Any]) -> str: 222 + def note_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 249 223 return self._admonition_open("Note") 250 - def note_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 251 - env: MutableMapping[str, Any]) -> str: 224 + def note_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 252 225 return self._admonition_close() 253 - def caution_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 254 - env: MutableMapping[str, Any]) -> str: 226 + def caution_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 255 227 return self._admonition_open( "Caution") 256 - def caution_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 257 - env: MutableMapping[str, Any]) -> str: 228 + def caution_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 258 229 return self._admonition_close() 259 - def important_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 260 - env: MutableMapping[str, Any]) -> str: 230 + def important_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 261 231 return self._admonition_open( "Important") 262 - def important_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 263 - env: MutableMapping[str, Any]) -> str: 232 + def important_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 264 233 return self._admonition_close() 265 - def tip_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 266 - env: MutableMapping[str, Any]) -> str: 234 + def tip_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 267 235 return self._admonition_open( "Tip") 268 - def tip_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 269 - env: MutableMapping[str, Any]) -> str: 236 + def tip_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 270 237 return self._admonition_close() 271 - def warning_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 272 - env: MutableMapping[str, Any]) -> str: 238 + def warning_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 273 239 return self._admonition_open( "Warning") 274 - def warning_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 275 - env: MutableMapping[str, Any]) -> str: 240 + def warning_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 276 241 return self._admonition_close() 277 - def dl_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 278 - env: MutableMapping[str, Any]) -> str: 242 + def dl_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 279 243 return ".RS 4" 280 - def dl_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 281 - env: MutableMapping[str, Any]) -> str: 244 + def dl_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 282 245 return ".RE" 283 - def dt_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 284 - env: MutableMapping[str, Any]) -> str: 246 + def dt_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 285 247 return ".PP" 286 - def dt_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 287 - env: MutableMapping[str, Any]) -> str: 248 + def dt_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 288 249 return "" 289 - def dd_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 290 - env: MutableMapping[str, Any]) -> str: 250 + def dd_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 291 251 self._enter_block() 292 252 return ".RS 4" 293 - def dd_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 294 - env: MutableMapping[str, Any]) -> str: 253 + def dd_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 295 254 self._leave_block() 296 255 return ".RE" 297 - def myst_role(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 298 - env: MutableMapping[str, Any]) -> str: 256 + def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str: 299 257 if token.meta['name'] in [ 'command', 'env', 'option' ]: 300 258 return f'\\fB{man_escape(token.content)}\\fP' 301 259 elif token.meta['name'] in [ 'file', 'var' ]: ··· 306 264 return f'\\fB{man_escape(page)}\\fP\\fR({man_escape(section)})\\fP' 307 265 else: 308 266 raise NotImplementedError("md node not supported yet", token) 309 - def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 310 - env: MutableMapping[str, Any]) -> str: 267 + def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str: 311 268 # mdoc knows no anchors so we can drop those, but classes must be rejected. 312 269 if 'class' in token.attrs: 313 - return super().attr_span_begin(token, tokens, i, options, env) 270 + return super().attr_span_begin(token, tokens, i) 314 271 return "" 315 - def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 316 - env: MutableMapping[str, Any]) -> str: 272 + def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str: 317 273 return "" 318 - def heading_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 319 - env: MutableMapping[str, Any]) -> str: 274 + def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 320 275 raise RuntimeError("md token not supported in manpages", token) 321 - def heading_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 322 - env: MutableMapping[str, Any]) -> str: 276 + def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 323 277 raise RuntimeError("md token not supported in manpages", token) 324 - def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 325 - env: MutableMapping[str, Any]) -> str: 278 + def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 326 279 # max item head width for a number, a dot, and one leading space and one trailing space 327 280 width = 3 + len(str(cast(int, token.meta['end']))) 328 281 self._list_stack.append( ··· 330 283 next_idx = cast(int, token.attrs.get('start', 1)), 331 284 compact = bool(token.meta['compact']))) 332 285 return self._maybe_parbreak() 333 - def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 334 - env: MutableMapping[str, Any]) -> str: 286 + def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 335 287 self._list_stack.pop() 336 288 return ""
+552 -140
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py
··· 1 1 import argparse 2 + import html 2 3 import json 4 + import re 5 + import xml.sax.saxutils as xml 3 6 4 7 from abc import abstractmethod 5 - from collections.abc import Mapping, MutableMapping, Sequence 8 + from collections.abc import Mapping, Sequence 6 9 from pathlib import Path 7 - from typing import Any, cast, NamedTuple, Optional, Union 8 - from xml.sax.saxutils import escape, quoteattr 10 + from typing import Any, cast, ClassVar, Generic, get_args, NamedTuple, Optional, Union 9 11 10 12 import markdown_it 11 13 from markdown_it.token import Token 12 - from markdown_it.utils import OptionsDict 14 + 15 + from . import md, options 16 + from .docbook import DocBookRenderer, Heading, make_xml_id 17 + from .html import HTMLRenderer, UnresolvedXrefError 18 + from .manual_structure import check_structure, FragmentType, is_include, TocEntry, TocEntryType, XrefTarget 19 + from .md import Converter, Renderer 20 + from .utils import Freezeable 21 + 22 + class BaseConverter(Converter[md.TR], Generic[md.TR]): 23 + # per-converter configuration for ns:arg=value arguments to include blocks, following 24 + # the include type. html converters need something like this to support chunking, or 25 + # another external method like the chunktocs docbook uses (but block options seem like 26 + # a much nicer of doing this). 27 + INCLUDE_ARGS_NS: ClassVar[str] 28 + INCLUDE_FRAGMENT_ALLOWED_ARGS: ClassVar[set[str]] = set() 29 + INCLUDE_OPTIONS_ALLOWED_ARGS: ClassVar[set[str]] = set() 30 + 31 + _base_paths: list[Path] 32 + _current_type: list[TocEntryType] 33 + 34 + def convert(self, infile: Path, outfile: Path) -> None: 35 + self._base_paths = [ infile ] 36 + self._current_type = ['book'] 37 + try: 38 + tokens = self._parse(infile.read_text()) 39 + self._postprocess(infile, outfile, tokens) 40 + converted = self._renderer.render(tokens) 41 + outfile.write_text(converted) 42 + except Exception as e: 43 + raise RuntimeError(f"failed to render manual {infile}") from e 44 + 45 + def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) -> None: 46 + pass 47 + 48 + def _parse(self, src: str) -> list[Token]: 49 + tokens = super()._parse(src) 50 + check_structure(self._current_type[-1], tokens) 51 + for token in tokens: 52 + if not is_include(token): 53 + continue 54 + directive = token.info[12:].split() 55 + if not directive: 56 + continue 57 + args = { k: v for k, _sep, v in map(lambda s: s.partition('='), directive[1:]) } 58 + typ = directive[0] 59 + if typ == 'options': 60 + token.type = 'included_options' 61 + self._process_include_args(token, args, self.INCLUDE_OPTIONS_ALLOWED_ARGS) 62 + self._parse_options(token, args) 63 + else: 64 + fragment_type = typ.removesuffix('s') 65 + if fragment_type not in get_args(FragmentType): 66 + raise RuntimeError(f"unsupported structural include type '{typ}'") 67 + self._current_type.append(cast(FragmentType, fragment_type)) 68 + token.type = 'included_' + typ 69 + self._process_include_args(token, args, self.INCLUDE_FRAGMENT_ALLOWED_ARGS) 70 + self._parse_included_blocks(token, args) 71 + self._current_type.pop() 72 + return tokens 73 + 74 + def _process_include_args(self, token: Token, args: dict[str, str], allowed: set[str]) -> None: 75 + ns = self.INCLUDE_ARGS_NS + ":" 76 + args = { k[len(ns):]: v for k, v in args.items() if k.startswith(ns) } 77 + if unknown := set(args.keys()) - allowed: 78 + assert token.map 79 + raise RuntimeError(f"unrecognized include argument in line {token.map[0] + 1}", unknown) 80 + token.meta['include-args'] = args 81 + 82 + def _parse_included_blocks(self, token: Token, block_args: dict[str, str]) -> None: 83 + assert token.map 84 + included = token.meta['included'] = [] 85 + for (lnum, line) in enumerate(token.content.splitlines(), token.map[0] + 2): 86 + line = line.strip() 87 + path = self._base_paths[-1].parent / line 88 + if path in self._base_paths: 89 + raise RuntimeError(f"circular include found in line {lnum}") 90 + try: 91 + self._base_paths.append(path) 92 + with open(path, 'r') as f: 93 + tokens = self._parse(f.read()) 94 + included.append((tokens, path)) 95 + self._base_paths.pop() 96 + except Exception as e: 97 + raise RuntimeError(f"processing included file {path} from line {lnum}") from e 98 + 99 + def _parse_options(self, token: Token, block_args: dict[str, str]) -> None: 100 + assert token.map 101 + 102 + items = {} 103 + for (lnum, line) in enumerate(token.content.splitlines(), token.map[0] + 2): 104 + if len(args := line.split(":", 1)) != 2: 105 + raise RuntimeError(f"options directive with no argument in line {lnum}") 106 + (k, v) = (args[0].strip(), args[1].strip()) 107 + if k in items: 108 + raise RuntimeError(f"duplicate options directive {k} in line {lnum}") 109 + items[k] = v 110 + try: 111 + id_prefix = items.pop('id-prefix') 112 + varlist_id = items.pop('list-id') 113 + source = items.pop('source') 114 + except KeyError as e: 115 + raise RuntimeError(f"options directive {e} missing in block at line {token.map[0] + 1}") 116 + if items.keys(): 117 + raise RuntimeError( 118 + f"unsupported options directives in block at line {token.map[0] + 1}", 119 + " ".join(items.keys())) 13 120 14 - from . import options 15 - from .docbook import DocBookRenderer, Heading 16 - from .md import Converter 121 + try: 122 + with open(self._base_paths[-1].parent / source, 'r') as f: 123 + token.meta['id-prefix'] = id_prefix 124 + token.meta['list-id'] = varlist_id 125 + token.meta['source'] = json.load(f) 126 + except Exception as e: 127 + raise RuntimeError(f"processing options block in line {token.map[0] + 1}") from e 17 128 18 - class ManualDocBookRenderer(DocBookRenderer): 129 + class RendererMixin(Renderer): 19 130 _toplevel_tag: str 131 + _revision: str 20 132 21 - def __init__(self, toplevel_tag: str, manpage_urls: Mapping[str, str], 22 - parser: Optional[markdown_it.MarkdownIt] = None): 23 - super().__init__(manpage_urls, parser) 133 + def __init__(self, toplevel_tag: str, revision: str, *args: Any, **kwargs: Any): 134 + super().__init__(*args, **kwargs) 24 135 self._toplevel_tag = toplevel_tag 136 + self._revision = revision 25 137 self.rules |= { 26 138 'included_sections': lambda *args: self._included_thing("section", *args), 27 139 'included_chapters': lambda *args: self._included_thing("chapter", *args), ··· 31 143 'included_options': self.included_options, 32 144 } 33 145 34 - def render(self, tokens: Sequence[Token], options: OptionsDict, 35 - env: MutableMapping[str, Any]) -> str: 36 - wanted = { 'h1': 'title' } 37 - wanted |= { 'h2': 'subtitle' } if self._toplevel_tag == 'book' else {} 38 - for (i, (tag, kind)) in enumerate(wanted.items()): 39 - if len(tokens) < 3 * (i + 1): 40 - raise RuntimeError(f"missing {kind} ({tag}) heading") 41 - token = tokens[3 * i] 42 - if token.type != 'heading_open' or token.tag != tag: 43 - assert token.map 44 - raise RuntimeError(f"expected {kind} ({tag}) heading in line {token.map[0] + 1}", token) 45 - for t in tokens[3 * len(wanted):]: 46 - if t.type != 'heading_open' or (info := wanted.get(t.tag)) is None: 47 - continue 48 - assert t.map 49 - raise RuntimeError( 50 - f"only one {info[0]} heading ({t.markup} [text...]) allowed per " 51 - f"{self._toplevel_tag}, but found a second in lines [{t.map[0] + 1}..{t.map[1]}]. " 52 - "please remove all such headings except the first or demote the subsequent headings.", 53 - t) 54 - 146 + def render(self, tokens: Sequence[Token]) -> str: 55 147 # books get special handling because they have *two* title tags. doing this with 56 148 # generic code is more complicated than it's worth. the checks above have verified 57 149 # that both titles actually exist. 58 150 if self._toplevel_tag == 'book': 59 - assert tokens[1].children 60 - assert tokens[4].children 61 - if (maybe_id := cast(str, tokens[0].attrs.get('id', ""))): 62 - maybe_id = "xml:id=" + quoteattr(maybe_id) 63 - return (f'<book xmlns="http://docbook.org/ns/docbook"' 64 - f' xmlns:xlink="http://www.w3.org/1999/xlink"' 65 - f' {maybe_id} version="5.0">' 66 - f' <title>{self.renderInline(tokens[1].children, options, env)}</title>' 67 - f' <subtitle>{self.renderInline(tokens[4].children, options, env)}</subtitle>' 68 - f' {super().render(tokens[6:], options, env)}' 69 - f'</book>') 151 + return self._render_book(tokens) 70 152 71 - return super().render(tokens, options, env) 153 + return super().render(tokens) 72 154 73 - def _heading_tag(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 74 - env: MutableMapping[str, Any]) -> tuple[str, dict[str, str]]: 75 - (tag, attrs) = super()._heading_tag(token, tokens, i, options, env) 155 + @abstractmethod 156 + def _render_book(self, tokens: Sequence[Token]) -> str: 157 + raise NotImplementedError() 158 + 159 + @abstractmethod 160 + def _included_thing(self, tag: str, token: Token, tokens: Sequence[Token], i: int) -> str: 161 + raise NotImplementedError() 162 + 163 + @abstractmethod 164 + def included_options(self, token: Token, tokens: Sequence[Token], i: int) -> str: 165 + raise NotImplementedError() 166 + 167 + class ManualDocBookRenderer(RendererMixin, DocBookRenderer): 168 + def __init__(self, toplevel_tag: str, revision: str, manpage_urls: Mapping[str, str]): 169 + super().__init__(toplevel_tag, revision, manpage_urls) 170 + 171 + def _render_book(self, tokens: Sequence[Token]) -> str: 172 + assert tokens[1].children 173 + assert tokens[4].children 174 + if (maybe_id := cast(str, tokens[0].attrs.get('id', ""))): 175 + maybe_id = "xml:id=" + xml.quoteattr(maybe_id) 176 + return (f'<book xmlns="http://docbook.org/ns/docbook"' 177 + f' xmlns:xlink="http://www.w3.org/1999/xlink"' 178 + f' {maybe_id} version="5.0">' 179 + f' <title>{self.renderInline(tokens[1].children)}</title>' 180 + f' <subtitle>{self.renderInline(tokens[4].children)}</subtitle>' 181 + f' {super(DocBookRenderer, self).render(tokens[6:])}' 182 + f'</book>') 183 + 184 + def _heading_tag(self, token: Token, tokens: Sequence[Token], i: int) -> tuple[str, dict[str, str]]: 185 + (tag, attrs) = super()._heading_tag(token, tokens, i) 76 186 # render() has already verified that we don't have supernumerary headings and since the 77 187 # book tag is handled specially we can leave the check this simple 78 188 if token.tag != 'h1': ··· 82 192 'xmlns:xlink': "http://www.w3.org/1999/xlink", 83 193 }) 84 194 85 - def _included_thing(self, tag: str, token: Token, tokens: Sequence[Token], i: int, 86 - options: OptionsDict, env: MutableMapping[str, Any]) -> str: 195 + def _included_thing(self, tag: str, token: Token, tokens: Sequence[Token], i: int) -> str: 87 196 result = [] 88 197 # close existing partintro. the generic render doesn't really need this because 89 198 # it doesn't have a concept of structure in the way the manual does. ··· 92 201 self._headings[-1] = self._headings[-1]._replace(partintro_closed=True) 93 202 # must nest properly for structural includes. this requires saving at least 94 203 # the headings stack, but creating new renderers is cheap and much easier. 95 - r = ManualDocBookRenderer(tag, self._manpage_urls, None) 204 + r = ManualDocBookRenderer(tag, self._revision, self._manpage_urls) 96 205 for (included, path) in token.meta['included']: 97 206 try: 98 - result.append(r.render(included, options, env)) 207 + result.append(r.render(included)) 99 208 except Exception as e: 100 209 raise RuntimeError(f"rendering {path}") from e 101 210 return "".join(result) 102 - def included_options(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 103 - env: MutableMapping[str, Any]) -> str: 104 - return cast(str, token.meta['rendered-options']) 211 + def included_options(self, token: Token, tokens: Sequence[Token], i: int) -> str: 212 + conv = options.DocBookConverter(self._manpage_urls, self._revision, False, 'fragment', 213 + token.meta['list-id'], token.meta['id-prefix']) 214 + conv.add_options(token.meta['source']) 215 + return conv.finalize(fragment=True) 105 216 106 217 # TODO minimize docbook diffs with existing conversions. remove soon. 107 - def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 108 - env: MutableMapping[str, Any]) -> str: 109 - return super().paragraph_open(token, tokens, i, options, env) + "\n " 110 - def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 111 - env: MutableMapping[str, Any]) -> str: 112 - return "\n" + super().paragraph_close(token, tokens, i, options, env) 113 - def code_block(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 114 - env: MutableMapping[str, Any]) -> str: 115 - return f"<programlisting>\n{escape(token.content)}</programlisting>" 116 - def fence(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 117 - env: MutableMapping[str, Any]) -> str: 118 - info = f" language={quoteattr(token.info)}" if token.info != "" else "" 119 - return f"<programlisting{info}>\n{escape(token.content)}</programlisting>" 218 + def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 219 + return super().paragraph_open(token, tokens, i) + "\n " 220 + def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 221 + return "\n" + super().paragraph_close(token, tokens, i) 222 + def code_block(self, token: Token, tokens: Sequence[Token], i: int) -> str: 223 + return f"<programlisting>\n{xml.escape(token.content)}</programlisting>" 224 + def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str: 225 + info = f" language={xml.quoteattr(token.info)}" if token.info != "" else "" 226 + return f"<programlisting{info}>\n{xml.escape(token.content)}</programlisting>" 227 + 228 + class DocBookConverter(BaseConverter[ManualDocBookRenderer]): 229 + INCLUDE_ARGS_NS = "docbook" 230 + 231 + def __init__(self, manpage_urls: Mapping[str, str], revision: str): 232 + super().__init__() 233 + self._renderer = ManualDocBookRenderer('book', revision, manpage_urls) 234 + 235 + 236 + class HTMLParameters(NamedTuple): 237 + generator: str 238 + stylesheets: Sequence[str] 239 + scripts: Sequence[str] 240 + toc_depth: int 241 + chunk_toc_depth: int 242 + 243 + class ManualHTMLRenderer(RendererMixin, HTMLRenderer): 244 + _base_path: Path 245 + _html_params: HTMLParameters 246 + 247 + def __init__(self, toplevel_tag: str, revision: str, html_params: HTMLParameters, 248 + manpage_urls: Mapping[str, str], xref_targets: dict[str, XrefTarget], 249 + base_path: Path): 250 + super().__init__(toplevel_tag, revision, manpage_urls, xref_targets) 251 + self._base_path, self._html_params = base_path, html_params 252 + 253 + def _push(self, tag: str, hlevel_offset: int) -> Any: 254 + result = (self._toplevel_tag, self._headings, self._attrspans, self._hlevel_offset) 255 + self._hlevel_offset += hlevel_offset 256 + self._toplevel_tag, self._headings, self._attrspans = tag, [], [] 257 + return result 258 + 259 + def _pop(self, state: Any) -> None: 260 + (self._toplevel_tag, self._headings, self._attrspans, self._hlevel_offset) = state 261 + 262 + def _render_book(self, tokens: Sequence[Token]) -> str: 263 + assert tokens[4].children 264 + title_id = cast(str, tokens[0].attrs.get('id', "")) 265 + title = self._xref_targets[title_id].title 266 + # subtitles don't have IDs, so we can't use xrefs to get them 267 + subtitle = self.renderInline(tokens[4].children) 120 268 121 - class DocBookConverter(Converter): 122 - def __renderer__(self, manpage_urls: Mapping[str, str], 123 - parser: Optional[markdown_it.MarkdownIt]) -> ManualDocBookRenderer: 124 - return ManualDocBookRenderer('book', manpage_urls, parser) 269 + toc = TocEntry.of(tokens[0]) 270 + return "\n".join([ 271 + self._file_header(toc), 272 + ' <div class="book">', 273 + ' <div class="titlepage">', 274 + ' <div>', 275 + f' <div><h1 class="title"><a id="{html.escape(title_id, True)}"></a>{title}</h1></div>', 276 + f' <div><h2 class="subtitle">{subtitle}</h2></div>', 277 + ' </div>', 278 + " <hr />", 279 + ' </div>', 280 + self._build_toc(tokens, 0), 281 + super(HTMLRenderer, self).render(tokens[6:]), 282 + ' </div>', 283 + self._file_footer(toc), 284 + ]) 125 285 126 - _base_paths: list[Path] 286 + def _file_header(self, toc: TocEntry) -> str: 287 + prev_link, up_link, next_link = "", "", "" 288 + prev_a, next_a, parent_title = "", "", "&nbsp;" 289 + home = toc.root 290 + if toc.prev: 291 + prev_link = f'<link rel="prev" href="{toc.prev.target.href()}" title="{toc.prev.target.title}" />' 292 + prev_a = f'<a accesskey="p" href="{toc.prev.target.href()}">Prev</a>' 293 + if toc.parent: 294 + up_link = ( 295 + f'<link rel="up" href="{toc.parent.target.href()}" ' 296 + f'title="{toc.parent.target.title}" />' 297 + ) 298 + if (part := toc.parent) and part.kind != 'book': 299 + assert part.target.title 300 + parent_title = part.target.title 301 + if toc.next: 302 + next_link = f'<link rel="next" href="{toc.next.target.href()}" title="{toc.next.target.title}" />' 303 + next_a = f'<a accesskey="n" href="{toc.next.target.href()}">Next</a>' 304 + return "\n".join([ 305 + '<?xml version="1.0" encoding="utf-8" standalone="no"?>', 306 + '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"', 307 + ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">', 308 + '<html xmlns="http://www.w3.org/1999/xhtml">', 309 + ' <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />', 310 + f' <title>{toc.target.title}</title>', 311 + "".join((f'<link rel="stylesheet" type="text/css" href="{html.escape(style, True)}" />' 312 + for style in self._html_params.stylesheets)), 313 + "".join((f'<script src="{html.escape(script, True)}" type="text/javascript"></script>' 314 + for script in self._html_params.scripts)), 315 + f' <meta name="generator" content="{html.escape(self._html_params.generator, True)}" />', 316 + f' <link rel="home" href="{home.target.href()}" title="{home.target.title}" />', 317 + f' {up_link}{prev_link}{next_link}', 318 + ' </head>', 319 + ' <body>', 320 + ' <div class="navheader">', 321 + ' <table width="100%" summary="Navigation header">', 322 + ' <tr>', 323 + f' <th colspan="3" align="center">{toc.target.title}</th>', 324 + ' </tr>', 325 + ' <tr>', 326 + f' <td width="20%" align="left">{prev_a}&nbsp;</td>', 327 + f' <th width="60%" align="center">{parent_title}</th>', 328 + f' <td width="20%" align="right">&nbsp;{next_a}</td>', 329 + ' </tr>', 330 + ' </table>', 331 + ' <hr />', 332 + ' </div>', 333 + ]) 334 + 335 + def _file_footer(self, toc: TocEntry) -> str: 336 + # prev, next = self._get_prev_and_next() 337 + prev_a, up_a, home_a, next_a = "", "&nbsp;", "&nbsp;", "" 338 + prev_text, up_text, next_text = "", "", "" 339 + home = toc.root 340 + if toc.prev: 341 + prev_a = f'<a accesskey="p" href="{toc.prev.target.href()}">Prev</a>' 342 + assert toc.prev.target.title 343 + prev_text = toc.prev.target.title 344 + if toc.parent: 345 + home_a = f'<a accesskey="h" href="{home.target.href()}">Home</a>' 346 + if toc.parent != home: 347 + up_a = f'<a accesskey="u" href="{toc.parent.target.href()}">Up</a>' 348 + if toc.next: 349 + next_a = f'<a accesskey="n" href="{toc.next.target.href()}">Next</a>' 350 + assert toc.next.target.title 351 + next_text = toc.next.target.title 352 + return "\n".join([ 353 + ' <div class="navfooter">', 354 + ' <hr />', 355 + ' <table width="100%" summary="Navigation footer">', 356 + ' <tr>', 357 + f' <td width="40%" align="left">{prev_a}&nbsp;</td>', 358 + f' <td width="20%" align="center">{up_a}</td>', 359 + f' <td width="40%" align="right">&nbsp;{next_a}</td>', 360 + ' </tr>', 361 + ' <tr>', 362 + f' <td width="40%" align="left" valign="top">{prev_text}&nbsp;</td>', 363 + f' <td width="20%" align="center">{home_a}</td>', 364 + f' <td width="40%" align="right" valign="top">&nbsp;{next_text}</td>', 365 + ' </tr>', 366 + ' </table>', 367 + ' </div>', 368 + ' </body>', 369 + '</html>', 370 + ]) 371 + 372 + def _heading_tag(self, token: Token, tokens: Sequence[Token], i: int) -> str: 373 + if token.tag == 'h1': 374 + return self._toplevel_tag 375 + return super()._heading_tag(token, tokens, i) 376 + def _build_toc(self, tokens: Sequence[Token], i: int) -> str: 377 + toc = TocEntry.of(tokens[i]) 378 + if toc.kind == 'section': 379 + return "" 380 + def walk_and_emit(toc: TocEntry, depth: int) -> list[str]: 381 + if depth <= 0: 382 + return [] 383 + result = [] 384 + for child in toc.children: 385 + result.append( 386 + f'<dt>' 387 + f' <span class="{html.escape(child.kind, True)}">' 388 + f' <a href="{child.target.href()}">{child.target.toc_html}</a>' 389 + f' </span>' 390 + f'</dt>' 391 + ) 392 + # we want to look straight through parts because docbook-xsl does too, but it 393 + # also makes for more uesful top-level tocs. 394 + next_level = walk_and_emit(child, depth - (0 if child.kind == 'part' else 1)) 395 + if next_level: 396 + result.append(f'<dd><dl>{"".join(next_level)}</dl></dd>') 397 + return result 398 + toc_depth = ( 399 + self._html_params.chunk_toc_depth 400 + if toc.starts_new_chunk and toc.kind != 'book' 401 + else self._html_params.toc_depth 402 + ) 403 + if not (items := walk_and_emit(toc, toc_depth)): 404 + return "" 405 + return ( 406 + f'<div class="toc">' 407 + f' <p><strong>Table of Contents</strong></p>' 408 + f' <dl class="toc">' 409 + f' {"".join(items)}' 410 + f' </dl>' 411 + f'</div>' 412 + ) 413 + 414 + def _make_hN(self, level: int) -> tuple[str, str]: 415 + # for some reason chapters don't increase the hN nesting count in docbook xslts. duplicate 416 + # this for consistency. 417 + if self._toplevel_tag == 'chapter': 418 + level -= 1 419 + # TODO docbook compat. these are never useful for us, but not having them breaks manual 420 + # compare workflows while docbook is still allowed. 421 + style = "" 422 + if level + self._hlevel_offset < 3 \ 423 + and (self._toplevel_tag == 'section' or (self._toplevel_tag == 'chapter' and level > 0)): 424 + style = "clear: both" 425 + tag, hstyle = super()._make_hN(max(1, level)) 426 + return tag, style 427 + 428 + def _included_thing(self, tag: str, token: Token, tokens: Sequence[Token], i: int) -> str: 429 + outer, inner = [], [] 430 + # since books have no non-include content the toplevel book wrapper will not count 431 + # towards nesting depth. other types will have at least a title+id heading which 432 + # *does* count towards the nesting depth. chapters give a -1 to included sections 433 + # mirroring the special handing in _make_hN. sigh. 434 + hoffset = ( 435 + 0 if not self._headings 436 + else self._headings[-1].level - 1 if self._toplevel_tag == 'chapter' 437 + else self._headings[-1].level 438 + ) 439 + outer.append(self._maybe_close_partintro()) 440 + into = token.meta['include-args'].get('into-file') 441 + fragments = token.meta['included'] 442 + state = self._push(tag, hoffset) 443 + if into: 444 + toc = TocEntry.of(fragments[0][0][0]) 445 + inner.append(self._file_header(toc)) 446 + # we do not set _hlevel_offset=0 because docbook doesn't either. 447 + else: 448 + inner = outer 449 + for included, path in fragments: 450 + try: 451 + inner.append(self.render(included)) 452 + except Exception as e: 453 + raise RuntimeError(f"rendering {path}") from e 454 + if into: 455 + inner.append(self._file_footer(toc)) 456 + (self._base_path / into).write_text("".join(inner)) 457 + self._pop(state) 458 + return "".join(outer) 459 + 460 + def included_options(self, token: Token, tokens: Sequence[Token], i: int) -> str: 461 + conv = options.HTMLConverter(self._manpage_urls, self._revision, False, 462 + token.meta['list-id'], token.meta['id-prefix'], 463 + self._xref_targets) 464 + conv.add_options(token.meta['source']) 465 + return conv.finalize() 466 + 467 + def _to_base26(n: int) -> str: 468 + return (_to_base26(n // 26) if n > 26 else "") + chr(ord("A") + n % 26) 469 + 470 + class HTMLConverter(BaseConverter[ManualHTMLRenderer]): 471 + INCLUDE_ARGS_NS = "html" 472 + INCLUDE_FRAGMENT_ALLOWED_ARGS = { 'into-file' } 473 + 127 474 _revision: str 475 + _html_params: HTMLParameters 476 + _manpage_urls: Mapping[str, str] 477 + _xref_targets: dict[str, XrefTarget] 478 + _redirection_targets: set[str] 479 + _appendix_count: int = 0 128 480 129 - def __init__(self, manpage_urls: Mapping[str, str], revision: str): 130 - super().__init__(manpage_urls) 131 - self._revision = revision 481 + def _next_appendix_id(self) -> str: 482 + self._appendix_count += 1 483 + return _to_base26(self._appendix_count - 1) 132 484 133 - def convert(self, file: Path) -> str: 134 - self._base_paths = [ file ] 135 - try: 136 - with open(file, 'r') as f: 137 - return self._render(f.read()) 138 - except Exception as e: 139 - raise RuntimeError(f"failed to render manual {file}") from e 485 + def __init__(self, revision: str, html_params: HTMLParameters, manpage_urls: Mapping[str, str]): 486 + super().__init__() 487 + self._revision, self._html_params, self._manpage_urls = revision, html_params, manpage_urls 488 + self._xref_targets = {} 489 + self._redirection_targets = set() 490 + # renderer not set on purpose since it has a dependency on the output path! 140 491 141 - def _parse(self, src: str, env: Optional[MutableMapping[str, Any]] = None) -> list[Token]: 142 - tokens = super()._parse(src, env) 492 + def convert(self, infile: Path, outfile: Path) -> None: 493 + self._renderer = ManualHTMLRenderer('book', self._revision, self._html_params, 494 + self._manpage_urls, self._xref_targets, outfile.parent) 495 + super().convert(infile, outfile) 496 + 497 + def _parse(self, src: str) -> list[Token]: 498 + tokens = super()._parse(src) 143 499 for token in tokens: 144 - if token.type != "fence" or not token.info.startswith("{=include=} "): 500 + if not token.type.startswith('included_') \ 501 + or not (into := token.meta['include-args'].get('into-file')): 145 502 continue 146 - typ = token.info[12:].strip() 147 - if typ == 'options': 148 - token.type = 'included_options' 149 - self._parse_options(token) 150 - elif typ in [ 'sections', 'chapters', 'preface', 'parts', 'appendix' ]: 151 - token.type = 'included_' + typ 152 - self._parse_included_blocks(token, env) 153 - else: 154 - raise RuntimeError(f"unsupported structural include type '{typ}'") 503 + assert token.map 504 + if len(token.meta['included']) == 0: 505 + raise RuntimeError(f"redirection target {into} in line {token.map[0] + 1} is empty!") 506 + # we use blender-style //path to denote paths relative to the origin file 507 + # (usually index.html). this makes everything a lot easier and clearer. 508 + if not into.startswith("//") or '/' in into[2:]: 509 + raise RuntimeError(f"html:into-file must be a relative-to-origin //filename", into) 510 + into = token.meta['include-args']['into-file'] = into[2:] 511 + if into in self._redirection_targets: 512 + raise RuntimeError(f"redirection target {into} in line {token.map[0] + 1} is already in use") 513 + self._redirection_targets.add(into) 155 514 return tokens 156 515 157 - def _parse_included_blocks(self, token: Token, env: Optional[MutableMapping[str, Any]]) -> None: 158 - assert token.map 159 - included = token.meta['included'] = [] 160 - for (lnum, line) in enumerate(token.content.splitlines(), token.map[0] + 2): 161 - line = line.strip() 162 - path = self._base_paths[-1].parent / line 163 - if path in self._base_paths: 164 - raise RuntimeError(f"circular include found in line {lnum}") 165 - try: 166 - self._base_paths.append(path) 167 - with open(path, 'r') as f: 168 - tokens = self._parse(f.read(), env) 169 - included.append((tokens, path)) 170 - self._base_paths.pop() 171 - except Exception as e: 172 - raise RuntimeError(f"processing included file {path} from line {lnum}") from e 516 + # xref | (id, type, heading inlines, file, starts new file) 517 + def _collect_ids(self, tokens: Sequence[Token], target_file: str, typ: str, file_changed: bool 518 + ) -> list[XrefTarget | tuple[str, str, Token, str, bool]]: 519 + result: list[XrefTarget | tuple[str, str, Token, str, bool]] = [] 520 + # collect all IDs and their xref substitutions. headings are deferred until everything 521 + # has been parsed so we can resolve links in headings. if that's even used anywhere. 522 + for (i, bt) in enumerate(tokens): 523 + if bt.type == 'heading_open' and (id := cast(str, bt.attrs.get('id', ''))): 524 + result.append((id, typ if bt.tag == 'h1' else 'section', tokens[i + 1], target_file, 525 + i == 0 and file_changed)) 526 + elif bt.type == 'included_options': 527 + id_prefix = bt.meta['id-prefix'] 528 + for opt in bt.meta['source'].keys(): 529 + id = make_xml_id(f"{id_prefix}{opt}") 530 + name = html.escape(opt) 531 + result.append(XrefTarget(id, f'<code class="option">{name}</code>', name, None, target_file)) 532 + elif bt.type.startswith('included_'): 533 + sub_file = bt.meta['include-args'].get('into-file', target_file) 534 + subtyp = bt.type.removeprefix('included_').removesuffix('s') 535 + for si, (sub, _path) in enumerate(bt.meta['included']): 536 + result += self._collect_ids(sub, sub_file, subtyp, si == 0 and sub_file != target_file) 537 + elif bt.type == 'inline': 538 + assert bt.children 539 + result += self._collect_ids(bt.children, target_file, typ, False) 540 + elif id := cast(str, bt.attrs.get('id', '')): 541 + # anchors and examples have no titles we could use, but we'll have to put 542 + # *something* here to communicate that there's no title. 543 + result.append(XrefTarget(id, "???", None, None, target_file)) 544 + return result 545 + 546 + def _render_xref(self, id: str, typ: str, inlines: Token, path: str, drop_fragment: bool) -> XrefTarget: 547 + assert inlines.children 548 + title_html = self._renderer.renderInline(inlines.children) 549 + if typ == 'appendix': 550 + # NOTE the docbook compat is strong here 551 + n = self._next_appendix_id() 552 + prefix = f"Appendix\u00A0{n}.\u00A0" 553 + # HACK for docbook compat: prefix the title inlines with appendix id if 554 + # necessary. the alternative is to mess with titlepage rendering in headings, 555 + # which seems just a lot worse than this 556 + prefix_tokens = [Token(type='text', tag='', nesting=0, content=prefix)] 557 + inlines.children = prefix_tokens + list(inlines.children) 558 + title = prefix + title_html 559 + toc_html = f"{n}. {title_html}" 560 + title_html = f"Appendix&nbsp;{n}" 561 + else: 562 + toc_html, title = title_html, title_html 563 + title_html = ( 564 + f"<em>{title_html}</em>" 565 + if typ == 'chapter' 566 + else title_html if typ in [ 'book', 'part' ] 567 + else f'the section called “{title_html}”' 568 + ) 569 + return XrefTarget(id, title_html, toc_html, re.sub('<.*?>', '', title), path, drop_fragment) 570 + 571 + def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) -> None: 572 + xref_queue = self._collect_ids(tokens, outfile.name, 'book', True) 173 573 174 - def _parse_options(self, token: Token) -> None: 175 - assert token.map 574 + failed = False 575 + deferred = [] 576 + while xref_queue: 577 + for item in xref_queue: 578 + try: 579 + target = item if isinstance(item, XrefTarget) else self._render_xref(*item) 580 + except UnresolvedXrefError as e: 581 + if failed: 582 + raise 583 + deferred.append(item) 584 + continue 176 585 177 - items = {} 178 - for (lnum, line) in enumerate(token.content.splitlines(), token.map[0] + 2): 179 - if len(args := line.split(":", 1)) != 2: 180 - raise RuntimeError(f"options directive with no argument in line {lnum}") 181 - (k, v) = (args[0].strip(), args[1].strip()) 182 - if k in items: 183 - raise RuntimeError(f"duplicate options directive {k} in line {lnum}") 184 - items[k] = v 185 - try: 186 - id_prefix = items.pop('id-prefix') 187 - varlist_id = items.pop('list-id') 188 - source = items.pop('source') 189 - except KeyError as e: 190 - raise RuntimeError(f"options directive {e} missing in block at line {token.map[0] + 1}") 191 - if items.keys(): 192 - raise RuntimeError( 193 - f"unsupported options directives in block at line {token.map[0] + 1}", 194 - " ".join(items.keys())) 586 + if target.id in self._xref_targets: 587 + raise RuntimeError(f"found duplicate id #{target.id}") 588 + self._xref_targets[target.id] = target 589 + if len(deferred) == len(xref_queue): 590 + failed = True # do another round and report the first error 591 + xref_queue = deferred 195 592 196 - try: 197 - conv = options.DocBookConverter( 198 - self._manpage_urls, self._revision, False, 'fragment', varlist_id, id_prefix) 199 - with open(self._base_paths[-1].parent / source, 'r') as f: 200 - conv.add_options(json.load(f)) 201 - token.meta['rendered-options'] = conv.finalize(fragment=True) 202 - except Exception as e: 203 - raise RuntimeError(f"processing options block in line {token.map[0] + 1}") from e 593 + TocEntry.collect_and_link(self._xref_targets, tokens) 204 594 205 595 206 596 ··· 210 600 p.add_argument('infile', type=Path) 211 601 p.add_argument('outfile', type=Path) 212 602 603 + def _build_cli_html(p: argparse.ArgumentParser) -> None: 604 + p.add_argument('--manpage-urls', required=True) 605 + p.add_argument('--revision', required=True) 606 + p.add_argument('--generator', default='nixos-render-docs') 607 + p.add_argument('--stylesheet', default=[], action='append') 608 + p.add_argument('--script', default=[], action='append') 609 + p.add_argument('--toc-depth', default=1, type=int) 610 + p.add_argument('--chunk-toc-depth', default=1, type=int) 611 + p.add_argument('infile', type=Path) 612 + p.add_argument('outfile', type=Path) 613 + 213 614 def _run_cli_db(args: argparse.Namespace) -> None: 214 615 with open(args.manpage_urls, 'r') as manpage_urls: 215 616 md = DocBookConverter(json.load(manpage_urls), args.revision) 216 - converted = md.convert(args.infile) 217 - args.outfile.write_text(converted) 617 + md.convert(args.infile, args.outfile) 618 + 619 + def _run_cli_html(args: argparse.Namespace) -> None: 620 + with open(args.manpage_urls, 'r') as manpage_urls: 621 + md = HTMLConverter( 622 + args.revision, 623 + HTMLParameters(args.generator, args.stylesheet, args.script, args.toc_depth, 624 + args.chunk_toc_depth), 625 + json.load(manpage_urls)) 626 + md.convert(args.infile, args.outfile) 218 627 219 628 def build_cli(p: argparse.ArgumentParser) -> None: 220 629 formats = p.add_subparsers(dest='format', required=True) 221 630 _build_cli_db(formats.add_parser('docbook')) 631 + _build_cli_html(formats.add_parser('html')) 222 632 223 633 def run_cli(args: argparse.Namespace) -> None: 224 634 if args.format == 'docbook': 225 635 _run_cli_db(args) 636 + elif args.format == 'html': 637 + _run_cli_html(args) 226 638 else: 227 639 raise RuntimeError('format not hooked up', args)
+186
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual_structure.py
··· 1 + from __future__ import annotations 2 + 3 + import dataclasses as dc 4 + import html 5 + import itertools 6 + 7 + from typing import cast, get_args, Iterable, Literal, Sequence 8 + 9 + from markdown_it.token import Token 10 + 11 + from .utils import Freezeable 12 + 13 + # FragmentType is used to restrict structural include blocks. 14 + FragmentType = Literal['preface', 'part', 'chapter', 'section', 'appendix'] 15 + 16 + # in the TOC all fragments are allowed, plus the all-encompassing book. 17 + TocEntryType = Literal['book', 'preface', 'part', 'chapter', 'section', 'appendix'] 18 + 19 + def is_include(token: Token) -> bool: 20 + return token.type == "fence" and token.info.startswith("{=include=} ") 21 + 22 + # toplevel file must contain only the title headings and includes, anything else 23 + # would cause strange rendering. 24 + def _check_book_structure(tokens: Sequence[Token]) -> None: 25 + for token in tokens[6:]: 26 + if not is_include(token): 27 + assert token.map 28 + raise RuntimeError(f"unexpected content in line {token.map[0] + 1}, " 29 + "expected structural include") 30 + 31 + # much like books, parts may not contain headings other than their title heading. 32 + # this is a limitation of the current renderers and TOC generators that do not handle 33 + # this case well even though it is supported in docbook (and probably supportable 34 + # anywhere else). 35 + def _check_part_structure(tokens: Sequence[Token]) -> None: 36 + _check_fragment_structure(tokens) 37 + for token in tokens[3:]: 38 + if token.type == 'heading_open': 39 + assert token.map 40 + raise RuntimeError(f"unexpected heading in line {token.map[0] + 1}") 41 + 42 + # two include blocks must either be adjacent or separated by a heading, otherwise 43 + # we cannot generate a correct TOC (since there'd be nothing to link to between 44 + # the two includes). 45 + def _check_fragment_structure(tokens: Sequence[Token]) -> None: 46 + for i, token in enumerate(tokens): 47 + if is_include(token) \ 48 + and i + 1 < len(tokens) \ 49 + and not (is_include(tokens[i + 1]) or tokens[i + 1].type == 'heading_open'): 50 + assert token.map 51 + raise RuntimeError(f"unexpected content in line {token.map[0] + 1}, " 52 + "expected heading or structural include") 53 + 54 + def check_structure(kind: TocEntryType, tokens: Sequence[Token]) -> None: 55 + wanted = { 'h1': 'title' } 56 + wanted |= { 'h2': 'subtitle' } if kind == 'book' else {} 57 + for (i, (tag, role)) in enumerate(wanted.items()): 58 + if len(tokens) < 3 * (i + 1): 59 + raise RuntimeError(f"missing {role} ({tag}) heading") 60 + token = tokens[3 * i] 61 + if token.type != 'heading_open' or token.tag != tag: 62 + assert token.map 63 + raise RuntimeError(f"expected {role} ({tag}) heading in line {token.map[0] + 1}", token) 64 + for t in tokens[3 * len(wanted):]: 65 + if t.type != 'heading_open' or not (role := wanted.get(t.tag, '')): 66 + continue 67 + assert t.map 68 + raise RuntimeError( 69 + f"only one {role} heading ({t.markup} [text...]) allowed per " 70 + f"{kind}, but found a second in line {t.map[0] + 1}. " 71 + "please remove all such headings except the first or demote the subsequent headings.", 72 + t) 73 + 74 + last_heading_level = 0 75 + for token in tokens: 76 + if token.type != 'heading_open': 77 + continue 78 + 79 + # book subtitle headings do not need an id, only book title headings do. 80 + # every other headings needs one too. we need this to build a TOC and to 81 + # provide stable links if the manual changes shape. 82 + if 'id' not in token.attrs and (kind != 'book' or token.tag != 'h2'): 83 + assert token.map 84 + raise RuntimeError(f"heading in line {token.map[0] + 1} does not have an id") 85 + 86 + level = int(token.tag[1:]) # because tag = h1..h6 87 + if level > last_heading_level + 1: 88 + assert token.map 89 + raise RuntimeError(f"heading in line {token.map[0] + 1} skips one or more heading levels, " 90 + "which is currently not allowed") 91 + last_heading_level = level 92 + 93 + if kind == 'book': 94 + _check_book_structure(tokens) 95 + elif kind == 'part': 96 + _check_part_structure(tokens) 97 + else: 98 + _check_fragment_structure(tokens) 99 + 100 + @dc.dataclass(frozen=True) 101 + class XrefTarget: 102 + id: str 103 + """link label for `[](#local-references)`""" 104 + title_html: str 105 + """toc label""" 106 + toc_html: str | None 107 + """text for `<title>` tags and `title="..."` attributes""" 108 + title: str | None 109 + """path to file that contains the anchor""" 110 + path: str 111 + """whether to drop the `#anchor` from links when expanding xrefs""" 112 + drop_fragment: bool = False 113 + 114 + def href(self) -> str: 115 + path = html.escape(self.path, True) 116 + return path if self.drop_fragment else f"{path}#{html.escape(self.id, True)}" 117 + 118 + @dc.dataclass 119 + class TocEntry(Freezeable): 120 + kind: TocEntryType 121 + target: XrefTarget 122 + parent: TocEntry | None = None 123 + prev: TocEntry | None = None 124 + next: TocEntry | None = None 125 + children: list[TocEntry] = dc.field(default_factory=list) 126 + starts_new_chunk: bool = False 127 + 128 + @property 129 + def root(self) -> TocEntry: 130 + return self.parent.root if self.parent else self 131 + 132 + @classmethod 133 + def of(cls, token: Token) -> TocEntry: 134 + entry = token.meta.get('TocEntry') 135 + if not isinstance(entry, TocEntry): 136 + raise RuntimeError('requested toc entry, none found', token) 137 + return entry 138 + 139 + @classmethod 140 + def collect_and_link(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token]) -> TocEntry: 141 + result = cls._collect_entries(xrefs, tokens, 'book') 142 + 143 + def flatten_with_parent(this: TocEntry, parent: TocEntry | None) -> Iterable[TocEntry]: 144 + this.parent = parent 145 + return itertools.chain([this], *[ flatten_with_parent(c, this) for c in this.children ]) 146 + 147 + flat = list(flatten_with_parent(result, None)) 148 + prev = flat[0] 149 + prev.starts_new_chunk = True 150 + paths_seen = set([prev.target.path]) 151 + for c in flat[1:]: 152 + if prev.target.path != c.target.path and c.target.path not in paths_seen: 153 + c.starts_new_chunk = True 154 + c.prev, prev.next = prev, c 155 + prev = c 156 + paths_seen.add(c.target.path) 157 + 158 + for c in flat: 159 + c.freeze() 160 + 161 + return result 162 + 163 + @classmethod 164 + def _collect_entries(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token], 165 + kind: TocEntryType) -> TocEntry: 166 + # we assume that check_structure has been run recursively over the entire input. 167 + # list contains (tag, entry) pairs that will collapse to a single entry for 168 + # the full sequence. 169 + entries: list[tuple[str, TocEntry]] = [] 170 + for token in tokens: 171 + if token.type.startswith('included_') and (included := token.meta.get('included')): 172 + fragment_type_str = token.type[9:].removesuffix('s') 173 + assert fragment_type_str in get_args(TocEntryType) 174 + fragment_type = cast(TocEntryType, fragment_type_str) 175 + for fragment, _path in included: 176 + entries[-1][1].children.append(cls._collect_entries(xrefs, fragment, fragment_type)) 177 + elif token.type == 'heading_open' and (id := cast(str, token.attrs.get('id', ''))): 178 + while len(entries) > 1 and entries[-1][0] >= token.tag: 179 + entries[-2][1].children.append(entries.pop()[1]) 180 + entries.append((token.tag, 181 + TocEntry(kind if token.tag == 'h1' else 'section', xrefs[id]))) 182 + token.meta['TocEntry'] = entries[-1][1] 183 + 184 + while len(entries) > 1: 185 + entries[-2][1].children.append(entries.pop()[1]) 186 + return entries[0][1]
+81 -117
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py
··· 1 1 from abc import ABC 2 2 from collections.abc import Mapping, MutableMapping, Sequence 3 - from typing import Any, Callable, cast, get_args, Iterable, Literal, NoReturn, Optional 3 + from typing import Any, Callable, cast, Generic, get_args, Iterable, Literal, NoReturn, Optional, TypeVar 4 4 5 5 import dataclasses 6 6 import re ··· 44 44 45 45 AdmonitionKind = Literal["note", "caution", "tip", "important", "warning"] 46 46 47 - class Renderer(markdown_it.renderer.RendererProtocol): 47 + class Renderer: 48 48 _admonitions: dict[AdmonitionKind, tuple[RenderFn, RenderFn]] 49 49 _admonition_stack: list[AdmonitionKind] 50 50 51 - def __init__(self, manpage_urls: Mapping[str, str], parser: Optional[markdown_it.MarkdownIt] = None): 51 + def __init__(self, manpage_urls: Mapping[str, str]): 52 52 self._manpage_urls = manpage_urls 53 53 self.rules = { 54 54 'text': self.text, ··· 104 104 def _join_inline(self, ls: Iterable[str]) -> str: 105 105 return "".join(ls) 106 106 107 - def admonition_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 108 - env: MutableMapping[str, Any]) -> str: 107 + def admonition_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 109 108 tag = token.meta['kind'] 110 109 self._admonition_stack.append(tag) 111 - return self._admonitions[tag][0](token, tokens, i, options, env) 112 - def admonition_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 113 - env: MutableMapping[str, Any]) -> str: 114 - return self._admonitions[self._admonition_stack.pop()][1](token, tokens, i, options, env) 110 + return self._admonitions[tag][0](token, tokens, i) 111 + def admonition_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 112 + return self._admonitions[self._admonition_stack.pop()][1](token, tokens, i) 115 113 116 - def render(self, tokens: Sequence[Token], options: OptionsDict, 117 - env: MutableMapping[str, Any]) -> str: 114 + def render(self, tokens: Sequence[Token]) -> str: 118 115 def do_one(i: int, token: Token) -> str: 119 116 if token.type == "inline": 120 117 assert token.children is not None 121 - return self.renderInline(token.children, options, env) 118 + return self.renderInline(token.children) 122 119 elif token.type in self.rules: 123 - return self.rules[token.type](tokens[i], tokens, i, options, env) 120 + return self.rules[token.type](tokens[i], tokens, i) 124 121 else: 125 122 raise NotImplementedError("md token not supported yet", token) 126 123 return self._join_block(map(lambda arg: do_one(*arg), enumerate(tokens))) 127 - def renderInline(self, tokens: Sequence[Token], options: OptionsDict, 128 - env: MutableMapping[str, Any]) -> str: 124 + def renderInline(self, tokens: Sequence[Token]) -> str: 129 125 def do_one(i: int, token: Token) -> str: 130 126 if token.type in self.rules: 131 - return self.rules[token.type](tokens[i], tokens, i, options, env) 127 + return self.rules[token.type](tokens[i], tokens, i) 132 128 else: 133 129 raise NotImplementedError("md token not supported yet", token) 134 130 return self._join_inline(map(lambda arg: do_one(*arg), enumerate(tokens))) 135 131 136 - def text(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 137 - env: MutableMapping[str, Any]) -> str: 132 + def text(self, token: Token, tokens: Sequence[Token], i: int) -> str: 138 133 raise RuntimeError("md token not supported", token) 139 - def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 140 - env: MutableMapping[str, Any]) -> str: 134 + def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 141 135 raise RuntimeError("md token not supported", token) 142 - def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 143 - env: MutableMapping[str, Any]) -> str: 136 + def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 144 137 raise RuntimeError("md token not supported", token) 145 - def hardbreak(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 146 - env: MutableMapping[str, Any]) -> str: 138 + def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 147 139 raise RuntimeError("md token not supported", token) 148 - def softbreak(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 149 - env: MutableMapping[str, Any]) -> str: 140 + def softbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 150 141 raise RuntimeError("md token not supported", token) 151 - def code_inline(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 152 - env: MutableMapping[str, Any]) -> str: 142 + def code_inline(self, token: Token, tokens: Sequence[Token], i: int) -> str: 153 143 raise RuntimeError("md token not supported", token) 154 - def code_block(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 155 - env: MutableMapping[str, Any]) -> str: 144 + def code_block(self, token: Token, tokens: Sequence[Token], i: int) -> str: 156 145 raise RuntimeError("md token not supported", token) 157 - def link_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 158 - env: MutableMapping[str, Any]) -> str: 146 + def link_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 159 147 raise RuntimeError("md token not supported", token) 160 - def link_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 161 - env: MutableMapping[str, Any]) -> str: 148 + def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 162 149 raise RuntimeError("md token not supported", token) 163 - def list_item_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 164 - env: MutableMapping[str, Any]) -> str: 150 + def list_item_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 165 151 raise RuntimeError("md token not supported", token) 166 - def list_item_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 167 - env: MutableMapping[str, Any]) -> str: 152 + def list_item_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 168 153 raise RuntimeError("md token not supported", token) 169 - def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 170 - env: MutableMapping[str, Any]) -> str: 154 + def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 171 155 raise RuntimeError("md token not supported", token) 172 - def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 173 - env: MutableMapping[str, Any]) -> str: 156 + def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 174 157 raise RuntimeError("md token not supported", token) 175 - def em_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 176 - env: MutableMapping[str, Any]) -> str: 158 + def em_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 177 159 raise RuntimeError("md token not supported", token) 178 - def em_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 179 - env: MutableMapping[str, Any]) -> str: 160 + def em_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 180 161 raise RuntimeError("md token not supported", token) 181 - def strong_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 182 - env: MutableMapping[str, Any]) -> str: 162 + def strong_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 183 163 raise RuntimeError("md token not supported", token) 184 - def strong_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 185 - env: MutableMapping[str, Any]) -> str: 164 + def strong_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 186 165 raise RuntimeError("md token not supported", token) 187 - def fence(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 188 - env: MutableMapping[str, Any]) -> str: 166 + def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str: 189 167 raise RuntimeError("md token not supported", token) 190 - def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 191 - env: MutableMapping[str, Any]) -> str: 168 + def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 192 169 raise RuntimeError("md token not supported", token) 193 - def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 194 - env: MutableMapping[str, Any]) -> str: 170 + def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 195 171 raise RuntimeError("md token not supported", token) 196 - def note_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 197 - env: MutableMapping[str, Any]) -> str: 172 + def note_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 198 173 raise RuntimeError("md token not supported", token) 199 - def note_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 200 - env: MutableMapping[str, Any]) -> str: 174 + def note_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 201 175 raise RuntimeError("md token not supported", token) 202 - def caution_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 203 - env: MutableMapping[str, Any]) -> str: 176 + def caution_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 204 177 raise RuntimeError("md token not supported", token) 205 - def caution_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 206 - env: MutableMapping[str, Any]) -> str: 178 + def caution_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 207 179 raise RuntimeError("md token not supported", token) 208 - def important_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 209 - env: MutableMapping[str, Any]) -> str: 180 + def important_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 210 181 raise RuntimeError("md token not supported", token) 211 - def important_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 212 - env: MutableMapping[str, Any]) -> str: 182 + def important_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 213 183 raise RuntimeError("md token not supported", token) 214 - def tip_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 215 - env: MutableMapping[str, Any]) -> str: 184 + def tip_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 216 185 raise RuntimeError("md token not supported", token) 217 - def tip_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 218 - env: MutableMapping[str, Any]) -> str: 186 + def tip_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 219 187 raise RuntimeError("md token not supported", token) 220 - def warning_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 221 - env: MutableMapping[str, Any]) -> str: 188 + def warning_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 222 189 raise RuntimeError("md token not supported", token) 223 - def warning_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 224 - env: MutableMapping[str, Any]) -> str: 190 + def warning_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 225 191 raise RuntimeError("md token not supported", token) 226 - def dl_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 227 - env: MutableMapping[str, Any]) -> str: 192 + def dl_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 228 193 raise RuntimeError("md token not supported", token) 229 - def dl_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 230 - env: MutableMapping[str, Any]) -> str: 194 + def dl_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 231 195 raise RuntimeError("md token not supported", token) 232 - def dt_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 233 - env: MutableMapping[str, Any]) -> str: 196 + def dt_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 234 197 raise RuntimeError("md token not supported", token) 235 - def dt_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 236 - env: MutableMapping[str, Any]) -> str: 198 + def dt_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 237 199 raise RuntimeError("md token not supported", token) 238 - def dd_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 239 - env: MutableMapping[str, Any]) -> str: 200 + def dd_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 240 201 raise RuntimeError("md token not supported", token) 241 - def dd_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 242 - env: MutableMapping[str, Any]) -> str: 202 + def dd_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 243 203 raise RuntimeError("md token not supported", token) 244 - def myst_role(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 245 - env: MutableMapping[str, Any]) -> str: 204 + def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str: 246 205 raise RuntimeError("md token not supported", token) 247 - def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 248 - env: MutableMapping[str, Any]) -> str: 206 + def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str: 249 207 raise RuntimeError("md token not supported", token) 250 - def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 251 - env: MutableMapping[str, Any]) -> str: 208 + def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str: 252 209 raise RuntimeError("md token not supported", token) 253 - def heading_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 254 - env: MutableMapping[str, Any]) -> str: 210 + def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 255 211 raise RuntimeError("md token not supported", token) 256 - def heading_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 257 - env: MutableMapping[str, Any]) -> str: 212 + def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 258 213 raise RuntimeError("md token not supported", token) 259 - def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 260 - env: MutableMapping[str, Any]) -> str: 214 + def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 261 215 raise RuntimeError("md token not supported", token) 262 - def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 263 - env: MutableMapping[str, Any]) -> str: 216 + def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 264 217 raise RuntimeError("md token not supported", token) 265 - def example_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 266 - env: MutableMapping[str, Any]) -> str: 218 + def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 267 219 raise RuntimeError("md token not supported", token) 268 - def example_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 269 - env: MutableMapping[str, Any]) -> str: 220 + def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 270 221 raise RuntimeError("md token not supported", token) 271 222 272 223 def _is_escaped(src: str, pos: int) -> bool: ··· 466 417 467 418 md.core.ruler.push("block_attr", block_attr) 468 419 469 - class Converter(ABC): 470 - __renderer__: Callable[[Mapping[str, str], markdown_it.MarkdownIt], Renderer] 420 + TR = TypeVar('TR', bound='Renderer') 421 + 422 + class Converter(ABC, Generic[TR]): 423 + # we explicitly disable markdown-it rendering support and use our own entirely. 424 + # rendering is well separated from parsing and our renderers carry much more state than 425 + # markdown-it easily acknowledges as 'good' (unless we used the untyped env args to 426 + # shuttle that state around, which is very fragile) 427 + class ForbiddenRenderer(markdown_it.renderer.RendererProtocol): 428 + __output__ = "none" 429 + 430 + def __init__(self, parser: Optional[markdown_it.MarkdownIt]): 431 + pass 432 + 433 + def render(self, tokens: Sequence[Token], options: OptionsDict, 434 + env: MutableMapping[str, Any]) -> str: 435 + raise NotImplementedError("do not use Converter._md.renderer. 'tis a silly place") 471 436 472 - def __init__(self, manpage_urls: Mapping[str, str]): 473 - self._manpage_urls = manpage_urls 437 + _renderer: TR 474 438 439 + def __init__(self) -> None: 475 440 self._md = markdown_it.MarkdownIt( 476 441 "commonmark", 477 442 { ··· 479 444 'html': False, # not useful since we target many formats 480 445 'typographer': True, # required for smartquotes 481 446 }, 482 - renderer_cls=lambda parser: self.__renderer__(self._manpage_urls, parser) 447 + renderer_cls=self.ForbiddenRenderer 483 448 ) 484 449 self._md.use( 485 450 container_plugin, ··· 496 461 self._md.use(_block_attr) 497 462 self._md.enable(["smartquotes", "replacements"]) 498 463 499 - def _parse(self, src: str, env: Optional[MutableMapping[str, Any]] = None) -> list[Token]: 500 - return self._md.parse(src, env if env is not None else {}) 464 + def _parse(self, src: str) -> list[Token]: 465 + return self._md.parse(src, {}) 501 466 502 - def _render(self, src: str, env: Optional[MutableMapping[str, Any]] = None) -> str: 503 - env = {} if env is None else env 504 - tokens = self._parse(src, env) 505 - return self._md.renderer.render(tokens, self._md.options, env) # type: ignore[no-any-return] 467 + def _render(self, src: str) -> str: 468 + tokens = self._parse(src) 469 + return self._renderer.render(tokens)
+150 -51
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/options.py
··· 1 1 from __future__ import annotations 2 2 3 3 import argparse 4 + import html 4 5 import json 6 + import xml.sax.saxutils as xml 5 7 6 8 from abc import abstractmethod 7 - from collections.abc import Mapping, MutableMapping, Sequence 8 - from markdown_it.utils import OptionsDict 9 + from collections.abc import Mapping, Sequence 9 10 from markdown_it.token import Token 10 - from typing import Any, Optional 11 + from typing import Any, Generic, Optional 11 12 from urllib.parse import quote 12 - from xml.sax.saxutils import escape, quoteattr 13 13 14 14 import markdown_it 15 15 16 + from . import md 16 17 from . import parallel 17 18 from .asciidoc import AsciiDocRenderer, asciidoc_escape 18 19 from .commonmark import CommonMarkRenderer 19 20 from .docbook import DocBookRenderer, make_xml_id 21 + from .html import HTMLRenderer 20 22 from .manpage import ManpageRenderer, man_escape 23 + from .manual_structure import XrefTarget 21 24 from .md import Converter, md_escape, md_make_code 22 25 from .types import OptionLoc, Option, RenderedOption 23 26 ··· 30 33 return None 31 34 return option[key] # type: ignore[return-value] 32 35 33 - class BaseConverter(Converter): 36 + class BaseConverter(Converter[md.TR], Generic[md.TR]): 34 37 __option_block_separator__: str 35 38 36 39 _options: dict[str, RenderedOption] 37 40 38 - def __init__(self, manpage_urls: Mapping[str, str], 39 - revision: str, 40 - markdown_by_default: bool): 41 - super().__init__(manpage_urls) 41 + def __init__(self, revision: str, markdown_by_default: bool): 42 + super().__init__() 42 43 self._options = {} 43 44 self._revision = revision 44 45 self._markdown_by_default = markdown_by_default ··· 153 154 # since it's good enough so far. 154 155 @classmethod 155 156 @abstractmethod 156 - def _parallel_render_init_worker(cls, a: Any) -> BaseConverter: raise NotImplementedError() 157 + def _parallel_render_init_worker(cls, a: Any) -> BaseConverter[md.TR]: raise NotImplementedError() 157 158 158 159 def _render_option(self, name: str, option: dict[str, Any]) -> RenderedOption: 159 160 try: ··· 162 163 raise Exception(f"Failed to render option {name}") from e 163 164 164 165 @classmethod 165 - def _parallel_render_step(cls, s: BaseConverter, a: Any) -> RenderedOption: 166 + def _parallel_render_step(cls, s: BaseConverter[md.TR], a: Any) -> RenderedOption: 166 167 return s._render_option(*a) 167 168 168 169 def add_options(self, options: dict[str, Any]) -> None: ··· 175 176 def finalize(self) -> str: raise NotImplementedError() 176 177 177 178 class OptionDocsRestrictions: 178 - def heading_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 179 - env: MutableMapping[str, Any]) -> str: 179 + def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 180 180 raise RuntimeError("md token not supported in options doc", token) 181 - def heading_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 182 - env: MutableMapping[str, Any]) -> str: 181 + def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 183 182 raise RuntimeError("md token not supported in options doc", token) 184 - def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 185 - env: MutableMapping[str, Any]) -> str: 183 + def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str: 186 184 raise RuntimeError("md token not supported in options doc", token) 187 - def example_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 188 - env: MutableMapping[str, Any]) -> str: 185 + def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 189 186 raise RuntimeError("md token not supported in options doc", token) 190 187 191 188 class OptionsDocBookRenderer(OptionDocsRestrictions, DocBookRenderer): 192 189 # TODO keep optionsDocBook diff small. remove soon if rendering is still good. 193 - def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 194 - env: MutableMapping[str, Any]) -> str: 190 + def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 195 191 token.meta['compact'] = False 196 - return super().ordered_list_open(token, tokens, i, options, env) 197 - def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict, 198 - env: MutableMapping[str, Any]) -> str: 192 + return super().ordered_list_open(token, tokens, i) 193 + def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 199 194 token.meta['compact'] = False 200 - return super().bullet_list_open(token, tokens, i, options, env) 195 + return super().bullet_list_open(token, tokens, i) 201 196 202 - class DocBookConverter(BaseConverter): 203 - __renderer__ = OptionsDocBookRenderer 197 + class DocBookConverter(BaseConverter[OptionsDocBookRenderer]): 204 198 __option_block_separator__ = "" 205 199 206 200 def __init__(self, manpage_urls: Mapping[str, str], ··· 209 203 document_type: str, 210 204 varlist_id: str, 211 205 id_prefix: str): 212 - super().__init__(manpage_urls, revision, markdown_by_default) 206 + super().__init__(revision, markdown_by_default) 207 + self._renderer = OptionsDocBookRenderer(manpage_urls) 213 208 self._document_type = document_type 214 209 self._varlist_id = varlist_id 215 210 self._id_prefix = id_prefix 216 211 217 212 def _parallel_render_prepare(self) -> Any: 218 - return (self._manpage_urls, self._revision, self._markdown_by_default, self._document_type, 213 + return (self._renderer._manpage_urls, self._revision, self._markdown_by_default, self._document_type, 219 214 self._varlist_id, self._id_prefix) 220 215 @classmethod 221 216 def _parallel_render_init_worker(cls, a: Any) -> DocBookConverter: ··· 248 243 249 244 def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]: 250 245 if href is not None: 251 - href = " xlink:href=" + quoteattr(href) 246 + href = " xlink:href=" + xml.quoteattr(href) 252 247 return [ 253 248 f"<member><filename{href}>", 254 - escape(name), 249 + xml.escape(name), 255 250 "</filename></member>" 256 251 ] 257 252 ··· 281 276 result += [ 282 277 "<varlistentry>", 283 278 # NOTE adding extra spaces here introduces spaces into xref link expansions 284 - (f"<term xlink:href={quoteattr('#' + id)} xml:id={quoteattr(id)}>" + 285 - f"<option>{escape(name)}</option></term>"), 279 + (f"<term xlink:href={xml.quoteattr('#' + id)} xml:id={xml.quoteattr(id)}>" + 280 + f"<option>{xml.escape(name)}</option></term>"), 286 281 "<listitem>" 287 282 ] 288 283 result += opt.lines ··· 300 295 class OptionsManpageRenderer(OptionDocsRestrictions, ManpageRenderer): 301 296 pass 302 297 303 - class ManpageConverter(BaseConverter): 304 - def __renderer__(self, manpage_urls: Mapping[str, str], 305 - parser: Optional[markdown_it.MarkdownIt] = None) -> OptionsManpageRenderer: 306 - return OptionsManpageRenderer(manpage_urls, self._options_by_id, parser) 307 - 298 + class ManpageConverter(BaseConverter[OptionsManpageRenderer]): 308 299 __option_block_separator__ = ".sp" 309 300 310 301 _options_by_id: dict[str, str] ··· 314 305 *, 315 306 # only for parallel rendering 316 307 _options_by_id: Optional[dict[str, str]] = None): 308 + super().__init__(revision, markdown_by_default) 317 309 self._options_by_id = _options_by_id or {} 318 - super().__init__({}, revision, markdown_by_default) 310 + self._renderer = OptionsManpageRenderer({}, self._options_by_id) 319 311 320 312 def _parallel_render_prepare(self) -> Any: 321 313 return ((self._revision, self._markdown_by_default), { '_options_by_id': self._options_by_id }) ··· 324 316 return cls(*a[0], **a[1]) 325 317 326 318 def _render_option(self, name: str, option: dict[str, Any]) -> RenderedOption: 327 - assert isinstance(self._md.renderer, OptionsManpageRenderer) 328 - links = self._md.renderer.link_footnotes = [] 319 + links = self._renderer.link_footnotes = [] 329 320 result = super()._render_option(name, option) 330 - self._md.renderer.link_footnotes = None 321 + self._renderer.link_footnotes = None 331 322 return result._replace(links=links) 332 323 333 324 def add_options(self, options: dict[str, Any]) -> None: ··· 339 330 if lit := option_is(option, key, 'literalDocBook'): 340 331 raise RuntimeError("can't render manpages in the presence of docbook") 341 332 else: 342 - assert isinstance(self._md.renderer, OptionsManpageRenderer) 343 333 try: 344 - self._md.renderer.inline_code_is_quoted = False 334 + self._renderer.inline_code_is_quoted = False 345 335 return super()._render_code(option, key) 346 336 finally: 347 - self._md.renderer.inline_code_is_quoted = True 337 + self._renderer.inline_code_is_quoted = True 348 338 349 339 def _render_description(self, desc: str | dict[str, Any]) -> list[str]: 350 340 if isinstance(desc, str) and not self._markdown_by_default: ··· 428 418 class OptionsCommonMarkRenderer(OptionDocsRestrictions, CommonMarkRenderer): 429 419 pass 430 420 431 - class CommonMarkConverter(BaseConverter): 432 - __renderer__ = OptionsCommonMarkRenderer 421 + class CommonMarkConverter(BaseConverter[OptionsCommonMarkRenderer]): 433 422 __option_block_separator__ = "" 434 423 424 + def __init__(self, manpage_urls: Mapping[str, str], revision: str, markdown_by_default: bool): 425 + super().__init__(revision, markdown_by_default) 426 + self._renderer = OptionsCommonMarkRenderer(manpage_urls) 427 + 435 428 def _parallel_render_prepare(self) -> Any: 436 - return (self._manpage_urls, self._revision, self._markdown_by_default) 429 + return (self._renderer._manpage_urls, self._revision, self._markdown_by_default) 437 430 @classmethod 438 431 def _parallel_render_init_worker(cls, a: Any) -> CommonMarkConverter: 439 432 return cls(*a) ··· 481 474 class OptionsAsciiDocRenderer(OptionDocsRestrictions, AsciiDocRenderer): 482 475 pass 483 476 484 - class AsciiDocConverter(BaseConverter): 485 - __renderer__ = AsciiDocRenderer 477 + class AsciiDocConverter(BaseConverter[OptionsAsciiDocRenderer]): 486 478 __option_block_separator__ = "" 487 479 480 + def __init__(self, manpage_urls: Mapping[str, str], revision: str, markdown_by_default: bool): 481 + super().__init__(revision, markdown_by_default) 482 + self._renderer = OptionsAsciiDocRenderer(manpage_urls) 483 + 488 484 def _parallel_render_prepare(self) -> Any: 489 - return (self._manpage_urls, self._revision, self._markdown_by_default) 485 + return (self._renderer._manpage_urls, self._revision, self._markdown_by_default) 490 486 @classmethod 491 487 def _parallel_render_init_worker(cls, a: Any) -> AsciiDocConverter: 492 488 return cls(*a) ··· 528 524 result.append(f"== {asciidoc_escape(name)}\n") 529 525 result += opt.lines 530 526 result.append("\n\n") 527 + 528 + return "\n".join(result) 529 + 530 + class OptionsHTMLRenderer(OptionDocsRestrictions, HTMLRenderer): 531 + # TODO docbook compat. must be removed together with the matching docbook handlers. 532 + def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 533 + token.meta['compact'] = False 534 + return super().ordered_list_open(token, tokens, i) 535 + def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 536 + token.meta['compact'] = False 537 + return super().bullet_list_open(token, tokens, i) 538 + def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str: 539 + # TODO use token.info. docbook doesn't so we can't yet. 540 + return f'<pre class="programlisting">{html.escape(token.content)}</pre>' 541 + 542 + class HTMLConverter(BaseConverter[OptionsHTMLRenderer]): 543 + __option_block_separator__ = "" 544 + 545 + def __init__(self, manpage_urls: Mapping[str, str], revision: str, markdown_by_default: bool, 546 + varlist_id: str, id_prefix: str, xref_targets: Mapping[str, XrefTarget]): 547 + super().__init__(revision, markdown_by_default) 548 + self._xref_targets = xref_targets 549 + self._varlist_id = varlist_id 550 + self._id_prefix = id_prefix 551 + self._renderer = OptionsHTMLRenderer(manpage_urls, self._xref_targets) 552 + 553 + def _parallel_render_prepare(self) -> Any: 554 + return (self._renderer._manpage_urls, self._revision, self._markdown_by_default, 555 + self._varlist_id, self._id_prefix, self._xref_targets) 556 + @classmethod 557 + def _parallel_render_init_worker(cls, a: Any) -> HTMLConverter: 558 + return cls(*a) 559 + 560 + def _render_code(self, option: dict[str, Any], key: str) -> list[str]: 561 + if lit := option_is(option, key, 'literalDocBook'): 562 + raise RuntimeError("can't render html in the presence of docbook") 563 + else: 564 + return super()._render_code(option, key) 565 + 566 + def _render_description(self, desc: str | dict[str, Any]) -> list[str]: 567 + if isinstance(desc, str) and not self._markdown_by_default: 568 + raise RuntimeError("can't render html in the presence of docbook") 569 + else: 570 + return super()._render_description(desc) 571 + 572 + def _related_packages_header(self) -> list[str]: 573 + return [ 574 + '<p><span class="emphasis"><em>Related packages:</em></span></p>', 575 + ] 576 + 577 + def _decl_def_header(self, header: str) -> list[str]: 578 + return [ 579 + f'<p><span class="emphasis"><em>{header}:</em></span></p>', 580 + '<table border="0" summary="Simple list" class="simplelist">' 581 + ] 582 + 583 + def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]: 584 + if href is not None: 585 + href = f' href="{html.escape(href, True)}"' 586 + return [ 587 + "<tr><td>", 588 + f'<code class="filename"><a class="filename" {href} target="_top">', 589 + f'{html.escape(name)}', 590 + '</a></code>', 591 + "</td></tr>" 592 + ] 593 + 594 + def _decl_def_footer(self) -> list[str]: 595 + return [ "</table>" ] 596 + 597 + def finalize(self) -> str: 598 + result = [] 599 + 600 + result += [ 601 + '<div class="variablelist">', 602 + f'<a id="{html.escape(self._varlist_id, True)}"></a>', 603 + ' <dl class="variablelist">', 604 + ] 605 + 606 + for (name, opt) in self._sorted_options(): 607 + id = make_xml_id(self._id_prefix + name) 608 + target = self._xref_targets[id] 609 + result += [ 610 + '<dt>', 611 + ' <span class="term">', 612 + # docbook compat, these could be one tag 613 + f' <a id="{html.escape(id, True)}"></a><a class="term" href="{target.href()}">' 614 + # no spaces here (and string merging) for docbook output compat 615 + f'<code class="option">{html.escape(name)}</code>', 616 + ' </a>', 617 + ' </span>', 618 + '</dt>', 619 + '<dd>', 620 + ] 621 + result += opt.lines 622 + result += [ 623 + "</dd>", 624 + ] 625 + 626 + result += [ 627 + " </dl>", 628 + "</div>" 629 + ] 531 630 532 631 return "\n".join(result) 533 632
+2 -3
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/types.py
··· 1 - from collections.abc import Sequence, MutableMapping 1 + from collections.abc import Sequence 2 2 from typing import Any, Callable, Optional, Tuple, NamedTuple 3 3 4 4 from markdown_it.token import Token 5 - from markdown_it.utils import OptionsDict 6 5 7 6 OptionLoc = str | dict[str, str] 8 7 Option = dict[str, str | dict[str, str] | list[OptionLoc]] ··· 12 11 lines: list[str] 13 12 links: Optional[list[str]] = None 14 13 15 - RenderFn = Callable[[Token, Sequence[Token], int, OptionsDict, MutableMapping[str, Any]], str] 14 + RenderFn = Callable[[Token, Sequence[Token], int], str]
+21
pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/utils.py
··· 1 + from typing import Any 2 + 3 + _frozen_classes: dict[type, type] = {} 4 + 5 + # make a derived class freezable (ie, disallow modifications). 6 + # we do this by changing the class of an instance at runtime when freeze() 7 + # is called, providing a derived class that is exactly the same except 8 + # for a __setattr__ that raises an error when called. this beats having 9 + # a field for frozenness and an unconditional __setattr__ that checks this 10 + # field because it does not insert anything into the class dict. 11 + class Freezeable: 12 + def freeze(self) -> None: 13 + cls = type(self) 14 + if not (frozen := _frozen_classes.get(cls)): 15 + def __setattr__(instance: Any, n: str, v: Any) -> None: 16 + raise TypeError(f'{cls.__name__} is frozen') 17 + frozen = type(cls.__name__, (cls,), { 18 + '__setattr__': __setattr__, 19 + }) 20 + _frozen_classes[cls] = frozen 21 + self.__class__ = frozen
+5 -3
pkgs/tools/nix/nixos-render-docs/src/tests/test_asciidoc.py
··· 1 - import nixos_render_docs 1 + import nixos_render_docs as nrd 2 2 3 3 from sample_md import sample1 4 4 5 - class Converter(nixos_render_docs.md.Converter): 6 - __renderer__ = nixos_render_docs.asciidoc.AsciiDocRenderer 5 + class Converter(nrd.md.Converter[nrd.asciidoc.AsciiDocRenderer]): 6 + def __init__(self, manpage_urls: dict[str, str]): 7 + super().__init__() 8 + self._renderer = nrd.asciidoc.AsciiDocRenderer(manpage_urls) 7 9 8 10 def test_lists() -> None: 9 11 c = Converter({})
+5 -3
pkgs/tools/nix/nixos-render-docs/src/tests/test_commonmark.py
··· 1 - import nixos_render_docs 1 + import nixos_render_docs as nrd 2 2 3 3 from sample_md import sample1 4 4 ··· 6 6 7 7 import markdown_it 8 8 9 - class Converter(nixos_render_docs.md.Converter): 10 - __renderer__ = nixos_render_docs.commonmark.CommonMarkRenderer 9 + class Converter(nrd.md.Converter[nrd.commonmark.CommonMarkRenderer]): 10 + def __init__(self, manpage_urls: Mapping[str, str]): 11 + super().__init__() 12 + self._renderer = nrd.commonmark.CommonMarkRenderer(manpage_urls) 11 13 12 14 # NOTE: in these tests we represent trailing spaces by ` ` and replace them with real space later, 13 15 # since a number of editors will strip trailing whitespace on save and that would break the tests.
+5 -3
pkgs/tools/nix/nixos-render-docs/src/tests/test_headings.py
··· 1 - import nixos_render_docs 1 + import nixos_render_docs as nrd 2 2 3 3 from markdown_it.token import Token 4 4 5 - class Converter(nixos_render_docs.md.Converter): 5 + class Converter(nrd.md.Converter[nrd.docbook.DocBookRenderer]): 6 6 # actual renderer doesn't matter, we're just parsing. 7 - __renderer__ = nixos_render_docs.docbook.DocBookRenderer 7 + def __init__(self, manpage_urls: dict[str, str]) -> None: 8 + super().__init__() 9 + self._renderer = nrd.docbook.DocBookRenderer(manpage_urls) 8 10 9 11 def test_heading_id_absent() -> None: 10 12 c = Converter({})
+179
pkgs/tools/nix/nixos-render-docs/src/tests/test_html.py
··· 1 + import nixos_render_docs as nrd 2 + import pytest 3 + 4 + from sample_md import sample1 5 + 6 + class Converter(nrd.md.Converter[nrd.html.HTMLRenderer]): 7 + def __init__(self, manpage_urls: dict[str, str], xrefs: dict[str, nrd.manual_structure.XrefTarget]): 8 + super().__init__() 9 + self._renderer = nrd.html.HTMLRenderer(manpage_urls, xrefs) 10 + 11 + def unpretty(s: str) -> str: 12 + return "".join(map(str.strip, s.splitlines())).replace('␣', ' ').replace('↵', '\n') 13 + 14 + def test_lists_styles() -> None: 15 + # nested lists rotate through a number of list style 16 + c = Converter({}, {}) 17 + assert c._render("- - - - foo") == unpretty(""" 18 + <div class="itemizedlist"><ul class="itemizedlist compact" style="list-style-type: disc;"> 19 + <li class="listitem"> 20 + <div class="itemizedlist"><ul class="itemizedlist compact" style="list-style-type: circle;"> 21 + <li class="listitem"> 22 + <div class="itemizedlist"><ul class="itemizedlist compact" style="list-style-type: square;"> 23 + <li class="listitem"> 24 + <div class="itemizedlist"><ul class="itemizedlist compact" style="list-style-type: disc;"> 25 + <li class="listitem"><p>foo</p></li> 26 + </ul></div> 27 + </li> 28 + </ul></div> 29 + </li> 30 + </ul></div> 31 + </li> 32 + </ul></div> 33 + """) 34 + assert c._render("1. 1. 1. 1. 1. 1. foo") == unpretty(""" 35 + <div class="orderedlist"><ol class="orderedlist compact" type="1"> 36 + <li class="listitem"> 37 + <div class="orderedlist"><ol class="orderedlist compact" type="a"> 38 + <li class="listitem"> 39 + <div class="orderedlist"><ol class="orderedlist compact" type="i"> 40 + <li class="listitem"> 41 + <div class="orderedlist"><ol class="orderedlist compact" type="A"> 42 + <li class="listitem"> 43 + <div class="orderedlist"><ol class="orderedlist compact" type="I"> 44 + <li class="listitem"> 45 + <div class="orderedlist"><ol class="orderedlist compact" type="1"> 46 + <li class="listitem"><p>foo</p></li> 47 + </ol></div> 48 + </li> 49 + </ol></div> 50 + </li> 51 + </ol></div> 52 + </li> 53 + </ol></div> 54 + </li> 55 + </ol></div> 56 + </li> 57 + </ol></div> 58 + """) 59 + 60 + def test_xrefs() -> None: 61 + # nested lists rotate through a number of list style 62 + c = Converter({}, { 63 + 'foo': nrd.manual_structure.XrefTarget('foo', '<hr/>', 'toc1', 'title1', 'index.html'), 64 + 'bar': nrd.manual_structure.XrefTarget('bar', '<br/>', 'toc2', 'title2', 'index.html', True), 65 + }) 66 + assert c._render("[](#foo)") == '<p><a class="xref" href="index.html#foo" title="title1" ><hr/></a></p>' 67 + assert c._render("[](#bar)") == '<p><a class="xref" href="index.html" title="title2" ><br/></a></p>' 68 + with pytest.raises(nrd.html.UnresolvedXrefError) as exc: 69 + c._render("[](#baz)") 70 + assert exc.value.args[0] == 'bad local reference, id #baz not known' 71 + 72 + def test_full() -> None: 73 + c = Converter({ 'man(1)': 'http://example.org' }, {}) 74 + assert c._render(sample1) == unpretty(""" 75 + <div class="warning"> 76 + <h3 class="title">Warning</h3> 77 + <p>foo</p> 78 + <div class="note"> 79 + <h3 class="title">Note</h3> 80 + <p>nested</p> 81 + </div> 82 + </div> 83 + <p> 84 + <a class="link" href="link" target="_top">↵ 85 + multiline↵ 86 + </a> 87 + </p> 88 + <p> 89 + <a class="link" href="http://example.org" target="_top"> 90 + <span class="citerefentry"><span class="refentrytitle">man</span>(1)</span> 91 + </a> reference 92 + </p> 93 + <p><a id="b" />some <a id="a" />nested anchors</p> 94 + <p> 95 + <span class="emphasis"><em>emph</em></span>␣ 96 + <span class="strong"><strong>strong</strong></span>␣ 97 + <span class="emphasis"><em>nesting emph <span class="strong"><strong>and strong</strong></span>␣ 98 + and <code class="literal">code</code></em></span> 99 + </p> 100 + <div class="itemizedlist"> 101 + <ul class="itemizedlist " style="list-style-type: disc;"> 102 + <li class="listitem"><p>wide bullet</p></li> 103 + <li class="listitem"><p>list</p></li> 104 + </ul> 105 + </div> 106 + <div class="orderedlist"> 107 + <ol class="orderedlist " type="1"> 108 + <li class="listitem"><p>wide ordered</p></li> 109 + <li class="listitem"><p>list</p></li> 110 + </ol> 111 + </div> 112 + <div class="itemizedlist"> 113 + <ul class="itemizedlist compact" style="list-style-type: disc;"> 114 + <li class="listitem"><p>narrow bullet</p></li> 115 + <li class="listitem"><p>list</p></li> 116 + </ul> 117 + </div> 118 + <div class="orderedlist"> 119 + <ol class="orderedlist compact" type="1"> 120 + <li class="listitem"><p>narrow ordered</p></li> 121 + <li class="listitem"><p>list</p></li> 122 + </ol> 123 + </div> 124 + <div class="blockquote"> 125 + <blockquote class="blockquote"> 126 + <p>quotes</p> 127 + <div class="blockquote"> 128 + <blockquote class="blockquote"> 129 + <p>with <span class="emphasis"><em>nesting</em></span></p> 130 + <pre class="programlisting">↵ 131 + nested code block↵ 132 + </pre> 133 + </blockquote> 134 + </div> 135 + <div class="itemizedlist"> 136 + <ul class="itemizedlist compact" style="list-style-type: disc;"> 137 + <li class="listitem"><p>and lists</p></li> 138 + <li class="listitem"> 139 + <pre class="programlisting">↵ 140 + containing code↵ 141 + </pre> 142 + </li> 143 + </ul> 144 + </div> 145 + <p>and more quote</p> 146 + </blockquote> 147 + </div> 148 + <div class="orderedlist"> 149 + <ol class="orderedlist compact" start="100" type="1"> 150 + <li class="listitem"><p>list starting at 100</p></li> 151 + <li class="listitem"><p>goes on</p></li> 152 + </ol> 153 + </div> 154 + <div class="variablelist"> 155 + <dl class="variablelist"> 156 + <dt><span class="term">deflist</span></dt> 157 + <dd> 158 + <div class="blockquote"> 159 + <blockquote class="blockquote"> 160 + <p> 161 + with a quote↵ 162 + and stuff 163 + </p> 164 + </blockquote> 165 + </div> 166 + <pre class="programlisting">↵ 167 + code block↵ 168 + </pre> 169 + <pre class="programlisting">↵ 170 + fenced block↵ 171 + </pre> 172 + <p>text</p> 173 + </dd> 174 + <dt><span class="term">more stuff in same deflist</span></dt> 175 + <dd> 176 + <p>foo</p> 177 + </dd> 178 + </dl> 179 + </div>""")
+5 -3
pkgs/tools/nix/nixos-render-docs/src/tests/test_lists.py
··· 1 - import nixos_render_docs 1 + import nixos_render_docs as nrd 2 2 import pytest 3 3 4 4 from markdown_it.token import Token 5 5 6 - class Converter(nixos_render_docs.md.Converter): 6 + class Converter(nrd.md.Converter[nrd.docbook.DocBookRenderer]): 7 7 # actual renderer doesn't matter, we're just parsing. 8 - __renderer__ = nixos_render_docs.docbook.DocBookRenderer 8 + def __init__(self, manpage_urls: dict[str, str]) -> None: 9 + super().__init__() 10 + self._renderer = nrd.docbook.DocBookRenderer(manpage_urls) 9 11 10 12 @pytest.mark.parametrize("ordered", [True, False]) 11 13 def test_list_wide(ordered: bool) -> None:
+8 -15
pkgs/tools/nix/nixos-render-docs/src/tests/test_manpage.py
··· 1 - import nixos_render_docs 1 + import nixos_render_docs as nrd 2 2 3 3 from sample_md import sample1 4 4 ··· 6 6 7 7 import markdown_it 8 8 9 - class Converter(nixos_render_docs.md.Converter): 10 - def __renderer__(self, manpage_urls: Mapping[str, str], 11 - parser: Optional[markdown_it.MarkdownIt] = None 12 - ) -> nixos_render_docs.manpage.ManpageRenderer: 13 - return nixos_render_docs.manpage.ManpageRenderer(manpage_urls, self.options_by_id, parser) 14 - 9 + class Converter(nrd.md.Converter[nrd.manpage.ManpageRenderer]): 15 10 def __init__(self, manpage_urls: Mapping[str, str], options_by_id: dict[str, str] = {}): 16 - self.options_by_id = options_by_id 17 - super().__init__(manpage_urls) 11 + super().__init__() 12 + self._renderer = nrd.manpage.ManpageRenderer(manpage_urls, options_by_id) 18 13 19 14 def test_inline_code() -> None: 20 15 c = Converter({}) ··· 32 27 33 28 def test_collect_links() -> None: 34 29 c = Converter({}, { '#foo': "bar" }) 35 - assert isinstance(c._md.renderer, nixos_render_docs.manpage.ManpageRenderer) 36 - c._md.renderer.link_footnotes = [] 30 + c._renderer.link_footnotes = [] 37 31 assert c._render("[a](link1) [b](link2)") == "\\fBa\\fR[1]\\fR \\fBb\\fR[2]\\fR" 38 - assert c._md.renderer.link_footnotes == ['link1', 'link2'] 32 + assert c._renderer.link_footnotes == ['link1', 'link2'] 39 33 40 34 def test_dedup_links() -> None: 41 35 c = Converter({}, { '#foo': "bar" }) 42 - assert isinstance(c._md.renderer, nixos_render_docs.manpage.ManpageRenderer) 43 - c._md.renderer.link_footnotes = [] 36 + c._renderer.link_footnotes = [] 44 37 assert c._render("[a](link) [b](link)") == "\\fBa\\fR[1]\\fR \\fBb\\fR[1]\\fR" 45 - assert c._md.renderer.link_footnotes == ['link'] 38 + assert c._renderer.link_footnotes == ['link'] 46 39 47 40 def test_full() -> None: 48 41 c = Converter({ 'man(1)': 'http://example.org' })
+5 -3
pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py
··· 1 - import nixos_render_docs 1 + import nixos_render_docs as nrd 2 2 3 3 from markdown_it.token import Token 4 4 5 - class Converter(nixos_render_docs.md.Converter): 5 + class Converter(nrd.md.Converter[nrd.docbook.DocBookRenderer]): 6 6 # actual renderer doesn't matter, we're just parsing. 7 - __renderer__ = nixos_render_docs.docbook.DocBookRenderer 7 + def __init__(self, manpage_urls: dict[str, str]) -> None: 8 + super().__init__() 9 + self._renderer = nrd.docbook.DocBookRenderer(manpage_urls) 8 10 9 11 def test_attr_span_parsing() -> None: 10 12 c = Converter({})