a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
93 lines
3.2 KiB
Swift
93 lines
3.2 KiB
Swift
import SwiftUI
|
|
|
|
/// Lightweight markdown renderer for assistant messages.
|
|
///
|
|
/// Handles fenced code blocks as monospaced cards and renders everything else
|
|
/// through SwiftUI's native AttributedString markdown (bold, italics, inline
|
|
/// code, links). Deliberately not a full CommonMark implementation.
|
|
struct MarkdownText: View {
|
|
let text: String
|
|
|
|
init(_ text: String) {
|
|
self.text = text
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
ForEach(Array(segments.enumerated()), id: \.offset) { _, segment in
|
|
switch segment {
|
|
case .prose(let prose):
|
|
Text(attributed(prose))
|
|
.font(.body)
|
|
.foregroundStyle(Theme.textPrimary)
|
|
.textSelection(.enabled)
|
|
case .code(let code, _):
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
Text(code)
|
|
.font(Theme.mono(12))
|
|
.foregroundStyle(Theme.textSecondary)
|
|
.padding(12)
|
|
.textSelection(.enabled)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(Theme.surfaceElevated)
|
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum Segment {
|
|
case prose(String)
|
|
case code(String, language: String?)
|
|
}
|
|
|
|
private var segments: [Segment] {
|
|
var result: [Segment] = []
|
|
var prose: [String] = []
|
|
var code: [String] = []
|
|
var language: String?
|
|
var inCode = false
|
|
|
|
for line in text.split(separator: "\n", omittingEmptySubsequences: false) {
|
|
if line.hasPrefix("```") {
|
|
if inCode {
|
|
result.append(.code(code.joined(separator: "\n"), language: language))
|
|
code = []
|
|
inCode = false
|
|
} else {
|
|
let joined = prose.joined(separator: "\n")
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if !joined.isEmpty {
|
|
result.append(.prose(joined))
|
|
}
|
|
prose = []
|
|
language = line.dropFirst(3).isEmpty ? nil : String(line.dropFirst(3))
|
|
inCode = true
|
|
}
|
|
} else if inCode {
|
|
code.append(String(line))
|
|
} else {
|
|
prose.append(String(line))
|
|
}
|
|
}
|
|
if inCode {
|
|
result.append(.code(code.joined(separator: "\n"), language: language))
|
|
} else {
|
|
let joined = prose.joined(separator: "\n")
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if !joined.isEmpty {
|
|
result.append(.prose(joined))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
private func attributed(_ string: String) -> AttributedString {
|
|
(try? AttributedString(
|
|
markdown: string,
|
|
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
|
)) ?? AttributedString(string)
|
|
}
|
|
}
|