Sources/OrgCore/Parser/Inline.swift
433 lines · 16679 bytes
1/// Characters allowed before an emphasis opener, besides whitespace and the start of the run.
2private let emphasisPre: Set<Unicode.Scalar> = ["-", "(", "{", "'", "\""]
3
4/// Characters allowed after an emphasis closer, besides whitespace and the end of the run.
5private let emphasisPost: Set<Unicode.Scalar> = ["-", ".", ",", ":", "!", "?", ";", "'", "\"", ")", "}", "\\", "["]
6
7private let angleLinkSchemes: Set<String> = [
8 "http", "https", "mailto", "file", "id", "doi", "ftp", "news", "shell", "elisp", "info", "help", "attachment",
9]
10
11private let plainLinkPrefixes: [[Unicode.Scalar]] = ["https://", "http://", "mailto:", "file:"].map { Array($0.unicodeScalars) }
12
13private func emphasisKind(_ c: Unicode.Scalar) -> SyntaxKind? {
14 switch c {
15 case "*": .bold
16 case "/": .italic
17 case "_": .underline
18 case "+": .strikeThrough
19 case "=": .verbatim
20 case "~": .code
21 default: nil
22 }
23}
24
25func isNewline(_ c: Unicode.Scalar) -> Bool {
26 c == "\n"
27}
28
29func isSpace(_ c: Unicode.Scalar) -> Bool {
30 c.isASCII ? (c == " " || c == "\t" || c == "\n" || c == "\r" || c == "\u{0B}" || c == "\u{0C}") : c.properties.isWhitespace
31}
32
33func isWordScalar(_ c: Unicode.Scalar) -> Bool {
34 if c.isASCII {
35 let v = c.value
36 return (v >= 48 && v <= 57) || (v >= 65 && v <= 90) || (v >= 97 && v <= 122)
37 }
38 return c.properties.isAlphabetic || c.properties.numericType != nil
39}
40
41enum InlineMatch {
42 case emphasis(SyntaxKind, open: Int, close: Int)
43 case link(path: Range<Int>, description: Range<Int>?, whole: Range<Int>)
44 case object(SyntaxKind, Range<Int>)
45
46 var end: Int {
47 switch self {
48 case .emphasis(_, _, let close): close + 1
49 case .link(_, _, let whole): whole.upperBound
50 case .object(_, let range): range.upperBound
51 }
52 }
53}
54
55/// Turns a run of text into text, newline and object tokens. Every scalar ends up in exactly
56/// one token. Works on unicode scalars: every recognizer starts on an ASCII character, so
57/// grapheme clusters never need to be formed.
58struct InlineScanner {
59 let chars: [Unicode.Scalar]
60 /// Where each scalar starts in `source`, plus its end, so token text is sliced from the
61 /// source instead of rebuilt scalar by scalar.
62 let source: Substring.UnicodeScalarView
63 let starts: [String.Index]
64
65 init(_ text: Substring) {
66 source = text.unicodeScalars
67 var chars: [Unicode.Scalar] = []
68 var starts: [String.Index] = []
69 chars.reserveCapacity(text.utf8.count)
70 starts.reserveCapacity(text.utf8.count + 1)
71 var index = source.startIndex
72 while index < source.endIndex {
73 chars.append(source[index])
74 starts.append(index)
75 index = source.index(after: index)
76 }
77 starts.append(source.endIndex)
78 self.chars = chars
79 self.starts = starts
80 }
81
82 func scan(_ range: Range<Int>, into b: inout GreenBuilder, inLink: Bool = false) {
83 var textStart = range.lowerBound
84 var i = range.lowerBound
85 while i < range.upperBound {
86 if Self.mayStartObject(chars[i]), let match = match(at: i, in: range, inLink: inLink) {
87 emitText(textStart..<i, into: &b)
88 emit(match, into: &b, inLink: inLink)
89 i = match.end
90 textStart = i
91 } else {
92 i += 1
93 }
94 }
95 emitText(textStart..<range.upperBound, into: &b)
96 }
97
98 /// The first characters any recognizer accepts.
99 static func mayStartObject(_ c: Unicode.Scalar) -> Bool {
100 switch c {
101 case "[", "<", "{", "\\", "^", "s", "h", "m", "f", "*", "/", "_", "+", "=", "~": true
102 default: false
103 }
104 }
105
106 /// Text with each line break as its own newline token; "\r\n" stays one token.
107 func emitText(_ range: Range<Int>, into b: inout GreenBuilder) {
108 var start = range.lowerBound
109 var k = range.lowerBound
110 while k < range.upperBound {
111 if chars[k] == "\n" {
112 let breakStart = k > start && chars[k - 1] == "\r" ? k - 1 : k
113 if start < breakStart { b.token(.text, string(start..<breakStart)) }
114 b.token(.newline, string(breakStart..<(k + 1)))
115 start = k + 1
116 }
117 k += 1
118 }
119 if start < range.upperBound { b.token(.text, string(start..<range.upperBound)) }
120 }
121
122 func emit(_ match: InlineMatch, into b: inout GreenBuilder, inLink: Bool) {
123 switch match {
124 case .emphasis(let kind, let open, let close):
125 b.start(kind)
126 b.token(.marker, string(open..<(open + 1)))
127 if kind == .verbatim || kind == .code {
128 emitText((open + 1)..<close, into: &b)
129 } else {
130 scan((open + 1)..<close, into: &b, inLink: inLink)
131 }
132 b.token(.marker, string(close..<(close + 1)))
133 b.finish()
134 case .link(let path, let description, let whole):
135 b.start(.link)
136 b.token(.marker, string(whole.lowerBound..<path.lowerBound))
137 b.token(.linkPath, string(path))
138 if let description {
139 b.token(.marker, string(path.upperBound..<description.lowerBound))
140 b.start(.linkDescription)
141 scan(description, into: &b, inLink: true)
142 b.finish()
143 b.token(.marker, string(description.upperBound..<whole.upperBound))
144 } else {
145 b.token(.marker, string(path.upperBound..<whole.upperBound))
146 }
147 b.finish()
148 case .object(let kind, let range):
149 b.start(kind)
150 emitText(range, into: &b)
151 b.finish()
152 }
153 }
154
155 func string(_ range: Range<Int>) -> String {
156 String(source[starts[range.lowerBound]..<starts[range.upperBound]])
157 }
158
159 func hasPrefix(_ s: [Unicode.Scalar], at i: Int, _ limit: Int) -> Bool {
160 guard i + s.count <= limit else { return false }
161 for (offset, c) in s.enumerated() where chars[i + offset] != c { return false }
162 return true
163 }
164
165 func hasPrefix(_ s: String, at i: Int, _ limit: Int) -> Bool {
166 hasPrefix(Array(s.unicodeScalars), at: i, limit)
167 }
168
169 // MARK: - Recognizers
170
171 func match(at i: Int, in range: Range<Int>, inLink: Bool) -> InlineMatch? {
172 let limit = range.upperBound
173 let previous: Unicode.Scalar? = i > range.lowerBound ? chars[i - 1] : nil
174 let afterWord = previous.map(isWordScalar) ?? false
175
176 switch chars[i] {
177 case "[":
178 if !inLink, let link = bracketLink(i, limit) { return link }
179 if let end = footnoteReference(i, limit) { return .object(.footnoteReference, i..<end) }
180 if let end = scanTimestamp(chars, at: i, limit: limit)?.end { return .object(.timestamp, i..<end) }
181 if let end = statisticsCookie(i, limit) { return .object(.statisticsCookie, i..<end) }
182 case "<":
183 if let end = scanTimestamp(chars, at: i, limit: limit)?.end { return .object(.timestamp, i..<end) }
184 if let end = target(i, limit) { return .object(.target, i..<end) }
185 if !inLink, let end = angleLink(i, limit) { return .object(.link, i..<end) }
186 case "{":
187 if let end = macro(i, limit) { return .object(.macro, i..<end) }
188 case "\\":
189 if let end = lineBreak(i, limit) { return .object(.lineBreak, i..<end) }
190 if let end = latexFragment(i, limit) { return .object(.latexFragment, i..<end) }
191 case "^":
192 if afterWord, let end = superscript(i, limit) { return .object(.superscript, i..<end) }
193 case "s":
194 if !afterWord, let end = inlineSourceBlock(i, limit) { return .object(.inlineSourceBlock, i..<end) }
195 case "h", "m", "f":
196 if !inLink, !afterWord, let end = plainLink(i, limit) { return .object(.link, i..<end) }
197 default:
198 break
199 }
200
201 if let kind = emphasisKind(chars[i]),
202 previous.map({ isSpace($0) || emphasisPre.contains($0) }) ?? true,
203 let close = emphasisClose(i, limit) {
204 return .emphasis(kind, open: i, close: close)
205 }
206 return nil
207 }
208
209 /// org's emphasis rules: the body neither starts nor ends with whitespace, spans at most
210 /// one line break, and the closer is followed by whitespace, punctuation or the end.
211 func emphasisClose(_ i: Int, _ limit: Int) -> Int? {
212 let marker = chars[i]
213 guard i + 1 < limit, !isSpace(chars[i + 1]) else { return nil }
214 var newlines = 0
215 var j = i + 1
216 while j < limit {
217 if isNewline(chars[j]) {
218 newlines += 1
219 if newlines > 1 { return nil }
220 } else if chars[j] == marker, j > i + 1, !isSpace(chars[j - 1]) {
221 if j + 1 == limit || isSpace(chars[j + 1]) || emphasisPost.contains(chars[j + 1]) { return j }
222 }
223 j += 1
224 }
225 return nil
226 }
227
228 /// `[[path]]` or `[[path][description]]`.
229 func bracketLink(_ i: Int, _ limit: Int) -> InlineMatch? {
230 guard i + 1 < limit, chars[i + 1] == "[" else { return nil }
231 var j = i + 2
232 while j < limit, chars[j] != "]" {
233 if chars[j] == "[" || isNewline(chars[j]) { return nil }
234 if chars[j] == "\\", j + 1 < limit { j += 1 }
235 j += 1
236 }
237 guard j > i + 2, j + 1 < limit else { return nil }
238 let path = (i + 2)..<j
239 if chars[j + 1] == "]" { return .link(path: path, description: nil, whole: i..<(j + 2)) }
240 guard chars[j + 1] == "[" else { return nil }
241 let descriptionStart = j + 2
242 var depth = 0
243 var k = descriptionStart
244 while k < limit {
245 if chars[k] == "[" {
246 depth += 1
247 } else if chars[k] == "]" {
248 if depth == 0 { break }
249 depth -= 1
250 }
251 k += 1
252 }
253 guard k > descriptionStart, k + 1 < limit, chars[k + 1] == "]" else { return nil }
254 return .link(path: path, description: descriptionStart..<k, whole: i..<(k + 2))
255 }
256
257 /// `[fn:label]`, `[fn:label:definition]` or `[fn::definition]`.
258 func footnoteReference(_ i: Int, _ limit: Int) -> Int? {
259 guard hasPrefix("[fn:", at: i, limit) else { return nil }
260 var j = i + 4
261 while j < limit, isWordScalar(chars[j]) || chars[j] == "_" || chars[j] == "-" { j += 1 }
262 guard j < limit else { return nil }
263 if chars[j] == "]" { return j > i + 4 ? j + 1 : nil }
264 guard chars[j] == ":" else { return nil }
265 var depth = 0
266 j += 1
267 while j < limit {
268 if chars[j] == "[" {
269 depth += 1
270 } else if chars[j] == "]" {
271 if depth == 0 { return j + 1 }
272 depth -= 1
273 }
274 j += 1
275 }
276 return nil
277 }
278
279 /// `[1/3]`, `[/]`, `[50%]` or `[%]`.
280 func statisticsCookie(_ i: Int, _ limit: Int) -> Int? {
281 var j = i + 1
282 while j < limit, isASCIIDigit(chars[j]) { j += 1 }
283 guard j < limit else { return nil }
284 if chars[j] == "%" {
285 j += 1
286 } else if chars[j] == "/" {
287 j += 1
288 while j < limit, isASCIIDigit(chars[j]) { j += 1 }
289 } else {
290 return nil
291 }
292 guard j < limit, chars[j] == "]" else { return nil }
293 return j + 1
294 }
295
296 /// `<<target>>`.
297 func target(_ i: Int, _ limit: Int) -> Int? {
298 guard hasPrefix("<<", at: i, limit), i + 2 < limit, chars[i + 2] != "<" else { return nil }
299 let start = i + 2
300 var j = start
301 while j < limit, chars[j] != ">" {
302 if chars[j] == "<" || isNewline(chars[j]) { return nil }
303 j += 1
304 }
305 guard j > start, j + 1 < limit, chars[j + 1] == ">",
306 !isSpace(chars[start]), !isSpace(chars[j - 1]) else { return nil }
307 return j + 2
308 }
309
310 /// `<scheme:path>` for a known scheme.
311 func angleLink(_ i: Int, _ limit: Int) -> Int? {
312 var j = i + 1
313 while j < limit, chars[j].properties.isAlphabetic { j += 1 }
314 guard j < limit, chars[j] == ":", angleLinkSchemes.contains(string((i + 1)..<j).lowercased()) else { return nil }
315 j += 1
316 let bodyStart = j
317 while j < limit, chars[j] != ">" {
318 if chars[j] == "<" || isNewline(chars[j]) { return nil }
319 j += 1
320 }
321 guard j < limit, j > bodyStart else { return nil }
322 return j + 1
323 }
324
325 /// A bare URL. Trailing sentence punctuation stays outside the link.
326 func plainLink(_ i: Int, _ limit: Int) -> Int? {
327 guard let prefix = plainLinkPrefixes.first(where: { hasPrefix($0, at: i, limit) }) else { return nil }
328 let bodyStart = i + prefix.count
329 var j = bodyStart
330 while j < limit, !isSpace(chars[j]), !"()<>[]\"".unicodeScalars.contains(chars[j]) { j += 1 }
331 while j > bodyStart, ".,;:!?'".unicodeScalars.contains(chars[j - 1]) { j -= 1 }
332 return j > bodyStart ? j : nil
333 }
334
335 /// `{{{name}}}` or `{{{name(arguments)}}}`.
336 func macro(_ i: Int, _ limit: Int) -> Int? {
337 guard hasPrefix("{{{", at: i, limit) else { return nil }
338 var j = i + 3
339 guard j < limit, chars[j].properties.isAlphabetic else { return nil }
340 while j < limit, isWordScalar(chars[j]) || chars[j] == "-" || chars[j] == "_" { j += 1 }
341 if j < limit, chars[j] == "(" {
342 var k = j + 1
343 while k < limit, !hasPrefix(")}}}", at: k, limit) {
344 if isNewline(chars[k]) { return nil }
345 k += 1
346 }
347 return k < limit ? k + 4 : nil
348 }
349 return hasPrefix("}}}", at: j, limit) ? j + 3 : nil
350 }
351
352 /// `\\` at the end of a line, before optional trailing blanks. The line break stays outside.
353 func lineBreak(_ i: Int, _ limit: Int) -> Int? {
354 guard hasPrefix("\\\\", at: i, limit) else { return nil }
355 var j = i + 2
356 while j < limit, chars[j] == " " || chars[j] == "\t" { j += 1 }
357 if j + 1 < limit, chars[j] == "\r", chars[j + 1] == "\n" { return j }
358 guard j == limit || isNewline(chars[j]) else { return nil }
359 return j
360 }
361
362 /// `\(...\)` or `\[...\]`.
363 func latexFragment(_ i: Int, _ limit: Int) -> Int? {
364 guard i + 1 < limit else { return nil }
365 let closer: String
366 switch chars[i + 1] {
367 case "(": closer = "\\)"
368 case "[": closer = "\\]"
369 default: return nil
370 }
371 let closing = Array(closer.unicodeScalars)
372 var j = i + 2
373 while j < limit {
374 if hasPrefix(closing, at: j, limit) { return j + 2 }
375 j += 1
376 }
377 return nil
378 }
379
380 /// `^word` or `^{group}` after a letter or digit.
381 func superscript(_ i: Int, _ limit: Int) -> Int? {
382 var j = i + 1
383 guard j < limit else { return nil }
384 if chars[j] == "{" {
385 j += 1
386 while j < limit, chars[j] != "}" {
387 if isNewline(chars[j]) { return nil }
388 j += 1
389 }
390 return j < limit ? j + 1 : nil
391 }
392 let start = j
393 while j < limit, isWordScalar(chars[j]) { j += 1 }
394 return j > start ? j : nil
395 }
396
397 /// `src_lang{body}` or `src_lang[headers]{body}`, on one line, with balanced braces.
398 func inlineSourceBlock(_ i: Int, _ limit: Int) -> Int? {
399 guard hasPrefix("src_", at: i, limit) else { return nil }
400 var j = i + 4
401 let languageStart = j
402 while j < limit, !isSpace(chars[j]), chars[j] != "[", chars[j] != "{" { j += 1 }
403 guard j > languageStart, j < limit else { return nil }
404 if chars[j] == "[" {
405 while j < limit, chars[j] != "]" {
406 if isNewline(chars[j]) { return nil }
407 j += 1
408 }
409 guard j < limit else { return nil }
410 j += 1
411 }
412 guard j < limit, chars[j] == "{" else { return nil }
413 var depth = 0
414 while j < limit {
415 if isNewline(chars[j]) { return nil }
416 if chars[j] == "{" {
417 depth += 1
418 } else if chars[j] == "}" {
419 depth -= 1
420 if depth == 0 { return j + 1 }
421 }
422 j += 1
423 }
424 return nil
425 }
426}
427
428extension Parser {
429 mutating func inline(_ text: Substring) {
430 let scanner = InlineScanner(text)
431 scanner.scan(0..<scanner.chars.count, into: &builder)
432 }
433}