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
|
import Foundation
/// Text helpers that avoid `CharacterSet` and allocations for performance.
enum TextHelper {
/// Splits text into lines as slices.
static func lineSlices(_ text: String) -> [Substring] {
var result: [Substring] = []
enumerateLines(in: text) { line, _ in result.append(line) }
return result
}
/// Invokes `body` once per line, passing a `Substring` view and its index.
static func enumerateLines(in text: String, _ body: (Substring, Int) -> Void) {
let utf8 = text.utf8
let end = utf8.endIndex
var lineStart = utf8.startIndex
var i = utf8.startIndex
var index = 0
while i < end {
if utf8[i] == 0x0A {
body(text[lineStart..<strippedLineEnd(utf8, from: lineStart, to: i)], index)
index += 1
lineStart = utf8.index(after: i)
}
i = utf8.index(after: i)
}
body(text[lineStart..<strippedLineEnd(utf8, from: lineStart, to: end)], index)
}
/// The end index of a line `[start, newline)`, backed up by one when the last
/// byte is a carriage return. Operating on the UTF-8 view avoids the
/// grapheme decode that `Substring.last` would pay on every line.
private static func strippedLineEnd(
_ utf8: String.UTF8View, from start: String.Index, to newline: String.Index
) -> String.Index {
guard newline > start else { return newline }
let last = utf8.index(before: newline)
return utf8[last] == 0x0D ? last : newline
}
/// Returns the line at `index` (zero-based) as a `Substring`, or `nil` when
/// there is no such line. Trailing `\r` is stripped and line counting matches
/// ``lines(_:)`` / ``enumerateLines(in:_:)``. Stops as soon as the line is
/// found, so locating an early line in a large document is cheap.
static func line(in text: String, at index: Int) -> Substring? {
guard index >= 0 else { return nil }
let utf8 = text.utf8
let end = utf8.endIndex
var lineStart = utf8.startIndex
var i = utf8.startIndex
var current = 0
while i < end {
if utf8[i] == 0x0A {
if current == index {
return text[lineStart..<strippedLineEnd(utf8, from: lineStart, to: i)]
}
current += 1
lineStart = utf8.index(after: i)
}
i = utf8.index(after: i)
}
return current == index
? text[lineStart..<strippedLineEnd(utf8, from: lineStart, to: end)] : nil
}
/// Trims leading and trailing whitespace from a slice using
/// `Character.isWhitespace`, avoiding the `CharacterSet` bridging cost that
/// `trimmingCharacters(in:)` pays per call. The result is a slice of the
/// input, so nothing is copied. This is the single trimming primitive used
/// across the parser, scanner, and detached-modifier recogniser.
static func whitespaceTrimmed(_ s: Substring) -> Substring {
// Trim over the UTF-8 view: Norg indentation and trailing space is always
// ASCII whitespace, whose bytes are all `< 0x80` and so never part of a
// multi-byte scalar — the trimmed bounds stay on scalar boundaries. This
// avoids the grapheme decode `Character.isWhitespace` pays on every line.
let utf8 = s.utf8
var start = utf8.startIndex
var end = utf8.endIndex
while start < end, ASCIIHelper.isWhitespace(utf8[start]) { start = utf8.index(after: start) }
while start < end {
let prev = utf8.index(before: end)
guard ASCIIHelper.isWhitespace(utf8[prev]) else { break }
end = prev
}
return s[start..<end]
}
/// Trims leading and trailing whitespace.
static func whitespaceTrimmed(_ s: String) -> String {
let trimmed = whitespaceTrimmed(s[...])
return trimmed.startIndex == s.startIndex && trimmed.endIndex == s.endIndex
? s : String(trimmed)
}
}
|