Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/mistune/inline_parser.py: 81%

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

237 statements  

1from __future__ import annotations 

2 

3import re 

4from typing import ( 

5 Any, 

6 Dict, 

7 List, 

8 Match, 

9 MutableMapping, 

10 Optional, 

11 Set, 

12 Tuple, 

13) 

14 

15from ._inline.emphasis import finalize_emphasis_tokens, is_entity_boundary 

16from ._inline.links import parse_link as parse_inline_link 

17from .core import InlineState, Parser 

18from .helpers import ( 

19 HTML_ATTRIBUTES, 

20 HTML_TAGNAME, 

21 PUNCTUATION, 

22 unescape_char, 

23) 

24from .util import escape_url 

25 

26_REGEX_META_CHARS = set(r"()[]{}?*+|.^$") 

27DEFAULT_MAX_EMPHASIS_DEPTH = 20 

28DEFAULT_MAX_IMAGE_DEPTH = 20 

29 

30AUTO_EMAIL = ( 

31 r"""<[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9]""" 

32 r"(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?" 

33 r"(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*>" 

34) 

35 

36INLINE_HTML = ( 

37 r"<" + HTML_TAGNAME + HTML_ATTRIBUTES + r"\s*/?>|" # open tag 

38 r"</" + HTML_TAGNAME + r"\s*>|" # close tag 

39 r"<!--(?!>|->)(?:(?!--)[\s\S])+?(?<!-)-->|" # comment 

40 r"<\?[\s\S]+?\?>|" # script like <?php?> 

41 r"<![A-Z][\s\S]+?>|" # doctype 

42 r"<!\[CDATA[\s\S]+?\]\]>" # cdata 

43) 

44 

45 

46class InlineParser(Parser[InlineState]): 

47 sc_flag = 0 

48 state_cls = InlineState 

49 

50 #: linebreak leaves two spaces at the end of line 

51 STD_LINEBREAK = r"(?:\\| {2,})\n\s*" 

52 

53 #: every new line becomes <br> 

54 HARD_LINEBREAK = r" *\n\s*" 

55 

56 # we only need to find the start pattern of an inline token 

57 SPECIFICATION = { 

58 # e.g. \`, \$ 

59 "escape": r"(?:\\" + PUNCTUATION + ")+", 

60 # `code, ```code 

61 "codespan": r"`{1,}", 

62 # *w, **w, _w, __w 

63 "emphasis": r"\*{1,3}(?=[^\s*])|\b_{1,3}(?=[^\s_])", 

64 # [link], ![img] 

65 "link": r"!?\[", 

66 # <https://example.com>. regex copied from commonmark.js 

67 "auto_link": r"<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*>", 

68 "auto_email": AUTO_EMAIL, 

69 "inline_html": INLINE_HTML, 

70 "linebreak": STD_LINEBREAK, 

71 "softbreak": HARD_LINEBREAK, 

72 "prec_auto_link": r"<[A-Za-z][A-Za-z\d.+-]{1,31}:", 

73 "prec_inline_html": r"</?" + HTML_TAGNAME + r"|<!|<\?", 

74 } 

75 DEFAULT_RULES = ( 

76 "escape", 

77 "codespan", 

78 "emphasis", 

79 "link", 

80 "auto_link", 

81 "auto_email", 

82 "inline_html", 

83 "linebreak", 

84 ) 

85 

86 def __init__( 

87 self, 

88 hard_wrap: bool = False, 

89 max_emphasis_depth: int = DEFAULT_MAX_EMPHASIS_DEPTH, 

90 max_image_depth: int = DEFAULT_MAX_IMAGE_DEPTH, 

91 ) -> None: 

92 super(InlineParser, self).__init__() 

93 

94 self.hard_wrap = hard_wrap 

95 self.max_emphasis_depth = max_emphasis_depth 

96 self.max_image_depth = max_image_depth 

97 self._fast_trigger_chars: Optional[Set[str]] = None 

