diff options
Diffstat (limited to 'pkgs/tools')
10 files changed, 504 insertions, 415 deletions
diff --git a/pkgs/tools/nix/nixdoc/Cargo.lock b/pkgs/tools/nix/nixdoc/Cargo.lock deleted file mode 100644 index 0f672357cb5f5..0000000000000 --- a/pkgs/tools/nix/nixdoc/Cargo.lock +++ /dev/null @@ -1,309 +0,0 @@ -[[package]] -name = "ansi_term" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "arenatree" -version = "0.1.1" -source = "git+https://gitlab.com/jD91mZM2/arenatree#f9bf7efa9a5ef4c2dd9e2acc5a4cc79a987cb648" - -[[package]] -name = "arrayvec" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "atty" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "backtrace" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "backtrace-sys 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "backtrace-sys" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "bitflags" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "cc" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "cfg-if" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "clap" -version = "2.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "failure" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "failure_derive" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.15.15 (registry+https://github.com/rust-lang/crates.io-index)", - "synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "libc" -version = "0.2.43" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "nixdoc" -version = "1.0.1" -dependencies = [ - "failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rnix 0.4.1 (git+https://gitlab.com/jD91mZM2/rnix.git?rev=10b86c94291b4864470158ef8750de85ddd8d4ba)", - "structopt 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", - "xml-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "nodrop" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "proc-macro2" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "quote" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "redox_syscall" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "redox_termios" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rnix" -version = "0.4.1" -source = "git+https://gitlab.com/jD91mZM2/rnix.git?rev=10b86c94291b4864470158ef8750de85ddd8d4ba#10b86c94291b4864470158ef8750de85ddd8d4ba" -dependencies = [ - "arenatree 0.1.1 (git+https://gitlab.com/jD91mZM2/arenatree)", - "arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "smol_str 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "smol_str" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "strsim" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "structopt" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", - "structopt-derive 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "structopt-derive" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.15.15 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "syn" -version = "0.15.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "synstructure" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.15.15 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "termion" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "textwrap" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "unicode-width" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "vec_map" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "winapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "xml-rs" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[metadata] -"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -"checksum arenatree 0.1.1 (git+https://gitlab.com/jD91mZM2/arenatree)" = "<none>" -"checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef" -"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" -"checksum backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "89a47830402e9981c5c41223151efcced65a0510c13097c769cede7efb34782a" -"checksum backtrace-sys 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)" = "c66d56ac8dabd07f6aacdaf633f4b8262f5b3601a810a0dcddffd5c22c69daa0" -"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" -"checksum cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "f159dfd43363c4d08055a07703eb7a3406b0dac4d0584d96965a3262db3c9d16" -"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" -"checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e" -"checksum failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6dd377bcc1b1b7ce911967e3ec24fa19c3224394ec05b54aa7b083d498341ac7" -"checksum failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "64c2d913fe8ed3b6c6518eedf4538255b989945c14c2a7d5cbff62a5e2120596" -"checksum libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)" = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d" -"checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" -"checksum proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)" = "3d7b7eaaa90b4a90a932a9ea6666c95a389e424eff347f0f793979289429feee" -"checksum quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "dd636425967c33af890042c483632d33fa7a18f19ad1d7ea72e8998c6ef8dea5" -"checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" -"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" -"checksum rnix 0.4.1 (git+https://gitlab.com/jD91mZM2/rnix.git?rev=10b86c94291b4864470158ef8750de85ddd8d4ba)" = "<none>" -"checksum rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "bcfe5b13211b4d78e5c2cadfebd7769197d95c639c35a50057eb4c05de811395" -"checksum smol_str 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f3ed6f19b800d76574926e458d5f8e2dbea86c2b58c08d33a982448f09ac8d0c" -"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" -"checksum structopt 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "d77af7242f18c40fd19cb270985930f239ee1646cfb482050bbae9da1d18743b" -"checksum structopt-derive 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "17ff01fe96de9d16e7372ae5f19dd7ece2c703b51043c3db9ea27f9e393ea311" -"checksum syn 0.15.15 (registry+https://github.com/rust-lang/crates.io-index)" = "0a9c2bf1e53c21704a7cce1b2a42768f1ae32a6777108a0d7f1faa4bfe7f7c04" -"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015" -"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" -"checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6" -"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" -"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" -"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" -"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" -"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -"checksum xml-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "541b12c998c5b56aa2b4e6f18f03664eef9a4fd0a246a55594efae6cc2d964b5" diff --git a/pkgs/tools/nix/nixdoc/default.nix b/pkgs/tools/nix/nixdoc/default.nix index 8562ff1c5e56d..785261a8e0925 100644 --- a/pkgs/tools/nix/nixdoc/default.nix +++ b/pkgs/tools/nix/nixdoc/default.nix @@ -2,38 +2,24 @@ rustPlatform.buildRustPackage rec { pname = "nixdoc"; - version = "1.0.1"; + version = "2.3.0"; src = fetchFromGitHub { - owner = "tazjin"; + owner = "nix-community"; repo = "nixdoc"; rev = "v${version}"; - sha256 = "14d4dq06jdqazxvv7fq5872zy0capxyb0fdkp8qg06gxl1iw201s"; + sha256 = "sha256-8pp6xlmdb3kZ6unTiO4yRruyEZ//GIHZF1k8f4kQr9Q="; }; - patches = [ - # Support nested identifiers https://github.com/nix-community/nixdoc/pull/27 - (fetchpatch { - url = "https://github.com/nix-community/nixdoc/pull/27/commits/ea542735bf675fe2ccd37edaffb9138d1a8c1b7e.patch"; - sha256 = "1fmz44jv2r9qsnjxvkkjfb0safy69l4x4vx1g5gisrp8nwdn94rj"; - }) - ]; + cargoSha256 = "sha256-k8/+BBMjQCsrgCi33fTdiSukaAZlg6XU3NwXaJdGYVw="; buildInputs = lib.optionals stdenv.isDarwin [ darwin.Security ]; - cargoLock = { - lockFile = ./Cargo.lock; - outputHashes = { - "arenatree-0.1.1" = "sha256-b3VVbYnWsjSjFMxvkfpJt13u+VC6baOIWD4qm1Gco4Q="; - "rnix-0.4.1" = "sha256-C1L/qXk6AimH7COrBlqpUA3giftaOYm/qNxs7rQgETA="; - }; - }; - meta = with lib; { description = "Generate documentation for Nix functions"; - homepage = "https://github.com/tazjin/nixdoc"; + homepage = "https://github.com/nix-community/nixdoc"; license = [ licenses.gpl3 ]; - maintainers = [ maintainers.tazjin ]; + maintainers = [ maintainers.asymmetric ]; platforms = platforms.unix; }; } diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/commonmark.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/commonmark.py index 440cf35f0d387..6287b60f0a51d 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/commonmark.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/commonmark.py @@ -184,3 +184,7 @@ class CommonMarkRenderer(Renderer): def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: self._list_stack.pop() return "" + def image(self, token: Token, tokens: Sequence[Token], i: int) -> str: + if title := cast(str, token.attrs.get('title', '')): + title = ' "' + title.replace('"', '\\"') + '"' + return f'![{token.content}]({token.attrs["src"]}{title})' diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py index d25253d1a2f92..ffe64cde4d345 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py @@ -44,6 +44,9 @@ class HTMLRenderer(Renderer): result += self._close_headings(None) return result + def _pull_image(self, path: str) -> str: + raise NotImplementedError() + def text(self, token: Token, tokens: Sequence[Token], i: int) -> str: return escape(token.content) def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: @@ -67,7 +70,8 @@ class HTMLRenderer(Renderer): if tokens[i + 1].type == 'link_close': tag, text = "xref", xref.title_html if xref.title: - title = f'title="{escape(xref.title, True)}"' + # titles are not attribute-safe on their own, so we need to replace quotes. + title = 'title="{}"'.format(xref.title.replace('"', '"')) target, href = "", xref.href() return f'<a class="{tag}" href="{href}" {title} {target}>{text}' def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: @@ -223,6 +227,106 @@ class HTMLRenderer(Renderer): return '<p class="title"><strong>' def example_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '</strong></p><div class="example-contents">' + def image(self, token: Token, tokens: Sequence[Token], i: int) -> str: + src = self._pull_image(cast(str, token.attrs['src'])) + alt = f'alt="{escape(token.content, True)}"' if token.content else "" + if title := cast(str, token.attrs.get('title', '')): + title = f'title="{escape(title, True)}"' + return ( + '<div class="mediaobject">' + f'<img src="{escape(src, True)}" {alt} {title} />' + '</div>' + ) + def figure_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + if anchor := cast(str, token.attrs.get('id', '')): + anchor = f'<a id="{escape(anchor, True)}"></a>' + return f'<div class="figure">{anchor}' + def figure_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return ( + ' </div>' + '</div><br class="figure-break" />' + ) + def figure_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return ( + '<p class="title">' + ' <strong>' + ) + def figure_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return ( + ' </strong>' + '</p>' + '<div class="figure-contents">' + ) + def table_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return ( + '<div class="informaltable">' + '<table class="informaltable" border="1">' + ) + def table_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return ( + '</table>' + '</div>' + ) + def thead_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + cols = [] + for j in range(i + 1, len(tokens)): + if tokens[j].type == 'thead_close': + break + elif tokens[j].type == 'th_open': + cols.append(cast(str, tokens[j].attrs.get('style', 'left')).removeprefix('text-align:')) + return "".join([ + "<colgroup>", + "".join([ f'<col align="{col}" />' for col in cols ]), + "</colgroup>", + "<thead>", + ]) + def thead_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return "</thead>" + def tr_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return "<tr>" + def tr_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return "</tr>" + def th_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return f'<th align="{cast(str, token.attrs.get("style", "left")).removeprefix("text-align:")}">' + def th_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return "</th>" + def tbody_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return "<tbody>" + def tbody_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return "</tbody>" + def td_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return f'<td align="{cast(str, token.attrs.get("style", "left")).removeprefix("text-align:")}">' + def td_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return "</td>" + def footnote_ref(self, token: Token, tokens: Sequence[Token], i: int) -> str: + href = self._xref_targets[token.meta['target']].href() + id = escape(cast(str, token.attrs["id"]), True) + return ( + f'<a href="{href}" class="footnote" id="{id}">' + f'<sup class="footnote">[{token.meta["id"] + 1}]</sup>' + '</a>' + ) + def footnote_block_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return ( + '<div class="footnotes">' + '<br />' + '<hr style="width:100; text-align:left;margin-left: 0" />' + ) + def footnote_block_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return "</div>" + def footnote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + # meta id,label + id = escape(self._xref_targets[token.meta["label"]].id, True) + return f'<div id="{id}" class="footnote">' + def footnote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + return "</div>" + def footnote_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str: + href = self._xref_targets[token.meta['target']].href() + return ( + f'<a href="{href}" class="para">' + f'<sup class="para">[{token.meta["id"] + 1}]</sup>' + '</a>' + ) def _make_hN(self, level: int) -> tuple[str, str]: return f"h{min(6, max(1, level + self._hlevel_offset))}", "" diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py index ef81e4d7e1ab8..03c5a5dd39604 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py @@ -1,4 +1,5 @@ import argparse +import hashlib import html import json import re @@ -235,27 +236,48 @@ class HTMLParameters(NamedTuple): generator: str stylesheets: Sequence[str] scripts: Sequence[str] + # number of levels in the rendered table of contents. tables are prepended to + # the content they apply to (entire document / document chunk / top-level section + # of a chapter), setting a depth of 0 omits the respective table. toc_depth: int chunk_toc_depth: int + section_toc_depth: int + media_dir: Path class ManualHTMLRenderer(RendererMixin, HTMLRenderer): _base_path: Path + _in_dir: Path _html_params: HTMLParameters def __init__(self, toplevel_tag: str, revision: str, html_params: HTMLParameters, manpage_urls: Mapping[str, str], xref_targets: dict[str, XrefTarget], - base_path: Path): + in_dir: Path, base_path: Path): super().__init__(toplevel_tag, revision, manpage_urls, xref_targets) - self._base_path, self._html_params = base_path, html_params + self._in_dir = in_dir + self._base_path = base_path.absolute() + self._html_params = html_params + + def _pull_image(self, src: str) -> str: + src_path = Path(src) + content = (self._in_dir / src_path).read_bytes() + # images may be used more than once, but we want to store them only once and + # in an easily accessible (ie, not input-file-path-dependent) location without + # having to maintain a mapping structure. hashing the file and using the hash + # as both the path of the final image provides both. + content_hash = hashlib.sha3_256(content).hexdigest() + target_name = f"{content_hash}{src_path.suffix}" + target_path = self._base_path / self._html_params.media_dir / target_name + target_path.write_bytes(content) + return f"./{self._html_params.media_dir}/{target_name}" def _push(self, tag: str, hlevel_offset: int) -> Any: - result = (self._toplevel_tag, self._headings, self._attrspans, self._hlevel_offset) + result = (self._toplevel_tag, self._headings, self._attrspans, self._hlevel_offset, self._in_dir) self._hlevel_offset += hlevel_offset self._toplevel_tag, self._headings, self._attrspans = tag, [], [] return result def _pop(self, state: Any) -> None: - (self._toplevel_tag, self._headings, self._attrspans, self._hlevel_offset) = state + (self._toplevel_tag, self._headings, self._attrspans, self._hlevel_offset, self._in_dir) = state def _render_book(self, tokens: Sequence[Token]) -> str: assert tokens[4].children @@ -284,6 +306,7 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer): def _file_header(self, toc: TocEntry) -> str: prev_link, up_link, next_link = "", "", "" prev_a, next_a, parent_title = "", "", " " + nav_html = "" home = toc.root if toc.prev: prev_link = f'<link rel="prev" href="{toc.prev.target.href()}" title="{toc.prev.target.title}" />' @@ -299,6 +322,22 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer): if toc.next: next_link = f'<link rel="next" href="{toc.next.target.href()}" title="{toc.next.target.title}" />' next_a = f'<a accesskey="n" href="{toc.next.target.href()}">Next</a>' + if toc.prev or toc.parent or toc.next: + nav_html = "\n".join([ + ' <div class="navheader">', + ' <table width="100%" summary="Navigation header">', + ' <tr>', + f' <th colspan="3" align="center">{toc.target.title}</th>', + ' </tr>', + ' <tr>', + f' <td width="20%" align="left">{prev_a} </td>', + f' <th width="60%" align="center">{parent_title}</th>', + f' <td width="20%" align="right"> {next_a}</td>', + ' </tr>', + ' </table>', + ' <hr />', + ' </div>', + ]) return "\n".join([ '<?xml version="1.0" encoding="utf-8" standalone="no"?>', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"', @@ -312,29 +351,18 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer): "".join((f'<script src="{html.escape(script, True)}" type="text/javascript"></script>' for script in self._html_params.scripts)), f' <meta name="generator" content="{html.escape(self._html_params.generator, True)}" />', - f' <link rel="home" href="{home.target.href()}" title="{home.target.title}" />', + f' <link rel="home" href="{home.target.href()}" title="{home.target.title}" />' if home.target.href() else "", f' {up_link}{prev_link}{next_link}', ' </head>', ' <body>', - ' <div class="navheader">', - ' <table width="100%" summary="Navigation header">', - ' <tr>', - f' <th colspan="3" align="center">{toc.target.title}</th>', - ' </tr>', - ' <tr>', - f' <td width="20%" align="left">{prev_a} </td>', - f' <th width="60%" align="center">{parent_title}</th>', - f' <td width="20%" align="right"> {next_a}</td>', - ' </tr>', - ' </table>', - ' <hr />', - ' </div>', + nav_html, ]) def _file_footer(self, toc: TocEntry) -> str: # prev, next = self._get_prev_and_next() prev_a, up_a, home_a, next_a = "", " ", " ", "" prev_text, up_text, next_text = "", "", "" + nav_html = "" home = toc.root if toc.prev: prev_a = f'<a accesskey="p" href="{toc.prev.target.href()}">Prev</a>' @@ -348,22 +376,26 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer): next_a = f'<a accesskey="n" href="{toc.next.target.href()}">Next</a>' assert toc.next.target.title next_text = toc.next.target.title + if toc.prev or toc.parent or toc.next: + nav_html = "\n".join([ + ' <div class="navfooter">', + ' <hr />', + ' <table width="100%" summary="Navigation footer">', + ' <tr>', + f' <td width="40%" align="left">{prev_a} </td>', + f' <td width="20%" align="center">{up_a}</td>', + f' <td width="40%" align="right"> {next_a}</td>', + ' </tr>', + ' <tr>', + f' <td width="40%" align="left" valign="top">{prev_text} </td>', + f' <td width="20%" align="center">{home_a}</td>', + f' <td width="40%" align="right" valign="top"> {next_text}</td>', + ' </tr>', + ' </table>', + ' </div>', + ]) return "\n".join([ - ' <div class="navfooter">', - ' <hr />', - ' <table width="100%" summary="Navigation footer">', - ' <tr>', - f' <td width="40%" align="left">{prev_a} </td>', - f' <td width="20%" align="center">{up_a}</td>', - f' <td width="40%" align="right"> {next_a}</td>', - ' </tr>', - ' <tr>', - f' <td width="40%" align="left" valign="top">{prev_text} </td>', - f' <td width="20%" align="center">{home_a}</td>', - f' <td width="40%" align="right" valign="top"> {next_text}</td>', - ' </tr>', - ' </table>', - ' </div>', + nav_html, ' </body>', '</html>', ]) @@ -374,7 +406,7 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer): return super()._heading_tag(token, tokens, i) def _build_toc(self, tokens: Sequence[Token], i: int) -> str: toc = TocEntry.of(tokens[i]) - if toc.kind == 'section': + if toc.kind == 'section' and self._html_params.section_toc_depth < 1: return "" def walk_and_emit(toc: TocEntry, depth: int) -> list[str]: if depth <= 0: @@ -394,34 +426,47 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer): if next_level: result.append(f'<dd><dl>{"".join(next_level)}</dl></dd>') return result - toc_depth = ( - self._html_params.chunk_toc_depth - if toc.starts_new_chunk and toc.kind != 'book' - else self._html_params.toc_depth - ) - if not (items := walk_and_emit(toc, toc_depth)): - return "" - examples = "" - if toc.examples: - examples_entries = [ - f'<dt>{i + 1}. <a href="{ex.target.href()}">{ex.target.toc_html}</a></dt>' - for i, ex in enumerate(toc.examples) + def build_list(kind: str, id: str, lst: Sequence[TocEntry]) -> str: + if not lst: + return "" + entries = [ + f'<dt>{i}. <a href="{e.target.href()}">{e.target.toc_html}</a></dt>' + for i, e in enumerate(lst, start=1) ] - examples = ( - '<div class="list-of-examples">' - '<p><strong>List of Examples</strong></p>' - f'<dl>{"".join(examples_entries)}</dl>' + return ( + f'<div class="{id}">' + f'<p><strong>List of {kind}</strong></p>' + f'<dl>{"".join(entries)}</dl>' '</div>' ) - return ( - f'<div class="toc">' - f' <p><strong>Table of Contents</strong></p>' + # we don't want to generate the "Title of Contents" header for sections, + # docbook doesn't and it's only distracting clutter unless it's the main table. + # we also want to generate tocs only for a top-level section (ie, one that is + # not itself contained in another section) + print_title = toc.kind != 'section' + if toc.kind == 'section': + if toc.parent and toc.parent.kind == 'section': + toc_depth = 0 + else: + toc_depth = self._html_params.section_toc_depth + elif toc.starts_new_chunk and toc.kind != 'book': + toc_depth = self._html_params.chunk_toc_depth + else: + toc_depth = self._html_params.toc_depth + if not (items := walk_and_emit(toc, toc_depth)): + return "" + figures = build_list("Figures", "list-of-figures", toc.figures) + examples = build_list("Examples", "list-of-examples", toc.examples) + return "".join([ + f'<div class="toc">', + ' <p><strong>Table of Contents</strong></p>' if print_title else "", f' <dl class="toc">' f' {"".join(items)}' f' </dl>' f'</div>' + f'{figures}' f'{examples}' - ) + ]) def _make_hN(self, level: int) -> tuple[str, str]: # for some reason chapters don't increase the hN nesting count in docbook xslts. duplicate @@ -458,8 +503,10 @@ class ManualHTMLRenderer(RendererMixin, HTMLRenderer): # we do not set _hlevel_offset=0 because docbook doesn't either. else: inner = outer + in_dir = self._in_dir for included, path in fragments: try: + self._in_dir = (in_dir / path).parent inner.append(self.render(included)) except Exception as e: raise RuntimeError(f"rendering {path}") from e @@ -502,8 +549,9 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]): # renderer not set on purpose since it has a dependency on the output path! def convert(self, infile: Path, outfile: Path) -> None: - self._renderer = ManualHTMLRenderer('book', self._revision, self._html_params, - self._manpage_urls, self._xref_targets, outfile.parent) + self._renderer = ManualHTMLRenderer( + 'book', self._revision, self._html_params, self._manpage_urls, self._xref_targets, + infile.parent, outfile.parent) super().convert(infile, outfile) def _parse(self, src: str) -> list[Token]: @@ -525,23 +573,24 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]): self._redirection_targets.add(into) return tokens - def _number_examples(self, tokens: Sequence[Token], start: int = 1) -> int: + def _number_block(self, block: str, prefix: str, tokens: Sequence[Token], start: int = 1) -> int: + title_open, title_close = f'{block}_title_open', f'{block}_title_close' for (i, token) in enumerate(tokens): - if token.type == "example_title_open": + if token.type == title_open: title = tokens[i + 1] assert title.type == 'inline' and title.children # the prefix is split into two tokens because the xref title_html will want # only the first of the two, but both must be rendered into the example itself. title.children = ( [ - Token('text', '', 0, content=f'Example {start}'), + Token('text', '', 0, content=f'{prefix} {start}'), Token('text', '', 0, content='. ') ] + title.children ) start += 1 elif token.type.startswith('included_') and token.type != 'included_options': for sub, _path in token.meta['included']: - start = self._number_examples(sub, start) + start = self._number_block(block, prefix, sub, start) return start # xref | (id, type, heading inlines, file, starts new file) @@ -567,6 +616,12 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]): result += self._collect_ids(sub, sub_file, subtyp, si == 0 and sub_file != target_file) elif bt.type == 'example_open' and (id := cast(str, bt.attrs.get('id', ''))): result.append((id, 'example', tokens[i + 2], target_file, False)) + elif bt.type == 'figure_open' and (id := cast(str, bt.attrs.get('id', ''))): + result.append((id, 'figure', tokens[i + 2], target_file, False)) + elif bt.type == 'footnote_open' and (id := cast(str, bt.attrs.get('id', ''))): + result.append(XrefTarget(id, "???", None, None, target_file)) + elif bt.type == 'footnote_ref' and (id := cast(str, bt.attrs.get('id', ''))): + result.append(XrefTarget(id, "???", None, None, target_file)) elif bt.type == 'inline': assert bt.children result += self._collect_ids(bt.children, target_file, typ, False) @@ -591,8 +646,8 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]): title = prefix + title_html toc_html = f"{n}. {title_html}" title_html = f"Appendix {n}" - elif typ == 'example': - # skip the prepended `Example N. ` from _number_examples + elif typ in ['example', 'figure']: + # skip the prepended `{Example,Figure} N. ` from numbering toc_html, title = self._renderer.renderInline(inlines.children[2:]), title_html # xref title wants only the prepended text, sans the trailing colon and space title_html = self._renderer.renderInline(inlines.children[0:1]) @@ -607,7 +662,8 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]): return XrefTarget(id, title_html, toc_html, re.sub('<.*?>', '', title), path, drop_fragment) def _postprocess(self, infile: Path, outfile: Path, tokens: Sequence[Token]) -> None: - self._number_examples(tokens) + self._number_block('example', "Example", tokens) + self._number_block('figure', "Figure", tokens) xref_queue = self._collect_ids(tokens, outfile.name, 'book', True) failed = False @@ -629,6 +685,22 @@ class HTMLConverter(BaseConverter[ManualHTMLRenderer]): failed = True # do another round and report the first error xref_queue = deferred + paths_seen = set() + for t in self._xref_targets.values(): + paths_seen.add(t.path) + + if len(paths_seen) == 1: + for (k, t) in self._xref_targets.items(): + self._xref_targets[k] = XrefTarget( + t.id, + t.title_html, + t.toc_html, + t.title, + t.path, + t.drop_fragment, + drop_target=True + ) + TocEntry.collect_and_link(self._xref_targets, tokens) @@ -647,6 +719,8 @@ def _build_cli_html(p: argparse.ArgumentParser) -> None: p.add_argument('--script', default=[], action='append') p.add_argument('--toc-depth', default=1, type=int) p.add_argument('--chunk-toc-depth', default=1, type=int) + p.add_argument('--section-toc-depth', default=0, type=int) + p.add_argument('--media-dir', default="media", type=Path) p.add_argument('infile', type=Path) p.add_argument('outfile', type=Path) @@ -660,7 +734,7 @@ def _run_cli_html(args: argparse.Namespace) -> None: md = HTMLConverter( args.revision, HTMLParameters(args.generator, args.stylesheet, args.script, args.toc_depth, - args.chunk_toc_depth), + args.chunk_toc_depth, args.section_toc_depth, args.media_dir), json.load(manpage_urls)) md.convert(args.infile, args.outfile) diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual_structure.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual_structure.py index 95e6e9474e73f..c6e6bf4293706 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual_structure.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual_structure.py @@ -14,7 +14,7 @@ from .utils import Freezeable FragmentType = Literal['preface', 'part', 'chapter', 'section', 'appendix'] # in the TOC all fragments are allowed, plus the all-encompassing book. -TocEntryType = Literal['book', 'preface', 'part', 'chapter', 'section', 'appendix', 'example'] +TocEntryType = Literal['book', 'preface', 'part', 'chapter', 'section', 'appendix', 'example', 'figure'] def is_include(token: Token) -> bool: return token.type == "fence" and token.info.startswith("{=include=} ") @@ -110,9 +110,12 @@ class XrefTarget: path: str """whether to drop the `#anchor` from links when expanding xrefs""" drop_fragment: bool = False + """whether to drop the `path.html` from links when expanding xrefs. + mostly useful for docbook compatibility""" + drop_target: bool = False def href(self) -> str: - path = html.escape(self.path, True) + path = "" if self.drop_target else html.escape(self.path, True) return path if self.drop_fragment else f"{path}#{html.escape(self.id, True)}" @dc.dataclass @@ -125,6 +128,7 @@ class TocEntry(Freezeable): children: list[TocEntry] = dc.field(default_factory=list) starts_new_chunk: bool = False examples: list[TocEntry] = dc.field(default_factory=list) + figures: list[TocEntry] = dc.field(default_factory=list) @property def root(self) -> TocEntry: @@ -139,7 +143,7 @@ class TocEntry(Freezeable): @classmethod def collect_and_link(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token]) -> TocEntry: - entries, examples = cls._collect_entries(xrefs, tokens, 'book') + entries, examples, figures = cls._collect_entries(xrefs, tokens, 'book') def flatten_with_parent(this: TocEntry, parent: TocEntry | None) -> Iterable[TocEntry]: this.parent = parent @@ -157,6 +161,7 @@ class TocEntry(Freezeable): paths_seen.add(c.target.path) flat[0].examples = examples + flat[0].figures = figures for c in flat: c.freeze() @@ -165,21 +170,23 @@ class TocEntry(Freezeable): @classmethod def _collect_entries(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token], - kind: TocEntryType) -> tuple[TocEntry, list[TocEntry]]: + kind: TocEntryType) -> tuple[TocEntry, list[TocEntry], list[TocEntry]]: # we assume that check_structure has been run recursively over the entire input. # list contains (tag, entry) pairs that will collapse to a single entry for # the full sequence. entries: list[tuple[str, TocEntry]] = [] examples: list[TocEntry] = [] + figures: list[TocEntry] = [] for token in tokens: if token.type.startswith('included_') and (included := token.meta.get('included')): fragment_type_str = token.type[9:].removesuffix('s') assert fragment_type_str in get_args(TocEntryType) fragment_type = cast(TocEntryType, fragment_type_str) for fragment, _path in included: - subentries, subexamples = cls._collect_entries(xrefs, fragment, fragment_type) + subentries, subexamples, subfigures = cls._collect_entries(xrefs, fragment, fragment_type) entries[-1][1].children.append(subentries) examples += subexamples + figures += subfigures elif token.type == 'heading_open' and (id := cast(str, token.attrs.get('id', ''))): while len(entries) > 1 and entries[-1][0] >= token.tag: entries[-2][1].children.append(entries.pop()[1]) @@ -188,7 +195,9 @@ class TocEntry(Freezeable): token.meta['TocEntry'] = entries[-1][1] elif token.type == 'example_open' and (id := cast(str, token.attrs.get('id', ''))): examples.append(TocEntry('example', xrefs[id])) + elif token.type == 'figure_open' and (id := cast(str, token.attrs.get('id', ''))): + figures.append(TocEntry('figure', xrefs[id])) while len(entries) > 1: entries[-2][1].children.append(entries.pop()[1]) - return (entries[0][1], examples) + return (entries[0][1], examples, figures) diff --git a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py index 78e05642552b0..f754b61b44393 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py +++ b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py @@ -1,6 +1,6 @@ from abc import ABC from collections.abc import Mapping, MutableMapping, Sequence -from typing import Any, cast, Generic, get_args, Iterable, Literal, NoReturn, Optional, TypeVar +from typing import Any, Callable, cast, Generic, get_args, Iterable, Literal, NoReturn, Optional, TypeVar import dataclasses import re @@ -12,6 +12,7 @@ from markdown_it.token import Token from markdown_it.utils import OptionsDict from mdit_py_plugins.container import container_plugin # type: ignore[attr-defined] from mdit_py_plugins.deflist import deflist_plugin # type: ignore[attr-defined] +from mdit_py_plugins.footnote import footnote_plugin # type: ignore[attr-defined] from mdit_py_plugins.myst_role import myst_role_plugin # type: ignore[attr-defined] _md_escape_table = { @@ -40,7 +41,7 @@ def md_make_code(code: str, info: str = "", multiline: Optional[bool] = None) -> ticks, sep = ('`' * (longest + (3 if multiline else 1)), '\n' if multiline else ' ') return f"{ticks}{info}{sep}{code}{sep}{ticks}" -AttrBlockKind = Literal['admonition', 'example'] +AttrBlockKind = Literal['admonition', 'example', 'figure'] AdmonitionKind = Literal["note", "caution", "tip", "important", "warning"] @@ -90,6 +91,29 @@ class Renderer: "example_close": self.example_close, "example_title_open": self.example_title_open, "example_title_close": self.example_title_close, + "image": self.image, + "figure_open": self.figure_open, + "figure_close": self.figure_close, + "figure_title_open": self.figure_title_open, + "figure_title_close": self.figure_title_close, + "table_open": self.table_open, + "table_close": self.table_close, + "thead_open": self.thead_open, + "thead_close": self.thead_close, + "tr_open": self.tr_open, + "tr_close": self.tr_close, + "th_open": self.th_open, + "th_close": self.th_close, + "tbody_open": self.tbody_open, + "tbody_close": self.tbody_close, + "td_open": self.td_open, + "td_close": self.td_close, + "footnote_ref": self.footnote_ref, + "footnote_block_open": self.footnote_block_open, + "footnote_block_close": self.footnote_block_close, + "footnote_open": self.footnote_open, + "footnote_close": self.footnote_close, + "footnote_anchor": self.footnote_anchor, } self._admonitions = { @@ -225,6 +249,52 @@ class Renderer: raise RuntimeError("md token not supported", token) def example_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: raise RuntimeError("md token not supported", token) + def image(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def figure_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def figure_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def figure_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def figure_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def table_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def table_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def thead_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def thead_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def tr_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def tr_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def th_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def th_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def tbody_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def tbody_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def td_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def td_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def footnote_ref(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def footnote_block_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def footnote_block_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def footnote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def footnote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) + def footnote_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str: + raise RuntimeError("md token not supported", token) def _is_escaped(src: str, pos: int) -> bool: found = 0 @@ -267,6 +337,8 @@ def _parse_blockattrs(info: str) -> Optional[tuple[AttrBlockKind, Optional[str], return ('admonition', id, classes) if classes == ['example']: return ('example', id, classes) + elif classes == ['figure']: + return ('figure', id, classes) return None def _attr_span_plugin(md: markdown_it.MarkdownIt) -> None: @@ -368,6 +440,32 @@ def _heading_ids(md: markdown_it.MarkdownIt) -> None: md.core.ruler.before("replacements", "heading_ids", heading_ids) +def _footnote_ids(md: markdown_it.MarkdownIt) -> None: + """generate ids for footnotes, their refs, and their backlinks. the ids we + generate here are derived from the footnote label, making numeric footnote + labels invalid. + """ + def generate_ids(tokens: Sequence[Token]) -> None: + for token in tokens: + if token.type == 'footnote_open': + if token.meta["label"][:1].isdigit(): + assert token.map + raise RuntimeError(f"invalid footnote label in line {token.map[0] + 1}") + token.attrs['id'] = token.meta["label"] + elif token.type == 'footnote_anchor': + token.meta['target'] = f'{token.meta["label"]}.__back.{token.meta["subId"]}' + elif token.type == 'footnote_ref': + token.attrs['id'] = f'{token.meta["label"]}.__back.{token.meta["subId"]}' + token.meta['target'] = token.meta["label"] + elif token.type == 'inline': + assert token.children + generate_ids(token.children) + + def footnote_ids(state: markdown_it.rules_core.StateCore) -> None: + generate_ids(state.tokens) + + md.core.ruler.after("footnote_tail", "footnote_ids", footnote_ids) + def _compact_list_attr(md: markdown_it.MarkdownIt) -> None: @dataclasses.dataclass class Entry: @@ -416,6 +514,11 @@ def _block_attr(md: markdown_it.MarkdownIt) -> None: if id is not None: token.attrs['id'] = id stack.append('example_close') + elif kind == 'figure': + token.type = 'figure_open' + if id is not None: + token.attrs['id'] = id + stack.append('figure_close') else: assert_never(kind) elif token.type == 'container_blockattr_close': @@ -423,31 +526,37 @@ def _block_attr(md: markdown_it.MarkdownIt) -> None: md.core.ruler.push("block_attr", block_attr) -def _example_titles(md: markdown_it.MarkdownIt) -> None: +def _block_titles(block: str) -> Callable[[markdown_it.MarkdownIt], None]: + open, close = f'{block}_open', f'{block}_close' + title_open, title_close = f'{block}_title_open', f'{block}_title_close' + """ - find title headings of examples and stick them into meta for renderers, then - remove them from the token stream. also checks whether any example contains a + find title headings of blocks and stick them into meta for renderers, then + remove them from the token stream. also checks whether any block contains a non-title heading since those would make toc generation extremely complicated. """ - def example_titles(state: markdown_it.rules_core.StateCore) -> None: + def block_titles(state: markdown_it.rules_core.StateCore) -> None: in_example = [False] for i, token in enumerate(state.tokens): - if token.type == 'example_open': + if token.type == open: if state.tokens[i + 1].type == 'heading_open': assert state.tokens[i + 3].type == 'heading_close' - state.tokens[i + 1].type = 'example_title_open' - state.tokens[i + 3].type = 'example_title_close' + state.tokens[i + 1].type = title_open + state.tokens[i + 3].type = title_close else: assert token.map - raise RuntimeError(f"found example without title in line {token.map[0] + 1}") + raise RuntimeError(f"found {block} without title in line {token.map[0] + 1}") in_example.append(True) - elif token.type == 'example_close': + elif token.type == close: in_example.pop() elif token.type == 'heading_open' and in_example[-1]: assert token.map - raise RuntimeError(f"unexpected non-title heading in example in line {token.map[0] + 1}") + raise RuntimeError(f"unexpected non-title heading in {block} in line {token.map[0] + 1}") + + def do_add(md: markdown_it.MarkdownIt) -> None: + md.core.ruler.push(f"{block}_titles", block_titles) - md.core.ruler.push("example_titles", example_titles) + return do_add TR = TypeVar('TR', bound='Renderer') @@ -478,20 +587,24 @@ class Converter(ABC, Generic[TR]): }, renderer_cls=self.ForbiddenRenderer ) + self._md.enable('table') self._md.use( container_plugin, name="blockattr", validate=lambda name, *args: _parse_blockattrs(name), ) self._md.use(deflist_plugin) + self._md.use(footnote_plugin) self._md.use(myst_role_plugin) self._md.use(_attr_span_plugin) self._md.use(_inline_comment_plugin) self._md.use(_block_comment_plugin) self._md.use(_heading_ids) + self._md.use(_footnote_ids) self._md.use(_compact_list_attr) self._md.use(_block_attr) - self._md.use(_example_titles) + self._md.use(_block_titles("example")) + self._md.use(_block_titles("figure")) self._md.enable(["smartquotes", "replacements"]) def _parse(self, src: str) -> list[Token]: diff --git a/pkgs/tools/nix/nixos-render-docs/src/tests/test_commonmark.py b/pkgs/tools/nix/nixos-render-docs/src/tests/test_commonmark.py index d808c5b50c345..4ff0bc3095c3d 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/tests/test_commonmark.py +++ b/pkgs/tools/nix/nixos-render-docs/src/tests/test_commonmark.py @@ -91,3 +91,9 @@ some nested anchors - *more stuff in same deflist* foo""".replace(' ', ' ') + +def test_images() -> None: + c = Converter({}) + assert c._render("![*alt text*](foo \"title \\\"quoted\\\" text\")") == ( + "![*alt text*](foo \"title \\\"quoted\\\" text\")" + ) diff --git a/pkgs/tools/nix/nixos-render-docs/src/tests/test_html.py b/pkgs/tools/nix/nixos-render-docs/src/tests/test_html.py index df366a8babd7e..96cf8d0b7dff1 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/tests/test_html.py +++ b/pkgs/tools/nix/nixos-render-docs/src/tests/test_html.py @@ -1,12 +1,17 @@ import nixos_render_docs as nrd import pytest +import textwrap from sample_md import sample1 +class Renderer(nrd.html.HTMLRenderer): + def _pull_image(self, src: str) -> str: + return src + class Converter(nrd.md.Converter[nrd.html.HTMLRenderer]): def __init__(self, manpage_urls: dict[str, str], xrefs: dict[str, nrd.manual_structure.XrefTarget]): super().__init__() - self._renderer = nrd.html.HTMLRenderer(manpage_urls, xrefs) + self._renderer = Renderer(manpage_urls, xrefs) def unpretty(s: str) -> str: return "".join(map(str.strip, s.splitlines())).replace('␣', ' ').replace('↵', '\n') @@ -69,6 +74,78 @@ def test_xrefs() -> None: c._render("[](#baz)") assert exc.value.args[0] == 'bad local reference, id #baz not known' +def test_images() -> None: + c = Converter({}, {}) + assert c._render("![*alt text*](foo \"title text\")") == unpretty(""" + <p> + <div class="mediaobject"> + <img src="foo" alt="*alt text*" title="title text" /> + </div> + </p> + """) + +def test_tables() -> None: + c = Converter({}, {}) + assert c._render(textwrap.dedent(""" + | d | l | m | r | + |---|:--|:-:|--:| + | a | b | c | d | + """)) == unpretty(""" + <div class="informaltable"> + <table class="informaltable" border="1"> + <colgroup> + <col align="left" /> + <col align="left" /> + <col align="center" /> + <col align="right" /> + </colgroup> + <thead> + <tr> + <th align="left">d</th> + <th align="left">l</th> + <th align="center">m</th> + <th align="right">r</th> + </tr> + </thead> + <tbody> + <tr> + <td align="left">a</td> + <td align="left">b</td> + <td align="center">c</td> + <td align="right">d</td> + </tr> + </tbody> + </table> + </div> + """) + +def test_footnotes() -> None: + c = Converter({}, { + "bar": nrd.manual_structure.XrefTarget("bar", "", None, None, ""), + "bar.__back.0": nrd.manual_structure.XrefTarget("bar.__back.0", "", None, None, ""), + "bar.__back.1": nrd.manual_structure.XrefTarget("bar.__back.1", "", None, None, ""), + }) + assert c._render(textwrap.dedent(""" + foo [^bar] baz [^bar] + + [^bar]: note + """)) == unpretty(""" + <p> + foo <a href="#bar" class="footnote" id="bar.__back.0"><sup class="footnote">[1]</sup></a>␣ + baz <a href="#bar" class="footnote" id="bar.__back.1"><sup class="footnote">[1]</sup></a> + </p> + <div class="footnotes"> + <br /> + <hr style="width:100; text-align:left;margin-left: 0" /> + <div id="bar" class="footnote"> + <p> + note<a href="#bar.__back.0" class="para"><sup class="para">[1]</sup></a> + <a href="#bar.__back.1" class="para"><sup class="para">[1]</sup></a> + </p> + </div> + </div> + """) + def test_full() -> None: c = Converter({ 'man(1)': 'http://example.org' }, {}) assert c._render(sample1) == unpretty(""" diff --git a/pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py b/pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py index fb7a4ab0117f7..8564297efdd3a 100644 --- a/pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py +++ b/pkgs/tools/nix/nixos-render-docs/src/tests/test_plugins.py @@ -501,3 +501,28 @@ def test_example() -> None: with pytest.raises(RuntimeError) as exc: c._parse("::: {.example}\n### foo\n### bar\n:::") assert exc.value.args[0] == 'unexpected non-title heading in example in line 3' + +def test_footnotes() -> None: + c = Converter({}) + assert c._parse("text [^foo]\n\n[^foo]: bar") == [ + Token(type='paragraph_open', tag='p', nesting=1, map=[0, 1], block=True), + Token(type='inline', tag='', nesting=0, map=[0, 1], level=1, content='text [^foo]', block=True, + children=[ + Token(type='text', tag='', nesting=0, content='text '), + Token(type='footnote_ref', tag='', nesting=0, attrs={'id': 'foo.__back.0'}, + meta={'id': 0, 'subId': 0, 'label': 'foo', 'target': 'foo'}) + ]), + Token(type='paragraph_close', tag='p', nesting=-1, block=True), + Token(type='footnote_block_open', tag='', nesting=1), + Token(type='footnote_open', tag='', nesting=1, attrs={'id': 'foo'}, meta={'id': 0, 'label': 'foo'}), + Token(type='paragraph_open', tag='p', nesting=1, map=[2, 3], level=1, block=True, hidden=False), + Token(type='inline', tag='', nesting=0, map=[2, 3], level=2, content='bar', block=True, + children=[ + Token(type='text', tag='', nesting=0, content='bar') + ]), + Token(type='footnote_anchor', tag='', nesting=0, + meta={'id': 0, 'label': 'foo', 'subId': 0, 'target': 'foo.__back.0'}), + Token(type='paragraph_close', tag='p', nesting=-1, level=1, block=True), + Token(type='footnote_close', tag='', nesting=-1), + Token(type='footnote_block_close', tag='', nesting=-1), + ] |