1from __future__ import annotations
2
3from dataclasses import dataclass
4from typing import TYPE_CHECKING, Any, Dict, Match, Optional, Tuple
5
6from ..core import InlineState
7from ..helpers import MAX_LINK_LABEL_LENGTH, parse_link, parse_link_label
8from ..util import unikey
9
10if TYPE_CHECKING:
11 from ..inline_parser import InlineParser
12
13
14@dataclass(slots=True)
15class Bracket:
16 start: int
17 token_index: int
18 image: bool
19 previous_link: Optional[Bracket] = None
20 active: bool = True
21 nested: bool = False
22 child_depth: int = 0
23
24
25def parse_link_open(inline: InlineParser, m: Match[str], state: InlineState) -> int:
26 if state.brackets:
27 state.brackets[-1].nested = True
28 bracket = Bracket(m.end(), len(state.tokens), m.group(0).startswith("!"), state.last_link)
29 if not bracket.image:
30 state.last_link = bracket
31 state.append_token({"type": "text", "raw": m.group(0)})
32 state.brackets.append(bracket)
33 return m.end()
34
35
36def parse_link_close(inline: InlineParser, m: Match[str], state: InlineState) -> int:
37 pos = m.start()
38 if not state.env.get("ref_links"):
39 # Without references, only a ']' immediately followed by '(' can
40 # produce a link. Retire the other openers without parsing candidates.
41 literal_end = m.end() - int(state.src.startswith("(", m.end()))
42 if literal_end > pos:
43 _discard_brackets(state, literal_end - pos)
44 inline.process_text(state.src[pos:literal_end], state)
45 pos = literal_end
46 while pos < m.end():
47 if not state.brackets:
48 inline.process_text(state.src[pos : m.end()], state)
49 return m.end()
50 pos = _close_bracket(inline, state, pos)
51 return pos
52
53
54def _discard_brackets(state: InlineState, count: int) -> None:
55 if count >= len(state.brackets):
56 state.brackets.clear()
57 state.last_link = None
58 return
59 discarded = state.brackets[-count:]
60 del state.brackets[-count:]
61 parent = state.brackets[-1]
62 found_link = False
63 for bracket in discarded:
64 parent.child_depth = max(parent.child_depth, bracket.child_depth)
65 if not bracket.image and not found_link:
66 state.last_link = bracket.previous_link
67 found_link = True
68
69
70def _close_bracket(inline: InlineParser, state: InlineState, close: int) -> int:
71 opener = state.brackets.pop()
72 if not opener.image:
73 state.last_link = opener.previous_link
74 if state.brackets:
75 state.brackets[-1].child_depth = max(state.brackets[-1].child_depth, opener.child_depth)
76 if not opener.active or (state.in_link and not opener.image):
77 inline.process_text("]", state)
78 return close + 1
79
80 # A rejected outer wrapper must not repeatedly copy an already deep subtree.
81 if state.depth + opener.child_depth >= inline.max_nested_level:
82 inline.process_text("]", state)
83 return close + 1
84
85 attrs: Optional[Dict[str, Any]] = None
86 end = close + 1
87 label: Optional[str] = None
88 if state.src.startswith("(", end):
89 attrs, next_pos = parse_link(state.src, end + 1, max_nested_level=inline.max_nested_level)
90 if next_pos is not None:
91 end = next_pos
92 if attrs is None:
93 reference = _parse_reference(state, opener, close, close + 1)
94 if reference is None:
95 inline.process_text("]", state)
96 return close + 1
97 attrs, label, end = reference
98
99 children = state.tokens[opener.token_index + 1 :]
100 if not opener.image:
101 # Automatic links inside explicit link text retain their literal text.
102 children = [
103 {"type": "text", "raw": child["_autolink"], "_emphasis": False} if "_autolink" in child else child
104 for child in children
105 ]
106 children = inline.finalize_tokens(children, state.depth + 1)
107 token: Dict[str, Any] = {"type": "image" if opener.image else "link", "children": children, "attrs": attrs}
108 if label is not None:
109 token.update(ref=unikey(label), label=label)
110 del state.tokens[opener.token_index :]
111 state.append_token(token)
112
113 if not opener.image:
114 # CommonMark: a successful link deactivates preceding link openers.
115 # Each opener is deactivated at most once; images remain eligible.
116 bracket = state.last_link
117 while bracket is not None and bracket.active:
118 bracket.active = False
119 bracket = bracket.previous_link
120 return end
121
122
123def _parse_reference(
124 state: InlineState, opener: Bracket, close: int, end: int
125) -> Optional[Tuple[Dict[str, Any], str, int]]:
126 refs = state.env.get("ref_links")
127 if not refs:
128 return None
129
130 label = None
131 next_pos = None
132 if state.src.startswith("[", end):
133 label, next_pos = parse_link_label(state.src, end + 1)
134 if label is None or label == "":
135 if opener.nested or close - opener.start > MAX_LINK_LABEL_LENGTH:
136 return None
137 label = state.src[opener.start : close]
138 ref = refs.get(unikey(label))
139 if ref is None:
140 return None
141 return {"url": ref["url"], "title": ref.get("title")}, label, next_pos or end