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