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/Parsers/InlineScanner.swift | 187 ++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 Sources/NorgKit/Parsers/InlineScanner.swift (limited to 'Sources/NorgKit/Parsers/InlineScanner.swift') diff --git a/Sources/NorgKit/Parsers/InlineScanner.swift b/Sources/NorgKit/Parsers/InlineScanner.swift new file mode 100644 index 0000000..881a069 --- /dev/null +++ b/Sources/NorgKit/Parsers/InlineScanner.swift @@ -0,0 +1,187 @@ +/// Scans for inline norg constructs. +enum InlineScanner { + + struct Bracketed: Equatable { + let open: Int + let body: Range + let close: Int + } + + enum AnchorSuffix: Equatable { + case target(Bracketed) + case description(Bracketed) + } + + enum InlineObject: Equatable { + /// `\x` — `escaped` is the index of the literal scalar, or `nil` for a + /// trailing backslash at the end of the range. + case escape(escaped: Int?, end: Int) + /// `%…%`, dropped from rendered output. + case comment(open: Int, body: Range, close: Int, end: Int) + /// Verbatim / math: `` `…` `` or `$…$`, inner content taken literally. + case verbatim(style: InlineStyle, open: Int, body: Range, close: Int, end: Int) + /// An attached modifier (`*…*`, `/…/`, …) whose body is parsed recursively. + case modifier(style: InlineStyle, open: Int, body: Range, close: Int, end: Int) + /// `{target}` optionally followed by `[description]`. + case link(open: Int, target: Bracketed, description: Bracketed?, end: Int) + /// `[name]` optionally followed by `{target}` or `[description]`. + case anchor(open: Int, name: Bracketed, suffix: AnchorSuffix?, end: Int) + + var end: Int { + switch self { + case .escape(_, let end), .comment(_, _, _, let end), .verbatim(_, _, _, _, let end), + .modifier(_, _, _, _, let end), .link(_, _, _, let end), .anchor(_, _, _, let end): + return end + } + } + } + + static let modifiers: [Unicode.Scalar: InlineStyle] = [ + "*": .bold, + "/": .italic, + "_": .underline, + "-": .strikethrough, + "^": .superscript, + ",": .subscript, + "!": .spoiler, + ] + + static let literalModifiers: [Unicode.Scalar: InlineStyle] = [ + "`": .verbatim, + "$": .math, + ] + + static let significant = ASCIIByteSet("*/_-^,!\u{60}$%{[\\") + + static func object(in chars: [Unicode.Scalar], at i: Int, to hi: Int) -> InlineObject? { + let c = chars[i] + let prev: Unicode.Scalar? = i > 0 ? chars[i - 1] : nil + let next: Unicode.Scalar? = i + 1 < chars.count ? chars[i + 1] : nil + + // Escapes: the next scalar is taken literally. + if c == "\\" { + return i + 1 < hi ? .escape(escaped: i + 1, end: i + 2) : .escape(escaped: nil, end: i + 1) + } + + // Comments are dropped from the rendered output. + if c == "%", isOpener(prev: prev, next: next), + let close = literalClose(chars, from: i + 1, to: hi, char: "%") { + return .comment(open: i, body: (i + 1).. Bool { + guard let c else { return true } + return c.properties.isWhitespace + } + + private static func isBoundary(_ c: Unicode.Scalar?) -> Bool { + guard let c else { return true } + if c.properties.isWhitespace { return true } + switch c.properties.generalCategory { + case .connectorPunctuation, .dashPunctuation, .openPunctuation, + .closePunctuation, .initialPunctuation, .finalPunctuation, .otherPunctuation, + .mathSymbol, .currencySymbol, .modifierSymbol, .otherSymbol: + return true + default: + return false + } + } + + private static func isOpener(prev: Unicode.Scalar?, next: Unicode.Scalar?) -> Bool { + isBoundary(prev) && !isSpace(next) + } + + private static func modifierClose( + _ chars: [Unicode.Scalar], from start: Int, to hi: Int, char: Unicode.Scalar + ) -> Int? { + var i = start + while i < hi { + if chars[i] == "\\" { + i += 2 + continue + } + if chars[i] == char { + let prev: Unicode.Scalar? = i > 0 ? chars[i - 1] : nil + let next: Unicode.Scalar? = i + 1 < chars.count ? chars[i + 1] : nil + if !isSpace(prev) && isBoundary(next) { return i } + } + i += 1 + } + return nil + } + + private static func literalClose( + _ chars: [Unicode.Scalar], from start: Int, to hi: Int, char: Unicode.Scalar + ) -> Int? { + var i = start + while i < hi { + if chars[i] == char { + let prev: Unicode.Scalar? = i > 0 ? chars[i - 1] : nil + if !isSpace(prev) { return i } + } + i += 1 + } + return nil + } + + private static func bracketClose( + _ chars: [Unicode.Scalar], from start: Int, to hi: Int, char: Unicode.Scalar + ) -> Int? { + var i = start + while i < hi { + if chars[i] == "\\" { + i += 2 + continue + } + if chars[i] == char { return i } + i += 1 + } + return nil + } +} -- cgit