Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/mistune/_inline/emphasis.py: 99%

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

302 statements  

1from __future__ import annotations 

2 

3import re 

4from dataclasses import dataclass 

5from typing import Any, Dict, List, Optional, Tuple, cast 

6 

7from ..core import token_depth 

8 

9_CHARREF_PREFIX = re.compile(r"(#[0-9]{1,7};|#[xX][0-9a-fA-F]+;|[^\t\n\f <&#;]{1,32};)") 

10 

11 

12def is_entity_boundary(left: str, right: str) -> bool: 

13 return left.endswith("&") and _CHARREF_PREFIX.match(right) is not None 

14 

15 

16@dataclass(slots=True) 

17class _Delimiter: 

18 index: int 

19 marker: str 

20 length: int 

21 can_open: bool 

22 can_close: bool 

23 #: original run length, used by the CommonMark multiple-of-3 rule even 

24 #: after the run has been partially consumed 

25 orig_length: int = 0 

26 order: int = 0 

27 

28 

29class _DelimiterIndex: 

30 """Track part positions without rewriting every delimiter after a splice.""" 

31 

32 def __init__(self, delimiters: List[_Delimiter], part_count: int) -> None: 

33 self._tree = [0] * (part_count + 1) 

34 self._next = list(range(len(delimiters) + 1)) 

35 

36 def current(self, delimiter: _Delimiter) -> int: 

37 index = delimiter.index 

38 total = 0 

39 cursor = index + 1 

40 while cursor: 

41 total += self._tree[cursor] 

42 cursor -= cursor & -cursor 

43 return index - total 

44 

45 def collapse(self, closer: _Delimiter, removed: int) -> None: 

46 if not removed: 

47 return 

48 cursor = closer.index + 1 

49 while cursor < len(self._tree): 

50 self._tree[cursor] += removed 

51 cursor += cursor & -cursor 

52 

53 def deactivate(self, order: int) -> None: 

54 self._next[order] = self._find(order + 1) 

55 

56 def deactivate_range(self, delimiters: List[_Delimiter], start: int, end: int) -> None: 

57 order = self._find(start) 

58 while order < end: 

59 delimiters[order].length = 0 

60 self._next[order] = self._find(order + 1) 

61 order = self._find(order) 

62 

63 def _find(self, order: int) -> int: 

64 root = order 

65 while self._next[root] != root: 

66 root = self._next[root] 

67 while self._next[order] != order: 

68 parent = self._next[order] 

69 self._next[order] = root 

70 order = parent 

71 return root 

72 

73 

74def finalize_emphasis_tokens( 

75 tokens: List[Dict[str, Any]], 

76 enabled: bool, 

77 max_depth: int, 

78) -> List[Dict[str, Any]]: 

79 if len(tokens) > 1: 

80 tokens = _coalesce_emphasis_text(tokens) 

81 if not enabled or not _contains_emphasis_marker(tokens): 

82 return [_clean_emphasis_token(token) for token in tokens] 

83 

84 parts: List[Dict[str, Any]] = [] 

85 delimiters: List[_Delimiter] = [] 

86 source = _emphasis_source_text(tokens) 

87 source_pos = 0 

88 for token in tokens: 

89 if token["type"] == "text" and token.get("_emphasis", True): 

90 _split_text_token(token, source, source_pos, parts, delimiters) 

91 else: 

92 parts.append(_clean_emphasis_token(token)) 

93 source_pos += _emphasis_source_length(token) 

94 

95 if _process_dense_emphasis(parts, delimiters): 

96 return _merge_text_tokens(parts) 

97 

98 _process_emphasis_delimiters(parts, delimiters, max_depth) 

99 return _merge_text_tokens(parts) 

100 

101 

