aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgKit/Parsers/InlineScanner.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/NorgKit/Parsers/InlineScanner.swift')
-rw-r--r--Sources/NorgKit/Parsers/InlineScanner.swift187
1 files changed, 187 insertions, 0 deletions
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<Int>
+ 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<Int>, close: Int, end: Int)
+ /// Verbatim / math: `` `…` `` or `$…$`, inner content taken literally.
+ case verbatim(style: InlineStyle, open: Int, body: Range<Int>, close: Int, end: Int)
+ /// An attached modifier (`*…*`, `/…/`, …) whose body is parsed recursively.
+ case modifier(style: InlineStyle, open: Int, body: Range<Int>, 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)..<close, close: close, end: close + 1)
+ }
+
+ // 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) {
+ return .verbatim(style: style, open: i, body: (i + 1)..<close, close: close, end: close + 1)
+ }
+
+ // Links: {location} optionally followed by [description].
+ if c == "{", let close = bracketClose(chars, from: i + 1, to: hi, char: "}") {
+ let target = Bracketed(open: i, body: (i + 1)..<close, close: close)
+ var j = close + 1
+ var description: Bracketed?
+ if j < hi, chars[j] == "[", let dclose = bracketClose(chars, from: j + 1, to: hi, char: "]") {
+ description = Bracketed(open: j, body: (j + 1)..<dclose, close: dclose)
+ j = dclose + 1
+ }
+ return .link(open: i, target: target, description: description, end: j)
+ }
+
+ // 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 = Bracketed(open: i, body: (i + 1)..<close, close: close)
+ var j = close + 1
+ var suffix: AnchorSuffix?
+ if j < hi, chars[j] == "{", let tclose = bracketClose(chars, from: j + 1, to: hi, char: "}") {
+ suffix = .target(Bracketed(open: j, body: (j + 1)..<tclose, close: tclose))
+ j = tclose + 1
+ } else if j < hi, chars[j] == "[",
+ let dclose = bracketClose(chars, from: j + 1, to: hi, char: "]") {
+ suffix = .description(Bracketed(open: j, body: (j + 1)..<dclose, close: dclose))
+ j = dclose + 1
+ }
+ return .anchor(open: i, name: name, suffix: suffix, end: j)
+ }
+
+ // 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) {
+ return .modifier(style: style, open: i, body: (i + 1)..<close, close: close, end: close + 1)
+ }
+
+ return nil
+ }
+
+ // MARK: - Boundary helpers
+
+ private static func isSpace(_ c: Unicode.Scalar?) -> 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
+ }
+}