aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgKit/Helpers/TextHelper.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/NorgKit/Helpers/TextHelper.swift')
-rw-r--r--Sources/NorgKit/Helpers/TextHelper.swift95
1 files changed, 95 insertions, 0 deletions
diff --git a/Sources/NorgKit/Helpers/TextHelper.swift b/Sources/NorgKit/Helpers/TextHelper.swift
new file mode 100644
index 0000000..b0d45fe
--- /dev/null
+++ b/Sources/NorgKit/Helpers/TextHelper.swift
@@ -0,0 +1,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)
+ }
+}