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
|
import Foundation
/// Scans Norg source for tasks and rewrites their status markers.
/// Handy to extract TODOs quicker than the parser can.
public enum TaskScanner {
private static let markers = ASCIIByteSet("*-~>$^:")
/// Extracts every task in `content`, attributing each to `fileURL`.
public static func scan(content: String, fileURL: URL) -> [NorgTask] {
var tasks: [NorgTask] = []
TextHelper.enumerateLines(in: content) { line, index in
if let task = task(in: line, fileURL: fileURL, line: index) {
tasks.append(task)
}
}
return tasks
}
/// Parses a single line into a task, if it carries a status extension.
public static func task(in raw: String, fileURL: URL, line: Int) -> NorgTask? {
task(in: raw[...], fileURL: fileURL, line: line)
}
/// Span-free fast path over a line slice.
static func task(in line: Substring, fileURL: URL, line index: Int) -> NorgTask? {
guard let m = DetachedModifier.parse(line, accepting: markers), let status = m.status else {
return nil
}
return NorgTask(
fileURL: fileURL,
line: index,
status: status,
text: NorgInlineParser.plainText(m.content)
)
}
/// Returns `content` with the status marker on `line` replaced by `status`,
/// or `nil` if the line carries no recognisable task marker. Line endings are
/// preserved, so a CRLF file round-trips unchanged.
public static func updatedContent(_ content: String, line index: Int, to status: TaskStatus)
-> String? {
guard let line = TextHelper.line(in: content, at: index),
let m = DetachedModifier.parse(line, accepting: markers),
let statusIndex = m.statusIndex
else { return nil }
var updated = content
updated.replaceSubrange(statusIndex...statusIndex, with: status.rawValue)
return updated
}
}
|