aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgKit/Parsers/TaskScanner.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/NorgKit/Parsers/TaskScanner.swift')
-rw-r--r--Sources/NorgKit/Parsers/TaskScanner.swift52
1 files changed, 52 insertions, 0 deletions
diff --git a/Sources/NorgKit/Parsers/TaskScanner.swift b/Sources/NorgKit/Parsers/TaskScanner.swift
new file mode 100644
index 0000000..39c16b5
--- /dev/null
+++ b/Sources/NorgKit/Parsers/TaskScanner.swift
@@ -0,0 +1,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
+ }
+}