From 501fdce29e4d2c46c4708ad7f44056878e0db3fa Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Tue, 16 Jun 2026 12:08:21 +0200 Subject: Initial extraction from Norganize --- Sources/NorgKit/Helpers/TextHelper.swift | 95 ++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 Sources/NorgKit/Helpers/TextHelper.swift (limited to 'Sources/NorgKit/Helpers/TextHelper.swift') 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.. 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.. 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.. String { + let trimmed = whitespaceTrimmed(s[...]) + return trimmed.startIndex == s.startIndex && trimmed.endIndex == s.endIndex + ? s : String(trimmed) + } +} -- cgit