1import logging
2
3from ..common.utils import charCodeAt, isSpace, normalizeReference
4from .state_block import StateBlock
5
6LOGGER = logging.getLogger(__name__)
7
8
9def reference(state: StateBlock, startLine: int, _endLine: int, silent: bool) -> bool:
10 LOGGER.debug(
11 "entering reference: %s, %s, %s, %s", state, startLine, _endLine, silent
12 )
13
14 lines = 0
15 pos = state.bMarks[startLine] + state.tShift[startLine]
16 maximum = state.eMarks[startLine]
17 nextLine = startLine + 1
18
19 if state.is_code_block(startLine):
20 return False
21
22 if state.src[pos] != "[":
23 return False
24
25 # Simple check to quickly interrupt scan on [link](url) at the start of line.
26 # Can be useful on practice: https:#github.com/markdown-it/markdown-it/issues/54
27 while pos < maximum:
28 # /* ] */ /* \ */ /* : */
29 if state.src[pos] == "]" and state.src[pos - 1] != "\\":
30 if pos + 1 == maximum:
31 return False
32 if state.src[pos + 1] != ":":
33 return False
34 break
35 pos += 1
36
37 endLine = state.lineMax
38
39 # jump line-by-line until empty one or EOF
40 terminatorRules = state.md.block.ruler.getRules("reference")
41
42 oldParentType = state.parentType
43 state.parentType = "reference"
44
45 while nextLine < endLine and not state.isEmpty(nextLine):
46 # this would be a code block normally, but after paragraph
47 # it's considered a lazy continuation regardless of what's there
48 if state.sCount[nextLine] - state.blkIndent > 3:
49 nextLine += 1
50 continue
51
52 # quirk for blockquotes, this line should already be checked by that rule
53 if state.sCount[nextLine] < 0:
54 nextLine += 1
55 continue
56
57 # Some tags can terminate paragraph without empty line.
58 terminate = False
59 for terminatorRule in terminatorRules:
60 if terminatorRule(state, nextLine, endLine, True):
61 terminate = True
62 break
63
64 if terminate:
65 break
66
67 nextLine += 1
68
69 string = state.getLines(startLine, nextLine, state.blkIndent, False).strip()
70 maximum = len(string)
71
72 labelEnd = None
73 pos = 1
74 while pos < maximum:
75 ch = charCodeAt(string, pos)
76 if ch == 0x5B: # /* [ */
77 return False
78 elif ch == 0x5D: # /* ] */
79 labelEnd = pos
80 break
81 elif ch == 0x0A: # /* \n */
82 lines += 1
83 elif ch == 0x5C: # /* \ */
84 pos += 1
85 if pos < maximum and charCodeAt(string, pos) == 0x0A:
86 lines += 1
87 pos += 1
88
89 if (
90 labelEnd is None or labelEnd < 0 or charCodeAt(string, labelEnd + 1) != 0x3A
91 ): # /* : */
92 return False
93
94 # [label]: destination 'title'
95 # ^^^ skip optional whitespace here
96 pos = labelEnd + 2
97 while pos < maximum:
98 ch = charCodeAt(string, pos)
99 if ch == 0x0A:
100 lines += 1
101 elif isSpace(ch):
102 pass
103 else:
104 break
105 pos += 1
106
107 # [label]: destination 'title'
108 # ^^^^^^^^^^^ parse this
109 res = state.md.helpers.parseLinkDestination(string, pos, maximum)
110 if not res.ok:
111 return False
112
113 href = state.md.normalizeLink(res.str)
114 if not state.md.validateLink(href):
115 return False
116
117 pos = res.pos
118 lines += res.lines
119
120 # save cursor state, we could require to rollback later
121 destEndPos = pos
122 destEndLineNo = lines
123
124 # [label]: destination 'title'
125 # ^^^ skipping those spaces
126 start = pos
127 while pos < maximum:
128 ch = charCodeAt(string, pos)
129 if ch == 0x0A:
130 lines += 1
131 elif isSpace(ch):
132 pass
133 else:
134 break
135 pos += 1
136
137 # [label]: destination 'title'
138 # ^^^^^^^ parse this
139 res = state.md.helpers.parseLinkTitle(string, pos, maximum)
140 if pos < maximum and start != pos and res.ok:
141 title = res.str
142 pos = res.pos
143 lines += res.lines
144 else:
145 title = ""
146 pos = destEndPos
147 lines = destEndLineNo
148
149 # skip trailing spaces until the rest of the line
150 while pos < maximum:
151 ch = charCodeAt(string, pos)
152 if not isSpace(ch):
153 break
154 pos += 1
155
156 if pos < maximum and charCodeAt(string, pos) != 0x0A and title:
157 # garbage at the end of the line after title,
158 # but it could still be a valid reference if we roll back
159 title = ""
160 pos = destEndPos
161 lines = destEndLineNo
162 while pos < maximum:
163 ch = charCodeAt(string, pos)
164 if not isSpace(ch):
165 break
166 pos += 1
167
168 if pos < maximum and charCodeAt(string, pos) != 0x0A:
169 # garbage at the end of the line
170 return False
171
172 label = normalizeReference(string[1:labelEnd])
173 if not label:
174 # CommonMark 0.20 disallows empty labels
175 return False
176
177 # Reference can not terminate anything. This check is for safety only.
178 if silent:
179 return True
180
181 if "references" not in state.env:
182 state.env["references"] = {}
183
184 state.line = startLine + lines + 1
185
186 # note, this is not part of markdown-it JS, but is useful for renderers
187 if state.md.options.get("inline_definitions", False):
188 token = state.push("definition", "", 0)
189 token.meta = {
190 "id": label,
191 "title": title,
192 "url": href,
193 "label": string[1:labelEnd],
194 }
195 token.map = [startLine, state.line]
196
197 if label not in state.env["references"]:
198 state.env["references"][label] = {
199 "title": title,
200 "href": href,
201 "map": [startLine, state.line],
202 }
203 else:
204 state.env.setdefault("duplicate_refs", []).append(
205 {
206 "title": title,
207 "href": href,
208 "label": label,
209 "map": [startLine, state.line],
210 }
211 )
212
213 state.parentType = oldParentType
214
215 return True