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/NorgInlineParser.swift | 214 +++++-------------------- 1 file changed, 41 insertions(+), 173 deletions(-) (limited to 'Sources/NorgKit/Parsers/NorgInlineParser.swift') diff --git a/Sources/NorgKit/Parsers/NorgInlineParser.swift b/Sources/NorgKit/Parsers/NorgInlineParser.swift index 1f19c43..b47ec50 100644 --- a/Sources/NorgKit/Parsers/NorgInlineParser.swift +++ b/Sources/NorgKit/Parsers/NorgInlineParser.swift @@ -3,31 +3,10 @@ import Foundation /// Converts inline Norg markup into a list of `InlineSpan`s. public enum NorgInlineParser { - /// Attached modifiers whose content is parsed recursively for nesting. - private static let modifiers: [Unicode.Scalar: InlineStyle] = [ - "*": .bold, - "/": .italic, - "_": .underline, - "-": .strikethrough, - "^": .superscript, - ",": .subscript, - "!": .spoiler, - ] - - /// Modifiers whose content is taken verbatim. - private static let literalModifiers: [Unicode.Scalar: InlineStyle] = [ - "`": .verbatim, - "$": .math, - ] - - /// Bytes that can begin an inline object. If none are found, it's plain - /// text. - private static let significant = ASCIIByteSet("*/_-^,!\u{60}$%{[\\") - /// Parses a string to a list of `InlineSpan`s public static func parse(_ text: String) -> [InlineSpan] { if text.isEmpty { return [] } - if !text.utf8.contains(where: significant.contains) { + if !text.utf8.contains(where: InlineScanner.significant.contains) { return [InlineSpan(text: text, styles: [])] } @@ -38,7 +17,7 @@ public enum NorgInlineParser { /// Renders the text as plain text, without styling or markup. static func plainText(_ text: Substring) -> String { if text.isEmpty { return "" } - if !text.utf8.contains(where: significant.contains) { + if !text.utf8.contains(where: InlineScanner.significant.contains) { return String(text) } @@ -59,179 +38,68 @@ public enum NorgInlineParser { var i = lo while i < hi { - let c = chars[i] - // `prev`/`next` deliberately peek outside `[lo, hi)`: within a nested - // range the enclosing modifier (e.g. the `_` around `_/x/_`) is a - // valid boundary, so boundary detection uses the whole line. - 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 == "\\" { - if i + 1 < hi { - buffer.append(chars[i + 1]) - i += 2 - } else { - i += 1 - } + guard let object = InlineScanner.object(in: chars, at: i, to: hi) else { + buffer.append(chars[i]) + i += 1 continue } - // Comments are dropped from the rendered output. - if c == "%", isOpener(prev: prev, next: next), - let close = literalClose(chars, from: i + 1, to: hi, char: "%") { + switch object { + case .escape(let escaped, _): + if let escaped { buffer.append(chars[escaped]) } + + case .comment: flush() - i = close + 1 - continue - } - // Verbatim / math objects: literal inner content. - if let style = literalModifiers[c], isOpener(prev: prev, next: next), - let close = literalClose(chars, from: i + 1, to: hi, char: c) { + case .verbatim(let style, _, let body, _, _): flush() - spans.append(InlineSpan(text: slice(chars, i + 1, close), styles: base.union(style))) - i = close + 1 - continue - } + spans.append(InlineSpan(text: slice(chars, body), styles: base.union(style))) - // Links: {location} optionally followed by [description]. - if c == "{", let close = bracketClose(chars, from: i + 1, to: hi, char: "}") { - let target = slice(chars, i + 1, close) - var j = close + 1 - var label = linkLabel(target) - if j < hi, chars[j] == "[", let dclose = bracketClose(chars, from: j + 1, to: hi, char: "]") { - // An explicit description replaces the derived label verbatim. - label = slice(chars, j + 1, dclose) - j = dclose + 1 - } + case .modifier(let style, _, let body, _, _): flush() spans.append( - InlineSpan(text: label, styles: base, link: InlineLink(kind: .link, target: target))) - i = j - continue - } + contentsOf: parse( + chars, from: body.lowerBound, to: body.upperBound, base: base.union(style))) - // Anchors: [name] (declaration), [name]{location} (definition), or - // [name][description] (declaration with a custom description). - if c == "[", let close = bracketClose(chars, from: i + 1, to: hi, char: "]") { - let name = slice(chars, i + 1, close) - var j = close + 1 - var label = name - var target: String? - if j < hi, chars[j] == "{", let tclose = bracketClose(chars, from: j + 1, to: hi, char: "}") { - target = slice(chars, j + 1, tclose) - j = tclose + 1 - } else if j < hi, chars[j] == "[", - let dclose = bracketClose(chars, from: j + 1, to: hi, char: "]") { - label = slice(chars, j + 1, dclose) - j = dclose + 1 - } + case .link(_, let target, let description, _): flush() + let targetText = slice(chars, target.body) + let label = description.map { slice(chars, $0.body) } ?? linkLabel(targetText) spans.append( - InlineSpan(text: label, styles: base, link: InlineLink(kind: .anchor, target: target))) - i = j - continue - } + InlineSpan(text: label, styles: base, link: InlineLink(kind: .link, target: targetText))) - // Attached modifiers with recursively parsed content. - if let style = modifiers[c], isOpener(prev: prev, next: next), - let close = modifierClose(chars, from: i + 1, to: hi, char: c) { + case .anchor(_, let name, let suffix, _): flush() - spans.append(contentsOf: parse(chars, from: i + 1, to: close, base: base.union(style))) - i = close + 1 - continue + let nameText = slice(chars, name.body) + let label: String + let target: String? + switch suffix { + case .target(let bracket): + label = nameText + target = slice(chars, bracket.body) + case .description(let bracket): + label = slice(chars, bracket.body) + target = nil + case nil: + label = nameText + target = nil + } + spans.append( + InlineSpan(text: label, styles: base, link: InlineLink(kind: .anchor, target: target))) } - buffer.append(c) - i += 1 + i = object.end } flush() return spans } - // MARK: - Boundary helpers - - /// Builds a `String` from a half-open scalar range `[from, to)`. - private static func slice(_ chars: [Unicode.Scalar], _ from: Int, _ to: Int) -> String { - String(String.UnicodeScalarView(chars[from.. Bool { - guard let c else { return true } - return c.properties.isWhitespace - } + // MARK: - Helpers - /// Whether a scalar (or the absence of one, at a line edge) counts as a - /// modifier boundary: whitespace, punctuation, or a symbol. - 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 - } - } - - /// A valid opener is preceded by a boundary and followed by non-whitespace. - private static func isOpener(prev: Unicode.Scalar?, next: Unicode.Scalar?) -> Bool { - isBoundary(prev) && !isSpace(next) - } - - /// Finds the closing modifier of the same character: preceded by - /// non-whitespace and followed by a boundary. Honours escapes. - 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 - } - - /// Finds the closing character for verbatim/comment content. No escapes. - 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 - } - - /// Finds a matching closing bracket, honouring escapes. - 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 + /// Builds a `String` from a half-open scalar range. + private static func slice(_ chars: [Unicode.Scalar], _ range: Range) -> String { + String(String.UnicodeScalarView(chars[range])) } /// Produces display text for a link target that has no explicit description -- cgit