import Foundation indirect enum MDInline: Sendable { case text(String) case code(String) case strong([MDInline]) case em([MDInline]) case image(src: String, alt: String) case link(href: String, [MDInline]) case mark(MDMark, [MDInline]) case lineBreak } enum MDMark: Sendable { case ok, warn, danger } private let marks: [String: MDMark] = [ "✓": .ok, "✔": .ok, "✔️": .ok, "✅": .ok, "☑": .ok, "☑️": .ok, "~": .warn, "≈": .warn, "〜": .warn, "⚠": .warn, "⚠️": .warn, "🟡": .warn, "x": .danger, "X": .danger, "×": .danger, "✗": .danger, "✘": .danger, "✕": .danger, "✖": .danger, "✖️": .danger, "❌": .danger, ] private let boldWrap = Re(#"^(\*\*|__)(.*)\1$"#) struct MDItem: Sendable { var choice: String? var choiceIndex = 0 var raw = "" var inline: [MDInline] = [] var blocks: [MDBlock] = [] } indirect enum MDBlock: Sendable { case paragraph([MDInline]) case heading(level: Int, [MDInline]) case rule case quote([MDBlock]) case list(ordered: Bool, start: Int, items: [MDItem], grid: Bool) case code(String) case diff([String]) case details(summary: [MDInline], body: [MDBlock]) case diagram(String) case table(head: [[MDInline]], rows: [[[MDInline]]]) } struct Re: @unchecked Sendable { let re: NSRegularExpression init(_ pattern: String, caseless: Bool = false) { re = try! NSRegularExpression(pattern: pattern, options: caseless ? [.caseInsensitive] : []) } func test(_ s: String) -> Bool { re.firstMatch(in: s, range: NSRange(s.startIndex..., in: s)) != nil } func exec(_ s: String) -> [String?]? { match(s)?.groups } func match(_ s: String) -> (range: Range, groups: [String?])? { guard let m = re.firstMatch(in: s, range: NSRange(s.startIndex..., in: s)) else { return nil } let groups = (0.. String? in let r = m.range(at: i) return r.location == NSNotFound ? nil : String(s[Range(r, in: s)!]) } return (Range(m.range, in: s)!, groups) } } private let choiceMarker = Re(#"^(\([ *]\)|\[[ xX]\])\s+(\S.*?)\s*$"#) private let detailsOpen = Re(#"^\s*
\s*(?:(.*?))?\s*$"#, caseless: true) private let summaryLine = Re(#"^\s*(.*?)\s*$"#, caseless: true) private let detailsClose = Re(#"^\s*
\s*$"#, caseless: true) private let fence = Re(#"^\s*(```|~~~)\s*(\S*)"#) private let heading = Re(#"^(#{1,6})\s+(.*)$"#) private let bullet = Re(#"^(\s*)[-*+]\s+(.*)$"#) private let ordered = Re(#"^(\s*)(\d+)[.)]\s+(.*)$"#) private let quote = Re(#"^\s*>\s?(.*)$"#) private let rule = Re(#"^\s*([-*_])(\s*\1){2,}\s*$"#) private let imageOnly = Re(#"^\s*!\[[^\]]*\]\((<[^>]*>|[^)]*)\)\s*$"#) private let tableSep = Re(#"^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$"#) private let inlineToken = Re(#"`([^`]+)`|\*\*(.+?)\*\*|__(.+?)__|\*([^*\s][^*]*?)\*|(?]+)>|([^)]+))\)|\[([^\]]+)\]\(([^)\s]+)\)|(https?://[^\s<>()]+[^\s<>().,;:!?'"])"#) func parseMarkdown(_ src: String, choices: Bool = false) -> [MDBlock] { let text = src.replacingOccurrences(of: "\r\n", with: "\n").replacingOccurrences(of: "\r", with: "\n") return blocks(text.components(separatedBy: "\n"), choices) } func parseInline(_ src: String) -> [MDInline] { inline(src) } private func trimStart(_ s: String) -> Substring { s.drop { $0 == " " || $0 == "\t" } } private func blocks(_ lines: [String], _ choices: Bool) -> [MDBlock] { var out: [MDBlock] = [] var i = 0 while i < lines.count { let line = lines[i] if trimmed(line).isEmpty { i += 1 continue } if let f = fence.exec(line) { let marker = f[1]! var body: [String] = [] i += 1 while i < lines.count && !trimStart(lines[i]).hasPrefix(marker) { body.append(lines[i]) i += 1 } i += 1 let lang = f[2]!.lowercased() if lang == "mermaid" { out.append(.diagram(body.joined(separator: "\n"))) continue } if lang == "diff" || lang == "patch" { out.append(.diff(body)) continue } out.append(.code(body.joined(separator: "\n"))) continue } if detailsClose.test(line) { i += 1 continue } if let d = detailsOpen.exec(line) { var title = d[1] i += 1 if title == nil && i < lines.count, let s = summaryLine.exec(lines[i]) { title = s[1] i += 1 } var body: [String] = [] var depth = 1 while i < lines.count { let l = lines[i] i += 1 if detailsOpen.test(l) { depth += 1 } else if detailsClose.test(l) { depth -= 1 if depth == 0 { break } } body.append(l) } let summary = title.map(trimmed) ?? "" out.append(.details(summary: inline(summary.isEmpty ? "Details" : summary), body: blocks(body, choices))) continue } if let hd = heading.exec(line) { out.append(.heading(level: min(hd[1]!.count + 2, 6), inline(hd[2]!))) i += 1 continue } if rule.test(line) { out.append(.rule) i += 1 continue } if quote.test(line) { var body: [String] = [] while i < lines.count, let q = quote.exec(lines[i]) { body.append(q[1]!) i += 1 } out.append(.quote(blocks(body, false))) continue } if bullet.test(line) || ordered.test(line) { let (list, next) = parseList(lines, i, choices) out.append(list) i = next continue } if line.contains("|") && i + 1 < lines.count && tableSep.test(lines[i + 1]) { var rows: [[String]] = [cells(line)] i += 2 while i < lines.count && lines[i].contains("|") && !trimmed(lines[i]).isEmpty { rows.append(cells(lines[i])) i += 1 } out.append(.table(head: rows[0].map(inline), rows: rows.dropFirst().map { $0.map(inline) })) continue } var para: [String] = [] while i < lines.count && !trimmed(lines[i]).isEmpty && !startsBlock(lines, i) { para.append(trimmed(lines[i])) i += 1 } out.append(.paragraph(joinLines(para))) } return out } private func startsBlock(_ lines: [String], _ i: Int) -> Bool { let line = lines[i] return fence.test(line) || detailsOpen.test(line) || detailsClose.test(line) || heading.test(line) || rule.test(line) || quote.test(line) || bullet.test(line) || ordered.test(line) || (line.contains("|") && i + 1 < lines.count && tableSep.test(lines[i + 1])) } private func joinLines(_ lines: [String]) -> [MDInline] { var out: [MDInline] = [] for (n, l) in lines.enumerated() { if n > 0 { out.append(.lineBreak) } out += inline(l) } return out } private func indentOf(_ line: String) -> Int { line.count - trimStart(line).count } private func parseList(_ lines: [String], _ start: Int, _ choices: Bool) -> (MDBlock, Int) { let first = lines[start] let isOrdered = !bullet.test(first) let base = indentOf(first) let startNumber = isOrdered ? Int(ordered.exec(first)![2]!) ?? 1 : 1 let marker = isOrdered ? ordered : bullet var items: [MDItem] = [] var i = start var imageChoices = !isOrdered && choices while i < lines.count { let line = lines[i] guard let m = marker.exec(line), indentOf(line) == base else { break } let text = isOrdered ? m[3]! : m[2]! var body = [text] i += 1 while i < lines.count { let next = lines[i] if trimmed(next).isEmpty { if i + 1 < lines.count, indentOf(lines[i + 1]) > base, !trimmed(lines[i + 1]).isEmpty { body.append("") i += 1 continue } break } if indentOf(next) <= base && (bullet.test(next) || ordered.test(next) || startsBlock(lines, i)) { break } body.append(indentOf(next) > base ? String(next.dropFirst(min(indentOf(next), base + 2))) : trimmed(next)) i += 1 } let detail = body.dropFirst().filter { !trimmed($0).isEmpty } imageChoices = imageChoices && choiceMarker.test(text) && detail.count == 1 && imageOnly.test(detail.first!) items.append(item(body, isOrdered ? false : choices)) while i < lines.count && trimmed(lines[i]).isEmpty && i + 1 < lines.count && indentOf(lines[i + 1]) == base && marker.test(lines[i + 1]) { i += 1 } } return (.list(ordered: isOrdered, start: startNumber, items: items, grid: imageChoices), i) } private func item(_ body: [String], _ choices: Bool) -> MDItem { var out = MDItem(raw: body[0]) if choices, let m = choiceMarker.exec(body[0]) { out.choice = m[2] } let first = out.choice != nil ? [] : [body[0]] let rest = Array(body.dropFirst()) if rest.indices.allSatisfy({ !startsBlock(body, $0 + 1) }) && !rest.contains("") { out.inline = joinLines((first + rest).map(trimmed)) return out } var head = first var k = 1 while k < body.count && body[k] != "" && !startsBlock(body, k) { head.append(trimmed(body[k])) k += 1 } out.inline = joinLines(head) out.blocks = blocks(Array(body[k...]), choices) return out } private func cells(_ line: String) -> [String] { var s = Substring(trimmed(line)) if s.hasPrefix("|") { s = s.dropFirst() } if s.hasSuffix("|") { s = s.dropLast() } return s.split(separator: "|", omittingEmptySubsequences: false).map { trimmed(String($0)) } } private func inline(_ src: String) -> [MDInline] { let t = trimmed(src) if let mark = marks[trimmed(boldWrap.exec(t)?[2] ?? t)] { return [.mark(mark, spans(src))] } return spans(src) } private func spans(_ src: String) -> [MDInline] { var out: [MDInline] = [] var rest = src while !rest.isEmpty { guard let (range, g) = inlineToken.match(rest) else { out.append(.text(rest)) break } if range.lowerBound > rest.startIndex { out.append(.text(String(rest[..