johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
import Foundation
/// The task lists in what someone wrote: `- [ ] a thing to do`. Its author ticks
/// one by rewriting that mark. These are the tasks `web/src/lib/tasks.ts` finds,
/// so a tick on either rewrites the same line, and never another.
enum MarkdownTasks {
/// A task's place in what's written: which it is, from 0, and on which line.
struct Task: Hashable {
let number: Int
let line: Int
let mark: Int
let done: Bool
}
/// Each one in `text`, leaving out what's in fenced code.
static func all(_ text: String) -> [Task] {
var found: [Task] = []
var fence: [Character] = []
for (at, line) in text.components(separatedBy: "\n").enumerated() {
let letters = Array(line)
let opens = self.fence(letters)
if !fence.isEmpty {
if let opens, opens[0] == fence[0], opens.count >= fence.count { fence = [] }
} else if let opens {
fence = opens
} else if let mark = self.mark(letters) {
found.append(Task(number: found.count, line: at, mark: mark, done: letters[mark] != " "))
}
}
return found
}
/// `text` with its task `number` ticked or unticked, or nil with no such task.
static func ticked(_ text: String, _ number: Int, done: Bool) -> String? {
let found = all(text)
guard found.indices.contains(number) else { return nil }
let task = found[number]
var lines = text.components(separatedBy: "\n")
var letters = Array(lines[task.line])
letters[task.mark] = done ? "x" : " "
lines[task.line] = String(letters)
return lines.joined(separator: "\n")
}
/// What a task says, past its box.
static func words(_ line: String, _ task: Task) -> String {
String(line.dropFirst(task.mark + 2)).trimmingCharacters(in: .whitespaces)
}
private static func blank(_ c: Character) -> Bool { c == " " || c == "\t" }
/// Past any quote marks and the space round them, as those lines can be nested.
private static func pastQuotes(_ line: [Character]) -> Int {
var at = 0
while true {
var next = at
while next < line.count, blank(line[next]) { next += 1 }
guard next < line.count, line[next] == ">" else { return at }
at = next + 1
}
}
/// The fence a line opens or closes: three or more of a backtick or tilde,
/// under four spaces in.
private static func fence(_ line: [Character]) -> [Character]? {
var at = pastQuotes(line)
var spaces = 0
while at < line.count, blank(line[at]), spaces < 3 {
at += 1
spaces += 1
}
guard at < line.count, line[at] == "`" || line[at] == "~" else { return nil }
let run = line[at...].prefix { $0 == line[at] }
return run.count >= 3 ? Array(run) : nil
}
/// Where a list item's box has its mark, if the line is one.
private static func mark(_ line: [Character]) -> Int? {
var at = pastQuotes(line)
while at < line.count, blank(line[at]) { at += 1 }
guard at < line.count else { return nil }
if "-*+".contains(line[at]) {
at += 1
} else {
let digits = line[at...].prefix { $0.isASCII && $0.isNumber }.count
guard (1...9).contains(digits), at + digits < line.count, ".)".contains(line[at + digits]) else { return nil }
at += digits + 1
}
let start = at
while at < line.count, blank(line[at]) { at += 1 }
guard at > start, at + 2 < line.count, line[at] == "[", line[at + 2] == "]", " xX".contains(line[at + 1]) else { return nil }
guard at + 3 == line.count || blank(line[at + 3]) else { return nil }
return at + 1
}
}