102def _coalesce_emphasis_text(tokens: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 

103 result: List[Dict[str, Any]] = [] 

104 chunks: List[str] = [] 

105 enabled = True 

106 

107 def flush() -> None: 

108 if chunks: 

109 token: Dict[str, Any] = {"type": "text", "raw": "".join(chunks)} 

110 if not enabled: 

111 token["_emphasis"] = False 

112 result.append(token) 

113 chunks.clear() 

114 

115 for token in tokens: 

116 if token["type"] != "text": 

117 flush() 

118 result.append(token) 

119 else: 

120 mode = token.get("_emphasis", True) 

121 if mode != enabled or (chunks and is_entity_boundary(chunks[-1], token["raw"])): 

122 flush() 

123 enabled = mode 

124 chunks.append(token["raw"]) 

125 flush() 

126 return result 

127 

128 

129def _process_dense_emphasis(parts: List[Dict[str, Any]], delimiters: List[_Delimiter]) -> bool: 

130 """Process a flat run such as ``*a*a*a`` without repeated list scans.""" 

131 if len(delimiters) < 4: 

132 return False 

133 

134 marker = delimiters[0].marker 

135 if marker not in ("*", "_") or len(parts) != len(delimiters) * 2: 

136 return False 

137 

138 for index, delimiter in enumerate(delimiters): 

139 if ( 

140 delimiter.marker != marker 

141 or delimiter.length != 1 

142 or delimiter.index != index * 2 

143 or parts[delimiter.index]["type"] != "text" 

144 or parts[delimiter.index + 1]["type"] != "text" 

145 ): 

146 return False 

147 

148 pair_count = len(delimiters) // 2 

149 processed: List[Dict[str, Any]] = [] 

150 for pair_index in range(pair_count): 

151 opener = delimiters[pair_index * 2] 

152 closer = delimiters[pair_index * 2 + 1] 

153 if ( 

154 not opener.can_open 

155 or not closer.can_close 

156 or not _can_match_emphasis_delimiters(opener, closer) 

157 or not _has_emphasis_content(parts, opener.index + 1, closer.index) 

158 ): 

159 return False 

160 

161 if pair_index: 

162 processed.append(parts[opener.index - 1]) 

163 processed.append( 

164 { 

165 "type": "emphasis", 

166 "children": [parts[opener.index + 1]], 

167 } 

168 ) 

169 

170 if pair_count: 

171 last_close = delimiters[pair_count * 2 - 1].index 

172 parts[:] = processed + parts[last_close + 1 :] 

173 return True 

174 

175 

176def _contains_emphasis_marker(tokens: List[Dict[str, Any]]) -> bool: 

177 for token in tokens: 

178 if token["type"] == "text" and token.get("_emphasis", True): 

179 raw = token["raw"] 

180 if "*" in raw or "_" in raw: 

181 return True 

182 return False 

183 

184 

185def _clean_emphasis_token(token: Dict[str, Any]) -> Dict[str, Any]: 

186 if "_emphasis" not in token and "_autolink" not in token: 

187 return token 

188 token = token.copy() 

189 token.pop("_emphasis", None) 

190 token.pop("_autolink", None) 

191 return token 

192 

193 

194def _emphasis_source_text(tokens: List[Dict[str, Any]]) -> str: 

195 values = [] 

196 for token in tokens: 

197 if token["type"] == "text": 

198 values.append(token["raw"]) 

199 elif token["type"] in ("softbreak", "linebreak"): 

200 values.append("\n") 

201 else: 

202 values.append("\ufffc") 

203 return "".join(values) 

204 

205 

206def _emphasis_source_length(token: Dict[str, Any]) -> int: 

207 if token["type"] == "text": 

208 return len(token["raw"]) 

209 return 1 

210 

211 

212def _split_text_token( 

213 token: Dict[str, Any], 

214 source: str, 

215 source_start: int, 

216 parts: List[Dict[str, Any]], 

217 delimiters: List[_Delimiter], 

218) -> None: 

219 text = token["raw"] 

220 pos = 0 

221 while pos < len(text): 

222 if text[pos] not in "*_": 

223 end = _next_delimiter_run(text, pos) 

224 parts.append({"type": "text", "raw": text[pos:end]}) 

225 pos = end 

226 continue 

227 

228 marker = text[pos] 

229 end = pos 

230 while end < len(text) and text[end] == marker: 

231 end += 1 

232 length = end - pos 

233 absolute = source_start + pos 

234 can_open = _can_open_emphasis(source, absolute, length, marker) 

235 can_close = _can_close_emphasis(source, absolute, length, marker) 

236 index = len(parts) 

237 parts.append({"type": "text", "raw": text[pos:end]}) 

238 if can_open or can_close: 

239 delimiters.append(_Delimiter(index, marker, length, can_open, can_close, length, len(delimiters))) 

240 pos = end 

241 

242 

243def _next_delimiter_run(text: str, pos: int) -> int: 

244 while pos < len(text) and text[pos] not in "*_": 

245 pos += 1 

246 return pos 

247 

248 

249def _process_emphasis_delimiters( 

250 parts: List[Dict[str, Any]], 

251 delimiters: List[_Delimiter], 

252 max_depth: int, 

253) -> None: 

254 index_map = _DelimiterIndex(delimiters, len(parts)) 

255 closer_pos = 0 

256 openers_bottom: Dict[Tuple[str, int, bool], int] = {} 

257 while closer_pos < len(delimiters): 

258 closer = delimiters[closer_pos] 

259 if not closer.can_close or closer.length == 0: 

260 closer_pos += 1 

261 continue 

262 

263 opener_key = (closer.marker, closer.length % 3, closer.can_open) 

264 opener_bottom = openers_bottom.get(opener_key, 0) 

265 match = _find_emphasis_opener(delimiters, closer_pos, closer, opener_bottom) 

266 if match is None: 

267 openers_bottom[opener_key] = closer_pos 

268 closer_pos += 1 

269 continue 

270 opener_pos, opener = match 

271 

272 opener_index = index_map.current(opener) 

273 closer_index = index_map.current(closer) 

274 use_length = _get_emphasis_use_length(parts, opener, closer, opener_index, closer_index) 

275 children = _get_emphasis_children(parts, opener_index, closer_index, max_depth) 

276 if use_length is None or children is None: 

277 closer_pos += 1 

278 continue 

279 

280 _apply_emphasis_match( 

281 parts, 

282 delimiters, 

283 index_map, 

284 opener, 

285 closer, 

286 opener_pos, 

287 closer_pos, 

288 opener_index, 

289 closer_index, 

290 use_length, 

291 children, 

292 ) 

293 

294 if opener.can_open or closer.can_close: 

295 closer_pos = max(opener_pos, openers_bottom.get(opener_key, 0)) 

296 else: 

297 closer_pos += 1 

298 

299 

300def _find_emphasis_opener( 

301 delimiters: List[_Delimiter], 

302 closer_pos: int, 

303 closer: _Delimiter, 

304 opener_bottom: int, 

305) -> Optional[Tuple[int, _Delimiter]]: 

306 opener_pos = closer_pos - 1 

307 while opener_pos >= opener_bottom: 

308 opener = delimiters[opener_pos] 

309 if ( 

310 opener.marker == closer.marker 

311 and opener.can_open 

312 and opener.length > 0 

313 and _can_match_emphasis_delimiters(opener, closer) 

314 ): 

315 return opener_pos, opener 

316 opener_pos -= 1 

317 return None 

318 

319 

320def _get_emphasis_use_length( 

321 parts: List[Dict[str, Any]], 

322 opener: _Delimiter, 

323 closer: _Delimiter, 

324 opener_index: int, 

325 closer_index: int, 

326) -> Optional[int]: 

327 use_length = 2 if opener.length >= 2 and closer.length >= 2 else 1 

328 if use_length == 2 and not _has_strong_enabled(parts, opener_index, closer_index): 

329 use_length = 1 

330 if use_length == 1 and not _has_emphasis_enabled(parts, opener_index, closer_index): 

331 return None 

332 return use_length 

333 

334 

335def _get_emphasis_children( 

336 parts: List[Dict[str, Any]], 

337 opener_index: int, 

338 closer_index: int, 

339 max_depth: int, 

340) -> Optional[List[Dict[str, Any]]]: 

341 if not _has_emphasis_content(parts, opener_index + 1, closer_index): 

342 return None 

343 

344 opener_text = parts[opener_index] 

345 closer_text = parts[closer_index] 

346 if opener_text["type"] != "text" or closer_text["type"] != "text": 

347 return None 

348 

349 children = parts[opener_index + 1 : closer_index] 

350 if max_depth > 0 and token_depth(children) >= max_depth: 

351 return None 

352 return children 

353 

354 

355def _apply_emphasis_match( 

356 parts: List[Dict[str, Any]], 

357 delimiters: List[_Delimiter], 

358 index_map: _DelimiterIndex, 

359 opener: _Delimiter, 

360 closer: _Delimiter, 

361 opener_pos: int, 

362 closer_pos: int, 

363 opener_index: int, 

364 closer_index: int, 

365 use_length: int, 

366 children: List[Dict[str, Any]], 

367) -> None: 

368 opener_text = parts[opener_index] 

369 closer_text = parts[closer_index] 

370 opener_text["raw"] = opener_text["raw"][:-use_length] 

371 closer_text["raw"] = closer_text["raw"][use_length:] 

372 node_type = "strong" if use_length == 2 else "emphasis" 

373 parts[opener_index + 1 : closer_index] = [{"type": node_type, "children": children}] 

374 

375 removed = closer_index - opener_index - 2 

376 if removed: 

377 index_map.deactivate_range(delimiters, opener_pos + 1, closer_pos) 

378 index_map.collapse(closer, removed) 

379 

380 opener.length -= use_length 

381 closer.length -= use_length 

382 if opener.length == 0: 

383 opener.can_open = False 

384 index_map.deactivate(opener.order) 

385 if closer.length == 0: 

386 closer.can_close = False 

387 index_map.deactivate(closer.order) 

388 

389 

390def _has_strong_enabled(parts: List[Dict[str, Any]], opener_index: int, closer_index: int) -> bool: 

391 return len(_text_raw(parts[opener_index])) >= 2 and len(_text_raw(parts[closer_index])) >= 2 

392 

393 

394def _has_emphasis_enabled(parts: List[Dict[str, Any]], opener_index: int, closer_index: int) -> bool: 

395 return bool(_text_raw(parts[opener_index]) and _text_raw(parts[closer_index])) 

396 

397 

398def _text_raw(token: Dict[str, Any]) -> str: 

399 if token["type"] == "text": 

400 return cast(str, token["raw"]) 

401 return "" 

402 

403 

404def _has_emphasis_content(parts: List[Dict[str, Any]], start: int, end: int) -> bool: 

405 for part in parts[start:end]: 

406 if part["type"] != "text" or part["raw"] != "": 

407 return True 

408 return False 

409 

410 

411def _can_match_emphasis_delimiters(opener: _Delimiter, closer: _Delimiter) -> bool: 

412 if opener.can_close or closer.can_open: 

413 open_len = opener.orig_length 

414 close_len = closer.orig_length 

415 return (open_len + close_len) % 3 != 0 or open_len % 3 == 0 and close_len % 3 == 0 

416 return True 

417 

418 

419def _can_open_emphasis(text: str, start: int, size: int, marker: str) -> bool: 

420 previous = text[start - 1] if start > 0 else "\n" 

421 next_char = text[start + size] if start + size < len(text) else "\n" 

422 if marker == "_" and previous.isalnum() and next_char.isalnum(): 

423 return False 

424 if next_char.isspace(): 

425 return False 

426 if _is_punctuation(next_char) and not previous.isspace() and not _is_punctuation(previous): 

427 return False 

428 return True 

429 

430 

431def _can_close_emphasis(text: str, start: int, size: int, marker: str) -> bool: 

432 previous = text[start - 1] if start > 0 else "\n" 

433 next_char = text[start + size] if start + size < len(text) else "\n" 

434 if marker == "_" and previous.isalnum() and next_char.isalnum(): 

435 return False 

436 if previous.isspace(): 

437 return False 

438 if _is_punctuation(previous) and not next_char.isspace() and not _is_punctuation(next_char): 

439 return False 

440 return True 

441 

442 

443def _is_punctuation(char: str) -> bool: 

444 return not char.isspace() and not char.isalnum() 

445 

446 

447def _merge_text_tokens(tokens: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 

448 result: List[Dict[str, Any]] = [] 

449 chunks: List[str] = [] 

450 for token in tokens: 

451 if token["type"] == "text": 

452 raw = token["raw"] 

453 if not raw: 

454 continue 

455 if chunks and is_entity_boundary(chunks[-1], raw): 

456 result.append({"type": "text", "raw": "".join(chunks)}) 

457 chunks = [] 

458 chunks.append(raw) 

459 else: 

460 if chunks: 

461 result.append({"type": "text", "raw": "".join(chunks)}) 

462 chunks = [] 

463 result.append(_clean_emphasis_token(token)) 

464 if chunks: 

465 result.append({"type": "text", "raw": "".join(chunks)}) 

466 return result