The phone learns to read whole blocks — CardBodyView renders BodyMarkup's full tree, and the title falls in line
The read view's inline-only AttributedString shim gives way to a real block renderer over BodyMarkup — the same parse the Mac Preview trusts, whose swift-markdown dependency project.yml said was already riding along for exactly this surface. Headings, nested lists with static task checkboxes, quotes, GFM tables with per-column alignment and clamped colspans, sideways-scrolling code blocks, literal HTML, dividers, and tappable absolute links (relative ones stay prose — no folder to resolve against). The title block sheds its inner padding so its leading edge sits flush with the body; the tint now grows outward via a negative background inset instead of pushing the text in. One rich fixture card body enriched to exercise every block kind; Mac fixture round-trip suites verified green against it. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -75,18 +75,32 @@ struct CardDetailScreen: View {
|
||||
/// card's own `background` swatch — passive display only, no swatches to tap here. An
|
||||
/// unrecognized colour name (the same leniency `ItemIconView`/`CardPalette` document for
|
||||
/// themselves) just skips the tint rather than guessing at one.
|
||||
///
|
||||
/// **Alignment constraint.** The title's leading edge has to land exactly where the body's
|
||||
/// paragraphs land, one block below — both sit at nothing but the `ScrollView`'s own outer
|
||||
/// `.padding()`, so this view carries no padding of its own around the icon/text content. The
|
||||
/// tint still wants breathing room rather than painting flush against the letterforms, but
|
||||
/// that room can't come from padding the content — that's exactly what would reintroduce the
|
||||
/// indent this fixes. It comes from the tint shape growing *outward* past the content's own
|
||||
/// bounds instead, via a negative inset in `.background`, which is invisible to layout: the
|
||||
/// row's frame — and so the title's leading edge — is unaffected by how far its background
|
||||
/// paints past it. A card with no tint (most of them) renders with zero indent, flush with the
|
||||
/// body below; a tinted card's title sits in exactly the same place, just with a soft wash
|
||||
/// bleeding past its edges.
|
||||
@ViewBuilder
|
||||
private func titleBlock(for card: Card) -> some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
ItemIconView(icon: card.icon.value, iconColor: card.iconColor.value)
|
||||
titleText(for: card)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
(tintColor(for: card)?.opacity(0.15) ?? Color.clear),
|
||||
in: RoundedRectangle(cornerRadius: 12)
|
||||
)
|
||||
.background {
|
||||
if let tintColor = tintColor(for: card) {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(tintColor.opacity(0.15))
|
||||
.padding(-10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -105,21 +119,11 @@ struct CardDetailScreen: View {
|
||||
card.background.value.flatMap { CardPalette.color(named: $0, in: CardPalette.backgrounds) }
|
||||
}
|
||||
|
||||
/// The body, rendered as Markdown — see `MarkdownBlocks` for why this is a per-paragraph
|
||||
/// `AttributedString(markdown:)` pass rather than a full block parser.
|
||||
/// The body, rendered as Markdown — see `CardBodyView` for the block-by-block renderer this
|
||||
/// delegates to, built on the same `BodyMarkup` model the Mac's Preview surface parses with.
|
||||
@ViewBuilder
|
||||
private func bodyBlock(for card: Card) -> some View {
|
||||
let paragraphs = MarkdownBlocks.paragraphs(in: card.body)
|
||||
if paragraphs.isEmpty {
|
||||
Text("No description")
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
ForEach(Array(paragraphs.enumerated()), id: \.offset) { _, paragraph in
|
||||
Text(MarkdownBlocks.rendered(paragraph))
|
||||
}
|
||||
}
|
||||
}
|
||||
CardBodyView(body: card.body)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -137,34 +141,3 @@ struct CardDetailScreen: View {
|
||||
date.formatted(date: .abbreviated, time: .shortened)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dependency-free Markdown rendering for a card body: no `swift-markdown` block parser here, just
|
||||
/// `Foundation`'s own `AttributedString(markdown:options:)` run once per paragraph.
|
||||
///
|
||||
/// **Why per-paragraph.** `AttributedString`'s built-in parser has no "render this as a sequence of
|
||||
/// blocks" mode short of `.full`, which produces a block tree this view would still have to walk —
|
||||
/// no simpler than doing the walk ourselves, and it collapses the very newlines a card body relies
|
||||
/// on to separate paragraphs. Splitting on blank lines first and parsing each paragraph with
|
||||
/// `.inlineOnlyPreservingWhitespace` gets both halves of what a card body needs cheaply: inline
|
||||
/// emphasis/links/code render as styled runs, and the newlines *inside* a paragraph (soft line
|
||||
/// breaks) survive as literal whitespace instead of being folded into a single space.
|
||||
enum MarkdownBlocks {
|
||||
|
||||
/// Splits `body` on runs of one or more blank lines — Markdown's own paragraph boundary — and
|
||||
/// trims each block, so a body with trailing blank lines or extra spacing between paragraphs
|
||||
/// doesn't render phantom empty blocks.
|
||||
static func paragraphs(in body: String) -> [String] {
|
||||
body
|
||||
.components(separatedBy: "\n\n")
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
}
|
||||
|
||||
/// One paragraph, inline-parsed. Malformed Markdown (an unterminated code span, say) falls back
|
||||
/// to the raw paragraph text rather than dropping it — the same "never lose the author's text"
|
||||
/// posture `CardPalette.color(named:in:)` takes for an unrecognized colour name.
|
||||
static func rendered(_ paragraph: String) -> AttributedString {
|
||||
let options = AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
||||
return (try? AttributedString(markdown: paragraph, options: options)) ?? AttributedString(paragraph)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user