johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
import Foundation
/// The parts of what someone wrote folded away by `<details>`, as GitHub's
/// Markdown and the web pages (`folds` in crates/knotless-serve/src/web.rs)
/// draw them: a line starting `<details>`, its `<summary>` on that line or the
/// next, then Markdown, to a `</details>` on a line of its own, or the end.
enum MarkdownFolds {
struct Fold: Equatable {
/// What its summary says, or "Details", as a browser says without one.
let summary: String
/// Whether it starts open, as `<details open>` does.
let open: Bool
/// Its lines past the summary.
let inside: Range<Int>
/// The line after its `</details>`.
let next: Int
}
/// The fold line `at` of `lines` opens, none past `end`, or nil.
static func fold(_ lines: [String], at: Int, end: Int) -> Fold? {
guard let opened = opens(trimmed(lines[at])) else { return nil }
let open = opened.0
var rest = opened.1
var from = at + 1
if rest.isEmpty, from < end, starts(trimmed(lines[from]), "<summary>") != nil {
rest = trimmed(lines[from])
from += 1
}
var summary = "Details"
if let after = starts(rest, "<summary>"), let close = rest.range(of: "</summary>", options: .caseInsensitive, range: after..<rest.endIndex) {
summary = rest[after..<close.lowerBound].trimmingCharacters(in: .whitespaces)
rest = rest[close.upperBound...].trimmingCharacters(in: .whitespaces)
}
if closes(rest) { return Fold(summary: summary, open: open, inside: from..<from, next: from) }
// Anything else beside it is no fold of ours.
guard rest.isEmpty else { return nil }
var line = from
var fenced = false
while line < end {
let words = trimmed(lines[line])
if words.hasPrefix("```") {
fenced.toggle()
} else if !fenced {
if closes(words) { return Fold(summary: summary, open: open, inside: from..<line, next: line + 1) }
// A fold inside, closed by its own `</details>`.
if let inner = fold(lines, at: line, end: end) {
line = inner.next
continue
}
}
line += 1
}
return Fold(summary: summary, open: open, inside: from..<end, next: end)
}
private static func trimmed(_ line: String) -> String { line.trimmingCharacters(in: .whitespaces) }
/// Where `line` goes on past `tag`, in any case, if it starts with it.
private static func starts(_ line: String, _ tag: String) -> String.Index? {
line.range(of: tag, options: [.caseInsensitive, .anchored])?.upperBound
}
/// Whether `line` starts with `<details>` (false) or `<details open>`
/// (true), and what follows it.
private static func opens(_ line: String) -> (Bool, String)? {
for (tag, open) in [("<details>", false), ("<details open>", true)] {
if let after = starts(line, tag) { return (open, line[after...].trimmingCharacters(in: .whitespaces)) }
}
return nil
}
private static func closes(_ line: String) -> Bool { line.caseInsensitiveCompare("</details>") == .orderedSame }
}