diff options
| author | Ruben Beltran del Rio <jj@r.bdr.sh> | 2026-06-14 17:07:38 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <jj@r.bdr.sh> | 2026-06-15 13:38:02 +0200 |
| commit | 3402f5764f794a60f85a152a13c4eacaa2a719f6 (patch) | |
| tree | 9fafe13da4caee303313033e313461a556f9f495 /Sources/NorgKeyboardToolbar/EditorSnippet.swift | |
Extract from Norganizer
Diffstat (limited to 'Sources/NorgKeyboardToolbar/EditorSnippet.swift')
| -rw-r--r-- | Sources/NorgKeyboardToolbar/EditorSnippet.swift | 90 |
1 files changed, 90 insertions, 0 deletions
diff --git a/Sources/NorgKeyboardToolbar/EditorSnippet.swift b/Sources/NorgKeyboardToolbar/EditorSnippet.swift new file mode 100644 index 0000000..b87d2b1 --- /dev/null +++ b/Sources/NorgKeyboardToolbar/EditorSnippet.swift @@ -0,0 +1,90 @@ +import Foundation + +/// The available editor snippets to show in the toolbar. +enum EditorSnippet: String, CaseIterable, Identifiable, Sendable { + case heading + case task + case weakReverse + case strongReverse + case code + + public var id: String { rawValue } + + /// The text inserted, with the cursor offset (in characters). + public var template: (text: String, cursor: Int) { + switch self { + case .heading: return ("* ", 2) + case .task: return ("- ( ) ", 6) + case .weakReverse: return ("---", 3) + case .strongReverse: return ("===", 3) + case .code: return ("@code\n\t\n@end", 7) + } + } + + /// Applies the snippet, returning the updated text and new cursor offset. + public func apply(to text: String, cursor: Int) -> (text: String, cursor: Int) { + let characters = Array(text) + let position = min(max(cursor, 0), characters.count) + + switch self { + case .heading where isInBareMarker(characters, at: position, markers: ["*"]): + return promoteHeading(characters, at: position) + case .task where isInBareMarker(characters, at: position, markers: ["*", "~"]): + return addTask(toMarker: characters, at: position) + default: + return insertOnNewLine(in: characters, cursor: position) + } + } + + // MARK: - Marker-aware behaviours + + /// Whether the is at a position with only `markers` and spaces, and contains + /// at least one marker. + private func isInBareMarker(_ characters: [Character], at position: Int, markers: Set<Character>) + -> Bool { + let lineStart = LineScanner.lineStart(in: characters, at: position) + let prefix = characters[lineStart..<position] + return prefix.contains(where: markers.contains) + && prefix.allSatisfy { markers.contains($0) || $0 == " " } + } + + /// Adds a `*` after the leading run of spaces and stars on the cursor's line. + private func promoteHeading(_ characters: [Character], at position: Int) -> ( + text: String, cursor: Int + ) { + let lineStart = LineScanner.lineStart(in: characters, at: position) + + var insertAt = lineStart + while insertAt < characters.count, characters[insertAt] == " " { insertAt += 1 } + while insertAt < characters.count, characters[insertAt] == "*" { insertAt += 1 } + + var result = characters + result.insert("*", at: insertAt) + return (String(result), insertAt <= position ? position + 1 : position) + } + + /// Inserts a task marker `( ) ` at the cursor, turning a heading or + /// ordered-list item into a task. + private func addTask(toMarker characters: [Character], at position: Int) -> ( + text: String, cursor: Int + ) { + var result = characters + result.insert(contentsOf: "( ) ", at: position) + return (String(result), position + 4) + } + + /// Inserts the snippet's text on a new line beneath the cursor's line. + private func insertOnNewLine(in characters: [Character], cursor position: Int) -> ( + text: String, cursor: Int + ) { + // An empty document takes the snippet as-is, with no leading newline. + guard !characters.isEmpty else { return template } + + let lineEnd = LineScanner.lineEnd(in: characters, at: position) + + let prefix = String(characters[0..<lineEnd]) + let suffix = String(characters[lineEnd...]) + let newText = prefix + "\n" + template.text + suffix + return (newText, lineEnd + 1 + template.cursor) + } +} |