~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
73 lines · 3.2 KB Raw History
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 }
}