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.
This commit is contained in:
pennae
2023-06-21 16:58:23 +02:00
parent b962ff92ff
commit 8c2d14a6b8
6 changed files with 70 additions and 8 deletions

View File

@@ -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})'

View File

@@ -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))}", ""

View File

@@ -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)

View File

@@ -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