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

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

191 statements  

1from __future__ import annotations 

2 

3from bisect import bisect_left 

4from typing import TYPE_CHECKING, Dict, List, Match, Optional, Tuple 

5 

6from ..core import InlineState 

7from ..helpers import ( 

8 parse_link as parse_link_destination, 

9 parse_link_label, 

10 parse_link_with_end, 

11) 

12from ..util import unikey 

13 

14if TYPE_CHECKING: 

15 from ..inline_parser import InlineParser 

16 

17 

18def parse_link(inline: "InlineParser", m: Match[str], state: InlineState) -> Optional[int]: 

19 pos = m.end() 

20 

21 marker = m.group(0) 

22 is_image = marker[0] == "!" 

23 if is_image and inline.max_image_depth > 0 and state.image_depth >= inline.max_image_depth: 

24 state.append_token({"type": "text", "raw": marker + state.src[pos:]}) 

25 return len(state.src) 

26 if not is_image and state.in_link: 

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

28 return pos 

29 if not is_image and pos <= state.no_link_before: 

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

31 return pos 

32 if is_image and pos <= state.no_image_before: 

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

34 return pos 

35 

36 text = None 

37 text_start = pos 

38 text_end = pos 

39 label, end_pos = parse_link_label(state.src, pos) 

40 if label is None: 

41 if pos <= state.no_close_bracket_before: 

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

43 return pos 

44 close_pos = find_closing_bracket(state, pos) 

45 if close_pos is None: 

46 if len(state.src) > state.no_close_bracket_before: 

47 state.no_close_bracket_before = len(state.src) 

48 return None 

49 text_start = pos 

50 text_end = close_pos 

51 end_pos = close_pos + 1 

52 

53 assert end_pos is not None 

54 

55 if label is not None: 

56 text = label 

57 text_start = pos 

58 text_end = end_pos - 1 

59 

60 body_end_pos = end_pos 

61 

62 has_nested_link = not is_image and label_contains_link(state, text_start, text_end) 

63 if has_nested_link: 

64 return None 

65 if end_pos >= len(state.src) and label is None: 

66 mark_no_link_before(state, body_end_pos) 

67 return None 

68 

69 if not is_image: 

70 rules = ["codespan", "prec_auto_link", "prec_inline_html"] 

71 prec_pos = inline.precedence_scan(m, state, end_pos, rules) 

72 if prec_pos: 

73 return prec_pos 

74 

75 if end_pos < len(state.src): 

76 char = state.src[end_pos] 

77 if char == "(": 

78 attrs, pos2, scan_end = parse_link_with_end(state.src, end_pos + 1) 

79 if pos2: 

80 if text is None: 

81 text = state.src[text_start:text_end] 

82 token = build_link_token(inline, is_image, text, attrs, state) 

83 state.append_token(token) 

84 return pos2 

85 if scan_end > body_end_pos: 

86 if is_image: 

87 mark_no_image_before(state, scan_end) 

88 else: 

89 mark_no_link_before(state, scan_end) 

90 elif char == "[": 

91 label2, pos2 = parse_link_label(state.src, end_pos + 1) 

92 if pos2: 

93 end_pos = pos2 

94 if label2: 

95 label = label2 

96 

97 if label is None: 

98 ref_links = state.env.get("ref_links") 

99 if not ref_links: 

100 mark_no_link_before(state, body_end_pos) 

101 return None 

102 if text is None: 

103 text = state.src[text_start:text_end] 

104 label = text 

105 ref_links = state.env.get("ref_links") 

106 if not ref_links: 

107 mark_no_link_before(state, body_end_pos) 

108 return None 

109 

110 key = unikey(label) 

111 env = ref_links.get(key) 

112 if env: 

113 if text is None: 

114 text = state.src[text_start:text_end] 

115 attrs = {"url": env["url"], "title": env.get("title")} 

116 token = build_link_token(inline, is_image, text, attrs, state) 

117 token["ref"] = key 

118 token["label"] = label 

119 state.append_token(token) 

120 return end_pos 

121 mark_no_link_before(state, body_end_pos) 

122 return None 

123 

124 

125def build_link_token( 

126 inline: "InlineParser", 

127 is_image: bool, 

128 text: str, 

129 attrs: Optional[Dict[str, object]], 

130 state: InlineState, 

131) -> Dict[str, object]: 

