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