aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgKit/Parsers/NorgInlineParser.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/NorgKit/Parsers/NorgInlineParser.swift')
-rw-r--r--Sources/NorgKit/Parsers/NorgInlineParser.swift214
1 files changed, 41 insertions, 173 deletions
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..<to]))
- }
-
- private static func isSpace(_ c: Unicode.Scalar?) -> Bool {
- guard let c else { return true }
- return c.properties.isWhitespace
- }
-
- /// 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)
- }
+ // MARK: - Helpers
- /// 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<Int>) -> String {
+ String(String.UnicodeScalarView(chars[range]))
}
/// Produces display text for a link target that has no explicit description