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/ASCIIByteSet.swift | 30 ++++++++++ Sources/NorgKit/Helpers/ASCIIHelper.swift | 5 ++ Sources/NorgKit/Helpers/TextHelper.swift | 95 ++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 Sources/NorgKit/Helpers/ASCIIByteSet.swift create mode 100644 Sources/NorgKit/Helpers/ASCIIHelper.swift create mode 100644 Sources/NorgKit/Helpers/TextHelper.swift (limited to 'Sources/NorgKit/Helpers') diff --git a/Sources/NorgKit/Helpers/ASCIIByteSet.swift b/Sources/NorgKit/Helpers/ASCIIByteSet.swift new file mode 100644 index 0000000..59efbe7 --- /dev/null +++ b/Sources/NorgKit/Helpers/ASCIIByteSet.swift @@ -0,0 +1,30 @@ +/// A membership test over the ASCII range (bytes `0`–`127`) +/// We use this for performance, since markers are always ASCII, and this this +/// faster than using Character. +struct ASCIIByteSet { + private let low: UInt64 + private let high: UInt64 + + /// Builds a set from the ASCII values. + init(_ characters: String) { + var lo: UInt64 = 0 + var hi: UInt64 = 0 + for byte in characters.utf8 { + if byte < 64 { + lo |= 1 << UInt64(byte) + } else if byte < 128 { + hi |= 1 << UInt64(byte - 64) + } + } + low = lo + high = hi + } + + /// Whether `byte` is in the set. + @inline(__always) + func contains(_ byte: UInt8) -> Bool { + if byte < 64 { return low & (1 << UInt64(byte)) != 0 } + if byte < 128 { return high & (1 << UInt64(byte &- 64)) != 0 } + return false + } +} diff --git a/Sources/NorgKit/Helpers/ASCIIHelper.swift b/Sources/NorgKit/Helpers/ASCIIHelper.swift new file mode 100644 index 0000000..98cce42 --- /dev/null +++ b/Sources/NorgKit/Helpers/ASCIIHelper.swift @@ -0,0 +1,5 @@ +struct ASCIIHelper { + static func isWhitespace(_ b: UInt8) -> Bool { + b == 0x20 || (0x09...0x0D).contains(b) + } +} 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