krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgCore/Parser/Inline.swift

778058aafc7ddacee4a613b1da87c60aa0c8e113
orgstar/Sources/OrgCore/Parser/Inline.swift history · blame · raw

759 lines · 32118 bytes

39 symbols in this file
  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}