From 3402f5764f794a60f85a152a13c4eacaa2a719f6 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Sun, 14 Jun 2026 17:07:38 +0200 Subject: Extract from Norganizer --- Sources/NorgKeyboardToolbar/EditorSnippet.swift | 90 +++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 Sources/NorgKeyboardToolbar/EditorSnippet.swift (limited to 'Sources/NorgKeyboardToolbar/EditorSnippet.swift') 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) + -> Bool { + let lineStart = LineScanner.lineStart(in: characters, at: position) + let prefix = characters[lineStart.. ( + 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..