Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/mistune/helpers.py: 88%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import re
2import string
3from typing import Any, Dict, Tuple, Union
5from .core import DEFAULT_MAX_NESTED_LEVEL
6from .util import escape_url
8PREVENT_BACKSLASH = r"(?<!\\)(?:\\\\)*"
9PUNCTUATION = r"[" + re.escape(string.punctuation) + r"]"
11MAX_LINK_LABEL_LENGTH = 999
12LINK_LABEL = r"(?=[\s\S]{0,999}\])(?:[^\\\[\]]|\\.){0,999}"
14ASCII_WHITESPACE = " \t\n\r\f"
16HTML_TAGNAME = r"[A-Za-z][A-Za-z0-9-]*"
17HTML_ATTRIBUTES = (
18 r"(?:\s+[A-Za-z_:][A-Za-z0-9_.:-]*"
19 r'(?:\s*=\s*(?:[^ !"\'=<>`]+|\'[^\']*?\'|"[^\"]*?"))?)*'
20)
22BLOCK_TAGS = (
23 "address",
24 "article",
25 "aside",
26 "base",
27 "basefont",
28 "blockquote",
29 "body",
30 "caption",
31 "center",
32 "col",
33 "colgroup",
34 "dd",
35 "details",
36 "dialog",
37 "dir",
38 "div",
39 "dl",
40 "dt",
41 "fieldset",
42 "figcaption",
43 "figure",
44 "footer",
45 "form",
46 "frame",
47 "frameset",
48 "h1",
49 "h2",
50 "h3",
51 "h4",
52 "h5",
53 "h6",
54 "head",
55 "header",
56 "hr",
57 "html",
58 "iframe",
59 "legend",
60 "li",
61 "link",
62 "main",
63 "menu",
64 "menuitem",
65 "meta",
66 "nav",
67 "noframes",
68 "ol",
69 "optgroup",
70 "option",
71 "p",
72 "param",
73 "section",
74 "source",
75 "summary",
76 "table",
77 "tbody",
78 "td",
79 "tfoot",
80 "th",
81 "thead",
82 "title",
83 "tr",
84 "track",
85 "ul",
86)
87PRE_TAGS = ("pre", "script", "style", "textarea")
89_INLINE_LINK_LABEL_RE = re.compile(LINK_LABEL + r"\]")
90_INLINE_SQUARE_BRACKET_RE = re.compile(PREVENT_BACKSLASH + r"[\[\]]")
91_ESCAPE_CHAR_RE = re.compile(r"\\(" + PUNCTUATION + r")")
92_BLANK_LINE_RE = re.compile(r"\n[ \t]*\n")
95def unescape_char(text: str) -> str:
96 return _ESCAPE_CHAR_RE.sub(r"\1", text)
99def parse_link_text(src: str, pos: int) -> Union[Tuple[str, int], Tuple[None, int]]:
100 level = 1
101 found = False
102 start_pos = pos
104 while pos < len(src):
105 m = _INLINE_SQUARE_BRACKET_RE.search(src, pos)
106 if not m:
107 pos = len(src) # FIX: record we scanned to end
108 break
110 pos = m.end()
111 marker = m.group(0)
112 if marker == "]":
113 level -= 1
114 if level == 0:
115 found = True
116 break
117 else:
118 level += 1
120 if found:
121 text = src[start_pos : pos - 1]
122 return text, pos
123 return None, pos # FIX: return pos instead of None
126def parse_link_label(src: str, start_pos: int) -> Union[Tuple[str, int], Tuple[None, None]]:
127 m = _INLINE_LINK_LABEL_RE.match(src, start_pos, start_pos + MAX_LINK_LABEL_LENGTH + 1)
128 if m:
129 label = m.group(0)[:-1]
130 return label, m.end()
131 return None, None
134def parse_link_href(
135 src: str, start_pos: int, block: bool = False, *, max_nested_level: int = DEFAULT_MAX_NESTED_LEVEL
136) -> Union[Tuple[str, int], Tuple[None, None]]:
137 href, href_pos, _end_pos = _parse_link_href(src, start_pos, block=block, max_nested_level=max_nested_level)
138 if href is None:
139 return None, None
140 assert href_pos is not None
141 return href, href_pos
144def _parse_link_href(
145 src: str, start_pos: int, block: bool = False, *, max_nested_level: int = DEFAULT_MAX_NESTED_LEVEL
146) -> Tuple[Union[str, None], Union[int, None], int]:
147 pos = _skip_link_start_whitespace(src, start_pos)
148 if pos >= len(src):
149 return None, None, pos
151 if src[pos] == "<":
152 href, href_pos = _parse_angle_link_href(src, pos)
153 if href is None:
154 return None, None, pos
155 assert href_pos is not None
156 return href, href_pos, href_pos
157 if block and src[pos] in ASCII_WHITESPACE:
158 return None, None, pos
160 start = pos
161 level = 0
162 while pos < len(src):
163 c = src[pos]
164 if c in ASCII_WHITESPACE:
165 break
166 if ord(c) < 32 or c == "\x7f":
167 return None, None, pos
168 if c == "\\" and pos + 1 < len(src) and src[pos + 1] in string.punctuation:
169 pos = min(pos + 2, len(src))
170 continue
171 if c == "(":
172 level += 1
173 if level > max_nested_level:
174 return None, None, pos
175 elif c == ")":
176 if level == 0:
177 break
178 level -= 1
179 pos += 1
181 if level != 0:
182 return None, None, pos
183 return src[start:pos], pos, pos
186def parse_link_title(src: str, start_pos: int, max_pos: int) -> Union[Tuple[str, int], Tuple[None, None]]:
187 pos = start_pos
188 if pos >= max_pos or src[pos] not in ASCII_WHITESPACE:
189 return None, None
191 pos = _skip_ascii_whitespace(src, pos, max_pos)
192 if pos >= max_pos:
193 return None, None
195 opener = src[pos]
196 closer = {"'": "'", '"': '"', "(": ")"}.get(opener)
197 if closer is None:
198 return None, None
200 pos += 1
201 title = []
202 while pos < max_pos:
203 c = src[pos]
204 if c == "\x00" or (opener == "(" and c == "("):
205 return None, None
206 if c == "\n" and _BLANK_LINE_RE.match(src, pos, max_pos):
207 return None, None
208 if c == "\\":
209 if pos + 1 < max_pos and src[pos + 1] in string.punctuation:
210 title.append(src[pos : pos + 2])
211 pos += 2
212 continue
213 if c == closer:
214 return unescape_char("".join(title)), pos + 1
215 title.append(src[pos])
216 pos += 1
217 return None, None
220def parse_link(
221 src: str, pos: int, *, max_nested_level: int = DEFAULT_MAX_NESTED_LEVEL
222) -> Union[Tuple[Dict[str, Any], int], Tuple[None, None]]:
223 attrs, next_pos, _end_pos = parse_link_with_end(src, pos, max_nested_level=max_nested_level)
224 if attrs is None:
225 return None, None
226 assert next_pos is not None
227 return attrs, next_pos
230def parse_link_with_end(
231 src: str, pos: int, *, max_nested_level: int = DEFAULT_MAX_NESTED_LEVEL
232) -> Tuple[Union[Dict[str, Any], None], Union[int, None], int]:
233 href, href_pos, scan_end = _parse_link_href(src, pos, max_nested_level=max_nested_level)
234 if href is None:
235 return None, None, scan_end
236 assert href_pos is not None
237 title, title_pos = parse_link_title(src, href_pos, len(src))
238 next_pos = title_pos or href_pos
239 next_pos = _skip_ascii_whitespace(src, next_pos)
240 if next_pos >= len(src) or src[next_pos] != ")":
241 return None, None, next_pos
243 href = unescape_char(href)
244 attrs = {"url": escape_url(href)}
245 if title:
246 attrs["title"] = title
247 return attrs, next_pos + 1, next_pos + 1
250def _skip_ascii_whitespace(src: str, pos: int, max_pos: Union[int, None] = None) -> int:
251 if max_pos is None:
252 max_pos = len(src)
253 while pos < max_pos and src[pos] in ASCII_WHITESPACE:
254 pos += 1
255 return pos
258def _skip_link_start_whitespace(src: str, pos: int) -> int:
259 while pos < len(src) and src[pos] in " \t":
260 pos += 1
261 if pos < len(src) and src[pos] in "\n\r":
262 if src[pos] == "\r" and pos + 1 < len(src) and src[pos + 1] == "\n":
263 pos += 2
264 else:
265 pos += 1
266 while pos < len(src) and src[pos] in " \t":
267 pos += 1
268 return pos
271def _parse_angle_link_href(src: str, pos: int) -> Union[Tuple[str, int], Tuple[None, None]]:
272 start = pos + 1
273 pos = start
274 while pos < len(src):
275 c = src[pos]
276 if c == ">":
277 return src[start:pos], pos + 1
278 if c == "\\" and pos + 1 < len(src) and src[pos + 1] in string.punctuation:
279 pos += 2
280 continue
281 if c in "<\n\r\x00":
282 return None, None
283 pos += 1
284 return None, None