mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-06 01:50:14 +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>
58 lines
1.8 KiB
TypeScript
58 lines
1.8 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* CodeBlockStatic — read-only lowlight-highlighted code block.
|
|
*
|
|
* Used by:
|
|
* - AttachmentPreviewModal's text-kind fallback (extracted from there).
|
|
* - HtmlBlockPreview's "source" toggle in ReadonlyContent.
|
|
*
|
|
* NOT used by Tiptap's editable code-block NodeView: that path must keep
|
|
* `<NodeViewContent as="code" />` so the user can continue typing into the
|
|
* code block. Replacing it with a static lowlight component would freeze
|
|
* the content and desync ProseMirror state from the DOM.
|
|
*/
|
|
|
|
import { useMemo } from "react";
|
|
import { createLowlight, common } from "lowlight";
|
|
// @ts-expect-error -- hast-util-to-html has no bundled type declarations
|
|
import { toHtml } from "hast-util-to-html";
|
|
import { cn } from "@multica/ui/lib/utils";
|
|
|
|
const lowlight = createLowlight(common);
|
|
|
|
interface CodeBlockStaticProps {
|
|
language: string | undefined;
|
|
body: string;
|
|
className?: string;
|
|
}
|
|
|
|
export function CodeBlockStatic({ language, body, className }: CodeBlockStaticProps) {
|
|
const html = useMemo(() => {
|
|
const code = body.replace(/\n$/, "");
|
|
try {
|
|
const tree = language
|
|
? lowlight.highlight(language, code)
|
|
: lowlight.highlightAuto(code);
|
|
return toHtml(tree) as string;
|
|
} catch {
|
|
// Unknown language tag — fall back to escaped plain text so we don't
|
|
// crash on an esoteric extension.
|
|
return escapeHtml(code);
|
|
}
|
|
}, [body, language]);
|
|
|
|
return (
|
|
<pre className={cn("rich-text-editor m-0 overflow-auto text-sm", className)}>
|
|
<code
|
|
className={cn("hljs", language && `language-${language}`)}
|
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
/>
|
|
</pre>
|
|
);
|
|
}
|
|
|
|
function escapeHtml(s: string): string {
|
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
}
|