1from abc import ABC
2from collections.abc import Mapping, MutableMapping, Sequence
3from typing import Any, Callable, cast, Generic, get_args, Iterable, Literal, NoReturn, Optional, TypeVar
4
5import dataclasses
6import re
7
8from .types import RenderFn
9
10import markdown_it
11from markdown_it.token import Token
12from markdown_it.utils import OptionsDict
13from mdit_py_plugins.container import container_plugin # type: ignore[attr-defined]
14from mdit_py_plugins.deflist import deflist_plugin # type: ignore[attr-defined]
15from mdit_py_plugins.footnote import footnote_plugin # type: ignore[attr-defined]
16from mdit_py_plugins.myst_role import myst_role_plugin # type: ignore[attr-defined]
17
18_md_escape_table = {
19 ord('*'): '\\*',
20 ord('<'): '\\<',
21 ord('['): '\\[',
22 ord('`'): '\\`',
23 ord('.'): '\\.',
24 ord('#'): '\\#',
25 ord('&'): '\\&',
26 ord('\\'): '\\\\',
27}
28def md_escape(s: str) -> str:
29 return s.translate(_md_escape_table)
30
31def md_make_code(code: str, info: str = "", multiline: Optional[bool] = None) -> str:
32 # for multi-line code blocks we only have to count ` runs at the beginning
33 # of a line, but this is much easier.
34 multiline = multiline or info != "" or '\n' in code
35 longest, current = (0, 0)
36 for c in code:
37 current = current + 1 if c == '`' else 0
38 longest = max(current, longest)
39 # inline literals need a space to separate ticks from content, code blocks
40 # need newlines. inline literals need one extra tick, code blocks need three.
41 ticks, sep = ('`' * (longest + (3 if multiline else 1)), '\n' if multiline else ' ')
42 return f"{ticks}{info}{sep}{code}{sep}{ticks}"
43
44AttrBlockKind = Literal['admonition', 'example', 'figure']
45
46AdmonitionKind = Literal["note", "caution", "tip", "important", "warning"]
47
48class Renderer:
49 _admonitions: dict[AdmonitionKind, tuple[RenderFn, RenderFn]]
50 _admonition_stack: list[AdmonitionKind]
51
52 def __init__(self, manpage_urls: Mapping[str, str]):
53 self._manpage_urls = manpage_urls
54 self.rules = {
55 'text': self.text,
56 'paragraph_open': self.paragraph_open,
57 'paragraph_close': self.paragraph_close,
58 'hardbreak': self.hardbreak,
59 'softbreak': self.softbreak,
60 'code_inline': self.code_inline,
61 'code_block': self.code_block,
62 'link_open': self.link_open,
63 'link_close': self.link_close,
64 'list_item_open': self.list_item_open,
65 'list_item_close': self.list_item_close,
66 'bullet_list_open': self.bullet_list_open,
67 'bullet_list_close': self.bullet_list_close,
68 'em_open': self.em_open,
69 'em_close': self.em_close,
70 'strong_open': self.strong_open,
71 'strong_close': self.strong_close,
72 'fence': self.fence,
73 'blockquote_open': self.blockquote_open,
74 'blockquote_close': self.blockquote_close,
75 'dl_open': self.dl_open,
76 'dl_close': self.dl_close,
77 'dt_open': self.dt_open,
78 'dt_close': self.dt_close,
79 'dd_open': self.dd_open,
80 'dd_close': self.dd_close,
81 'myst_role': self.myst_role,
82 "admonition_open": self.admonition_open,
83 "admonition_close": self.admonition_close,
84 "attr_span_begin": self.attr_span_begin,
85 "attr_span_end": self.attr_span_end,
86 "heading_open": self.heading_open,
87 "heading_close": self.heading_close,
88 "ordered_list_open": self.ordered_list_open,
89 "ordered_list_close": self.ordered_list_close,
90 "example_open": self.example_open,
91 "example_close": self.example_close,
92 "example_title_open": self.example_title_open,
93 "example_title_close": self.example_title_close,
94 "image": self.image,
95 "figure_open": self.figure_open,
96 "figure_close": self.figure_close,
97 "figure_title_open": self.figure_title_open,
98 "figure_title_close": self.figure_title_close,
99 "table_open": self.table_open,
100 "table_close": self.table_close,
101 "thead_open": self.thead_open,
102 "thead_close": self.thead_close,
103 "tr_open": self.tr_open,
104 "tr_close": self.tr_close,
105 "th_open": self.th_open,
106 "th_close": self.th_close,
107 "tbody_open": self.tbody_open,
108 "tbody_close": self.tbody_close,
109 "td_open": self.td_open,
110 "td_close": self.td_close,
111 "footnote_ref": self.footnote_ref,
112 "footnote_block_open": self.footnote_block_open,
113 "footnote_block_close": self.footnote_block_close,
114 "footnote_open": self.footnote_open,
115 "footnote_close": self.footnote_close,
116 "footnote_anchor": self.footnote_anchor,
117 }
118
119 self._admonitions = {
120 "note": (self.note_open, self.note_close),
121 "caution": (self.caution_open,self.caution_close),
122 "tip": (self.tip_open, self.tip_close),
123 "important": (self.important_open, self.important_close),
124 "warning": (self.warning_open, self.warning_close),
125 }
126 self._admonition_stack = []
127
128 def _join_block(self, ls: Iterable[str]) -> str:
129 return "".join(ls)
130 def _join_inline(self, ls: Iterable[str]) -> str:
131 return "".join(ls)
132
133 def admonition_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
134 tag = token.meta['kind']
135 self._admonition_stack.append(tag)
136 return self._admonitions[tag][0](token, tokens, i)
137 def admonition_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
138 return self._admonitions[self._admonition_stack.pop()][1](token, tokens, i)
139
140 def render(self, tokens: Sequence[Token]) -> str:
141 def do_one(i: int, token: Token) -> str:
142 if token.type == "inline":
143 assert token.children is not None
144 return self.renderInline(token.children)
145 elif token.type in self.rules:
146 return self.rules[token.type](tokens[i], tokens, i)
147 else:
148 raise NotImplementedError("md token not supported yet", token)
149 return self._join_block(map(lambda arg: do_one(*arg), enumerate(tokens)))
150 def renderInline(self, tokens: Sequence[Token]) -> str:
151 def do_one(i: int, token: Token) -> str:
152 if token.type in self.rules:
153 return self.rules[token.type](tokens[i], tokens, i)
154 else:
155 raise NotImplementedError("md token not supported yet", token)
156 return self._join_inline(map(lambda arg: do_one(*arg), enumerate(tokens)))
157
158 def text(self, token: Token, tokens: Sequence[Token], i: int) -> str:
159 raise RuntimeError("md token not supported", token)
160 def paragraph_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
161 raise RuntimeError("md token not supported", token)
162 def paragraph_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
163 raise RuntimeError("md token not supported", token)
164 def hardbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str:
165 raise RuntimeError("md token not supported", token)
166 def softbreak(self, token: Token, tokens: Sequence[Token], i: int) -> str:
167 raise RuntimeError("md token not supported", token)
168 def code_inline(self, token: Token, tokens: Sequence[Token], i: int) -> str:
169 raise RuntimeError("md token not supported", token)
170 def code_block(self, token: Token, tokens: Sequence[Token], i: int) -> str:
171 raise RuntimeError("md token not supported", token)
172 def link_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
173 raise RuntimeError("md token not supported", token)
174 def link_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
175 raise RuntimeError("md token not supported", token)
176 def list_item_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
177 raise RuntimeError("md token not supported", token)
178 def list_item_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
179 raise RuntimeError("md token not supported", token)
180 def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
181 raise RuntimeError("md token not supported", token)
182 def bullet_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
183 raise RuntimeError("md token not supported", token)
184 def em_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
185 raise RuntimeError("md token not supported", token)
186 def em_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
187 raise RuntimeError("md token not supported", token)
188 def strong_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
189 raise RuntimeError("md token not supported", token)
190 def strong_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
191 raise RuntimeError("md token not supported", token)
192 def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str:
193 raise RuntimeError("md token not supported", token)
194 def blockquote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
195 raise RuntimeError("md token not supported", token)
196 def blockquote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
197 raise RuntimeError("md token not supported", token)
198 def note_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
199 raise RuntimeError("md token not supported", token)
200 def note_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
201 raise RuntimeError("md token not supported", token)
202 def caution_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
203 raise RuntimeError("md token not supported", token)
204 def caution_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
205 raise RuntimeError("md token not supported", token)
206 def important_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
207 raise RuntimeError("md token not supported", token)
208 def important_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
209 raise RuntimeError("md token not supported", token)
210 def tip_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
211 raise RuntimeError("md token not supported", token)
212 def tip_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
213 raise RuntimeError("md token not supported", token)
214 def warning_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
215 raise RuntimeError("md token not supported", token)
216 def warning_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
217 raise RuntimeError("md token not supported", token)
218 def dl_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
219 raise RuntimeError("md token not supported", token)
220 def dl_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
221 raise RuntimeError("md token not supported", token)
222 def dt_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
223 raise RuntimeError("md token not supported", token)
224 def dt_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
225 raise RuntimeError("md token not supported", token)
226 def dd_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
227 raise RuntimeError("md token not supported", token)
228 def dd_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
229 raise RuntimeError("md token not supported", token)
230 def myst_role(self, token: Token, tokens: Sequence[Token], i: int) -> str:
231 raise RuntimeError("md token not supported", token)
232 def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str:
233 raise RuntimeError("md token not supported", token)
234 def attr_span_end(self, token: Token, tokens: Sequence[Token], i: int) -> str:
235 raise RuntimeError("md token not supported", token)
236 def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
237 raise RuntimeError("md token not supported", token)
238 def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
239 raise RuntimeError("md token not supported", token)
240 def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
241 raise RuntimeError("md token not supported", token)
242 def ordered_list_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
243 raise RuntimeError("md token not supported", token)
244 def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
245 raise RuntimeError("md token not supported", token)
246 def example_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
247 raise RuntimeError("md token not supported", token)
248 def example_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
249 raise RuntimeError("md token not supported", token)
250 def example_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
251 raise RuntimeError("md token not supported", token)
252 def image(self, token: Token, tokens: Sequence[Token], i: int) -> str:
253 raise RuntimeError("md token not supported", token)
254 def figure_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
255 raise RuntimeError("md token not supported", token)
256 def figure_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
257 raise RuntimeError("md token not supported", token)
258 def figure_title_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
259 raise RuntimeError("md token not supported", token)
260 def figure_title_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
261 raise RuntimeError("md token not supported", token)
262 def table_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
263 raise RuntimeError("md token not supported", token)
264 def table_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
265 raise RuntimeError("md token not supported", token)
266 def thead_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
267 raise RuntimeError("md token not supported", token)
268 def thead_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
269 raise RuntimeError("md token not supported", token)
270 def tr_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
271 raise RuntimeError("md token not supported", token)
272 def tr_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
273 raise RuntimeError("md token not supported", token)
274 def th_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
275 raise RuntimeError("md token not supported", token)
276 def th_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
277 raise RuntimeError("md token not supported", token)
278 def tbody_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
279 raise RuntimeError("md token not supported", token)
280 def tbody_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
281 raise RuntimeError("md token not supported", token)
282 def td_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
283 raise RuntimeError("md token not supported", token)
284 def td_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
285 raise RuntimeError("md token not supported", token)
286 def footnote_ref(self, token: Token, tokens: Sequence[Token], i: int) -> str:
287 raise RuntimeError("md token not supported", token)
288 def footnote_block_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
289 raise RuntimeError("md token not supported", token)
290 def footnote_block_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
291 raise RuntimeError("md token not supported", token)
292 def footnote_open(self, token: Token, tokens: Sequence[Token], i: int) -> str:
293 raise RuntimeError("md token not supported", token)
294 def footnote_close(self, token: Token, tokens: Sequence[Token], i: int) -> str:
295 raise RuntimeError("md token not supported", token)
296 def footnote_anchor(self, token: Token, tokens: Sequence[Token], i: int) -> str:
297 raise RuntimeError("md token not supported", token)
298
299def _is_escaped(src: str, pos: int) -> bool:
300 found = 0
301 while pos >= 0 and src[pos] == '\\':
302 found += 1
303 pos -= 1
304 return found % 2 == 1
305
306# the contents won't be split apart in the regex because spacing rules get messy here
307_ATTR_SPAN_PATTERN = re.compile(r"\{([^}]*)\}")
308# this one is for blocks with attrs. we want to use it with fullmatch() to deconstruct an info.
309_ATTR_BLOCK_PATTERN = re.compile(r"\s*\{([^}]*)\}\s*")
310
311def _parse_attrs(s: str) -> Optional[tuple[Optional[str], list[str]]]:
312 (id, classes) = (None, [])
313 for part in s.split():
314 if part.startswith('#'):
315 if id is not None:
316 return None # just bail on multiple ids instead of trying to recover
317 id = part[1:]
318 elif part.startswith('.'):
319 classes.append(part[1:])
320 else:
321 return None # no support for key=value attrs like in pandoc
322
323 return (id, classes)
324
325def _parse_blockattrs(info: str) -> Optional[tuple[AttrBlockKind, Optional[str], list[str]]]:
326 if (m := _ATTR_BLOCK_PATTERN.fullmatch(info)) is None:
327 return None
328 if (parsed_attrs := _parse_attrs(m[1])) is None:
329 return None
330 id, classes = parsed_attrs
331 # check that we actually support this kind of block, and that is adheres to
332 # whetever restrictions we want to enforce for that kind of block.
333 if len(classes) == 1 and classes[0] in get_args(AdmonitionKind):
334 # don't want to support ids for admonitions just yet
335 if id is not None:
336 return None
337 return ('admonition', id, classes)
338 if classes == ['example']:
339 return ('example', id, classes)
340 elif classes == ['figure']:
341 return ('figure', id, classes)
342 return None
343
344def _attr_span_plugin(md: markdown_it.MarkdownIt) -> None:
345 def attr_span(state: markdown_it.rules_inline.StateInline, silent: bool) -> bool:
346 if state.src[state.pos] != '[':
347 return False
348 if _is_escaped(state.src, state.pos - 1):
349 return False
350
351 # treat the inline span like a link label for simplicity.
352 label_begin = state.pos + 1
353 label_end = markdown_it.helpers.parseLinkLabel(state, state.pos)
354 input_end = state.posMax
355 if label_end < 0:
356 return False
357
358 # match id and classes in any combination
359 match = _ATTR_SPAN_PATTERN.match(state.src[label_end + 1 : ])
360 if not match:
361 return False
362
363 if not silent:
364 if (parsed_attrs := _parse_attrs(match[1])) is None:
365 return False
366 id, classes = parsed_attrs
367
368 token = state.push("attr_span_begin", "span", 1) # type: ignore[no-untyped-call]
369 if id:
370 token.attrs['id'] = id
371 if classes:
372 token.attrs['class'] = " ".join(classes)
373
374 state.pos = label_begin
375 state.posMax = label_end
376 state.md.inline.tokenize(state)
377
378 state.push("attr_span_end", "span", -1) # type: ignore[no-untyped-call]
379
380 state.pos = label_end + match.end() + 1
381 state.posMax = input_end
382 return True
383
384 md.inline.ruler.before("link", "attr_span", attr_span)
385
386def _inline_comment_plugin(md: markdown_it.MarkdownIt) -> None:
387 def inline_comment(state: markdown_it.rules_inline.StateInline, silent: bool) -> bool:
388 if state.src[state.pos : state.pos + 4] != '<!--':
389 return False
390 if _is_escaped(state.src, state.pos - 1):
391 return False
392 for i in range(state.pos + 4, state.posMax - 2):
393 if state.src[i : i + 3] == '-->': # -->
394 state.pos = i + 3
395 return True
396
397 return False
398
399 md.inline.ruler.after("autolink", "inline_comment", inline_comment)
400
401def _block_comment_plugin(md: markdown_it.MarkdownIt) -> None:
402 def block_comment(state: markdown_it.rules_block.StateBlock, startLine: int, endLine: int,
403 silent: bool) -> bool:
404 pos = state.bMarks[startLine] + state.tShift[startLine]
405 posMax = state.eMarks[startLine]
406
407 if state.src[pos : pos + 4] != '<!--':
408 return False
409
410 nextLine = startLine
411 while nextLine < endLine:
412 pos = state.bMarks[nextLine] + state.tShift[nextLine]
413 posMax = state.eMarks[nextLine]
414
415 if state.src[posMax - 3 : posMax] == '-->':
416 state.line = nextLine + 1
417 return True
418
419 nextLine += 1
420
421 return False
422
423 md.block.ruler.after("code", "block_comment", block_comment)
424
425_HEADER_ID_RE = re.compile(r"\s*\{\s*\#([\w.-]+)\s*\}\s*$")
426
427def _heading_ids(md: markdown_it.MarkdownIt) -> None:
428 def heading_ids(state: markdown_it.rules_core.StateCore) -> None:
429 tokens = state.tokens
430 # this is purposely simple and doesn't support classes or other kinds of attributes.
431 for (i, token) in enumerate(tokens):
432 if token.type == 'heading_open':
433 children = tokens[i + 1].children
434 assert children is not None
435 if len(children) == 0 or children[-1].type != 'text':
436 continue
437 if m := _HEADER_ID_RE.search(children[-1].content):
438 tokens[i].attrs['id'] = m[1]
439 children[-1].content = children[-1].content[:-len(m[0])].rstrip()
440
441 md.core.ruler.before("replacements", "heading_ids", heading_ids)
442
443def _footnote_ids(md: markdown_it.MarkdownIt) -> None:
444 """generate ids for footnotes, their refs, and their backlinks. the ids we
445 generate here are derived from the footnote label, making numeric footnote
446 labels invalid.
447 """
448 def generate_ids(tokens: Sequence[Token]) -> None:
449 for token in tokens:
450 if token.type == 'footnote_open':
451 if token.meta["label"][:1].isdigit():
452 assert token.map
453 raise RuntimeError(f"invalid footnote label in line {token.map[0] + 1}")
454 token.attrs['id'] = token.meta["label"]
455 elif token.type == 'footnote_anchor':
456 token.meta['target'] = f'{token.meta["label"]}.__back.{token.meta["subId"]}'
457 elif token.type == 'footnote_ref':
458 token.attrs['id'] = f'{token.meta["label"]}.__back.{token.meta["subId"]}'
459 token.meta['target'] = token.meta["label"]
460 elif token.type == 'inline':
461 assert token.children is not None
462 generate_ids(token.children)
463
464 def footnote_ids(state: markdown_it.rules_core.StateCore) -> None:
465 generate_ids(state.tokens)
466
467 md.core.ruler.after("footnote_tail", "footnote_ids", footnote_ids)
468
469def _compact_list_attr(md: markdown_it.MarkdownIt) -> None:
470 @dataclasses.dataclass
471 class Entry:
472 head: Token
473 end: int
474 compact: bool = True
475
476 def compact_list_attr(state: markdown_it.rules_core.StateCore) -> None:
477 # markdown-it signifies wide lists by setting the wrapper paragraphs
478 # of each item to hidden. this is not useful for our stylesheets, which
479 # signify this with a special css class on list elements instead.
480 stack = []
481 for token in state.tokens:
482 if token.type in [ 'bullet_list_open', 'ordered_list_open' ]:
483 stack.append(Entry(token, cast(int, token.attrs.get('start', 1))))
484 elif token.type in [ 'bullet_list_close', 'ordered_list_close' ]:
485 lst = stack.pop()
486 lst.head.meta['compact'] = lst.compact
487 if token.type == 'ordered_list_close':
488 lst.head.meta['end'] = lst.end - 1
489 elif len(stack) > 0 and token.type == 'paragraph_open' and not token.hidden:
490 stack[-1].compact = False
491 elif token.type == 'list_item_open':
492 stack[-1].end += 1
493
494 md.core.ruler.push("compact_list_attr", compact_list_attr)
495
496def _block_attr(md: markdown_it.MarkdownIt) -> None:
497 def assert_never(value: NoReturn) -> NoReturn:
498 assert False
499
500 def block_attr(state: markdown_it.rules_core.StateCore) -> None:
501 stack = []
502 for token in state.tokens:
503 if token.type == 'container_blockattr_open':
504 if (parsed_attrs := _parse_blockattrs(token.info)) is None:
505 # if we get here we've missed a possible case in the plugin validate function
506 raise RuntimeError("this should be unreachable")
507 kind, id, classes = parsed_attrs
508 if kind == 'admonition':
509 token.type = 'admonition_open'
510 token.meta['kind'] = classes[0]
511 stack.append('admonition_close')
512 elif kind == 'example':
513 token.type = 'example_open'
514 if id is not None:
515 token.attrs['id'] = id
516 stack.append('example_close')
517 elif kind == 'figure':
518 token.type = 'figure_open'
519 if id is not None:
520 token.attrs['id'] = id
521 stack.append('figure_close')
522 else:
523 assert_never(kind)
524 elif token.type == 'container_blockattr_close':
525 token.type = stack.pop()
526
527 md.core.ruler.push("block_attr", block_attr)
528
529def _block_titles(block: str) -> Callable[[markdown_it.MarkdownIt], None]:
530 open, close = f'{block}_open', f'{block}_close'
531 title_open, title_close = f'{block}_title_open', f'{block}_title_close'
532
533 """
534 find title headings of blocks and stick them into meta for renderers, then
535 remove them from the token stream. also checks whether any block contains a
536 non-title heading since those would make toc generation extremely complicated.
537 """
538 def block_titles(state: markdown_it.rules_core.StateCore) -> None:
539 in_example = [False]
540 for i, token in enumerate(state.tokens):
541 if token.type == open:
542 if state.tokens[i + 1].type == 'heading_open':
543 assert state.tokens[i + 3].type == 'heading_close'
544 state.tokens[i + 1].type = title_open
545 state.tokens[i + 3].type = title_close
546 else:
547 assert token.map
548 raise RuntimeError(f"found {block} without title in line {token.map[0] + 1}")
549 in_example.append(True)
550 elif token.type == close:
551 in_example.pop()
552 elif token.type == 'heading_open' and in_example[-1]:
553 assert token.map
554 raise RuntimeError(f"unexpected non-title heading in {block} in line {token.map[0] + 1}")
555
556 def do_add(md: markdown_it.MarkdownIt) -> None:
557 md.core.ruler.push(f"{block}_titles", block_titles)
558
559 return do_add
560
561TR = TypeVar('TR', bound='Renderer')
562
563class Converter(ABC, Generic[TR]):
564 # we explicitly disable markdown-it rendering support and use our own entirely.
565 # rendering is well separated from parsing and our renderers carry much more state than
566 # markdown-it easily acknowledges as 'good' (unless we used the untyped env args to
567 # shuttle that state around, which is very fragile)
568 class ForbiddenRenderer(markdown_it.renderer.RendererProtocol):
569 __output__ = "none"
570
571 def __init__(self, parser: Optional[markdown_it.MarkdownIt]):
572 pass
573
574 def render(self, tokens: Sequence[Token], options: OptionsDict,
575 env: MutableMapping[str, Any]) -> str:
576 raise NotImplementedError("do not use Converter._md.renderer. 'tis a silly place")
577
578 _renderer: TR
579
580 def __init__(self) -> None:
581 self._md = markdown_it.MarkdownIt(
582 "commonmark",
583 {
584 'maxNesting': 100, # default is 20
585 'html': False, # not useful since we target many formats
586 'typographer': True, # required for smartquotes
587 },
588 renderer_cls=self.ForbiddenRenderer
589 )
590 self._md.enable('table')
591 self._md.use(
592 container_plugin,
593 name="blockattr",
594 validate=lambda name, *args: _parse_blockattrs(name),
595 )
596 self._md.use(deflist_plugin)
597 self._md.use(footnote_plugin)
598 self._md.use(myst_role_plugin)
599 self._md.use(_attr_span_plugin)
600 self._md.use(_inline_comment_plugin)
601 self._md.use(_block_comment_plugin)
602 self._md.use(_heading_ids)
603 self._md.use(_footnote_ids)
604 self._md.use(_compact_list_attr)
605 self._md.use(_block_attr)
606 self._md.use(_block_titles("example"))
607 self._md.use(_block_titles("figure"))
608 self._md.enable(["smartquotes", "replacements"])
609
610 def _parse(self, src: str) -> list[Token]:
611 return self._md.parse(src, {})
612
613 def _render(self, src: str) -> str:
614 tokens = self._parse(src)
615 return self._renderer.render(tokens)