132 new_state = state.copy() 

133 new_state.src = text 

134 if is_image: 

135 new_state.in_image = True 

136 new_state.image_depth += 1 

137 return { 

138 "type": "image", 

139 "children": inline.render(new_state), 

140 "attrs": attrs, 

141 } 

142 new_state.in_link = True 

143 return { 

144 "type": "link", 

145 "children": inline.render(new_state), 

146 "attrs": attrs, 

147 } 

148 

149 

150def mark_no_link_before(state: InlineState, end_pos: int) -> None: 

151 if end_pos > state.no_link_before: 

152 state.no_link_before = end_pos 

153 

154 

155def mark_no_image_before(state: InlineState, end_pos: int) -> None: 

156 if end_pos > state.no_image_before: 

157 state.no_image_before = end_pos 

158 

159 

160def find_closing_bracket(state: InlineState, pos: int) -> Optional[int]: 

161 return get_closing_bracket_map(state).get(pos) 

162 

163 

164def label_contains_link(state: InlineState, start: int, end: int) -> bool: 

165 if start >= end: 

166 return False 

167 

168 starts, suffix_min_ends = get_link_range_index(state) 

169 index = bisect_left(starts, start) 

170 return index < len(starts) and starts[index] < end and suffix_min_ends[index] <= end 

171 

172 

173def get_link_range_index(state: InlineState) -> Tuple[List[int], List[int]]: 

174 cache = state.link_ranges.get(id(state.src)) 

175 if cache is not None and cache[0] is state.src: 

176 return cache[1], cache[2] 

177 

178 pairs = get_closing_bracket_map(state) 

179 ranges: List[Tuple[int, int]] = [] 

180 for label_start, close_pos in pairs.items(): 

181 opener = label_start - 1 

182 if opener > 0 and state.src[opener - 1] == "!": 

183 continue 

184 link_end = find_link_range_end(state.src, label_start, close_pos, state) 

185 if link_end is not None: 

186 ranges.append((opener, link_end)) 

187 

188 ranges.sort() 

189 starts = [start for start, _end in ranges] 

190 suffix_min_ends = [0] * len(ranges) 

191 min_end = len(state.src) + 1 

192 for index in range(len(ranges) - 1, -1, -1): 

193 end = ranges[index][1] 

194 if end < min_end: 

195 min_end = end 

196 suffix_min_ends[index] = min_end 

197 

198 state.link_ranges[id(state.src)] = (state.src, starts, suffix_min_ends) 

199 return starts, suffix_min_ends 

200 

201 

202def get_closing_bracket_map(state: InlineState) -> Dict[int, int]: 

203 cache = state.link_brackets.get(id(state.src)) 

204 if cache is not None and cache[0] is state.src: 

205 return cache[1] 

206 

207 pairs = build_closing_bracket_map(state.src) 

208 state.link_brackets[id(state.src)] = (state.src, pairs) 

209 return pairs 

210 

211 

212def find_link_range_end(src: str, label_start: int, close_pos: int, state: InlineState) -> Optional[int]: 

213 end_pos = close_pos + 1 

214 if end_pos < len(src): 

215 marker = src[end_pos] 

216 if marker == "(": 

217 _attrs, new_pos = parse_link_destination(src, end_pos + 1) 

218 return new_pos 

219 

220 if marker == "[": 

221 label, new_pos = parse_link_label(src, end_pos + 1) 

222 if not new_pos: 

223 return None 

224 ref_label = label or src[label_start:close_pos] 

225 ref_links = state.env.get("ref_links") 

226 if ref_links and unikey(ref_label) in ref_links: 

227 return new_pos 

228 return None 

229 

230 ref_links = state.env.get("ref_links") 

231 if ref_links and unikey(src[label_start:close_pos]) in ref_links: 

232 return end_pos 

233 return None 

234 

235 

236def build_closing_bracket_map(src: str) -> Dict[int, int]: 

237 pairs: Dict[int, int] = {} 

238 stack: List[int] = [] 

239 pos = 0 

240 while pos < len(src): 

241 char = src[pos] 

242 if char == "\\": 

243 pos += 2 

244 continue 

245 if char == "[": 

246 stack.append(pos + 1) 

247 elif char == "]" and stack: 

248 pairs[stack.pop()] = pos 

249 pos += 1 

250 return pairs