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 } }