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

168 statements  

1import re 

2import string 

3from typing import Any, Dict, Tuple, Union 

4 

5from .core import DEFAULT_MAX_NESTED_LEVEL 

6from .util import escape_url 

7 

8PREVENT_BACKSLASH = r"(?<!\\)(?:\\\\)*" 

9PUNCTUATION = r"[" + re.escape(string.punctuation) + r"]" 

10 

11MAX_LINK_LABEL_LENGTH = 999 

12LINK_LABEL = r"(?=[\s\S]{0,999}\])(?:[^\\\[\]]|\\.){0,999}" 

13 

14ASCII_WHITESPACE = " \t\n\r\f" 

15 

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) 

21 

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") 

88 

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") 

93 

94 

95def unescape_char(text: str) -> str: 

96 return _ESCAPE_CHAR_RE.sub(r"\1", text) 

97 

98 

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 

103 

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 

109 

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 

119 

120 if found: 

121 text = src[start_pos : pos - 1] 

122 return text, pos 

123 return None, pos # FIX: return pos instead of None 

124 

125 

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 

132 

133 

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 

142 

143 

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 

150 

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 

159 

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 

180 

181 if level != 0: 

182 return None, None, pos 

183 return src[start:pos], pos, pos 

184 

185 

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 

190 

191 pos = _skip_ascii_whitespace(src, pos, max_pos) 

192 if pos >= max_pos: 

193 return None, None 

194 

195 opener = src[pos] 

196 closer = {"'": "'", '"': '"', "(": ")"}.get(opener) 

197 if closer is None: 

198 return None, None 

199 

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 

218 

219 

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 

228 

229 

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 

242 

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 

248 

249 

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 

256 

257 

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 

269 

270 

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