/// Characters allowed before an emphasis opener, besides whitespace and the start of the run. private let emphasisPre: Set = ["-", "(", "{", "'", "\""] /// Characters allowed after an emphasis closer, besides whitespace and the end of the run. private let emphasisPost: Set = ["-", ".", ",", ":", "!", "?", ";", "'", "\"", ")", "}", "\\", "["] private let angleLinkSchemes: Set = [ "http", "https", "mailto", "file", "id", "doi", "ftp", "news", "shell", "elisp", "info", "help", "attachment", ] private let plainLinkPrefixes: [[Unicode.Scalar]] = ["https://", "http://", "mailto:", "file:"].map { Array($0.unicodeScalars) } private func emphasisKind(_ c: Unicode.Scalar) -> SyntaxKind? { switch c { case "*": .bold case "/": .italic case "_": .underline case "+": .strikeThrough case "=": .verbatim case "~": .code default: nil } } func isNewline(_ c: Unicode.Scalar) -> Bool { c == "\n" } func isSpace(_ c: Unicode.Scalar) -> Bool { c.isASCII ? (c == " " || c == "\t" || c == "\n" || c == "\r" || c == "\u{0B}" || c == "\u{0C}") : c.properties.isWhitespace } func isWordScalar(_ c: Unicode.Scalar) -> Bool { if c.isASCII { let v = c.value return (v >= 48 && v <= 57) || (v >= 65 && v <= 90) || (v >= 97 && v <= 122) } return c.properties.isAlphabetic || c.properties.numericType != nil } enum InlineMatch { case emphasis(SyntaxKind, open: Int, close: Int) case link(path: Range, description: Range?, whole: Range) case object(SyntaxKind, Range) /// An object whose contents hold objects, between markers. case container(SyntaxKind, contents: Range, whole: Range) /// `[cite/style:prefix; refs…; suffix]`: the common prefix and suffix, and each reference /// with where its key is. case citation(whole: Range, opener: Int, prefix: Range?, references: [(range: Range, key: Range)], suffix: Range?) /// `@@backend:value@@`. case exportSnippet(whole: Range, value: Range) var end: Int { switch self { case .emphasis(_, _, let close): close + 1 case .link(_, _, let whole): whole.upperBound case .object(_, let range): range.upperBound case .container(_, _, let whole): whole.upperBound case .citation(let whole, _, _, _, _): whole.upperBound case .exportSnippet(let whole, _): whole.upperBound } } } /// Turns a run of text into text, newline and object tokens. Every scalar ends up in exactly /// one token. Works on unicode scalars: every recognizer starts on an ASCII character, so /// grapheme clusters never need to be formed. struct InlineScanner { let chars: [Unicode.Scalar] /// Radio targets, lowercased, longest first, and the scalars they start with. let radioTargets: [[Unicode.Scalar]] let radioStarts: Set /// Where each scalar starts in `source`, plus its end, so token text is sliced from the /// source instead of rebuilt scalar by scalar. let source: Substring.UnicodeScalarView let starts: [String.Index] init(_ text: Substring, radioTargets: [String] = []) { self.radioTargets = radioTargets.map { Array($0.lowercased().unicodeScalars) } self.radioStarts = Set(self.radioTargets.compactMap(\.first).flatMap { [$0] + Array(String($0).uppercased().unicodeScalars) }) source = text.unicodeScalars var chars: [Unicode.Scalar] = [] var starts: [String.Index] = [] chars.reserveCapacity(text.utf8.count) starts.reserveCapacity(text.utf8.count + 1) var index = source.startIndex while index < source.endIndex { chars.append(source[index]) starts.append(index) index = source.index(after: index) } starts.append(source.endIndex) self.chars = chars self.starts = starts } func scan(_ range: Range, into b: inout GreenBuilder, inLink: Bool = false) { var textStart = range.lowerBound var i = range.lowerBound while i < range.upperBound { if Self.mayStartObject(chars[i]) || radioStarts.contains(chars[i]), let match = match(at: i, in: range, inLink: inLink) { emitText(textStart.. Bool { switch c { case "[", "<", "{", "\\", "^", "s", "h", "m", "f", "*", "/", "_", "+", "=", "~", "$", "@", "c": true default: false } } /// Text with each line break as its own newline token; "\r\n" stays one token. func emitText(_ range: Range, into b: inout GreenBuilder) { var start = range.lowerBound var k = range.lowerBound while k < range.upperBound { if chars[k] == "\n" { let breakStart = k > start && chars[k - 1] == "\r" ? k - 1 : k if start < breakStart { b.token(.text, string(start.. reference.key.upperBound && chars[reference.range.upperBound - 1] == ";" let suffixEnd = separator ? reference.range.upperBound - 1 : reference.range.upperBound scan(reference.key.upperBound..) -> String { String(source[starts[range.lowerBound].. Bool { guard i + s.count <= limit else { return false } for (offset, c) in s.enumerated() where chars[i + offset] != c { return false } return true } func hasPrefix(_ s: String, at i: Int, _ limit: Int) -> Bool { hasPrefix(Array(s.unicodeScalars), at: i, limit) } // MARK: - Recognizers func match(at i: Int, in range: Range, inLink: Bool) -> InlineMatch? { let limit = range.upperBound let previous: Unicode.Scalar? = i > range.lowerBound ? chars[i - 1] : nil let afterWord = previous.map(isWordScalar) ?? false let afterNonSpace = previous.map { !isSpace($0) } ?? false // Radio links: a radio target's words, case aside, between non-alphanumerics. if !inLink, !afterWord, radioStarts.contains(chars[i]), let end = radioLink(i, limit) { return .object(.link, i.. Int? { let marker = chars[i] guard i + 1 < limit, !isSpace(chars[i + 1]) else { return nil } var newlines = 0 var j = i + 1 while j < limit { if isNewline(chars[j]) { newlines += 1 if newlines > 1 { return nil } } else if chars[j] == marker, j > i + 1, !isSpace(chars[j - 1]) { if j + 1 == limit || isSpace(chars[j + 1]) || emphasisPost.contains(chars[j + 1]) { return j } } j += 1 } return nil } /// `[[path]]` or `[[path][description]]`. func bracketLink(_ i: Int, _ limit: Int) -> InlineMatch? { guard i + 1 < limit, chars[i + 1] == "[" else { return nil } var j = i + 2 while j < limit, chars[j] != "]" { if chars[j] == "[" || isNewline(chars[j]) { return nil } if chars[j] == "\\", j + 1 < limit { j += 1 } j += 1 } guard j > i + 2, j + 1 < limit else { return nil } let path = (i + 2).. descriptionStart, k + 1 < limit, chars[k + 1] == "]" else { return nil } return .link(path: path, description: descriptionStart.. Int? { guard hasPrefix("[fn:", at: i, limit) else { return nil } var j = i + 4 while j < limit, isWordScalar(chars[j]) || chars[j] == "_" || chars[j] == "-" { j += 1 } guard j < limit else { return nil } if chars[j] == "]" { return j > i + 4 ? j + 1 : nil } guard chars[j] == ":" else { return nil } var depth = 0 j += 1 while j < limit { if chars[j] == "[" { depth += 1 } else if chars[j] == "]" { if depth == 0 { return j + 1 } depth -= 1 } j += 1 } return nil } /// `[1/3]`, `[/]`, `[50%]` or `[%]`. func statisticsCookie(_ i: Int, _ limit: Int) -> Int? { var j = i + 1 while j < limit, isASCIIDigit(chars[j]) { j += 1 } guard j < limit else { return nil } if chars[j] == "%" { j += 1 } else if chars[j] == "/" { j += 1 while j < limit, isASCIIDigit(chars[j]) { j += 1 } } else { return nil } guard j < limit, chars[j] == "]" else { return nil } return j + 1 } /// `org-element-citation-parser`: `[cite` and an optional `/style`, `:`, then up to the /// matching `]` at least one `@key`. A common prefix ends at the `;` before the first key, /// a common suffix starts at the last `;` with no key after it. func citation(_ i: Int, _ limit: Int) -> InlineMatch? { guard hasPrefix("[cite", at: i, limit) else { return nil } var k = i + 5 if k < limit, chars[k] == "/" { let style = k + 1 k = style while k < limit, chars[k] == "/" || chars[k] == "_" || chars[k] == "-" || isWordScalar(chars[k]) && chars[k].isASCII { k += 1 } guard k > style else { return nil } } guard k < limit, chars[k] == ":" else { return nil } k += 1 while k < limit, chars[k] == " " || chars[k] == "\t" || chars[k] == "\n" { k += 1 } let start = k // `scan-lists` with only square brackets paired. var depth = 0 var closing: Int? var j = i while j < limit { if chars[j] == "[" { depth += 1 } if chars[j] == "]" { depth -= 1 if depth == 0 { closing = j + 1; break } } j += 1 } guard let closing, let first = citationKey(from: start, to: closing) else { return nil } var prefix: Range? var contentsBegin = start if let semi = (start.. first.upperBound, [" ", "\t", "\n", "\r"].contains(chars[end - 1]) { end -= 1 } var suffix: Range? var contentsEnd = end if let semi = (first.upperBound.., key: Range)] = [] var at = contentsBegin while at < contentsEnd, let key = citationKey(from: at, to: contentsEnd) { let separator = (key.upperBound.. Range? { var k = from while k < limit { if chars[k] == "@" { var e = k + 1 while e < limit, isWordScalar(chars[e]) || "-.:?!`'/*@+|(){}<>&_^$#%~".unicodeScalars.contains(chars[e]) { e += 1 } if e > k + 1 { return k.. InlineMatch? { guard hasPrefix("@@", at: i, limit) else { return nil } var k = i + 2 while k < limit, chars[k] == "-" || (chars[k].isASCII && isWordScalar(chars[k])) { k += 1 } guard k > i + 2, k < limit, chars[k] == ":" else { return nil } let value = k + 1 var e = value while e + 1 < limit { if chars[e] == "@", chars[e + 1] == "@" { return .exportSnippet(whole: i..<(e + 2), value: value.. Int? { guard hasPrefix("call_", at: i, limit) else { return nil } var k = i + 5 while k < limit, !" \t\n[(".unicodeScalars.contains(chars[k]) { k += 1 } guard k > i + 5, k < limit, chars[k] == "(" || chars[k] == "[" else { return nil } func paired(_ open: Unicode.Scalar, _ close: Unicode.Scalar) -> Int? { guard k < limit, chars[k] == open else { return nil } var depth = 0 var j = k while j < limit { if chars[j] == open { depth += 1 } if chars[j] == close { depth -= 1 if depth == 0 { return j + 1 } } j += 1 } return nil } if let end = paired("[", "]") { k = end } guard let arguments = paired("(", ")") else { return nil } k = arguments if let end = paired("[", "]") { k = end } return k } /// `<>`. func target(_ i: Int, _ limit: Int) -> Int? { guard hasPrefix("<<", at: i, limit), i + 2 < limit, chars[i + 2] != "<" else { return nil } let start = i + 2 var j = start while j < limit, chars[j] != ">" { if chars[j] == "<" || isNewline(chars[j]) { return nil } j += 1 } guard j > start, j + 1 < limit, chars[j + 1] == ">", !isSpace(chars[start]), !isSpace(chars[j - 1]) else { return nil } return j + 2 } /// `` for a known scheme. func angleLink(_ i: Int, _ limit: Int) -> Int? { var j = i + 1 while j < limit, chars[j].properties.isAlphabetic { j += 1 } guard j < limit, chars[j] == ":", angleLinkSchemes.contains(string((i + 1).. bodyStart else { return nil } return j + 1 } /// A bare URL. Trailing sentence punctuation stays outside the link. func plainLink(_ i: Int, _ limit: Int) -> Int? { guard let prefix = plainLinkPrefixes.first(where: { hasPrefix($0, at: i, limit) }) else { return nil } let bodyStart = i + prefix.count var j = bodyStart while j < limit, !isSpace(chars[j]), !"()<>[]\"".unicodeScalars.contains(chars[j]) { j += 1 } while j > bodyStart, ".,;:!?'".unicodeScalars.contains(chars[j - 1]) { j -= 1 } return j > bodyStart ? j : nil } /// `{{{name}}}` or `{{{name(arguments)}}}`. func macro(_ i: Int, _ limit: Int) -> Int? { guard hasPrefix("{{{", at: i, limit) else { return nil } var j = i + 3 guard j < limit, chars[j].properties.isAlphabetic else { return nil } while j < limit, isWordScalar(chars[j]) || chars[j] == "-" || chars[j] == "_" { j += 1 } if j < limit, chars[j] == "(" { var k = j + 1 while k < limit, !hasPrefix(")}}}", at: k, limit) { if isNewline(chars[k]) { return nil } k += 1 } return k < limit ? k + 4 : nil } return hasPrefix("}}}", at: j, limit) ? j + 3 : nil } /// `\\` at the end of a line, before optional trailing blanks. The line break stays outside. func lineBreak(_ i: Int, _ limit: Int) -> Int? { guard hasPrefix("\\\\", at: i, limit) else { return nil } var j = i + 2 while j < limit, chars[j] == " " || chars[j] == "\t" { j += 1 } if j + 1 < limit, chars[j] == "\r", chars[j + 1] == "\n" { return j } guard j == limit || isNewline(chars[j]) else { return nil } return j } /// `org-element-entity-parser`: `\name`, `\name{}` or `\_` and spaces, for a name in /// `org-entities`. func entity(_ i: Int, _ limit: Int) -> Int? { var j = i + 1 guard j < limit else { return nil } if chars[j] == "_" { j += 1 while j < limit, chars[j] == " " { j += 1 } guard j > i + 2, OrgEntities.display[string((i + 1).. i + 1, OrgEntities.display[string((i + 1).. Int? { guard i + 1 < limit else { return nil } if chars[i + 1] == "$" { var j = i + 2 while j + 1 < limit { if chars[j] == "$", chars[j + 1] == "$" { return j + 2 } j += 1 } return nil } let next = chars[i + 1] guard ![" ", "\t", "\n", ",", ".", ";"].contains(next) else { return nil } var j = i + 1 while j < limit, chars[j] != "$" { j += 1 } guard j < limit, ![" ", "\t", "\n", ",", "."].contains(chars[j - 1]) else { return nil } if j + 1 < limit { let after = chars[j + 1] guard isSpace(after) || ".,;:!?'\"()[]{}<>-".unicodeScalars.contains(after) else { return nil } } return j + 1 } /// `org-match-substring-regexp` after its first character: `{…}` or `(…)` nested up to three /// deep, `*`, or `[+-]?[[:alnum:].,\\]*[[:alnum:]]`. func script(_ i: Int, _ limit: Int) -> Int? { let j = i + 1 guard j < limit else { return nil } if chars[j] == "{" || chars[j] == "(" { let open = chars[j], close: Unicode.Scalar = open == "{" ? "}" : ")" var depth = 0 var k = j while k < limit { if chars[k] == open { depth += 1 if depth > 4 { return nil } } else if chars[k] == close { depth -= 1 if depth == 0 { return k + 1 } } k += 1 } return nil } if chars[j] == "*" { return j + 1 } var k = j if k < limit, chars[k] == "+" || chars[k] == "-" { k += 1 } var lastAlnum: Int? while k < limit, isWordScalar(chars[k]) || chars[k] == "." || chars[k] == "," || chars[k] == "\\" { if isWordScalar(chars[k]) { lastAlnum = k } k += 1 } return lastAlnum.map { $0 + 1 } } /// `<<>>`. /// A diary timestamp, `<%%(SEXP)REST>` (`org-element--timestamp-regexp`). It ends at the /// first `]` or `>`, as its raw value does. func diaryTimestamp(_ i: Int, _ limit: Int) -> Int? { guard hasPrefix("<%%(", at: i, limit) else { return nil } var close = i + 4 while close < limit, chars[close] != ">", chars[close] != "\n" { close += 1 } guard close < limit, chars[close] == ">", close > i + 5, chars[(i + 5).. Int? { guard hasPrefix("<<<", at: i, limit) else { return nil } let start = i + 3 var j = start while j < limit, chars[j] != ">", chars[j] != "<", !isNewline(chars[j]) { j += 1 } guard j > start, hasPrefix(">>>", at: j, limit), !isSpace(chars[start]), !isSpace(chars[j - 1]) else { return nil } return j + 3 } /// A radio target's text at `i`, its blanks matching any run of blanks, not followed by a /// letter or digit. func radioLink(_ i: Int, _ limit: Int) -> Int? { for target in radioTargets { var j = i var t = 0 var ok = true while t < target.count { guard j < limit else { ok = false; break } if target[t] == " " { guard isSpace(chars[j]) else { ok = false; break } while j < limit, isSpace(chars[j]) { j += 1 } while t < target.count, target[t] == " " { t += 1 } continue } guard String(chars[j]).lowercased().unicodeScalars.first == target[t] else { ok = false; break } j += 1 t += 1 } if ok, j == limit || !isWordScalar(chars[j]) { return j } } return nil } /// `\(...\)`, `\[...\]`, or a LaTeX command with its `[options]` and `{arguments}`. func latexFragment(_ i: Int, _ limit: Int) -> Int? { guard i + 1 < limit else { return nil } let closer: String switch chars[i + 1] { case "(": closer = "\\)" case "[": closer = "\\]" default: var j = i + 1 while j < limit, chars[j].isASCII, chars[j].properties.isAlphabetic { j += 1 } guard j > i + 1 else { return nil } if j < limit, chars[j] == "*" { j += 1 } while j < limit, chars[j] == "[" || chars[j] == "{" { let close: Unicode.Scalar = chars[j] == "[" ? "]" : "}" var k = j + 1 while k < limit, chars[k] != close { if isNewline(chars[k]) || chars[k] == "{" || chars[k] == "}" || (close == "]" && chars[k] == "[") { return j } k += 1 } guard k < limit else { return j } j = k + 1 } return j } let closing = Array(closer.unicodeScalars) var j = i + 2 while j < limit { if hasPrefix(closing, at: j, limit) { return j + 2 } j += 1 } return nil } /// `^word` or `^{group}` after a letter or digit. func superscript(_ i: Int, _ limit: Int) -> Int? { var j = i + 1 guard j < limit else { return nil } if chars[j] == "{" { j += 1 while j < limit, chars[j] != "}" { if isNewline(chars[j]) { return nil } j += 1 } return j < limit ? j + 1 : nil } let start = j while j < limit, isWordScalar(chars[j]) { j += 1 } return j > start ? j : nil } /// `src_lang{body}` or `src_lang[headers]{body}`, on one line, with balanced braces. func inlineSourceBlock(_ i: Int, _ limit: Int) -> Int? { guard hasPrefix("src_", at: i, limit) else { return nil } var j = i + 4 let languageStart = j while j < limit, !isSpace(chars[j]), chars[j] != "[", chars[j] != "{" { j += 1 } guard j > languageStart, j < limit else { return nil } if chars[j] == "[" { while j < limit, chars[j] != "]" { if isNewline(chars[j]) { return nil } j += 1 } guard j < limit else { return nil } j += 1 } guard j < limit, chars[j] == "{" else { return nil } var depth = 0 while j < limit { if isNewline(chars[j]) { return nil } if chars[j] == "{" { depth += 1 } else if chars[j] == "}" { depth -= 1 if depth == 0 { return j + 1 } } j += 1 } return nil } } extension Parser { mutating func inline(_ text: Substring) { let scanner = InlineScanner(text, radioTargets: settings.radioTargets) scanner.scan(0..