1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
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<Int>) -> 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)
}
}
|