Add the frontend half of the block-level rich text facet work: posts and
comments now render canonical plaintext with byte-range facet annotations
(mention, link, bold, italic, strikethrough, spoiler, blockquote, heading,
code, codeBlock) instead of ignoring record.facets. Records without facets
keep the legacy Markdown path, so bridged pre-facet content is unaffected.
This must land before the tidepool bridge starts stripping markdown
markers, or quoted text would render as unmarked body text.
The tree builder (src/lib/app/richtext/facets.ts) is a pure, dependency-
free module implementing the lexicon's reader conventions for untrusted
network input: unknown $types degrade to plain text (open union), malformed
or out-of-range byte slices are dropped or clamped, offsets snap inward to
UTF-8 codepoint boundaries (no U+FFFD from mid-emoji ranges), block ranges
extend to line boundaries, quote-in-quote containment is dropped while
cross-type containment (codeBlock inside blockquote) nests, and the
backend's schema caps (200 facets / 20 features) are enforced client-side
against uncapped federated data.
Changes:
- facets.ts: buildRichText() tree builder with layered inline resolution
(spoiler > link/mention > marks), typed render tree whose nesting rules
are enforced structurally (LinkSpan children exclude links, blockquote
children exclude blockquotes), http(s)-only link hrefs, isValidDID-gated
mention routes to /u/<did> and /c/<did>
- RichText.svelte: snippet-based renderer matching the markdown renderers'
styling; text-bearing elements stay single-line so pre-wrap paragraphs
never show formatter whitespace
- RichTextSpoiler.svelte: blur-until-clicked reveal; concealed content is
inert (hidden links unclickable/unfocusable), plain span once revealed
- RichTextCodeBlock.svelte: language label + copy button with awaited
clipboard write and failure toast
- Comment.svelte / PostBody.svelte / Post.svelte: switch to RichText when
record.facets is non-empty; comment edits drop facets when content
changed (stale byte offsets silently corrupt annotations; the backend
only rejects out-of-bounds ones) and mirror the sent value locally
- facets.test.ts: 55 behavioral cases covering the malformed-input matrix,
UTF-8 offsets, mid-codepoint snapping, overlap splitting, block
containment rules, hostile-input caps, and a bridged-Lemmy conformance
fixture asserting full text preservation
Reviewed via multi-model second opinion (6 specialty agents + GPT-5.6 +
Kimi K3); all Critical/Important findings fixed and re-verified end-to-end
against the live dev stack.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>