aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgKit/Parsers/TaskScanner.swift
blob: 39c16b5696f549ce7dca125b14be9ad9ae9a7583 (plain)
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
  }
}