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)