|
| 1 | +""" |
| 2 | +Markdown-it-py plugin to introduce <sub> markup using ~subscript~. |
| 3 | +
|
| 4 | +Ported from |
| 5 | +https://github.com/markdown-it/markdown-it-sub/blob/master/index.mjs |
| 6 | +
|
| 7 | +Originally ported during implementation of https://github.com/hasgeek/funnel/blob/main/funnel/utils/markdown/mdit_plugins/sub_tag.py |
| 8 | +""" |
| 9 | + |
| 10 | +from __future__ import annotations |
| 11 | + |
| 12 | +from collections.abc import Sequence |
| 13 | +import re |
| 14 | + |
| 15 | +from markdown_it import MarkdownIt |
| 16 | +from markdown_it.renderer import RendererHTML |
| 17 | +from markdown_it.rules_inline import StateInline |
| 18 | +from markdown_it.token import Token |
| 19 | +from markdown_it.utils import EnvType, OptionsDict |
| 20 | + |
| 21 | +__all__ = ["sub_plugin"] |
| 22 | + |
| 23 | +TILDE_CHAR = "~" |
| 24 | + |
| 25 | +WHITESPACE_RE = re.compile(r"(^|[^\\])(\\\\)*\s") |
| 26 | +UNESCAPE_RE = re.compile(r'\\([ \\!"#$%&\'()*+,.\/:;<=>?@[\]^_`{|}~-])') |
| 27 | + |
| 28 | + |
| 29 | +def tokenize(state: StateInline, silent: bool) -> bool: |
| 30 | + """Parse a ~subscript~ token.""" |
| 31 | + start = state.pos |
| 32 | + ch = state.src[start] |
| 33 | + maximum = state.posMax |
| 34 | + found = False |
| 35 | + |
| 36 | + # Don't run any pairs in validation mode |
| 37 | + if silent: |
| 38 | + return False |
| 39 | + |
| 40 | + if ch != TILDE_CHAR: |
| 41 | + return False |
| 42 | + |
| 43 | + if start + 2 >= maximum: |
| 44 | + return False |
| 45 | + |
| 46 | + state.pos = start + 1 |
| 47 | + |
| 48 | + while state.pos < maximum: |
| 49 | + if state.src[state.pos] == TILDE_CHAR: |
| 50 | + found = True |
| 51 | + break |
| 52 | + state.md.inline.skipToken(state) |
| 53 | + |
| 54 | + if not found or start + 1 == state.pos: |
| 55 | + state.pos = start |
| 56 | + return False |
| 57 | + |
| 58 | + content = state.src[start + 1 : state.pos] |
| 59 | + |
| 60 | + # Don't allow unescaped spaces/newlines inside |
| 61 | + if WHITESPACE_RE.search(content) is not None: |
| 62 | + state.pos = start |
| 63 | + return False |
| 64 | + |
| 65 | + # Found a valid pair, so update posMax and pos |
| 66 | + state.posMax = state.pos |
| 67 | + state.pos = start + 1 |
| 68 | + |
| 69 | + # Earlier we checked "not silent", but this implementation does not need it |
| 70 | + token = state.push("sub_open", "sub", 1) |
| 71 | + token.markup = TILDE_CHAR |
| 72 | + |
| 73 | + token = state.push("text", "", 0) |
| 74 | + token.content = UNESCAPE_RE.sub(r"\1", content) |
| 75 | + |
| 76 | + token = state.push("sub_close", "sub", -1) |
| 77 | + token.markup = TILDE_CHAR |
| 78 | + |
| 79 | + state.pos = state.posMax + 1 |
| 80 | + state.posMax = maximum |
| 81 | + return True |
| 82 | + |
| 83 | + |
| 84 | +def sub_open( |
| 85 | + renderer: RendererHTML, |
| 86 | + tokens: Sequence[Token], |
| 87 | + idx: int, |
| 88 | + options: OptionsDict, |
| 89 | + env: EnvType, |
| 90 | +) -> str: |
| 91 | + """Render the opening tag for a ~subscript~ token.""" |
| 92 | + return "<sub>" |
| 93 | + |
| 94 | + |
| 95 | +def sub_close( |
| 96 | + renderer: RendererHTML, |
| 97 | + tokens: Sequence[Token], |
| 98 | + idx: int, |
| 99 | + options: OptionsDict, |
| 100 | + env: EnvType, |
| 101 | +) -> str: |
| 102 | + """Render the closing tag for a ~subscript~ token.""" |
| 103 | + return "</sub>" |
| 104 | + |
| 105 | + |
| 106 | +def sub_plugin(md: MarkdownIt) -> None: |
| 107 | + """ |
| 108 | + Markdown-it-py plugin to introduce <sub> markup using ~subscript~. |
| 109 | +
|
| 110 | + Ported from |
| 111 | + https://github.com/markdown-it/markdown-it-sub/blob/master/index.mjs |
| 112 | +
|
| 113 | + Originally ported during implementation of https://github.com/hasgeek/funnel/blob/main/funnel/utils/markdown/mdit_plugins/sub_tag.py |
| 114 | + """ |
| 115 | + md.inline.ruler.after("emphasis", "sub", tokenize) |
| 116 | + md.add_render_rule("sub_open", sub_open) |
| 117 | + md.add_render_rule("sub_close", sub_close) |
0 commit comments