mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-02 10:05:41 +02:00
* fix(editor): bump hast-util-to-html to v9 so lowlight output actually serializes
Source view of fenced ```html (and any other code block falling through to
the lowlight branch in ReadonlyContent) silently rendered as un-highlighted
escaped text. Root cause was a stale dep pin: `hast-util-to-html: ^4.0.1`
predates the package's ESM/named-export rewrite — v4 only exports a CJS
default function, so the `import { toHtml } from "hast-util-to-html"` in
code-block-static.tsx:19 and readonly-content.tsx:32 resolved to
`undefined` at runtime. The try/catch in both call sites caught the
"toHtml is not a function" throw and fell through to escapeHtml plain
text, so no `.hljs-*` spans ever made it to the DOM and the syntax-color
CSS added in #2808 had nothing to attach to.
Bumping to ^9.0.5 (matches the v9 line that lowlight@3 / remark / rehype
ship in the rest of the tree) makes the named `toHtml` export available
and source-view highlighting works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): open HTML attachment in new tab + full-page preview route
Adds a third toolbar button to HtmlAttachmentPreview between Maximize and
Download: open the attachment in a new app tab (desktop) or browser tab
(web). The full-screen modal stays — they serve different scenarios:
modal for a quick "see it bigger" without leaving the issue context,
new-tab when the user wants to keep the rendered HTML around while
working on something else.
Components:
- New workspace path: `/{slug}/attachments/{id}/preview?name={filename}`.
Lives outside the (dashboard) group on web so the iframe gets the full
viewport — sidebar would defeat the point. Desktop registers the route
inside `WorkspaceRouteLayout` so workspace context resolution still
runs (no slug → no path is built).
- `packages/views/attachments/attachment-preview-page.tsx`: shared full-
page view that reuses `useAttachmentHtmlText` for the iframe srcDoc.
Sandbox stays `allow-scripts` (no allow-same-origin) — same security
posture as the inline preview.
- `HtmlAttachmentPreview`: adds Open-in-new-tab button. Routes through
`useNavigation().openInNewTab` when available (desktop), falls back to
`window.open(getShareableUrl(path))` on web. Button is hidden when no
workspace slug is in scope (shouldn't happen in practice, but the
shared component must not throw outside a workspace route).
Tests cover: desktop openInNewTab call args, web window.open fallback,
and that the failure-mode toolbar still surfaces all three actions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): drop now-stale @ts-expect-error on hast-util-to-html imports
v9 ships bundled type declarations, so the directives added for v4 trigger
TS2578 ("Unused '@ts-expect-error' directive") on CI typecheck.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
57 lines
1.7 KiB
TypeScript
57 lines
1.7 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";
|
|
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, ">");
|
|
}
|