1from typing import TYPE_CHECKING, Match, Optional
2
3from ..helpers import PREVENT_BACKSLASH
4
5if TYPE_CHECKING:
6 from ..core import BaseRenderer, InlineState
7 from ..inline_parser import InlineParser
8 from ..markdown import Markdown
9
10__all__ = ["strikethrough", "mark", "insert", "superscript", "subscript"]
11
12SUPERSCRIPT_PATTERN = r"\^(?:" + PREVENT_BACKSLASH + r"\\\^|\S|\\ )+?\^"
13SUBSCRIPT_PATTERN = r"~(?:" + PREVENT_BACKSLASH + r"\\~|\S|\\ )+?~"
14
15
16def parse_strikethrough(inline: "InlineParser", m: Match[str], state: "InlineState") -> Optional[int]:
17 return _parse_to_end(inline, m, state, "strikethrough", "~~")
18
19
20def render_strikethrough(renderer: "BaseRenderer", text: str) -> str:
21 return "<del>" + text + "</del>"
22
23
24def parse_mark(inline: "InlineParser", m: Match[str], state: "InlineState") -> Optional[int]:
25 return _parse_to_end(inline, m, state, "mark", "==")
26
27
28def render_mark(renderer: "BaseRenderer", text: str) -> str:
29 return "<mark>" + text + "</mark>"
30
31
32def parse_insert(inline: "InlineParser", m: Match[str], state: "InlineState") -> Optional[int]:
33 return _parse_to_end(inline, m, state, "insert", "^^")
34
35
36def render_insert(renderer: "BaseRenderer", text: str) -> str:
37 return "<ins>" + text + "</ins>"
38
39
40def parse_superscript(inline: "InlineParser", m: Match[str], state: "InlineState") -> int:
41 return _parse_script(inline, m, state, "superscript")
42
43
44def render_superscript(renderer: "BaseRenderer", text: str) -> str:
45 return "<sup>" + text + "</sup>"
46
47
48def parse_subscript(inline: "InlineParser", m: Match[str], state: "InlineState") -> int:
49 return _parse_script(inline, m, state, "subscript")
50
51
52def render_subscript(renderer: "BaseRenderer", text: str) -> str:
53 return "<sub>" + text + "</sub>"
54
55
56def _parse_to_end(
57 inline: "InlineParser",
58 m: Match[str],
59 state: "InlineState",
60 tok_type: str,
61 marker: str,
62) -> Optional[int]:
63 pos = m.end()
64 cache_key = (id(state.src), marker)
65 cache = state.formatting_no_end.get(cache_key)
66 if cache is not None and cache[0] is state.src and pos <= cache[1]:
67 return None
68
69 end_pos = _find_end_marker(state.src, pos, marker)
70 if end_pos is None:
71 state.formatting_no_end[cache_key] = (state.src, len(state.src))
72 return None
73 text = state.src[pos : end_pos - 2]
74 new_state = state.copy()
75 new_state.src = text
76 children = inline.render(new_state)
77 state.append_token({"type": tok_type, "children": children})
78 return end_pos
79
80
81def _find_end_marker(src: str, pos: int, marker: str) -> Optional[int]:
82 c = marker[0]
83 marker_len = len(marker)
84 end = src.find(marker, pos)
85 while end != -1:
86 marker_end = end + marker_len
87 if marker_end < len(src) and src[marker_end] == c and not src.startswith(marker, marker_end):
88 end = src.find(marker, end + 1)
89 continue
90
91 if end > pos:
92 prev = src[end - 1]
93 escaped_marker_before = (
94 prev == c and end >= pos + 2 and src[end - 2] == "\\" and not _has_odd_backslashes(src, end - 2)
95 )
96 if (not prev.isspace() and prev != c) or escaped_marker_before:
97 return marker_end
98
99 end = src.find(marker, end + 1)
100 return None
101
102
103def _has_odd_backslashes(src: str, pos: int) -> bool:
104 count = 0
105 pos -= 1
106 while pos >= 0 and src[pos] == "\\":
107 count += 1
108 pos -= 1
109 return count % 2 == 1
110
111
112def _parse_script(inline: "InlineParser", m: Match[str], state: "InlineState", tok_type: str) -> int:
113 text = m.group(0)
114 new_state = state.copy()
115 new_state.src = text[1:-1].replace("\\ ", " ")
116 children = inline.render(new_state)
117 state.append_token({"type": tok_type, "children": children})
118 return m.end()
119
120
121def strikethrough(md: "Markdown") -> None:
122 """A mistune plugin to support strikethrough. Spec defined by
123 GitHub flavored Markdown and commonly used by many parsers:
124
125 .. code-block:: text
126
127 ~~This was mistaken text~~
128
129 It will be converted into HTML:
130
131 .. code-block:: html
132
133 <del>This was mistaken text</del>
134
135 :param md: Markdown instance
136 """
137 md.inline.register(
138 "strikethrough",
139 r"~~(?=[^\s~])",
140 parse_strikethrough,
141 before="link",
142 )
143 if md.renderer and md.renderer.NAME == "html":
144 md.renderer.register("strikethrough", render_strikethrough)
145
146
147def mark(md: "Markdown") -> None:
148 """A mistune plugin to add ``<mark>`` tag. Spec defined at
149 https://facelessuser.github.io/pymdown-extensions/extensions/mark/:
150
151 .. code-block:: text
152
153 ==mark me== ==mark \\=\\= equal==
154
155 :param md: Markdown instance
156 """
157 md.inline.register(
158 "mark",
159 r"==(?=[^\s=])",
160 parse_mark,
161 before="link",
162 )
163 if md.renderer and md.renderer.NAME == "html":
164 md.renderer.register("mark", render_mark)
165
166
167def insert(md: "Markdown") -> None:
168 """A mistune plugin to add ``<ins>`` tag. Spec defined at
169 https://facelessuser.github.io/pymdown-extensions/extensions/caret/#insert:
170
171 .. code-block:: text
172
173 ^^insert me^^
174
175 :param md: Markdown instance
176 """
177 md.inline.register(
178 "insert",
179 r"\^\^(?=[^\s\^])",
180 parse_insert,
181 before="link",
182 )
183 if md.renderer and md.renderer.NAME == "html":
184 md.renderer.register("insert", render_insert)
185
186
187def superscript(md: "Markdown") -> None:
188 """A mistune plugin to add ``<sup>`` tag. Spec defined at
189 https://pandoc.org/MANUAL.html#superscripts-and-subscripts:
190
191 .. code-block:: text
192
193 2^10^ is 1024.
194
195 :param md: Markdown instance
196 """
197 md.inline.register("superscript", SUPERSCRIPT_PATTERN, parse_superscript, before="linebreak")
198 if md.renderer and md.renderer.NAME == "html":
199 md.renderer.register("superscript", render_superscript)
200
201
202def subscript(md: "Markdown") -> None:
203 """A mistune plugin to add ``<sub>`` tag. Spec defined at
204 https://pandoc.org/MANUAL.html#superscripts-and-subscripts:
205
206 .. code-block:: text
207
208 H~2~O is a liquid.
209
210 :param md: Markdown instance
211 """
212 md.inline.register("subscript", SUBSCRIPT_PATTERN, parse_subscript, before="linebreak")
213 if md.renderer and md.renderer.NAME == "html":
214 md.renderer.register("subscript", render_subscript)