98 self._fast_trigger_re: Optional[re.Pattern[str]] = None 

99 self._fast_trigger_re_chars: Optional[Tuple[str, ...]] = None 

100 # lazy add linebreak 

101 if hard_wrap: 

102 self.specification["linebreak"] = self.HARD_LINEBREAK 

103 else: 

104 self.rules.append("softbreak") 

105 

106 self._methods = {name: getattr(self, "parse_" + name) for name in self.rules} 

107 

108 def register( 

109 self, 

110 name: str, 

111 pattern: Optional[str], 

112 func: Any, 

113 before: Optional[str] = None, 

114 ) -> None: 

115 super().register(name, pattern, func, before=before) 

116 self._fast_trigger_chars = None 

117 self._fast_trigger_re = None 

118 self._fast_trigger_re_chars = None 

119 

120 def parse_escape(self, m: Match[str], state: InlineState) -> int: 

121 text = m.group(0) 

122 text = unescape_char(text) 

123 self.process_text(text, state, parse_emphasis=False) 

124 return m.end() 

125 

126 def parse_link(self, m: Match[str], state: InlineState) -> Optional[int]: 

127 return parse_inline_link(self, m, state) 

128 

129 def parse_auto_link(self, m: Match[str], state: InlineState) -> int: 

130 text = m.group(0) 

131 pos = m.end() 

132 if state.in_link: 

133 self.process_text(text, state) 

134 return pos 

135 

136 text = text[1:-1] 

137 self._add_auto_link(text, text, state) 

138 return pos 

139 

140 def parse_auto_email(self, m: Match[str], state: InlineState) -> int: 

141 text = m.group(0) 

142 pos = m.end() 

143 if state.in_link: 

144 self.process_text(text, state) 

145 return pos 

146 

147 text = text[1:-1] 

148 url = "mailto:" + text 

149 self._add_auto_link(url, text, state) 

150 return pos 

151 

152 def _add_auto_link(self, url: str, text: str, state: InlineState) -> None: 

153 state.append_token( 

154 { 

155 "type": "link", 

156 "children": [{"type": "text", "raw": text}], 

157 "attrs": {"url": escape_url(url)}, 

158 } 

159 ) 

160 

161 def parse_emphasis(self, m: Match[str], state: InlineState) -> int: 

162 # Keep a delimiter run separate from the preceding text token. The 

163 # emphasis finalizer needs to inspect these markers later, and merging 

164 # one-character markers with a growing text token makes inputs such as 

165 # ``*a*a*a`` repeatedly copy the whole accumulated string. 

166 marker = m.group(0) 

167 if len(marker) == 1: 

168 state.append_token({"type": "text", "raw": marker}) 

169 else: 

170 self.process_text(marker, state) 

171 return m.end() 

172 

173 def parse_codespan(self, m: Match[str], state: InlineState) -> int: 

174 marker = m.group(0) 

175 # require same marker with same length at end 

176 

177 pattern = re.compile(r"(.*?[^`])" + marker + r"(?!`)", re.S) 

178 

179 pos = m.end() 

180 m2 = pattern.match(state.src, pos) 

181 if m2: 

182 end_pos = m2.end() 

183 code = m2.group(1) 

184 # Line endings are treated like spaces 

185 code = code.replace("\n", " ") 

186 if len(code.strip()): 

187 if code.startswith(" ") and code.endswith(" "): 

188 code = code[1:-1] 

189 state.append_token({"type": "codespan", "raw": code}) 

190 return end_pos 

191 else: 

192 state.append_token({"type": "text", "raw": marker}) 

193 return pos 

194 

195 def parse_linebreak(self, m: Match[str], state: InlineState) -> int: 

196 state.append_token({"type": "linebreak"}) 

197 return m.end() 

198 

199 def parse_softbreak(self, m: Match[str], state: InlineState) -> int: 

200 state.append_token({"type": "softbreak"}) 

201 return m.end() 

202 

