2023-02-17 21:29:22 +01:00
|
|
|
from collections.abc import Mapping, Sequence
|
2023-02-04 23:16:30 +01:00
|
|
|
from dataclasses import dataclass
|
2023-06-14 14:39:00 +12:00
|
|
|
from typing import cast, Optional
|
2023-02-04 23:16:30 +01:00
|
|
|
|
|
|
|
from .md import md_escape, md_make_code, Renderer
|
|
|
|
|
|
|
|
from markdown_it.token import Token
|
|
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
|
|
class List:
|
|
|
|
next_idx: Optional[int] = None
|
|
|
|
compact: bool
|
|
|
|
first_item_seen: bool = False
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class Par:
|
|
|
|
indent: str
|
|
|
|
continuing: bool = False
|
|
|
|
|
|
|
|
class CommonMarkRenderer(Renderer):
|
|
|
|
__output__ = "commonmark"
|
|
|
|
|
|
|
|
_parstack: list[Par]
|
|
|
|
_link_stack: list[str]
|
|
|
|
_list_stack: list[List]
|
|
|
|
|
2023-02-17 17:49:08 +01:00
|
|
|
def __init__(self, manpage_urls: Mapping[str, str]):
|
|
|
|
super().__init__(manpage_urls)
|
2023-02-04 23:16:30 +01:00
|
|
|
self._parstack = [ Par("") ]
|
|
|
|
self._link_stack = []
|
|
|
|
self._list_stack = []
|
|
|
|
|
|
|
|
def _enter_block(self, extra_indent: str) -> None:
|
|
|
|
self._parstack.append(Par(self._parstack[-1].indent + extra_indent))
|
|
|
|
def _leave_block(self) -> None:
|
|
|
|
self._parstack.pop()
|
|
|
|
self._parstack[-1].continuing = True
|
|
|
|
def _break(self) -> str:
|
|
|
|
self._parstack[-1].continuing = True
|
|
|
|
return f"\n{self._parstack[-1].indent}"
|
|
|
|
def _maybe_parbreak(self) -> str:
|
|
|
|
result = f"\n{self._parstack[-1].indent}" * 2 if self._parstack[-1].continuing else ""
|
|
|
|
self._parstack[-1].continuing = True
|
|
|
|
return result
|
|
|
|
|
|
|
|
def _admonition_open(self, kind: str) -> str:
|
|
|
|
pbreak = self._maybe_parbreak()
|
|
|
|
self._enter_block("")
|
|
|
|
return f"{pbreak}**{kind}:** "
|
|
|
|
def _admonition_close(self) -> str:
|
|
|
|
self._leave_block()
|
|
|
|
return ""
|
|
|
|
|
|
|
|
def _indent_raw(self, s: str) -> str:
|
|
|
|
if '\n' not in s:
|
|
|
|
return s
|
|
|
|
return f"\n{self._parstack[-1].indent}".join(s.splitlines())
|
|
|
|
|
2023-02-17 21:29:22 +01:00
|
|
|
def text(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._parstack[-1].continuing = True
|
|
|
|
return self._indent_raw(md_escape(token.content))
|
2023-02-17 21:29:22 +01:00
|
|
|
def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return self._maybe_parbreak()
|
2023-02-17 21:29:22 +01:00
|
|
|
def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return ""
|
2023-02-17 21:29:22 +01:00
|
|
|
def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return f" {self._break()}"
|
2023-02-17 21:29:22 +01:00
|
|
|
def softbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return self._break()
|
2023-02-17 21:29:22 +01:00
|
|
|
def code_inline(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._parstack[-1].continuing = True
|
|
|
|
return md_make_code(token.content)
|
2023-02-17 21:29:22 +01:00
|
|
|
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:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._parstack[-1].continuing = True
|
|
|
|
self._link_stack.append(cast(str, token.attrs['href']))
|
|
|
|
return "["
|
2023-02-17 21:29:22 +01:00
|
|
|
def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return f"]({md_escape(self._link_stack.pop())})"
|
2023-02-17 21:29:22 +01:00
|
|
|
def list_item_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
lst = self._list_stack[-1]
|
|
|
|
lbreak = "" if not lst.first_item_seen else self._break() * (1 if lst.compact else 2)
|
|
|
|
lst.first_item_seen = True
|
|
|
|
head = " -"
|
|
|
|
if lst.next_idx is not None:
|
|
|
|
head = f" {lst.next_idx}."
|
|
|
|
lst.next_idx += 1
|
|
|
|
self._enter_block(" " * (len(head) + 1))
|
|
|
|
return f'{lbreak}{head} '
|
2023-02-17 21:29:22 +01:00
|
|
|
def list_item_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._leave_block()
|
|
|
|
return ""
|
2023-02-17 21:29:22 +01:00
|
|
|
def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._list_stack.append(List(compact=bool(token.meta['compact'])))
|
|
|
|
return self._maybe_parbreak()
|
2023-02-17 21:29:22 +01:00
|
|
|
def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._list_stack.pop()
|
|
|
|
return ""
|
2023-02-17 21:29:22 +01:00
|
|
|
def em_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return "*"
|
2023-02-17 21:29:22 +01:00
|
|
|
def em_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return "*"
|
2023-02-17 21:29:22 +01:00
|
|
|
def strong_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return "**"
|
2023-02-17 21:29:22 +01:00
|
|
|
def strong_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return "**"
|
2023-02-17 21:29:22 +01:00
|
|
|
def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
code = token.content
|
|
|
|
if code.endswith('\n'):
|
|
|
|
code = code[:-1]
|
|
|
|
pbreak = self._maybe_parbreak()
|
|
|
|
return pbreak + self._indent_raw(md_make_code(code, info=token.info, multiline=True))
|
2023-02-17 21:29:22 +01:00
|
|
|
def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
pbreak = self._maybe_parbreak()
|
|
|
|
self._enter_block("> ")
|
|
|
|
return pbreak + "> "
|
2023-02-17 21:29:22 +01:00
|
|
|
def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._leave_block()
|
|
|
|
return ""
|
2023-02-17 21:29:22 +01:00
|
|
|
def note_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return self._admonition_open("Note")
|
2023-02-17 21:29:22 +01:00
|
|
|
def note_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return self._admonition_close()
|
2023-02-17 21:29:22 +01:00
|
|
|
def caution_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return self._admonition_open("Caution")
|
2023-02-17 21:29:22 +01:00
|
|
|
def caution_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return self._admonition_close()
|
2023-02-17 21:29:22 +01:00
|
|
|
def important_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return self._admonition_open("Important")
|
2023-02-17 21:29:22 +01:00
|
|
|
def important_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return self._admonition_close()
|
2023-02-17 21:29:22 +01:00
|
|
|
def tip_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return self._admonition_open("Tip")
|
2023-02-17 21:29:22 +01:00
|
|
|
def tip_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return self._admonition_close()
|
2023-02-17 21:29:22 +01:00
|
|
|
def warning_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return self._admonition_open("Warning")
|
2023-02-17 21:29:22 +01:00
|
|
|
def warning_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return self._admonition_close()
|
2023-02-17 21:29:22 +01:00
|
|
|
def dl_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._list_stack.append(List(compact=False))
|
|
|
|
return ""
|
2023-02-17 21:29:22 +01:00
|
|
|
def dl_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._list_stack.pop()
|
|
|
|
return ""
|
2023-02-17 21:29:22 +01:00
|
|
|
def dt_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
pbreak = self._maybe_parbreak()
|
|
|
|
self._enter_block(" ")
|
|
|
|
# add an opening zero-width non-joiner to separate *our* emphasis from possible
|
|
|
|
# emphasis in the provided term
|
|
|
|
return f'{pbreak} - *{chr(0x200C)}'
|
2023-02-17 21:29:22 +01:00
|
|
|
def dt_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return f"{chr(0x200C)}*"
|
2023-02-17 21:29:22 +01:00
|
|
|
def dd_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._parstack[-1].continuing = True
|
|
|
|
return ""
|
2023-02-17 21:29:22 +01:00
|
|
|
def dd_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._leave_block()
|
|
|
|
return ""
|
2023-02-17 21:29:22 +01:00
|
|
|
def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._parstack[-1].continuing = True
|
|
|
|
content = md_make_code(token.content)
|
|
|
|
if token.meta['name'] == 'manpage' and (url := self._manpage_urls.get(token.content)):
|
|
|
|
return f"[{content}]({url})"
|
|
|
|
return content # no roles in regular commonmark
|
2023-02-17 21:29:22 +01:00
|
|
|
def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
# there's no way we can emit attrspans correctly in all cases. we could use inline
|
|
|
|
# html for ids, but that would not round-trip. same holds for classes. since this
|
|
|
|
# renderer is only used for approximate options export and all of these things are
|
|
|
|
# not allowed in options we can ignore them for now.
|
|
|
|
return ""
|
2023-02-17 21:29:22 +01:00
|
|
|
def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return ""
|
2023-02-17 21:29:22 +01:00
|
|
|
def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return token.markup + " "
|
2023-02-17 21:29:22 +01:00
|
|
|
def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
return "\n"
|
2023-02-17 21:29:22 +01:00
|
|
|
def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._list_stack.append(
|
|
|
|
List(next_idx = cast(int, token.attrs.get('start', 1)),
|
|
|
|
compact = bool(token.meta['compact'])))
|
|
|
|
return self._maybe_parbreak()
|
2023-02-17 21:29:22 +01:00
|
|
|
def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
|
2023-02-04 23:16:30 +01:00
|
|
|
self._list_stack.pop()
|
|
|
|
return ""
|