mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-06 19:00:01 +02:00
* feat(editor): inline HTML attachment preview + ```html block render (MUL-2345) * attachment-preview-modal: switch HTML iframe sandbox from "" to "allow-scripts" so JS-driven chart libraries render. The opaque-origin iframe still cannot touch cookies, localStorage, parent state, or top-nav — only scripts run. * New shared AttachmentCard wired into the three attachment surfaces (file-card NodeView, ReadonlyContent file-card branch, comment-card standalone AttachmentList). HTML attachments now render inline via a sandboxed iframe pulled through the existing /content proxy; other kinds keep the original chrome behavior. * New HtmlBlockPreview for fenced ```html blocks in ReadonlyContent — default preview iframe, source/Copy toggle. Two-layer code+pre unwrap mirrors the Mermaid pattern; unwrap now matches on language-* class because react-markdown invokes pre before the code renderer runs. * CodeBlockView (Tiptap NodeView) renders an iframe preview for language=html with a CSS-hidden toggle to the editable source — the <NodeViewContent as="code"/> mount must remain in the tree. * Shared use-attachment-html-text hook keeps inline and modal HTML rendering on the same React Query cache. * Vitest coverage: allow-scripts assertion, attachment-card kind branches, readonly HTML iframe + Mermaid unwrap regression, NodeView editable + preview/source toggle. No backend changes; server-side text/plain + nosniff defense kept. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(editor): tighten attachment preview and pre unwrap gates (MUL-2345) Addresses Reviewer REQUEST CHANGES on PR #2790: 1. URL-only text/html attachment cards no longer surface a dead Eye button. `AttachmentCard` previously allowed preview when `previewableFromUrl=true` regardless of kind, but the modal's `tryOpen` rejects URL-only text kinds because the `/content` proxy is ID-keyed. Drop the `previewableFromUrl` prop and gate the no-attachmentId path strictly to URL-previewable media kinds (pdf/video/audio). 2. Readonly `pre` unwrap now uses exact class-token matching. The previous `className.includes("language-html")` check also fired on `language-htmlbars`, silently stripping its `<pre>` wrapper. Use `/(^|\s)language-(html|mermaid)(\s|$)/` so only the exact tokens unwrap. Regression tests: - `report.html + no attachmentId` asserts no Preview button. - `pdf URL-only` asserts Preview button still appears. - `htmlbars` / `mermaidx` fences keep their `<pre><code>` wrapper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
59 lines
1.9 KiB
TypeScript
59 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Shared HTML preview iframe.
|
|
*
|
|
* Used by:
|
|
* - InlineHtmlIframe inside AttachmentCard (HTML attachments inline preview)
|
|
* - CodeBlockView for fenced ```html blocks (editable Tiptap NodeView)
|
|
* - HtmlBlockPreview for fenced ```html blocks (ReadonlyContent)
|
|
* - AttachmentPreviewModal's full-screen HTML kind
|
|
*
|
|
* Sandbox semantics:
|
|
* sandbox="allow-scripts" (NOT "allow-same-origin")
|
|
* → iframe runs in an opaque origin: scripts execute (chart JS works),
|
|
* but cookie / localStorage / parent access / top-nav / popups / forms
|
|
* remain blocked. This is the standard "preview untrusted HTML" model
|
|
* (HTML spec §iframe sandbox, MDN, Claude artifacts, v0.dev preview).
|
|
*
|
|
* The server-side `text/plain` + `nosniff` defense at
|
|
* /api/attachments/{id}/content remains untouched — we only feed iframe.srcDoc
|
|
* the text body we fetched, never point iframe.src at the proxy URL.
|
|
*/
|
|
|
|
import { cn } from "@multica/ui/lib/utils";
|
|
|
|
interface CodeBlockIframeProps {
|
|
/** Document source for srcDoc. Empty string renders a blank frame. */
|
|
html: string;
|
|
/** Iframe title for accessibility. */
|
|
title: string;
|
|
className?: string;
|
|
/** Tailwind height token; defaults to h-[320px]. */
|
|
heightClassName?: string;
|
|
}
|
|
|
|
export function CodeBlockIframe({
|
|
html,
|
|
title,
|
|
className,
|
|
heightClassName = "h-[320px]",
|
|
}: CodeBlockIframeProps) {
|
|
return (
|
|
<iframe
|
|
// srcDoc keeps the body in the parent's process but isolated to an
|
|
// opaque origin via sandbox. Critical that we never combine
|
|
// `allow-scripts` with `allow-same-origin` — that pairing defeats the
|
|
// sandbox per the HTML spec (notes on the sandbox attribute).
|
|
srcDoc={html}
|
|
sandbox="allow-scripts"
|
|
title={title}
|
|
className={cn(
|
|
"w-full rounded-md border border-border bg-background",
|
|
heightClassName,
|
|
className,
|
|
)}
|
|
/>
|
|
);
|
|
}
|