import Foundation /// Converts inline Norg markup into a list of `InlineSpan`s. public enum NorgInlineParser { /// 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: InlineScanner.significant.contains) { return [InlineSpan(text: text, styles: [])] } let chars = Array(text.unicodeScalars) return parse(chars, from: 0, to: chars.count, base: []) } /// 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: InlineScanner.significant.contains) { return String(text) } let chars = Array(text.unicodeScalars) return parse(chars, from: 0, to: chars.count, base: []).plainText } private static func parse(_ chars: [Unicode.Scalar], from lo: Int, to hi: Int, base: InlineStyle) -> [InlineSpan] { var spans: [InlineSpan] = [] var buffer = String.UnicodeScalarView() func flush() { guard !buffer.isEmpty else { return } spans.append(InlineSpan(text: String(buffer), styles: base)) buffer = String.UnicodeScalarView() } var i = lo while i < hi { guard let object = InlineScanner.object(in: chars, at: i, to: hi) else { buffer.append(chars[i]) i += 1 continue } switch object { case .escape(let escaped, _): if let escaped { buffer.append(chars[escaped]) } case .comment: flush() case .verbatim(let style, _, let body, _, _): flush() spans.append(InlineSpan(text: slice(chars, body), styles: base.union(style))) case .modifier(let style, _, let body, _, _): flush() spans.append( contentsOf: parse( chars, from: body.lowerBound, to: body.upperBound, base: base.union(style))) 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: .link, target: targetText))) case .anchor(_, let name, let suffix, _): flush() 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))) } i = object.end } flush() return spans } // MARK: - Helpers /// 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 /// by stripping the leading location prefix (`*`, `#`, `/`, `$`, `:file:`). private static func linkLabel(_ target: String) -> String { var s = Substring(target) // Strip a leading `:path:` file specifier. if s.first == ":", let end = s.dropFirst().firstIndex(of: ":") { s = s[s.index(after: end)...] } s = s.drop { "*#/$ ".contains($0) } return s.isEmpty ? target : String(s) } }