1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
import Foundation
import Testing
@testable import NorgKit
struct TaskScannerTests {
private let file = URL(filePath: "/tmp/notes.norg")
@Test func scansTasksWithLineNumbers() {
let content = """
* Project
- ( ) first task
- (x) done task
plain text
~ (-) ordered pending
"""
let tasks = TaskScanner.scan(content: content, fileURL: file)
#expect(tasks.count == 3)
#expect(tasks[0].line == 1)
#expect(tasks[0].status == .undone)
#expect(tasks[0].text == "first task")
#expect(tasks[1].status == .done)
#expect(tasks[2].status == .pending)
}
@Test func ignoresNonTaskLines() {
let tasks = TaskScanner.scan(content: "- just a list item\n* heading", fileURL: file)
#expect(tasks.isEmpty)
}
@Test func parsesSingleLineIntoTask() {
let task = TaskScanner.task(in: "- (!) urgent thing", fileURL: file, line: 9)
#expect(task?.line == 9)
#expect(task?.status == .urgent)
#expect(task?.text == "urgent thing")
#expect(task?.fileURL == file)
}
@Test func parsesSingleLineReturnsNilForNonTask() {
#expect(TaskScanner.task(in: "- plain item", fileURL: file, line: 0) == nil)
}
@Test func scansEveryStatusBearingModifier() {
// The Norg spec allows any detached modifier to carry a TODO status. We
// model the structural, nestable, and range-able modifiers (`%`
// attributes are intentionally not treated as tasks).
let content = """
* (x) heading task
- ( ) list task
~ (-) ordered task
> (!) quote task
$ (?) definition task
^ (=) footnote task
: (+) table cell task
"""
let tasks = TaskScanner.scan(content: content, fileURL: file)
#expect(tasks.count == 7)
#expect(
tasks.map(\.status) == [
.done, .undone, .pending, .urgent, .needsInput, .onHold, .recurring,
])
#expect(tasks[3].text == "quote task")
#expect(tasks[4].text == "definition task")
}
@Test func stripsInlineMarkupFromTaskText() {
let tasks = TaskScanner.scan(content: "- ( ) buy *milk*", fileURL: file)
#expect(tasks.first?.text == "buy milk")
}
@Test func updatesStatusMarker() {
let content = "- ( ) toggle me"
let updated = TaskScanner.updatedContent(content, line: 0, to: .done)
#expect(updated == "- (x) toggle me")
}
@Test func updatesStatusPreservingIndentation() {
let content = "* Heading\n -- (x) nested"
let updated = TaskScanner.updatedContent(content, line: 1, to: .cancelled)
#expect(updated == "* Heading\n -- (_) nested")
}
@Test func updateReturnsNilWhenNoMarker() {
#expect(TaskScanner.updatedContent("plain line", line: 0, to: .done) == nil)
}
@Test func updateReturnsNilForOutOfRangeLine() {
#expect(TaskScanner.updatedContent("- ( ) a", line: 5, to: .done) == nil)
}
@Test func updatesStatusOnLaterLineLeavingRestIntact() {
let content = "* heading\n> ( ) quote task\n- plain item"
let updated = TaskScanner.updatedContent(content, line: 1, to: .urgent)
#expect(updated == "* heading\n> (!) quote task\n- plain item")
}
@Test func scansCrlfContent() {
let tasks = TaskScanner.scan(content: "- ( ) first\r\n- (x) second\r\n", fileURL: file)
#expect(tasks.count == 2)
#expect(tasks[0].text == "first")
#expect(tasks[1].status == .done)
}
@Test func updatePreservesCrlfLineEndings() {
let content = "- ( ) a\r\n- ( ) b"
let updated = TaskScanner.updatedContent(content, line: 1, to: .done)
#expect(updated == "- ( ) a\r\n- (x) b")
}
}
|