from collections.abc import Mapping, Sequence from typing import cast, Optional, NamedTuple from html import escape from markdown_it.token import Token from .manual_structure import XrefTarget from .md import Renderer class UnresolvedXrefError(Exception): pass class Heading(NamedTuple): container_tag: str level: int html_tag: str # special handling for part content: whether partinfo div was already closed from # elsewhere or still needs closing. partintro_closed: bool # tocs are generated when the heading opens, but have to be emitted into the file # after the heading titlepage (and maybe partinfo) has been closed. toc_fragment: str _bullet_list_styles = [ 'disc', 'circle', 'square' ] _ordered_list_styles = [ '1', 'a', 'i', 'A', 'I' ] class HTMLRenderer(Renderer): _xref_targets: Mapping[str, XrefTarget] _headings: list[Heading] _attrspans: list[str] _hlevel_offset: int = 0 _bullet_list_nesting: int = 0 _ordered_list_nesting: int = 0 def __init__(self, manpage_urls: Mapping[str, str], xref_targets: Mapping[str, XrefTarget]): super().__init__(manpage_urls) self._headings = [] self._attrspans = [] self._xref_targets = xref_targets def render(self, tokens: Sequence[Token]) -> str: result = super().render(tokens) 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: return "

" def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "

" def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
" def softbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "\n" def code_inline(self, token: Token, tokens: Sequence[Token], i: int) -> str: return f'{escape(token.content)}' def code_block(self, token: Token, tokens: Sequence[Token], i: int) -> str: return self.fence(token, tokens, i) def link_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: href = escape(cast(str, token.attrs['href']), True) tag, title, target, text = "link", "", 'target="_top"', "" if href.startswith('#'): if not (xref := self._xref_targets.get(href[1:])): raise UnresolvedXrefError(f"bad local reference, id {href} not known") if tokens[i + 1].type == 'link_close': tag, text = "xref", xref.title_html if xref.title: # 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'{text}' def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "" def list_item_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '
  • ' def list_item_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
  • " def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: extra = 'compact' if token.meta.get('compact', False) else '' style = _bullet_list_styles[self._bullet_list_nesting % len(_bullet_list_styles)] self._bullet_list_nesting += 1 return f'
    " def em_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '' def em_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "" def strong_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '' def strong_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "" def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str: # TODO use token.info. docbook doesn't so we can't yet. return f'
    \n{escape(token.content)}
    ' def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '
    ' def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
    " def note_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '

    Note

    ' def note_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
    " def caution_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '

    Caution

    ' def caution_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
    " def important_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '

    Important

    ' def important_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
    " def tip_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '

    Tip

    ' def tip_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
    " def warning_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '

    Warning

    ' def warning_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
    " def dl_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '
    ' def dl_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
    " def dt_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '
    ' def dt_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
    " def dd_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
    " def dd_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
    " def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str: if token.meta['name'] == 'command': return f'{escape(token.content)}' if token.meta['name'] == 'file': return f'{escape(token.content)}' if token.meta['name'] == 'var': return f'{escape(token.content)}' if token.meta['name'] == 'env': return f'{escape(token.content)}' if token.meta['name'] == 'option': return f'{escape(token.content)}' if token.meta['name'] == 'manpage': [page, section] = [ s.strip() for s in token.content.rsplit('(', 1) ] section = section[:-1] man = f"{page}({section})" title = f'{escape(page)}' vol = f"({escape(section)})" ref = f'{title}{vol}' if man in self._manpage_urls: return f'{ref}' else: return ref return super().myst_role(token, tokens, i) def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str: # we currently support *only* inline anchors and the special .keycap class to produce # keycap-styled spans. (id_part, class_part) = ("", "") if s := token.attrs.get('id'): id_part = f'' if s := token.attrs.get('class'): if s == 'keycap': class_part = '' self._attrspans.append("") else: return super().attr_span_begin(token, tokens, i) else: self._attrspans.append("") return id_part + class_part def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str: return self._attrspans.pop() def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: hlevel = int(token.tag[1:]) htag, hstyle = self._make_hN(hlevel) if hstyle: hstyle = f'style="{escape(hstyle, True)}"' if anchor := cast(str, token.attrs.get('id', '')): anchor = f'' result = self._close_headings(hlevel) tag = self._heading_tag(token, tokens, i) toc_fragment = self._build_toc(tokens, i) self._headings.append(Heading(tag, hlevel, htag, tag != 'part', toc_fragment)) return ( f'{result}' f'
    ' f'
    ' f'
    ' f'
    ' f' <{htag} class="title" {hstyle}>' f' {anchor}' ) def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: heading = self._headings[-1] result = ( f' ' f'
    ' f'
    ' f'
    ' ) if heading.container_tag == 'part': result += '
    ' else: result += heading.toc_fragment return result def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: extra = 'compact' if token.meta.get('compact', False) else '' start = f'start="{token.attrs["start"]}"' if 'start' in token.attrs else "" style = _ordered_list_styles[self._ordered_list_nesting % len(_ordered_list_styles)] self._ordered_list_nesting += 1 return f'
      ' def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: self._ordered_list_nesting -= 1 return "
    " def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: if id := cast(str, token.attrs.get('id', '')): id = f'id="{escape(id, True)}"' if id else '' return f'

    ' def example_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '

    ' def example_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return '

    ' 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 ( '
    ' f'' '
    ' ) def figure_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: if anchor := cast(str, token.attrs.get('id', '')): anchor = f'
    ' return f'
    {anchor}' def figure_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return ( '
    ' '

    ' ) def figure_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return ( '

    ' ' ' ) def figure_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return ( ' ' '

    ' '
    ' ) def table_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return ( '
    ' '' ) def table_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return ( '
    ' '
    ' ) 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([ "", "".join([ f'' for col in cols ]), "", "", ]) def thead_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "" def tr_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "" def tr_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "" def th_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return f'' def th_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "" def tbody_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "" def tbody_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "" def td_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return f'' def td_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "" 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'' f'[{token.meta["id"] + 1}]' '' ) def footnote_block_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: return ( '
    ' '
    ' '
    ' ) def footnote_block_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
    " 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'
    ' def footnote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "
    " def footnote_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str: href = self._xref_targets[token.meta['target']].href() return ( f'' f'[{token.meta["id"] + 1}]' '' ) def _make_hN(self, level: int) -> tuple[str, str]: return f"h{min(6, max(1, level + self._hlevel_offset))}", "" def _maybe_close_partintro(self) -> str: if self._headings: heading = self._headings[-1] if heading.container_tag == 'part' and not heading.partintro_closed: self._headings[-1] = heading._replace(partintro_closed=True) return heading.toc_fragment + "
    " return "" def _close_headings(self, level: Optional[int]) -> str: result = [] while len(self._headings) and (level is None or self._headings[-1].level >= level): result.append(self._maybe_close_partintro()) result.append("
    ") self._headings.pop() return "\n".join(result) def _heading_tag(self, token: Token, tokens: Sequence[Token], i: int) -> str: return "section" def _build_toc(self, tokens: Sequence[Token], i: int) -> str: return ""