import Foundation public protocol MarkdownRendering: Sendable { func blocks(for markdown: String) -> [RenderedMarkdownBlock] func inlineMarkdown(for source: String) -> AttributedString } public enum RenderedMarkdownBlock: Identifiable, Hashable, Sendable { case heading(id: UUID, level: Int, text: String) case paragraph(id: UUID, text: AttributedString) case codeBlock(id: UUID, language: String?, code: String) case task(id: UUID, checked: Bool, text: AttributedString) case image(id: UUID, altText: String, source: String) case blank(id: UUID) public var id: UUID { switch self { case .heading(let id, _, _): id case .paragraph(let id, _): id case .codeBlock(let id, _, _): id case .task(let id, _, _): id case .image(let id, _, _): id case .blank(let id): id } } } public struct MarkdownRenderer: MarkdownRendering { public init() {} public func blocks(for markdown: String) -> [RenderedMarkdownBlock] { var blocks: [RenderedMarkdownBlock] = [] var codeLines: [String] = [] var codeLanguage: String? var isInCodeBlock = false for line in markdown.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) { if line.hasPrefix("```") { if isInCodeBlock { blocks.append(.codeBlock(id: UUID(), language: codeLanguage, code: codeLines.joined(separator: "\n"))) codeLines.removeAll() codeLanguage = nil isInCodeBlock = false } else { isInCodeBlock = true codeLanguage = String(line.dropFirst(3)).nilIfEmpty } continue } if isInCodeBlock { codeLines.append(line) continue } blocks.append(block(for: line)) } if isInCodeBlock { blocks.append(.codeBlock(id: UUID(), language: codeLanguage, code: codeLines.joined(separator: "\n"))) } return blocks } public func inlineMarkdown(for source: String) -> AttributedString { (try? AttributedString(markdown: source)) ?? AttributedString(source) } private func block(for line: String) -> RenderedMarkdownBlock { let trimmed = line.trimmingCharacters(in: .whitespaces) guard !trimmed.isEmpty else { return .blank(id: UUID()) } if let heading = parseHeading(trimmed) { return .heading(id: UUID(), level: heading.level, text: heading.text) } if let task = parseTask(trimmed) { return .task(id: UUID(), checked: task.checked, text: inlineMarkdown(for: task.text)) } if let image = parseImage(trimmed) { return .image(id: UUID(), altText: image.altText, source: image.source) } return .paragraph(id: UUID(), text: inlineMarkdown(for: line)) } private func parseHeading(_ line: String) -> (level: Int, text: String)? { let markerCount = line.prefix { $0 == "#" }.count guard (1...6).contains(markerCount), line.dropFirst(markerCount).first == " " else { return nil } return (markerCount, String(line.dropFirst(markerCount + 1))) } private func parseTask(_ line: String) -> (checked: Bool, text: String)? { if line.hasPrefix("- [x] ") || line.hasPrefix("- [X] ") { return (true, String(line.dropFirst(6))) } if line.hasPrefix("- [ ] ") { return (false, String(line.dropFirst(6))) } return nil } private func parseImage(_ line: String) -> (altText: String, source: String)? { guard line.hasPrefix("!["), let closeAlt = line.firstIndex(of: "]") else { return nil } let afterAlt = line[line.index(after: closeAlt)...] guard afterAlt.hasPrefix("("), afterAlt.hasSuffix(")") else { return nil } let alt = String(line[line.index(line.startIndex, offsetBy: 2)..