203 def parse_inline_html(self, m: Match[str], state: InlineState) -> int: 

204 end_pos = m.end() 

205 html = m.group(0) 

206 state.append_token({"type": "inline_html", "raw": html}) 

207 if html.startswith(("<a ", "<a>", "<A ", "<A>")): 

208 state.in_link = True 

209 elif html.startswith(("</a ", "</a>", "</A ", "</A>")): 

210 state.in_link = False 

211 return end_pos 

212 

213 def process_text(self, text: str, state: InlineState, parse_emphasis: bool = True) -> None: 

214 if ( 

215 parse_emphasis 

216 and state.tokens 

217 and state.tokens[-1]["type"] == "text" 

218 and state.tokens[-1].get("_emphasis", True) 

219 and not is_entity_boundary(state.tokens[-1]["raw"], text) 

220 ): 

221 state.tokens[-1]["raw"] += text 

222 else: 

223 token: Dict[str, Any] = {"type": "text", "raw": text} 

224 if not parse_emphasis: 

225 token["_emphasis"] = False 

226 state.append_token(token) 

227 

228 def parse(self, state: InlineState) -> List[Dict[str, Any]]: 

229 pos = 0 

230 sc = self.compile_sc() 

231 while pos < len(state.src): 

232 fast_end = self._find_fast_text_end(state.src, pos) 

233 if fast_end is None: 

234 m = sc.search(state.src, pos) 

235 else: 

236 if fast_end > pos: 

237 self.process_text(state.src[pos:fast_end], state) 

238 pos = fast_end 

239 if pos >= len(state.src): 

240 break 

241 m = sc.match(state.src, pos) 

242 

243 if not m: 

244 if fast_end is not None: 

245 self.process_text(state.src[pos : pos + 1], state) 

246 pos += 1 

247 continue 

248 break 

249 

250 end_pos = m.start() 

251 if end_pos > pos: 

252 hole = state.src[pos:end_pos] 

253 self.process_text(hole, state) 

254 

255 new_pos = self.parse_method(m, state) 

256 if not new_pos: 

257 # move cursor 1 character forward 

258 pos = end_pos + 1 

259 hole = state.src[end_pos:pos] 

260 self.process_text(hole, state) 

261 else: 

262 pos = new_pos 

263 

264 if pos == 0: 

265 # special case, just pure text 

266 self.process_text(state.src, state) 

267 elif pos < len(state.src): 

268 self.process_text(state.src[pos:], state) 

269 state.tokens = finalize_emphasis_tokens( 

270 state.tokens, 

271 "emphasis" in self.rules, 

272 self.max_emphasis_depth, 

273 ) 

274 return state.tokens 

275 

276 def _find_fast_text_end(self, src: str, pos: int) -> Optional[int]: 

277 chars = self._get_fast_trigger_chars() 

278 if chars is None: 

279 return None 

280 

281 trigger_re = self._get_fast_trigger_re(chars) 

282 m = trigger_re.search(src, pos) 

283 if m is None: 

284 return len(src) 

285 

286 if m.group(0) == "\n": 

287 return self._find_linebreak_start(src, pos, m.start()) 

288 return m.start() 

289 

290 def _get_fast_trigger_re(self, chars: Set[str]) -> re.Pattern[str]: 

291 key = tuple(sorted(chars)) 

292 if self._fast_trigger_re is None or self._fast_trigger_re_chars != key: 

293 pattern = "[" + re.escape("".join(key)) + "]" 

294 self._fast_trigger_re = re.compile(pattern) 

295 self._fast_trigger_re_chars = key 

296 assert self._fast_trigger_re is not None 

297 return self._fast_trigger_re 

298 

299 def _find_linebreak_start(self, src: str, min_pos: int, newline_pos: int) -> int: 

300 pos = newline_pos 

301 while pos > min_pos and src[pos - 1] == " ": 

302 pos -= 1 

303 if pos == newline_pos and pos > min_pos and src[pos - 1] == "\\": 

