From 647296d5a0799c2e9de05cd41b50bd161cdeac85 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Fri, 19 Jun 2026 22:06:26 +0200 Subject: Add NorgLexer --- Sources/NorgKit/Lexer/NorgLexer.swift | 193 ++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 Sources/NorgKit/Lexer/NorgLexer.swift (limited to 'Sources/NorgKit/Lexer/NorgLexer.swift') diff --git a/Sources/NorgKit/Lexer/NorgLexer.swift b/Sources/NorgKit/Lexer/NorgLexer.swift new file mode 100644 index 0000000..ad9d457 --- /dev/null +++ b/Sources/NorgKit/Lexer/NorgLexer.swift @@ -0,0 +1,193 @@ +/// 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: []) + } +} -- cgit