mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-10 02:16:45 +08:00
* feat(server): accept JSON Lines in the attachment text preview proxy The viewer now shows .jsonl / .ndjson files as a tree, so the text proxy's whitelist takes them too (extension and application/x-ndjson). The whitelist test gains the CSV / TSV / log / JSON Lines cases and says it is mirrored by the client-side table in packages/views/editor/utils/preview.test.ts. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(views): table, tree, numbered-line and viewport views in the attachment viewer MUL-7650, step 3 of MUL-7642. Fills in how the full-window viewer shows the file types agents produce most. - CSV / TSV (`table` kind): parsed to a table on the shared DataTable — pinned header, virtualized rows, resizable columns, a frozen row-number column. Clicking a header sorts ascending / descending / file order; numbers sort numerically, empty cells stay last, all-number columns align right. `;`-separated exports are sniffed. DataTable headers now carry aria-sort when the table sorts through TanStack's own state. - JSON / JSON Lines / YAML (`structured` kind): a collapsible tree with inline previews of collapsed records and paged long lists; Tree / Raw in the top bar. A file that does not parse falls back to the source and says why. YAML alias expansion stays capped by the parser. - Code, text and logs: numbered lines (the number is a pseudo-element, so copying skips it) with a sticky gutter, and a wrap toggle. Logs and plain text wrap by default, code does not; the reader's choice carries across the sequence. - HTML: Fit / Desktop 1440 / Tablet 768 / Phone 390. A device width lays the document out at that width and scales it down when the stage is narrower, with the size and scale shown under it; switching never remounts the iframe. A source toggle shows the HTML with line numbers. - Open in new tab works for every previewable kind: /{slug}/attachments/{id} /preview loads the record by id and renders the viewer's own top bar and stage (AttachmentPreviewStandalone), keeping the desktop scroll restore for HTML. - Whitelist: .jsonl / .ndjson join the text types on both sides, and the client test mirrors the server's case table. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]>
99 lines
3.2 KiB
TypeScript
99 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* CodeBlockStatic — read-only lowlight-highlighted code block.
|
|
*
|
|
* Used by:
|
|
* - AttachmentPreviewModal's text kinds, with numbered lines and a wrap
|
|
* toggle.
|
|
* - 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, type CSSProperties } from "react";
|
|
import { toHtml } from "hast-util-to-html";
|
|
import { cn } from "@multica/ui/lib/utils";
|
|
import { highlightCode } from "./syntax-highlight";
|
|
import { highlightToLines } from "../common/task-transcript/diff-highlight";
|
|
import "./styles/code.css";
|
|
|
|
interface CodeBlockStaticProps {
|
|
language: string | undefined;
|
|
body: string;
|
|
className?: string;
|
|
/** Numbers every line in a gutter that stays put while the code scrolls. */
|
|
lineNumbers?: boolean;
|
|
/**
|
|
* With `lineNumbers`: wrap long lines under their own number instead of
|
|
* scrolling sideways. Plain blocks always wrap.
|
|
*/
|
|
wrap?: boolean;
|
|
}
|
|
|
|
export function CodeBlockStatic({
|
|
language,
|
|
body,
|
|
className,
|
|
lineNumbers = false,
|
|
wrap = false,
|
|
}: CodeBlockStaticProps) {
|
|
const { html, lineCount } = useMemo(() => {
|
|
const code = body.replace(/\n$/, "");
|
|
if (lineNumbers) {
|
|
// Highlighted as one block, then split — a multi-line comment or
|
|
// string keeps its colour on every line it spans.
|
|
const lines =
|
|
highlightToLines(code, language) ?? code.split("\n").map(escapeHtml);
|
|
return { html: numberedLinesHtml(lines), lineCount: lines.length };
|
|
}
|
|
try {
|
|
const tree = highlightCode(code, language);
|
|
return { html: toHtml(tree) as string, lineCount: 0 };
|
|
} catch {
|
|
// Keep an unexpected highlighter failure from breaking the preview.
|
|
return { html: escapeHtml(code), lineCount: 0 };
|
|
}
|
|
}, [body, language, lineNumbers]);
|
|
|
|
return (
|
|
<pre
|
|
className={cn(
|
|
"rich-text-editor m-0 overflow-auto text-body",
|
|
lineNumbers && "code-lines",
|
|
lineNumbers && wrap && "code-lines-wrap",
|
|
className,
|
|
)}
|
|
style={
|
|
lineNumbers
|
|
? ({ "--line-digits": String(lineCount).length } as CSSProperties)
|
|
: undefined
|
|
}
|
|
>
|
|
<code
|
|
className={cn("hljs", language && `language-${language}`)}
|
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
/>
|
|
</pre>
|
|
);
|
|
}
|
|
|
|
// One element per line, numbered through a data attribute: the number is a
|
|
// pseudo-element, so selecting and copying the code never picks it up. Built
|
|
// as one string and set once — a log file can run to tens of thousands of
|
|
// lines, far too many to hand React as elements.
|
|
function numberedLinesHtml(lines: string[]): string {
|
|
let html = "";
|
|
for (let i = 0; i < lines.length; i++) {
|
|
html += `<span class="code-line" data-line="${i + 1}"><span class="code-text">${lines[i]}</span></span>`;
|
|
}
|
|
return html;
|
|
}
|
|
|
|
function escapeHtml(s: string): string {
|
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
}
|