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