1from collections.abc import Mapping, Sequence 2from dataclasses import dataclass 3from typing import cast 4from urllib.parse import quote 5 6from .md import Renderer 7 8from markdown_it.token import Token 9 10_asciidoc_escapes = { 11 # escape all dots, just in case one is pasted at SOL 12 ord('.'): "{zwsp}.", 13 # may be replaced by typographic variants 14 ord("'"): "{apos}", 15 ord('"'): "{quot}", 16 # passthrough character 17 ord('+'): "{plus}", 18 # table marker 19 ord('|'): "{vbar}", 20 # xml entity reference 21 ord('&'): "{amp}", 22 # crossrefs. < needs extra escaping because links break in odd ways if they start with it 23 ord('<'): "{zwsp}+<+{zwsp}", 24 ord('>'): "{gt}", 25 # anchors, links, block attributes 26 ord('['): "{startsb}", 27 ord(']'): "{endsb}", 28 # superscript, subscript 29 ord('^'): "{caret}", 30 ord('~'): "{tilde}", 31 # bold 32 ord('*'): "{asterisk}", 33 # backslash 34 ord('\\'): "{backslash}", 35 # inline code 36 ord('`'): "{backtick}", 37} 38def asciidoc_escape(s: str) -> str: 39 s = s.translate(_asciidoc_escapes) 40 # :: is deflist item, ;; is has a replacement but no idea why 41 return s.replace("::", "{two-colons}").replace(";;", "{two-semicolons}") 42 43@dataclass(kw_only=True) 44class List: 45 head: str 46 47@dataclass() 48class Par: 49 sep: str 50 block_delim: str 51 continuing: bool = False 52 53class AsciiDocRenderer(Renderer): 54 __output__ = "asciidoc" 55 56 _parstack: list[Par] 57 _list_stack: list[List] 58 _attrspans: list[str] 59 60 def __init__(self, manpage_urls: Mapping[str, str]): 61 super().__init__(manpage_urls) 62 self._parstack = [ Par("\n\n", "====") ] 63 self._list_stack = [] 64 self._attrspans = [] 65 66 def _enter_block(self, is_list: bool) -> None: 67 self._parstack.append(Par("\n+\n" if is_list else "\n\n", self._parstack[-1].block_delim + "=")) 68 def _leave_block(self) -> None: 69 self._parstack.pop() 70 def _break(self, force: bool = False) -> str: 71 result = self._parstack[-1].sep if force or self._parstack[-1].continuing else "" 72 self._parstack[-1].continuing = True 73 return result 74 75 def _admonition_open(self, kind: str) -> str: 76 pbreak = self._break() 77 self._enter_block(False) 78 return f"{pbreak}[{kind}]\n{self._parstack[-2].block_delim}\n" 79 def _admonition_close(self) -> str: 80 self._leave_block() 81 return f"\n{self._parstack[-1].block_delim}\n" 82 83 def _list_open(self, token: Token, head: str) -> str: 84 attrs = [] 85 if (idx := token.attrs.get('start')) is not None: 86 attrs.append(f"start={idx}") 87 if token.meta['compact']: 88 attrs.append('options="compact"') 89 if self._list_stack: 90 head *= len(self._list_stack[0].head) + 1 91 self._list_stack.append(List(head=head)) 92 return f"{self._break()}[{','.join(attrs)}]" 93 def _list_close(self) -> str: 94 self._list_stack.pop() 95 return "" 96 97 def text(self, token: Token, tokens: Sequence[Token], i: int) -> str: 98 self._parstack[-1].continuing = True 99 return asciidoc_escape(token.content) 100 def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 101 return self._break() 102 def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 103 return "" 104 def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 105 return " +\n" 106 def softbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str: 107 return " " 108 def code_inline(self, token: Token, tokens: Sequence[Token], i: int) -> str: 109 self._parstack[-1].continuing = True 110 return f"``{asciidoc_escape(token.content)}``" 111 def code_block(self, token: Token, tokens: Sequence[Token], i: int) -> str: 112 return self.fence(token, tokens, i) 113 def link_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 114 self._parstack[-1].continuing = True 115 return f"link:{quote(cast(str, token.attrs['href']), safe='/:')}[" 116 def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 117 return "]" 118 def list_item_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 119 self._enter_block(True) 120 # allow the next token to be a block or an inline. 121 return f'\n{self._list_stack[-1].head} {{empty}}' 122 def list_item_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 123 self._leave_block() 124 return "\n" 125 def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 126 return self._list_open(token, '*') 127 def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 128 return self._list_close() 129 def em_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 130 return "__" 131 def em_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 132 return "__" 133 def strong_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 134 return "**" 135 def strong_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 136 return "**" 137 def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str: 138 attrs = f"[source,{token.info}]\n" if token.info else "" 139 code = token.content 140 if code.endswith('\n'): 141 code = code[:-1] 142 return f"{self._break(True)}{attrs}----\n{code}\n----" 143 def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 144 pbreak = self._break(True) 145 self._enter_block(False) 146 return f"{pbreak}[quote]\n{self._parstack[-2].block_delim}\n" 147 def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 148 self._leave_block() 149 return f"\n{self._parstack[-1].block_delim}" 150 def note_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 151 return self._admonition_open("NOTE") 152 def note_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 153 return self._admonition_close() 154 def caution_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 155 return self._admonition_open("CAUTION") 156 def caution_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 157 return self._admonition_close() 158 def important_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 159 return self._admonition_open("IMPORTANT") 160 def important_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 161 return self._admonition_close() 162 def tip_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 163 return self._admonition_open("TIP") 164 def tip_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 165 return self._admonition_close() 166 def warning_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 167 return self._admonition_open("WARNING") 168 def warning_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 169 return self._admonition_close() 170 def dl_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 171 return f"{self._break()}[]" 172 def dl_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 173 return "" 174 def dt_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 175 return self._break() 176 def dt_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 177 self._enter_block(True) 178 return ":: {empty}" 179 def dd_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 180 return "" 181 def dd_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 182 self._leave_block() 183 return "\n" 184 def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str: 185 self._parstack[-1].continuing = True 186 content = asciidoc_escape(token.content) 187 if token.meta['name'] == 'manpage' and (url := self._manpage_urls.get(token.content)): 188 return f"link:{quote(url, safe='/:')}[{content}]" 189 return f"[.{token.meta['name']}]``{asciidoc_escape(token.content)}``" 190 def inline_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str: 191 self._parstack[-1].continuing = True 192 return f"[[{token.attrs['id']}]]" 193 def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str: 194 self._parstack[-1].continuing = True 195 (id_part, class_part) = ("", "") 196 if id := token.attrs.get('id'): 197 id_part = f"[[{id}]]" 198 if s := token.attrs.get('class'): 199 if s == 'keycap': 200 class_part = "kbd:[" 201 self._attrspans.append("]") 202 else: 203 return super().attr_span_begin(token, tokens, i) 204 else: 205 self._attrspans.append("") 206 return id_part + class_part 207 def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str: 208 return self._attrspans.pop() 209 def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 210 return token.markup.replace("#", "=") + " " 211 def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 212 return "\n" 213 def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 214 return self._list_open(token, '.') 215 def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 216 return self._list_close()