mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 09:56:47 +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]>
66 lines
2.0 KiB
TypeScript
66 lines
2.0 KiB
TypeScript
/**
|
|
* Parses a `structured` attachment (JSON, JSON Lines, YAML) for the viewer's
|
|
* tree. A failure carries the parser's own message — it names the line — so
|
|
* the viewer can say why it fell back to the source.
|
|
*/
|
|
|
|
import { parseAllDocuments } from "yaml";
|
|
import type { StructuredFormat } from "./preview";
|
|
|
|
export type StructuredParse =
|
|
| { ok: true; value: unknown }
|
|
| { ok: false; message: string };
|
|
|
|
function errorMessage(error: unknown): string {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|
|
|
|
function parseJsonLines(text: string): StructuredParse {
|
|
const values: unknown[] = [];
|
|
const lines = text.split(/\r\n|\n|\r/);
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i]!;
|
|
if (line.trim() === "") continue;
|
|
try {
|
|
values.push(JSON.parse(line));
|
|
} catch (error) {
|
|
return { ok: false, message: `Line ${i + 1}: ${errorMessage(error)}` };
|
|
}
|
|
}
|
|
return { ok: true, value: values };
|
|
}
|
|
|
|
function parseYaml(text: string): StructuredParse {
|
|
// Every document in the stream: a multi-document file (`---` separated,
|
|
// e.g. Kubernetes manifests) shows as a list of its documents.
|
|
const documents = parseAllDocuments(text);
|
|
const values: unknown[] = [];
|
|
for (const document of documents) {
|
|
const [firstError] = document.errors;
|
|
if (firstError) return { ok: false, message: firstError.message };
|
|
// toJS enforces yaml's alias expansion cap, so a "billion laughs" file
|
|
// throws here instead of exhausting memory.
|
|
values.push(document.toJS());
|
|
}
|
|
if (values.length === 0) return { ok: true, value: null };
|
|
return { ok: true, value: values.length === 1 ? values[0] : values };
|
|
}
|
|
|
|
export function parseStructured(
|
|
text: string,
|
|
format: StructuredFormat,
|
|
): StructuredParse {
|
|
try {
|
|
switch (format) {
|
|
case "json":
|
|
return { ok: true, value: JSON.parse(text) };
|
|
case "jsonl":
|
|
return parseJsonLines(text);
|
|
case "yaml":
|
|
return parseYaml(text);
|
|
}
|
|
} catch (error) {
|
|
return { ok: false, message: errorMessage(error) };
|
|
}
|
|
}
|