summary refs log tree commit diff
path: root/pkgs/tools/nix/nixos-render-docs
diff options
context:
space:
mode:
authorpennae <github@quasiparticle.net>2023-06-21 16:58:23 +0200
committerpennae <github@quasiparticle.net>2023-07-01 20:27:28 +0200
commit8c2d14a6b850458020fd4ce079a90ee87e64c7a6 (patch)
treeb6bfc3ea242ebf84d7a9dae2bfba0984494aafbe /pkgs/tools/nix/nixos-render-docs
parentb962ff92ff414b325c1b73f24f2ff3b0ae613dd7 (diff)
downloadnixpkgs-8c2d14a6b850458020fd4ce079a90ee87e64c7a6.tar
nixpkgs-8c2d14a6b850458020fd4ce079a90ee87e64c7a6.tar.gz
nixpkgs-8c2d14a6b850458020fd4ce079a90ee87e64c7a6.tar.bz2
nixpkgs-8c2d14a6b850458020fd4ce079a90ee87e64c7a6.tar.lz
nixpkgs-8c2d14a6b850458020fd4ce079a90ee87e64c7a6.tar.xz
nixpkgs-8c2d14a6b850458020fd4ce079a90ee87e64c7a6.tar.zst
nixpkgs-8c2d14a6b850458020fd4ce079a90ee87e64c7a6.zip
nixos-render-docs: add image support
currently only supported for html. docbook could also support images,
but it's on the way out for manual generation anyway so we won't add
image support there. options docs can't use images because they also
target manpages, which leaves no viable users.
Diffstat (limited to 'pkgs/tools/nix/nixos-render-docs')
-rw-r--r--pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/commonmark.py4
-rw-r--r--pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/html.py13
-rw-r--r--pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/manual.py36
-rw-r--r--pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py3
-rw-r--r--pkgs/tools/nix/nixos-render-docs/src/tests/test_commonmark.py6
-rw-r--r--pkgs/tools/nix/nixos-render-docs/src/tests/test_html.py16
6 files changed, 70 insertions, 8 deletions
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 440cf35f0d3..6287b60f0a5 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 1bffc601f99..4a0504e61b0 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:
@@ -224,6 +227,16 @@ 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 _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 82e8f3e8a6d..acf562b839e 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
@@ -241,25 +242,42 @@ class HTMLParameters(NamedTuple):
     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
@@ -481,8 +499,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
@@ -525,8 +545,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]:
@@ -687,6 +708,7 @@ def _build_cli_html(p: argparse.ArgumentParser) -> None:
     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)
 
@@ -700,7 +722,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.section_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/md.py b/pkgs/tools/nix/nixos-render-docs/src/nixos_render_docs/md.py
index 78e05642552..edd4238dd9f 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
@@ -90,6 +90,7 @@ class Renderer:
             "example_close": self.example_close,
             "example_title_open": self.example_title_open,
             "example_title_close": self.example_title_close,
+            "image": self.image,
         }
 
         self._admonitions = {
@@ -225,6 +226,8 @@ 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 _is_escaped(src: str, pos: int) -> bool:
     found = 0
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 d808c5b50c3..4ff0bc3095c 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 df366a8babd..863aa68e382 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
@@ -3,10 +3,14 @@ import pytest
 
 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 +73,16 @@ 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_full() -> None:
     c = Converter({ 'man(1)': 'http://example.org' }, {})
     assert c._render(sample1) == unpretty("""