Sources/OrgCore/Parser/Inline.swift
759 lines · 32118 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 /// An object whose contents hold objects, between markers.
46 case container(SyntaxKind, contents: Range<Int>, whole: Range<Int>)
47 /// `[cite/style:prefix; refs…; suffix]`: the common prefix and suffix, and each reference
48 /// with where its key is.
49 case citation(whole: Range<Int>, opener: Int, prefix: Range<Int>?, references: [(range: Range<Int>, key: Range<Int>)], suffix: Range<Int>?)
50 /// `@@backend:value@@`.
51 case exportSnippet(whole: Range<Int>, value: Range<Int>)
52
53 var end: Int {
54 switch self {
55 case .emphasis(_, _, let close): close + 1
56 case .link(_, _, let whole): whole.upperBound
57 case .object(_, let range): range.upperBound
58 case .container(_, _, let whole): whole.upperBound
59 case .citation(let whole, _, _, _, _): whole.upperBound
60 case .exportSnippet(let whole, _): whole.upperBound
61 }
62 }
63}
64
65/// Turns a run of text into text, newline and object tokens. Every scalar ends up in exactly
66/// one token. Works on unicode scalars: every recognizer starts on an ASCII character, so
67/// grapheme clusters never need to be formed.
68struct InlineScanner {
69 let chars: [Unicode.Scalar]
70 /// Radio targets, lowercased, longest first, and the scalars they start with.
71 let radioTargets: [[Unicode.Scalar]]
72 let radioStarts: Set<Unicode.Scalar>
73 /// Where each scalar starts in `source`, plus its end, so token text is sliced from the
74 /// source instead of rebuilt scalar by scalar.
75 let source: Substring.UnicodeScalarView
76 let starts: [String.Index]
77
78 init(_ text: Substring, radioTargets: [String] = []) {
79 self.radioTargets = radioTargets.map { Array($0.lowercased().unicodeScalars) }
80 self.radioStarts = Set(self.radioTargets.compactMap(\.first).flatMap { [$0] + Array(String($0).uppercased().unicodeScalars) })
81 source = text.unicodeScalars
82 var chars: [Unicode.Scalar] = []
83 var starts: [String.Index] = []
84 chars.reserveCapacity(text.utf8.count)
85 starts.reserveCapacity(text.utf8.count + 1)
86 var index = source.startIndex
87 while index < source.endIndex {
88 chars.append(source[index])
89 starts.append(index)
90 index = source.index(after: index)
91 }
92 starts.append(source.endIndex)
93 self.chars = chars
94 self.starts = starts
95 }
96
97 func scan(_ range: Range<Int>, into b: inout GreenBuilder, inLink: Bool = false) {
98 var textStart = range.lowerBound
99 var i = range.lowerBound
100 while i < range.upperBound {
101 if Self.mayStartObject(chars[i]) || radioStarts.contains(chars[i]), let match = match(at: i, in: range, inLink: inLink) {
102 emitText(textStart..<i, into: &b)
103 emit(match, into: &b, inLink: inLink)
104 i = match.end
105 textStart = i
106 } else {
107 i += 1
108 }
109 }
110 emitText(textStart..<range.upperBound, into: &b)
111 }
112
113 /// The first characters any recognizer accepts.
114 static func mayStartObject(_ c: Unicode.Scalar) -> Bool {
115 switch c {
116 case "[", "<", "{", "\\", "^", "s", "h", "m", "f", "*", "/", "_", "+", "=", "~", "$", "@", "c": true
117 default: false
118 }
119 }
120
121 /// Text with each line break as its own newline token; "\r\n" stays one token.
122 func emitText(_ range: Range<Int>, into b: inout GreenBuilder) {
123 var start = range.lowerBound
124 var k = range.lowerBound
125 while k < range.upperBound {
126 if chars[k] == "\n" {
127 let breakStart = k > start && chars[k - 1] == "\r" ? k - 1 : k
128 if start < breakStart { b.token(.text, string(start..<breakStart)) }
129 b.token(.newline, string(breakStart..<(k + 1)))
130 start = k + 1
131 }
132 k += 1
133 }
134 if start < range.upperBound { b.token(.text, string(start..<range.upperBound)) }
135 }
136
137 func emit(_ match: InlineMatch, into b: inout GreenBuilder, inLink: Bool) {
138 switch match {
139 case .emphasis(let kind, let open, let close):
140 b.start(kind)
141 b.token(.marker, string(open..<(open + 1)))
142 if kind == .verbatim || kind == .code {
143 emitText((open + 1)..<close, into: &b)
144 } else {
145 scan((open + 1)..<close, into: &b, inLink: inLink)
146 }
147 b.token(.marker, string(close..<(close + 1)))
148 b.finish()
149 case .link(let path, let description, let whole):
150 b.start(.link)
151 b.token(.marker, string(whole.lowerBound..<path.lowerBound))
152 b.token(.linkPath, string(path))
153 if let description {
154 b.token(.marker, string(path.upperBound..<description.lowerBound))
155 b.start(.linkDescription)
156 scan(description, into: &b, inLink: true)
157 b.finish()
158 b.token(.marker, string(description.upperBound..<whole.upperBound))
159 } else {
160 b.token(.marker, string(path.upperBound..<whole.upperBound))
161 }
162 b.finish()
163 case .container(let kind, let contents, let whole):
164 b.start(kind)
165 b.token(.marker, string(whole.lowerBound..<contents.lowerBound))
166 scan(contents, into: &b, inLink: inLink)
167 b.token(.marker, string(contents.upperBound..<whole.upperBound))
168 b.finish()
169 case .object(let kind, let range):
170 b.start(kind)
171 emitText(range, into: &b)
172 b.finish()
173 case .citation(let whole, let opener, let prefix, let references, let suffix):
174 b.start(.citation)
175 var at = whole.lowerBound
176 func marker(to end: Int) {
177 if at < end { b.token(.marker, string(at..<end)) }
178 at = max(at, end)
179 }
180 marker(to: opener)
181 if let prefix {
182 scan(prefix, into: &b, inLink: true)
183 at = prefix.upperBound
184 }
185 for reference in references {
186 marker(to: reference.range.lowerBound)
187 b.start(.citationReference)
188 scan(reference.range.lowerBound..<reference.key.lowerBound, into: &b, inLink: true)
189 b.token(.citationKey, string(reference.key))
190 let separator = reference.range.upperBound > reference.key.upperBound && chars[reference.range.upperBound - 1] == ";"
191 let suffixEnd = separator ? reference.range.upperBound - 1 : reference.range.upperBound
192 scan(reference.key.upperBound..<suffixEnd, into: &b, inLink: true)
193 if separator { b.token(.marker, string(suffixEnd..<reference.range.upperBound)) }
194 b.finish()
195 at = reference.range.upperBound
196 }
197 if let suffix {
198 marker(to: suffix.lowerBound)
199 scan(suffix, into: &b, inLink: true)
200 at = suffix.upperBound
201 }
202 marker(to: whole.upperBound)
203 b.finish()
204 case .exportSnippet(let whole, let value):
205 b.start(.exportSnippet)
206 b.token(.marker, string(whole.lowerBound..<value.lowerBound))
207 emitText(value, into: &b)
208 b.token(.marker, string(value.upperBound..<whole.upperBound))
209 b.finish()
210 }
211 }
212
213 func string(_ range: Range<Int>) -> String {
214 String(source[starts[range.lowerBound]..<starts[range.upperBound]])
215 }
216
217 func hasPrefix(_ s: [Unicode.Scalar], at i: Int, _ limit: Int) -> Bool {
218 guard i + s.count <= limit else { return false }
219 for (offset, c) in s.enumerated() where chars[i + offset] != c { return false }
220 return true
221 }
222
223 func hasPrefix(_ s: String, at i: Int, _ limit: Int) -> Bool {
224 hasPrefix(Array(s.unicodeScalars), at: i, limit)
225 }
226
227 // MARK: - Recognizers
228
229 func match(at i: Int, in range: Range<Int>, inLink: Bool) -> InlineMatch? {
230 let limit = range.upperBound
231 let previous: Unicode.Scalar? = i > range.lowerBound ? chars[i - 1] : nil
232 let afterWord = previous.map(isWordScalar) ?? false
233 let afterNonSpace = previous.map { !isSpace($0) } ?? false
234
235 // Radio links: a radio target's words, case aside, between non-alphanumerics.
236 if !inLink, !afterWord, radioStarts.contains(chars[i]), let end = radioLink(i, limit) { return .object(.link, i..<end) }
237
238 switch chars[i] {
239 case "[":
240 if !inLink, let link = bracketLink(i, limit) { return link }
241 if !inLink, let cite = citation(i, limit) { return cite }
242 if let end = footnoteReference(i, limit) { return .object(.footnoteReference, i..<end) }
243 if !inLink, let end = timestampEnd(chars, at: i, limit: limit) { return .object(.timestamp, i..<end) }
244 if let end = statisticsCookie(i, limit) { return .object(.statisticsCookie, i..<end) }
245 case "<":
246 if !inLink, let end = timestampEnd(chars, at: i, limit: limit) ?? diaryTimestamp(i, limit) { return .object(.timestamp, i..<end) }
247 if let end = radioTarget(i, limit) { return .object(.radioTarget, i..<end) }
248 if let end = target(i, limit) { return .object(.target, i..<end) }
249 if !inLink, let end = angleLink(i, limit) { return .object(.link, i..<end) }
250 case "{":
251 if let end = macro(i, limit) { return .object(.macro, i..<end) }
252 case "\\":
253 if let end = lineBreak(i, limit) { return .object(.lineBreak, i..<end) }
254 if let end = entity(i, limit) { return .object(.entity, i..<end) }
255 if let end = latexFragment(i, limit) { return .object(.latexFragment, i..<end) }
256 case "$":
257 if previous != "$", let end = dollarFragment(i, limit) { return .object(.latexFragment, i..<end) }
258 case "^", "_":
259 if afterNonSpace, let end = script(i, limit) {
260 let kind: SyntaxKind = chars[i] == "^" ? .superscript : .subscript
261 return chars[i + 1] == "{" ? .container(kind, contents: (i + 2)..<(end - 1), whole: i..<end) : .object(kind, i..<end)
262 }
263 case "s":
264 if !afterWord, let end = inlineSourceBlock(i, limit) { return .object(.inlineSourceBlock, i..<end) }
265 case "c":
266 if !afterWord, let end = inlineBabelCall(i, limit) { return .object(.inlineBabelCall, i..<end) }
267 case "@":
268 if let snippet = exportSnippet(i, limit) { return snippet }
269 case "h", "m", "f":
270 if !inLink, !afterWord, let end = plainLink(i, limit) { return .object(.link, i..<end) }
271 default:
272 break
273 }
274
275 if let kind = emphasisKind(chars[i]),
276 previous.map({ isSpace($0) || emphasisPre.contains($0) }) ?? true,
277 let close = emphasisClose(i, limit) {
278 return .emphasis(kind, open: i, close: close)
279 }
280 return nil
281 }
282
283 /// org's emphasis rules: the body neither starts nor ends with whitespace, spans at most
284 /// one line break, and the closer is followed by whitespace, punctuation or the end.
285 func emphasisClose(_ i: Int, _ limit: Int) -> Int? {
286 let marker = chars[i]
287 guard i + 1 < limit, !isSpace(chars[i + 1]) else { return nil }
288 var newlines = 0
289 var j = i + 1
290 while j < limit {
291 if isNewline(chars[j]) {
292 newlines += 1
293 if newlines > 1 { return nil }
294 } else if chars[j] == marker, j > i + 1, !isSpace(chars[j - 1]) {
295 if j + 1 == limit || isSpace(chars[j + 1]) || emphasisPost.contains(chars[j + 1]) { return j }
296 }
297 j += 1
298 }
299 return nil
300 }
301
302 /// `[[path]]` or `[[path][description]]`.
303 func bracketLink(_ i: Int, _ limit: Int) -> InlineMatch? {
304 guard i + 1 < limit, chars[i + 1] == "[" else { return nil }
305 var j = i + 2
306 while j < limit, chars[j] != "]" {
307 if chars[j] == "[" || isNewline(chars[j]) { return nil }
308 if chars[j] == "\\", j + 1 < limit { j += 1 }
309 j += 1
310 }
311 guard j > i + 2, j + 1 < limit else { return nil }
312 let path = (i + 2)..<j
313 if chars[j + 1] == "]" { return .link(path: path, description: nil, whole: i..<(j + 2)) }
314 guard chars[j + 1] == "[" else { return nil }
315 let descriptionStart = j + 2
316 var depth = 0
317 var k = descriptionStart
318 while k < limit {
319 if chars[k] == "[" {
320 depth += 1
321 } else if chars[k] == "]" {
322 if depth == 0 { break }
323 depth -= 1
324 }
325 k += 1
326 }
327 guard k > descriptionStart, k + 1 < limit, chars[k + 1] == "]" else { return nil }
328 return .link(path: path, description: descriptionStart..<k, whole: i..<(k + 2))
329 }
330
331 /// `[fn:label]`, `[fn:label:definition]` or `[fn::definition]`.
332 func footnoteReference(_ i: Int, _ limit: Int) -> Int? {
333 guard hasPrefix("[fn:", at: i, limit) else { return nil }
334 var j = i + 4
335 while j < limit, isWordScalar(chars[j]) || chars[j] == "_" || chars[j] == "-" { j += 1 }
336 guard j < limit else { return nil }
337 if chars[j] == "]" { return j > i + 4 ? j + 1 : nil }
338 guard chars[j] == ":" else { return nil }
339 var depth = 0
340 j += 1
341 while j < limit {
342 if chars[j] == "[" {
343 depth += 1
344 } else if chars[j] == "]" {
345 if depth == 0 { return j + 1 }
346 depth -= 1
347 }
348 j += 1
349 }
350 return nil
351 }
352
353 /// `[1/3]`, `[/]`, `[50%]` or `[%]`.
354 func statisticsCookie(_ i: Int, _ limit: Int) -> Int? {
355 var j = i + 1
356 while j < limit, isASCIIDigit(chars[j]) { j += 1 }
357 guard j < limit else { return nil }
358 if chars[j] == "%" {
359 j += 1
360 } else if chars[j] == "/" {
361 j += 1
362 while j < limit, isASCIIDigit(chars[j]) { j += 1 }
363 } else {
364 return nil
365 }
366 guard j < limit, chars[j] == "]" else { return nil }
367 return j + 1
368 }
369
370 /// `org-element-citation-parser`: `[cite` and an optional `/style`, `:`, then up to the
371 /// matching `]` at least one `@key`. A common prefix ends at the `;` before the first key,
372 /// a common suffix starts at the last `;` with no key after it.
373 func citation(_ i: Int, _ limit: Int) -> InlineMatch? {
374 guard hasPrefix("[cite", at: i, limit) else { return nil }
375 var k = i + 5
376 if k < limit, chars[k] == "/" {
377 let style = k + 1
378 k = style
379 while k < limit, chars[k] == "/" || chars[k] == "_" || chars[k] == "-" || isWordScalar(chars[k]) && chars[k].isASCII { k += 1 }
380 guard k > style else { return nil }
381 }
382 guard k < limit, chars[k] == ":" else { return nil }
383 k += 1
384 while k < limit, chars[k] == " " || chars[k] == "\t" || chars[k] == "\n" { k += 1 }
385 let start = k
386 // `scan-lists` with only square brackets paired.
387 var depth = 0
388 var closing: Int?
389 var j = i
390 while j < limit {
391 if chars[j] == "[" { depth += 1 }
392 if chars[j] == "]" {
393 depth -= 1
394 if depth == 0 { closing = j + 1; break }
395 }
396 j += 1
397 }
398 guard let closing, let first = citationKey(from: start, to: closing) else { return nil }
399 var prefix: Range<Int>?
400 var contentsBegin = start
401 if let semi = (start..<first.lowerBound).last(where: { chars[$0] == ";" }) {
402 if start < semi { prefix = start..<semi }
403 contentsBegin = semi + 1
404 }
405 var end = closing - 1
406 while end > first.upperBound, [" ", "\t", "\n", "\r"].contains(chars[end - 1]) { end -= 1 }
407 var suffix: Range<Int>?
408 var contentsEnd = end
409 if let semi = (first.upperBound..<end).last(where: { chars[$0] == ";" }), citationKey(from: semi, to: end) == nil {
410 if semi + 1 < end { suffix = (semi + 1)..<end }
411 contentsEnd = semi + 1
412 }
413 var references: [(range: Range<Int>, key: Range<Int>)] = []
414 var at = contentsBegin
415 while at < contentsEnd, let key = citationKey(from: at, to: contentsEnd) {
416 let separator = (key.upperBound..<contentsEnd).first { chars[$0] == ";" }
417 let referenceEnd = separator.map { $0 + 1 } ?? contentsEnd
418 references.append((at..<referenceEnd, key))
419 at = referenceEnd
420 }
421 return .citation(whole: i..<closing, opener: start, prefix: prefix, references: references, suffix: suffix)
422 }
423
424 /// `org-element-citation-key-re`: `@` and word or symbol characters.
425 func citationKey(from: Int, to limit: Int) -> Range<Int>? {
426 var k = from
427 while k < limit {
428 if chars[k] == "@" {
429 var e = k + 1
430 while e < limit, isWordScalar(chars[e]) || "-.:?!`'/*@+|(){}<>&_^$#%~".unicodeScalars.contains(chars[e]) { e += 1 }
431 if e > k + 1 { return k..<e }
432 }
433 k += 1
434 }
435 return nil
436 }
437
438 /// `org-element-export-snippet-parser`: `@@backend:value@@`.
439 func exportSnippet(_ i: Int, _ limit: Int) -> InlineMatch? {
440 guard hasPrefix("@@", at: i, limit) else { return nil }
441 var k = i + 2
442 while k < limit, chars[k] == "-" || (chars[k].isASCII && isWordScalar(chars[k])) { k += 1 }
443 guard k > i + 2, k < limit, chars[k] == ":" else { return nil }
444 let value = k + 1
445 var e = value
446 while e + 1 < limit {
447 if chars[e] == "@", chars[e + 1] == "@" { return .exportSnippet(whole: i..<(e + 2), value: value..<e) }
448 e += 1
449 }
450 return nil
451 }
452
453 /// `org-element-inline-babel-call-parser`: `call_NAME`, an optional `[inside header]`,
454 /// `(arguments)` and an optional `[end header]`, each balanced in its own brackets.
455 func inlineBabelCall(_ i: Int, _ limit: Int) -> Int? {
456 guard hasPrefix("call_", at: i, limit) else { return nil }
457 var k = i + 5
458 while k < limit, !" \t\n[(".unicodeScalars.contains(chars[k]) { k += 1 }
459 guard k > i + 5, k < limit, chars[k] == "(" || chars[k] == "[" else { return nil }
460 func paired(_ open: Unicode.Scalar, _ close: Unicode.Scalar) -> Int? {
461 guard k < limit, chars[k] == open else { return nil }
462 var depth = 0
463 var j = k
464 while j < limit {
465 if chars[j] == open { depth += 1 }
466 if chars[j] == close {
467 depth -= 1
468 if depth == 0 { return j + 1 }
469 }
470 j += 1
471 }
472 return nil
473 }
474 if let end = paired("[", "]") { k = end }
475 guard let arguments = paired("(", ")") else { return nil }
476 k = arguments
477 if let end = paired("[", "]") { k = end }
478 return k
479 }
480
481 /// `<<target>>`.
482 func target(_ i: Int, _ limit: Int) -> Int? {
483 guard hasPrefix("<<", at: i, limit), i + 2 < limit, chars[i + 2] != "<" else { return nil }
484 let start = i + 2
485 var j = start
486 while j < limit, chars[j] != ">" {
487 if chars[j] == "<" || isNewline(chars[j]) { return nil }
488 j += 1
489 }
490 guard j > start, j + 1 < limit, chars[j + 1] == ">",
491 !isSpace(chars[start]), !isSpace(chars[j - 1]) else { return nil }
492 return j + 2
493 }
494
495 /// `<scheme:path>` for a known scheme.
496 func angleLink(_ i: Int, _ limit: Int) -> Int? {
497 var j = i + 1
498 while j < limit, chars[j].properties.isAlphabetic { j += 1 }
499 guard j < limit, chars[j] == ":", angleLinkSchemes.contains(string((i + 1)..<j).lowercased()) else { return nil }
500 j += 1
501 let bodyStart = j
502 while j < limit, chars[j] != ">" {
503 if chars[j] == "<" || isNewline(chars[j]) { return nil }
504 j += 1
505 }
506 guard j < limit, j > bodyStart else { return nil }
507 return j + 1
508 }
509
510 /// A bare URL. Trailing sentence punctuation stays outside the link.
511 func plainLink(_ i: Int, _ limit: Int) -> Int? {
512 guard let prefix = plainLinkPrefixes.first(where: { hasPrefix($0, at: i, limit) }) else { return nil }
513 let bodyStart = i + prefix.count
514 var j = bodyStart
515 while j < limit, !isSpace(chars[j]), !"()<>[]\"".unicodeScalars.contains(chars[j]) { j += 1 }
516 while j > bodyStart, ".,;:!?'".unicodeScalars.contains(chars[j - 1]) { j -= 1 }
517 return j > bodyStart ? j : nil
518 }
519
520 /// `{{{name}}}` or `{{{name(arguments)}}}`.
521 func macro(_ i: Int, _ limit: Int) -> Int? {
522 guard hasPrefix("{{{", at: i, limit) else { return nil }
523 var j = i + 3
524 guard j < limit, chars[j].properties.isAlphabetic else { return nil }
525 while j < limit, isWordScalar(chars[j]) || chars[j] == "-" || chars[j] == "_" { j += 1 }
526 if j < limit, chars[j] == "(" {
527 var k = j + 1
528 while k < limit, !hasPrefix(")}}}", at: k, limit) {
529 if isNewline(chars[k]) { return nil }
530 k += 1
531 }
532 return k < limit ? k + 4 : nil
533 }
534 return hasPrefix("}}}", at: j, limit) ? j + 3 : nil
535 }
536
537 /// `\\` at the end of a line, before optional trailing blanks. The line break stays outside.
538 func lineBreak(_ i: Int, _ limit: Int) -> Int? {
539 guard hasPrefix("\\\\", at: i, limit) else { return nil }
540 var j = i + 2
541 while j < limit, chars[j] == " " || chars[j] == "\t" { j += 1 }
542 if j + 1 < limit, chars[j] == "\r", chars[j + 1] == "\n" { return j }
543 guard j == limit || isNewline(chars[j]) else { return nil }
544 return j
545 }
546
547 /// `org-element-entity-parser`: `\name`, `\name{}` or `\_` and spaces, for a name in
548 /// `org-entities`.
549 func entity(_ i: Int, _ limit: Int) -> Int? {
550 var j = i + 1
551 guard j < limit else { return nil }
552 if chars[j] == "_" {
553 j += 1
554 while j < limit, chars[j] == " " { j += 1 }
555 guard j > i + 2, OrgEntities.display[string((i + 1)..<j)] != nil else { return nil }
556 return j
557 }
558 while j < limit, chars[j].isASCII, chars[j].properties.isAlphabetic { j += 1 }
559 // `there4`, `sup1`…, `frac12`… end in digits.
560 if j < limit, chars[j].isASCII, chars[j].properties.numericType != nil {
561 var k = j
562 while k < limit, chars[k].isASCII, chars[k].properties.numericType != nil { k += 1 }
563 if OrgEntities.display[string((i + 1)..<k)] != nil { j = k }
564 }
565 guard j > i + 1, OrgEntities.display[string((i + 1)..<j)] != nil else { return nil }
566 if j < limit, chars[j].properties.isAlphabetic { return nil }
567 if hasPrefix("{}", at: j, limit) { return j + 2 }
568 return j
569 }
570
571 /// `$$...$$`, or `$...$` with no blank inside its ends and punctuation, a blank or the end
572 /// after it, as `org-element-latex-fragment-parser`.
573 func dollarFragment(_ i: Int, _ limit: Int) -> Int? {
574 guard i + 1 < limit else { return nil }
575 if chars[i + 1] == "$" {
576 var j = i + 2
577 while j + 1 < limit {
578 if chars[j] == "$", chars[j + 1] == "$" { return j + 2 }
579 j += 1
580 }
581 return nil
582 }
583 let next = chars[i + 1]
584 guard ![" ", "\t", "\n", ",", ".", ";"].contains(next) else { return nil }
585 var j = i + 1
586 while j < limit, chars[j] != "$" { j += 1 }
587 guard j < limit, ![" ", "\t", "\n", ",", "."].contains(chars[j - 1]) else { return nil }
588 if j + 1 < limit {
589 let after = chars[j + 1]
590 guard isSpace(after) || ".,;:!?'\"()[]{}<>-".unicodeScalars.contains(after) else { return nil }
591 }
592 return j + 1
593 }
594
595 /// `org-match-substring-regexp` after its first character: `{…}` or `(…)` nested up to three
596 /// deep, `*`, or `[+-]?[[:alnum:].,\\]*[[:alnum:]]`.
597 func script(_ i: Int, _ limit: Int) -> Int? {
598 let j = i + 1
599 guard j < limit else { return nil }
600 if chars[j] == "{" || chars[j] == "(" {
601 let open = chars[j], close: Unicode.Scalar = open == "{" ? "}" : ")"
602 var depth = 0
603 var k = j
604 while k < limit {
605 if chars[k] == open {
606 depth += 1
607 if depth > 4 { return nil }
608 } else if chars[k] == close {
609 depth -= 1
610 if depth == 0 { return k + 1 }
611 }
612 k += 1
613 }
614 return nil
615 }
616 if chars[j] == "*" { return j + 1 }
617 var k = j
618 if k < limit, chars[k] == "+" || chars[k] == "-" { k += 1 }
619 var lastAlnum: Int?
620 while k < limit, isWordScalar(chars[k]) || chars[k] == "." || chars[k] == "," || chars[k] == "\\" {
621 if isWordScalar(chars[k]) { lastAlnum = k }
622 k += 1
623 }
624 return lastAlnum.map { $0 + 1 }
625 }
626
627 /// `<<<target>>>`.
628 /// A diary timestamp, `<%%(SEXP)REST>` (`org-element--timestamp-regexp`). It ends at the
629 /// first `]` or `>`, as its raw value does.
630 func diaryTimestamp(_ i: Int, _ limit: Int) -> Int? {
631 guard hasPrefix("<%%(", at: i, limit) else { return nil }
632 var close = i + 4
633 while close < limit, chars[close] != ">", chars[close] != "\n" { close += 1 }
634 guard close < limit, chars[close] == ">", close > i + 5, chars[(i + 5)..<close].contains(")") else { return nil }
635 var end = i + 1
636 while chars[end] != "]", chars[end] != ">" { end += 1 }
637 return end + 1
638 }
639
640 func radioTarget(_ i: Int, _ limit: Int) -> Int? {
641 guard hasPrefix("<<<", at: i, limit) else { return nil }
642 let start = i + 3
643 var j = start
644 while j < limit, chars[j] != ">", chars[j] != "<", !isNewline(chars[j]) { j += 1 }
645 guard j > start, hasPrefix(">>>", at: j, limit), !isSpace(chars[start]), !isSpace(chars[j - 1]) else { return nil }
646 return j + 3
647 }
648
649 /// A radio target's text at `i`, its blanks matching any run of blanks, not followed by a
650 /// letter or digit.
651 func radioLink(_ i: Int, _ limit: Int) -> Int? {
652 for target in radioTargets {
653 var j = i
654 var t = 0
655 var ok = true
656 while t < target.count {
657 guard j < limit else { ok = false; break }
658 if target[t] == " " {
659 guard isSpace(chars[j]) else { ok = false; break }
660 while j < limit, isSpace(chars[j]) { j += 1 }
661 while t < target.count, target[t] == " " { t += 1 }
662 continue
663 }
664 guard String(chars[j]).lowercased().unicodeScalars.first == target[t] else { ok = false; break }
665 j += 1
666 t += 1
667 }
668 if ok, j == limit || !isWordScalar(chars[j]) { return j }
669 }
670 return nil
671 }
672
673 /// `\(...\)`, `\[...\]`, or a LaTeX command with its `[options]` and `{arguments}`.
674 func latexFragment(_ i: Int, _ limit: Int) -> Int? {
675 guard i + 1 < limit else { return nil }
676 let closer: String
677 switch chars[i + 1] {
678 case "(": closer = "\\)"
679 case "[": closer = "\\]"
680 default:
681 var j = i + 1
682 while j < limit, chars[j].isASCII, chars[j].properties.isAlphabetic { j += 1 }
683 guard j > i + 1 else { return nil }
684 if j < limit, chars[j] == "*" { j += 1 }
685 while j < limit, chars[j] == "[" || chars[j] == "{" {
686 let close: Unicode.Scalar = chars[j] == "[" ? "]" : "}"
687 var k = j + 1
688 while k < limit, chars[k] != close {
689 if isNewline(chars[k]) || chars[k] == "{" || chars[k] == "}" || (close == "]" && chars[k] == "[") { return j }
690 k += 1
691 }
692 guard k < limit else { return j }
693 j = k + 1
694 }
695 return j
696 }
697 let closing = Array(closer.unicodeScalars)
698 var j = i + 2
699 while j < limit {
700 if hasPrefix(closing, at: j, limit) { return j + 2 }
701 j += 1
702 }
703 return nil
704 }
705
706 /// `^word` or `^{group}` after a letter or digit.
707 func superscript(_ i: Int, _ limit: Int) -> Int? {
708 var j = i + 1
709 guard j < limit else { return nil }
710 if chars[j] == "{" {
711 j += 1
712 while j < limit, chars[j] != "}" {
713 if isNewline(chars[j]) { return nil }
714 j += 1
715 }
716 return j < limit ? j + 1 : nil
717 }
718 let start = j
719 while j < limit, isWordScalar(chars[j]) { j += 1 }
720 return j > start ? j : nil
721 }
722
723 /// `src_lang{body}` or `src_lang[headers]{body}`, on one line, with balanced braces.
724 func inlineSourceBlock(_ i: Int, _ limit: Int) -> Int? {
725 guard hasPrefix("src_", at: i, limit) else { return nil }
726 var j = i + 4
727 let languageStart = j
728 while j < limit, !isSpace(chars[j]), chars[j] != "[", chars[j] != "{" { j += 1 }
729 guard j > languageStart, j < limit else { return nil }
730 if chars[j] == "[" {
731 while j < limit, chars[j] != "]" {
732 if isNewline(chars[j]) { return nil }
733 j += 1
734 }
735 guard j < limit else { return nil }
736 j += 1
737 }
738 guard j < limit, chars[j] == "{" else { return nil }
739 var depth = 0
740 while j < limit {
741 if isNewline(chars[j]) { return nil }
742 if chars[j] == "{" {
743 depth += 1
744 } else if chars[j] == "}" {
745 depth -= 1
746 if depth == 0 { return j + 1 }
747 }
748 j += 1
749 }
750 return nil
751 }
752}
753
754extension Parser {
755 mutating func inline(_ text: Substring) {
756 let scanner = InlineScanner(text, radioTargets: settings.radioTargets)
757 scanner.scan(0..<scanner.chars.count, into: &builder)
758 }
759}