/// 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 } }