import Foundation import SwiftUI /// README rendering follows the file's format. Org is parsed as Org; /// everything else keeps the established Markdown renderer. struct ReadmeView: View { let name: String let content: String /// Format follows the file name, never a guess at the content. var isOrg: Bool { name.lowercased().hasSuffix(".org") } var body: some View { if isOrg { OrgDocumentView(document: OrgDocument.parse(content)) } else { MarkdownView(markdown: content) } } } nonisolated struct OrgDocument: Sendable, Hashable { let blocks: [OrgBlock] static func parse(_ source: String) -> OrgDocument { var result: [OrgBlock] = [] var lines = source.components(separatedBy: "\n")[...] while let raw = lines.first { let line = raw.trimmingCharacters(in: .whitespaces) if line.isEmpty { lines = lines.dropFirst() continue } let lower = line.lowercased() if lower.hasPrefix("#+title:") { result.append(.heading(level: 1, text: String(line.dropFirst(8)).trimmingCharacters(in: .whitespaces))) lines = lines.dropFirst() continue } if lower.hasPrefix("#+begin_src") || lower.hasPrefix("#+begin_example") { let language = lower.hasPrefix("#+begin_src") ? String(line.dropFirst(11)).trimmingCharacters(in: .whitespaces) : "" lines = lines.dropFirst() var body: [String] = [] while let next = lines.first, !next.trimmingCharacters(in: .whitespaces).lowercased().hasPrefix("#+end_") { body.append(next) lines = lines.dropFirst() } if !lines.isEmpty { lines = lines.dropFirst() } result.append(.code(language: language, text: body.joined(separator: "\n"))) continue } if lower == "#+begin_quote" { lines = lines.dropFirst() var body: [String] = [] while let next = lines.first, next.trimmingCharacters(in: .whitespaces).lowercased() != "#+end_quote" { body.append(next.trimmingCharacters(in: .whitespaces)) lines = lines.dropFirst() } if !lines.isEmpty { lines = lines.dropFirst() } result.append(.quote(body.joined(separator: " "))) continue } if line.first == "*" { let stars = line.prefix(while: { $0 == "*" }).count if line.dropFirst(stars).first == " " { result.append(.heading( level: min(stars, 4), text: String(line.dropFirst(stars)).trimmingCharacters(in: .whitespaces) )) lines = lines.dropFirst() continue } } if line.hasPrefix("- ") || line.hasPrefix("+ ") { var items: [String] = [] while let next = lines.first?.trimmingCharacters(in: .whitespaces), next.hasPrefix("- ") || next.hasPrefix("+ ") { items.append(String(next.dropFirst(2))) lines = lines.dropFirst() } result.append(.bullet(items)) continue } if line.range(of: #"^\d+[.)] "#, options: .regularExpression) != nil { var items: [String] = [] while let next = lines.first?.trimmingCharacters(in: .whitespaces), let range = next.range(of: #"^\d+[.)] "#, options: .regularExpression) { items.append(String(next[range.upperBound...])) lines = lines.dropFirst() } result.append(.ordered(items)) continue } if lower.hasPrefix("#+") { lines = lines.dropFirst() // document metadata, not prose continue } var paragraph: [String] = [] while let next = lines.first { let trimmed = next.trimmingCharacters(in: .whitespaces) let nextLower = trimmed.lowercased() if trimmed.isEmpty || trimmed.hasPrefix("* ") || trimmed.hasPrefix("- ") || trimmed.hasPrefix("+ ") || nextLower.hasPrefix("#+") { break } paragraph.append(trimmed) lines = lines.dropFirst() } result.append(.paragraph(paragraph.joined(separator: " "))) } return OrgDocument(blocks: result) } } nonisolated enum OrgBlock: Sendable, Hashable { case heading(level: Int, text: String) case code(language: String, text: String) case quote(String) case bullet([String]) case ordered([String]) case paragraph(String) } private struct OrgDocumentView: View { let document: OrgDocument var body: some View { VStack(alignment: .leading, spacing: 12) { ForEach(Array(document.blocks.enumerated()), id: \.offset) { _, block in blockView(block) } } } @ViewBuilder private func blockView(_ block: OrgBlock) -> some View { switch block { case .heading(let level, let text): inline(text) .font(headingFont(level)) .padding(.top, level <= 2 ? 8 : 4) case .code(let language, let text): VStack(alignment: .leading, spacing: 4) { if !language.isEmpty { Text(language) .font(.gbMono(.caption2)) .foregroundStyle(.secondary) } ScrollView(.horizontal) { Text(text) .font(.gbMono(.caption)) .padding(10) } } .background(Color.gbCodeBackground, in: RoundedRectangle(cornerRadius: 2)) case .quote(let text): HStack(spacing: 10) { RoundedRectangle(cornerRadius: 2).fill(.tertiary).frame(width: 3) inline(text).foregroundStyle(.secondary) } .fixedSize(horizontal: false, vertical: true) case .bullet(let items): list(items, ordered: false) case .ordered(let items): list(items, ordered: true) case .paragraph(let text): inline(text) } } private func list(_ items: [String], ordered: Bool) -> some View { VStack(alignment: .leading, spacing: 4) { ForEach(Array(items.enumerated()), id: \.offset) { index, item in HStack(alignment: .firstTextBaseline, spacing: 8) { Text(ordered ? "\(index + 1)." : "•") inline(item) } } } } private func inline(_ source: String) -> Text { var markdown = source markdown = replacing(markdown, pattern: #"\[\[([^\]]+)\]\[([^\]]+)\]\]"#, template: "[$2]($1)") markdown = replacing(markdown, pattern: #"\[\[([^\]]+)\]\]"#, template: "<$1>") markdown = replacing(markdown, pattern: #"=([^=]+)="#, template: "`$1`") if let attributed = try? AttributedString( markdown: markdown, options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace) ) { return Text(attributed) } return Text(source) } private func replacing(_ source: String, pattern: String, template: String) -> String { guard let expression = try? NSRegularExpression(pattern: pattern) else { return source } let range = NSRange(source.startIndex..., in: source) return expression.stringByReplacingMatches(in: source, range: range, withTemplate: template) } private func headingFont(_ level: Int) -> Font { switch level { case 1: .gbSans(.title2).bold() case 2: .gbSans(.title3).bold() case 3: .gbSans(.headline) default: .gbSans(.subheadline).bold() } } }