From 12893fdc2d4a3a14cab5e0db2f3c628678fa3e43 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 18:01:12 -0700 Subject: [PATCH] Session View Animation Nucleic-Session: A8FFA1A6-443F-444A-A735-C53958D7E570 Co-authored-by: Nucleic --- .../Views/Transcript/MarkdownText.swift | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/MarkdownText.swift b/NucleicRemote/NucleicRemote/Views/Transcript/MarkdownText.swift index 4655543..82a5457 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/MarkdownText.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/MarkdownText.swift @@ -167,26 +167,54 @@ struct MarkdownText: View { .lineSpacing(4) } + /// One prose line's classification: the exact string that gets inline-parsed — the cache key + /// `prewarm` must match — paired with how it's styled. Single source of truth for both the + /// live `lineView` and the off-main `prewarm`, so a warmed key can never drift from the key + /// the layout later looks up (any drift and the warm entry would silently miss). + private enum LineStyle { + case blank + case heading(content: String, scale: CGFloat, weight: Font.Weight) + case bullet(content: String) + case plain(content: String) + + /// The string handed to `inline(_:)`, or nil for a line that renders without a parse. + var inlineContent: String? { + switch self { + case .blank: return nil + case .heading(let c, _, _), .bullet(let c), .plain(let c): return c + } + } + } + + /// Classify one raw prose line. Headings are checked before bullets (a heading marker wins), + /// and the plain case keeps the *raw* line (not the trimmed one) exactly as the old cascade + /// did — the inline parser preserves leading whitespace under `.inlineOnlyPreservingWhitespace`. + private static func classify(_ raw: String) -> LineStyle { + let trimmed = raw.trimmingCharacters(in: .whitespaces) + if trimmed.isEmpty { return .blank } + // Headings scale relative to the base prose size so the hierarchy holds at any base and + // never collapses to the body size. + if trimmed.hasPrefix("### ") { return .heading(content: String(trimmed.dropFirst(4)), scale: 1.13, weight: .semibold) } + if trimmed.hasPrefix("## ") { return .heading(content: String(trimmed.dropFirst(3)), scale: 1.28, weight: .bold) } + if trimmed.hasPrefix("# ") { return .heading(content: String(trimmed.dropFirst(2)), scale: 1.5, weight: .bold) } + if let bullet = bulletContent(trimmed) { return .bullet(content: bullet) } + return .plain(content: raw) + } + @ViewBuilder private func lineView(_ raw: String) -> some View { - let trimmed = raw.trimmingCharacters(in: .whitespaces) - if trimmed.isEmpty { + switch Self.classify(raw) { + case .blank: Color.clear.frame(height: 3) - } else if trimmed.hasPrefix("### ") { - // Headings scale relative to the base prose size so the hierarchy holds - // at any base and never collapses to the body size. - inline(String(trimmed.dropFirst(4))).font(.system(size: bodySize * 1.13, weight: .semibold)) - } else if trimmed.hasPrefix("## ") { - inline(String(trimmed.dropFirst(3))).font(.system(size: bodySize * 1.28, weight: .bold)) - } else if trimmed.hasPrefix("# ") { - inline(String(trimmed.dropFirst(2))).font(.system(size: bodySize * 1.5, weight: .bold)) - } else if let bullet = Self.bulletContent(trimmed) { + case .heading(let content, let scale, let weight): + inline(content).font(.system(size: bodySize * scale, weight: weight)) + case .bullet(let content): HStack(alignment: .firstTextBaseline, spacing: 6) { Text("•").foregroundStyle(.secondary) - inline(bullet) + inline(content) } - } else { - inline(raw) + case .plain(let content): + inline(content) } }