diff options
| author | Ruben Beltran del Rio <jj@r.bdr.sh> | 2026-06-16 15:22:08 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <jj@r.bdr.sh> | 2026-06-16 15:22:08 +0200 |
| commit | e5e024a5f4c8709cf2a28dc3bfab79d8037516e1 (patch) | |
| tree | b458cc77b4bbd0580ed5248e90e78148b697199c | |
| parent | 3402f5764f794a60f85a152a13c4eacaa2a719f6 (diff) | |
Support all bare modifiers1.0.0
| -rw-r--r-- | Sources/NorgKeyboardToolbar/EditorSnippet.swift | 29 | ||||
| -rw-r--r-- | Tests/NorgKeyboardToolbarTests/EditorSnippetTests.swift | 38 |
2 files changed, 66 insertions, 1 deletions
diff --git a/Sources/NorgKeyboardToolbar/EditorSnippet.swift b/Sources/NorgKeyboardToolbar/EditorSnippet.swift index b87d2b1..4773eac 100644 --- a/Sources/NorgKeyboardToolbar/EditorSnippet.swift +++ b/Sources/NorgKeyboardToolbar/EditorSnippet.swift @@ -29,7 +29,7 @@ enum EditorSnippet: String, CaseIterable, Identifiable, Sendable { switch self { case .heading where isInBareMarker(characters, at: position, markers: ["*"]): return promoteHeading(characters, at: position) - case .task where isInBareMarker(characters, at: position, markers: ["*", "~"]): + case .task where isInBareDetachedModifier(characters, at: position): return addTask(toMarker: characters, at: position) default: return insertOnNewLine(in: characters, cursor: position) @@ -38,6 +38,11 @@ enum EditorSnippet: String, CaseIterable, Identifiable, Sendable { // MARK: - Marker-aware behaviours + /// The detached-modifier markers that can carry a task status, matching + /// NorgKit's task scanner. Any of these — headings, un/ordered lists, + /// quotes, definitions, footnotes — may be turned into a task in place. + private static let detachedModifierMarkers: Set<Character> = ["*", "-", "~", ">", "$", "^", ":"] + /// 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>) @@ -48,6 +53,28 @@ enum EditorSnippet: String, CaseIterable, Identifiable, Sendable { && prefix.allSatisfy { markers.contains($0) || $0 == " " } } + /// Whether the cursor sits just past a bare detached modifier: leading + /// spaces, a run of a single marker, then at least one space, with nothing + /// else before the cursor. The trailing space requirement keeps delimiters + /// such as `---` (which carry no whitespace) from being treated as tasks. + private func isInBareDetachedModifier(_ characters: [Character], at position: Int) -> Bool { + let lineStart = LineScanner.lineStart(in: characters, at: position) + var index = lineStart + while index < position, characters[index] == " " { index += 1 } + + guard index < position, Self.detachedModifierMarkers.contains(characters[index]) else { + return false + } + let marker = characters[index] + while index < position, characters[index] == marker { index += 1 } + + // A space must separate the marker run from the cursor, and the cursor may + // only sit within that run of separating spaces (i.e. before any content). + guard index < position, characters[index] == " " else { return false } + while index < position, characters[index] == " " { index += 1 } + return index == position + } + /// 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 diff --git a/Tests/NorgKeyboardToolbarTests/EditorSnippetTests.swift b/Tests/NorgKeyboardToolbarTests/EditorSnippetTests.swift index 00a9332..a1ff135 100644 --- a/Tests/NorgKeyboardToolbarTests/EditorSnippetTests.swift +++ b/Tests/NorgKeyboardToolbarTests/EditorSnippetTests.swift @@ -147,4 +147,42 @@ struct EditorSnippetTests { let result = EditorSnippet.heading.apply(to: "~ ", cursor: 2) #expect(result.text == "~ \n* ") } + + // MARK: - Task button supports every detached modifier + + @Test func taskAddsMarkerToBareUnorderedListItem() { + // "- |" -> "- ( ) |" + let result = EditorSnippet.task.apply(to: "- ", cursor: 2) + #expect(result.text == "- ( ) ") + #expect(result.cursor == 6) + } + + @Test func taskAddsMarkerToEveryDetachedModifier() { + // Quotes, definitions, footnotes and the like all carry tasks per the + // Norg spec, matching NorgKit's task scanner. + for marker in [">", "$", "^", ":"] { + let result = EditorSnippet.task.apply(to: "\(marker) ", cursor: 2) + #expect(result.text == "\(marker) ( ) ") + #expect(result.cursor == 6) + } + } + + @Test func taskAddsMarkerToNestedDetachedModifierPreservingContent() { + // ">> |Quote" -> ">> ( ) |Quote" + let result = EditorSnippet.task.apply(to: ">> Quote", cursor: 3) + #expect(result.text == ">> ( ) Quote") + #expect(result.cursor == 7) + } + + @Test func taskIgnoresWeakDelimiterMadeOfDashes() { + // "---|" is a delimiter, not a list item — start a new task line instead. + let result = EditorSnippet.task.apply(to: "---", cursor: 3) + #expect(result.text == "---\n- ( ) ") + } + + @Test func taskInsertsNewLineWhenMarkerHasNoSeparatingSpace() { + // A bare marker with no trailing space isn't a detached modifier yet. + let result = EditorSnippet.task.apply(to: ">", cursor: 1) + #expect(result.text == ">\n- ( ) ") + } } |