/// Tokenizes Norg source into `NorgToken`s for syntax highlighting. public enum NorgLexer { /// Tokenizes text for syntax highlighting. public static func tokenize(_ text: String) -> [NorgToken] { var tokens: [NorgToken] = [] let lines = TextHelper.lineSlices(text) var i = 0 while i < lines.count { let raw = lines[i] let trimmed = TextHelper.whitespaceTrimmed(raw) if trimmed.isEmpty { i += 1 continue } if trimmed.first == "@" { i = tagBlock(lines, at: i, into: &tokens) continue } if let delimiter = BlockScanner.delimiter(trimmed) { let kind: NorgToken.Kind switch delimiter { case .weak: kind = .weakDelimiter case .strong: kind = .strongDelimiter case .rule: kind = .horizontalRule } tokens.append(NorgToken(kind: kind, range: trimmed.startIndex.. Int { tagHeaderTokens(lines[i], into: &tokens) let header = TextHelper.whitespaceTrimmed(TextHelper.whitespaceTrimmed(lines[i]).dropFirst()) if header == "end" { return i + 1 } var j = i + 1 while j < lines.count { if TextHelper.whitespaceTrimmed(lines[j]) == "@end" { tagHeaderTokens(lines[j], into: &tokens) return j + 1 } let body = lines[j] if body.startIndex < body.endIndex { tokens.append(NorgToken(kind: .verbatimBlock, range: body.startIndex.. NorgToken.Kind? { switch marker { case "*": return .heading(level: level) case "-": return .unorderedList(level: level) case "~": return .orderedList(level: level) case ">": return .quote(level: level) case "$": return .definition(level: level) case "^": return .footnote(level: level) case ":": return .tableCell(level: level) default: return nil } } // MARK: - Inline tokens private static func inlineTokens(in slice: Substring, into tokens: inout [NorgToken]) { if slice.isEmpty { return } if !slice.utf8.contains(where: InlineScanner.significant.contains) { return } let chars = Array(slice.unicodeScalars) var bound = Array(slice.unicodeScalars.indices) bound.append(slice.endIndex) func append(_ kind: NorgToken.Kind, _ from: Int, _ to: Int) { tokens.append(NorgToken(kind: kind, range: bound[from]..= 0 { if !style.isEmpty { append(.styledText(style), runStart, upTo) } runStart = -1 } } var i = lo while i < hi { guard let object = InlineScanner.object(in: chars, at: i, to: hi) else { if runStart < 0 { runStart = i } i += 1 continue } flushRun(i) switch object { case .escape(let escaped, let end): append(.escape, i, i + 1) if let escaped, !style.isEmpty { append(.styledText(style), escaped, end) } case .comment(let open, _, _, let end): append(.comment, open, end) case .verbatim(let vstyle, let open, let body, let close, _): let s = style.union(vstyle) append(.modifierDelimiter(s), open, open + 1) if body.lowerBound < body.upperBound { append(.styledText(s), body.lowerBound, body.upperBound) } append(.modifierDelimiter(s), close, close + 1) case .modifier(let mstyle, let open, let body, let close, _): let s = style.union(mstyle) append(.modifierDelimiter(s), open, open + 1) emit(body.lowerBound, body.upperBound, style: s) append(.modifierDelimiter(s), close, close + 1) case .link(_, let target, let description, _): bracketed(target, body: .linkTarget) if let description { bracketed(description, body: .linkDescription) } case .anchor(_, let name, let suffix, _): bracketed(name, body: .linkDescription) switch suffix { case .target(let b): bracketed(b, body: .linkTarget) case .description(let b): bracketed(b, body: .linkDescription) case nil: break } } i = object.end } flushRun(hi) } emit(0, chars.count, style: []) } }