Bundle of follow-ups to the HTML/SVG fence renderer landed earlier in
this PR. Each item came out of either the parallel reviewer pass or a
manual Playwright probe against the live Studio with an Anthropic
provider attached.
Sanitizer:
- filter, mask, and clip-path are now in FORBID_ATTR. They accept
url(https://...) values and the CSS engine still fetches that URL
when the SVG renders, which previously slipped past the FORBID
list.
- href and xlink:href are no longer blanket-forbidden; they survive
only when the value is a same-document fragment (href="#id"),
which is what textPath, gradient, and use refs need. External
schemes are dropped via a uponSanitizeAttribute hook so a beacon
href cannot make it through.
- The hook approach replaces DOMPurify's ALLOWED_URI_REGEXP, which
also filtered presentation attrs (cx, cy, r, fill, width, height)
and rendered circles with r=0.
SVG preview:
- Inner stylesheet caps both max-width AND max-height so a square
viewBox (200x200) scaled to the container width no longer
overflows the fixed-height iframe and clips at the bottom.
HTML preview:
- srcdoc carries a defense-in-depth meta-CSP (default-src 'none',
connect-src 'none', frame-src 'none', img-src data: blob:,
script-src 'self' 'unsafe-inline', style-src 'self' 'unsafe-inline').
The host CSP already blocks inline scripts; this layer also blocks
network egress, nested iframes, and form submission so a future
host-CSP relaxation does not silently turn the preview into an
exfiltration channel.
- Sandbox grows allow-modals so alert/confirm/prompt are not
silently no-oped if the host CSP ever permits inline scripts.
- Pop-out spacer now uses the live HTML iframe height instead of
hardcoded DEFAULT_PREVIEW_HEIGHT, so popping out a short preview
does not leave a 500px hole in the chat bubble.
- autoHeight resets on source change so a long-running session that
swaps from a tall demo to a short one no longer keeps the previous
iframe size during the gap before the new doc posts its height.
Streaming and a11y:
- parseIncompleteCodeFence parses an in-flight open fence (no closing
backticks yet). markdown-text falls back to it when streaming is
incomplete, so the advertised isIncomplete -> Code-tab-lock path
actually runs.
- Tab buttons gain aria-controls / aria-labelledby wiring and a
roving tabindex so the WAI-ARIA tab pattern is complete.
- Pop-out modal gets role="dialog" and aria-modal.
Tooling:
- vitest now runs in the Studio Frontend CI workflow so sanitizer or
renderer regressions block the gate.
- test-setup shims URL.createObjectURL / revokeObjectURL for jsdom in
case future iframe work needs it.
- frame-src in the host CSP is now declared explicitly as 'self' so
a future change that loosens it leaves a visible diff for review.
Tests added: ARIA wiring, SVG height fit, srcdoc meta-CSP shape,
incomplete-fence helper, filter/mask/clip-path attr stripping, safe
fragment-href survival, external-href rejection. Vitest passes 21/21,
tsc -b and vite build are clean.