304 return pos - 1 

305 return pos 

306 

307 def _get_fast_trigger_chars(self) -> Optional[Set[str]]: 

308 chars = self._fast_trigger_chars 

309 if chars is not None: 

310 return chars 

311 

312 chars = set() 

313 for name in self.rules: 

314 pattern = self.specification.get(name) 

315 rule_chars = _get_rule_start_chars(name, pattern) 

316 if rule_chars is None: 

317 self._fast_trigger_chars = None 

318 return None 

319 chars.update(rule_chars) 

320 self._fast_trigger_chars = chars 

321 return chars 

322 

323 def precedence_scan( 

324 self, 

325 m: Match[str], 

326 state: InlineState, 

327 end_pos: int, 

328 rules: Optional[List[str]] = None, 

329 ) -> Optional[int]: 

330 if rules is None: 

331 rules = ["codespan", "link", "prec_auto_link", "prec_inline_html"] 

332 

333 mark_pos = m.end() 

334 sc = self.compile_sc(rules) 

335 m1 = sc.search(state.src, mark_pos, end_pos) 

336 if not m1: 

337 return None 

338 

339 lastgroup = m1.lastgroup 

340 if not lastgroup: 

341 return None 

342 rule_name = lastgroup.replace("prec_", "") 

343 sc = self.compile_sc([rule_name]) 

344 m2 = sc.match(state.src, m1.start()) 

345 if not m2: 

346 return None 

347 

348 func = self._methods[rule_name] 

349 new_state = state.copy() 

350 new_state.src = state.src 

351 m2_pos = func(m2, new_state) 

352 if not m2_pos or m2_pos < end_pos: 

353 return None 

354 

355 raw_text = state.src[m.start() : m2.start()] 

356 state.append_token({"type": "text", "raw": raw_text}) 

357 for token in new_state.tokens: 

358 state.append_token(token) 

359 return m2_pos 

360 

361 def render(self, state: InlineState) -> List[Dict[str, Any]]: 

362 self.parse(state) 

363 return state.tokens 

364 

365 def __call__(self, s: str, env: MutableMapping[str, Any]) -> List[Dict[str, Any]]: 

366 state = self.state_cls(env) 

367 state.src = s 

368 return self.render(state) 

369 

370 

371def _get_rule_start_chars(name: str, pattern: Optional[str]) -> Optional[Set[str]]: 

372 known = { 

373 "escape": {"\\"}, 

374 "codespan": {"`"}, 

375 "emphasis": {"*", "_"}, 

376 "link": {"!", "["}, 

377 "auto_link": {"<"}, 

378 "auto_email": {"<"}, 

379 "inline_html": {"<"}, 

380 "linebreak": {"\n"}, 

381 "softbreak": {"\n"}, 

382 "prec_auto_link": {"<"}, 

383 "prec_inline_html": {"<"}, 

384 # built-in plugins 

385 "url_link": {"h"}, 

386 "strikethrough": {"~"}, 

387 "mark": {"="}, 

388 "insert": {"^"}, 

389 "superscript": {"^"}, 

390 "subscript": {"~"}, 

391 "footnote": {"["}, 

392 "inline_math": {"$"}, 

393 "ruby": {"["}, 

394 "inline_spoiler": {">"}, 

395 } 

396 if name in known: 

397 return known[name] 

398 if not pattern: 

399 return set() 

400 return _guess_pattern_start_chars(pattern) 

401 

402 

403def _guess_pattern_start_chars(pattern: str) -> Optional[Set[str]]: 

404 if not pattern: 

405 return set() 

406 

407 if pattern.startswith("\\") and len(pattern) > 1: 

408 c = pattern[1] 

409 if c in _REGEX_META_CHARS or c in PUNCTUATION: 

410 return {c} 

411 return None 

412 

413 c = pattern[0] 

414 if c in _REGEX_META_CHARS or c.isspace(): 

415 return None 

416 return {c}