@ai-matrx/rich-content 0.2.9 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/copy/copy-commands.d.ts +42 -0
- package/dist/copy/copy-commands.js +119 -0
- package/dist/copy/markdown-copy-html.d.ts +3 -0
- package/dist/copy/markdown-copy-html.js +8 -0
- package/dist/copy/markdown-readable-text.d.ts +4 -0
- package/dist/copy/markdown-readable-text.js +62 -0
- package/dist/display/blocks/json/json-tabular-utils.d.ts +7 -5
- package/dist/display/blocks/json/json-tabular-utils.js +26 -17
- package/dist/display/blocks/table/StreamingTableRenderer.js +11 -16
- package/dist/display/chat-markdown/BasicMarkdownContent.js +8 -6
- package/dist/display/chat-markdown/ConfigurableMarkdownContent.js +8 -6
- package/dist/display/chat-markdown/block-registry/BlockComponentRegistry.js +2 -0
- package/dist/display/chat-markdown/block-registry/block-dispatch.js +2 -0
- package/dist/display/tables/MarkdownTable.js +11 -16
- package/dist/display/tables/TableWithSeparatedControls.js +11 -16
- package/dist/display/tables/table-csv.d.ts +7 -0
- package/dist/display/tables/table-csv.js +19 -0
- package/dist/host/app-bindings.d.ts +32 -0
- package/dist/host/app-bindings.js +40 -0
- package/dist/host/feedback-subject.d.ts +16 -0
- package/dist/host/feedback-subject.js +0 -0
- package/dist/host/manual-copy.d.ts +7 -0
- package/dist/host/manual-copy.js +11 -0
- package/dist/host/registry.d.ts +2 -0
- package/dist/host/registry.js +10 -0
- package/dist/host/session-knob.d.ts +5 -0
- package/dist/host/session-knob.js +7 -0
- package/dist/host/types.d.ts +24 -0
- package/dist/rich-document/RegistryContextMenu.d.ts +29 -0
- package/dist/rich-document/RegistryContextMenu.js +40 -0
- package/dist/rich-document/RichDocument.d.ts +78 -0
- package/dist/rich-document/RichDocument.js +218 -0
- package/dist/rich-document/RichDocumentActionProvider.d.ts +18 -0
- package/dist/rich-document/RichDocumentActionProvider.js +18 -0
- package/dist/rich-document/RichDocumentActionSurface.d.ts +11 -0
- package/dist/rich-document/RichDocumentActionSurface.js +59 -0
- package/dist/rich-document/RichDocumentActions.d.ts +12 -0
- package/dist/rich-document/RichDocumentActions.js +29 -0
- package/dist/rich-document/actions/hostActions.d.ts +5 -0
- package/dist/rich-document/actions/hostActions.js +31 -0
- package/dist/rich-document/actions/labels.d.ts +9 -0
- package/dist/rich-document/actions/labels.js +8 -0
- package/dist/rich-document/actions/markdownTable.d.ts +9 -0
- package/dist/rich-document/actions/markdownTable.js +39 -0
- package/dist/rich-document/actions/provider.d.ts +58 -0
- package/dist/rich-document/actions/provider.js +245 -0
- package/dist/rich-document/actions/resumePendingAuthAction.d.ts +3 -0
- package/dist/rich-document/actions/resumePendingAuthAction.js +31 -0
- package/dist/rich-document/actions/sources/index.d.ts +9 -0
- package/dist/rich-document/actions/sources/index.js +22 -0
- package/dist/rich-document/actions/sources/raw.d.ts +4 -0
- package/dist/rich-document/actions/sources/raw.js +35 -0
- package/dist/rich-document/actions/useRichDocumentProvider.d.ts +1 -0
- package/dist/rich-document/actions/useRichDocumentProvider.js +13 -0
- package/dist/rich-document/host-types.d.ts +51 -0
- package/dist/rich-document/host-types.js +0 -0
- package/dist/rich-document/registrations.d.ts +1 -0
- package/dist/rich-document/registrations.js +13 -0
- package/dist/rich-document/review/applyTargets.d.ts +16 -0
- package/dist/rich-document/review/applyTargets.js +32 -0
- package/dist/rich-document/review/proposedEdit.d.ts +22 -0
- package/dist/rich-document/review/proposedEdit.js +48 -0
- package/dist/rich-document/review/protectedSpans.d.ts +14 -0
- package/dist/rich-document/review/protectedSpans.js +61 -0
- package/dist/rich-document/runtime/actionSurfacesStore.d.ts +30 -0
- package/dist/rich-document/runtime/actionSurfacesStore.js +79 -0
- package/dist/rich-document/runtime/contextMenuFor.d.ts +6 -0
- package/dist/rich-document/runtime/contextMenuFor.js +7 -0
- package/dist/rich-document/runtime/providerBridge.d.ts +20 -0
- package/dist/rich-document/runtime/providerBridge.js +23 -0
- package/dist/rich-document/runtime/useActionSurfaceProvider.d.ts +33 -0
- package/dist/rich-document/runtime/useActionSurfaceProvider.js +214 -0
- package/dist/rich-document/search/ContentFindControl.d.ts +8 -0
- package/dist/rich-document/search/ContentFindControl.js +46 -0
- package/dist/rich-document/search/RenderedFindBar.d.ts +8 -0
- package/dist/rich-document/search/RenderedFindBar.js +110 -0
- package/dist/rich-document/search/renderedFind.d.ts +1 -0
- package/dist/rich-document/search/renderedFind.js +1 -0
- package/dist/rich-document/types.d.ts +384 -0
- package/dist/rich-document/types.js +0 -0
- package/dist/rich-document/variants/ActionBar.d.ts +17 -0
- package/dist/rich-document/variants/ActionBar.js +48 -0
- package/dist/rich-document/variants/MenuVariant.d.ts +15 -0
- package/dist/rich-document/variants/MenuVariant.js +17 -0
- package/dist/rich-document/variants/MiniActionBar.d.ts +5 -0
- package/dist/rich-document/variants/MiniActionBar.js +11 -0
- package/dist/rich-document/variants/RegistryActionList.d.ts +17 -0
- package/dist/rich-document/variants/RegistryActionList.js +94 -0
- package/dist/rich-document/variants/RegistryActionMenu.d.ts +24 -0
- package/dist/rich-document/variants/RegistryActionMenu.js +74 -0
- package/dist/rich-document/variants/shared/AlchemyDocumentMenu.d.ts +7 -0
- package/dist/rich-document/variants/shared/AlchemyDocumentMenu.js +83 -0
- package/dist/rich-document/variants/shared/OpenOneMenuButton.d.ts +9 -0
- package/dist/rich-document/variants/shared/OpenOneMenuButton.js +70 -0
- package/dist/rich-document/variants/shared/StandaloneOneMenu.d.ts +8 -0
- package/dist/rich-document/variants/shared/StandaloneOneMenu.js +32 -0
- package/dist/rich-document/variants/shared/categories.d.ts +4 -0
- package/dist/rich-document/variants/shared/categories.js +31 -0
- package/dist/rich-document/variants/shared/iconMap.d.ts +2 -0
- package/dist/rich-document/variants/shared/iconMap.js +73 -0
- package/dist/rich-document/variants/shared/menuStructure.d.ts +81 -0
- package/dist/rich-document/variants/shared/menuStructure.js +268 -0
- package/dist/rich-document/variants/shared/runAction.d.ts +17 -0
- package/dist/rich-document/variants/shared/runAction.js +24 -0
- package/dist/selection-toolbar/SelectionToolbarFrame.d.ts +64 -0
- package/dist/selection-toolbar/SelectionToolbarFrame.js +191 -0
- package/dist/selection-toolbar/common-actions.d.ts +2 -0
- package/dist/selection-toolbar/common-actions.js +103 -0
- package/dist/selection-toolbar/selection-actions.d.ts +126 -0
- package/dist/selection-toolbar/selection-actions.js +240 -0
- package/dist/selection-toolbar/selection-copy.d.ts +19 -0
- package/dist/selection-toolbar/selection-copy.js +98 -0
- package/dist/selection-toolbar/selection-shape.d.ts +1 -0
- package/dist/selection-toolbar/selection-shape.js +15 -0
- package/dist/selection-toolbar/selection-zones.d.ts +90 -0
- package/dist/selection-toolbar/selection-zones.js +85 -0
- package/dist/selection-toolbar/textarea-caret.d.ts +8 -0
- package/dist/selection-toolbar/textarea-caret.js +69 -0
- package/dist/utils/alchemy-icon-keys.d.ts +18 -0
- package/dist/utils/alchemy-icon-keys.js +29 -0
- package/dist/utils/rendered-find.d.ts +9 -0
- package/dist/utils/rendered-find.js +68 -0
- package/package.json +21 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.10
|
|
4
|
+
|
|
5
|
+
- Markdown copy goes through the host's `app.CopyButtons` (the copy menu; matrx-frontend ALC-19).
|
|
6
|
+
- Table CSV/TSV and workbook export through Alchemy (`toDelimitedText`, `buildFile`); `display/tables/table-csv`.
|
|
7
|
+
|
|
8
|
+
### Consumer action
|
|
9
|
+
|
|
10
|
+
- Register `app.CopyButtons` (matrx-frontend: `components/agent-copy/CopyButtons`).
|
|
11
|
+
|
|
3
12
|
## 0.2.9
|
|
4
13
|
|
|
5
14
|
AP-2 (2026-10-07) — the kit and Alchemy doors are the only doors in this package (aidream guard `apps/shared/scripts/check-shared-doors.mjs`). New dependency `@ai-matrx/alchemy`; `html2canvas` dependency removed. Every table/code/SVG/JSON/mermaid/diagram download saves through `@ai-matrx/kit/download` (`downloadFile` / `downloadUrl`) — the tables' "Download Again" toast action now really downloads again (it clicked a revoked URL before). Link/URL/mermaid copies go through `@ai-matrx/kit/clipboard` `copyText` (copied state from its boolean; `copyMermaidSource`/`copyMermaidSvg` throw when refused, as before); ImageBlock's image copy uses `useClipboard().copyImage`. The diagram PNG export captures through `@ai-matrx/alchemy/operate/capture` `captureElement` with `safeColors` (the local getComputedStyle patch is gone). No consumer action.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { markdownToReadableText } from "./markdown-readable-text.js";
|
|
2
|
+
interface CopyOptions {
|
|
3
|
+
isMarkdown?: boolean;
|
|
4
|
+
formatForGoogleDocs?: boolean;
|
|
5
|
+
formatForWordPress?: boolean;
|
|
6
|
+
formatJson?: boolean;
|
|
7
|
+
showHtmlPreview?: boolean;
|
|
8
|
+
includeThinking?: boolean;
|
|
9
|
+
onSuccess?: () => void;
|
|
10
|
+
onError?: (err: unknown) => void;
|
|
11
|
+
onShowHtmlPreview?: (html: string) => boolean | void | Promise<boolean | void>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Copy CONTENT (markdown, text, or any JSON-able value) through Alchemy's serialization and browser transport.
|
|
15
|
+
* Not the plain-string copy — that is `copyToClipboard` in `@/lib/clipboard/copy`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function copyContent(content: unknown, options?: CopyOptions): Promise<boolean>;
|
|
18
|
+
export type CopyFlavor = "default" | "markdown" | "text";
|
|
19
|
+
export { markdownToReadableText };
|
|
20
|
+
/** The knob that decides what a single Copy click puts in a plain field. */
|
|
21
|
+
export declare const COPY_DEFAULT_FLAVOR_KNOB: {
|
|
22
|
+
readonly feature: "copy";
|
|
23
|
+
readonly key: "default_flavor";
|
|
24
|
+
};
|
|
25
|
+
/** The plain-text flavor of a single Copy click for this session ("markdown" until the knob answers). */
|
|
26
|
+
export declare function defaultCopyFlavor(): "markdown" | "text";
|
|
27
|
+
export interface RichCopyOptions {
|
|
28
|
+
/** Keep `<thinking>` / reasoning blocks (default: removed). */
|
|
29
|
+
includeThinking?: boolean;
|
|
30
|
+
/** Toast on success: a label, or false for none. Default: the flavor's label. */
|
|
31
|
+
toast?: string | false;
|
|
32
|
+
}
|
|
33
|
+
/** The bytes a flavor writes — pure enough to test; the HTML is loaded at click time. */
|
|
34
|
+
export declare function richCopyPlainText(markdown: string, flavor: CopyFlavor, defaultFlavor?: "markdown" | "text"): string;
|
|
35
|
+
/**
|
|
36
|
+
* Write the plain flavor, or html + plain in one clipboard item (kit's `copyRich`; `html` may be a
|
|
37
|
+
* promise so the item is created inside the click). A refused rich write degrades to plain text and
|
|
38
|
+
* says so; when nothing lands the person is handed the text to copy by hand.
|
|
39
|
+
*/
|
|
40
|
+
export declare function writeClipboardFlavors(plain: string, html?: Promise<string> | string): Promise<boolean>;
|
|
41
|
+
/** THE copy of rich content. Returns true when the clipboard holds it. */
|
|
42
|
+
export declare function copyRichContent(markdown: string, flavor?: CopyFlavor, options?: RichCopyOptions): Promise<boolean>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBrowserTransport,
|
|
3
|
+
createDraft,
|
|
4
|
+
capture,
|
|
5
|
+
formatAdapter,
|
|
6
|
+
normalizeTransferJson,
|
|
7
|
+
serialize
|
|
8
|
+
} from "@ai-matrx/alchemy/operate";
|
|
9
|
+
import { copyRich, copyText } from "@ai-matrx/kit/clipboard";
|
|
10
|
+
import { showManualCopy } from "../host/manual-copy.js";
|
|
11
|
+
import { toast } from "../host/toast.js";
|
|
12
|
+
import { getSessionKnob } from "../host/session-knob.js";
|
|
13
|
+
import { markdownToReadableText } from "./markdown-readable-text.js";
|
|
14
|
+
import { formattedCopyHtml, removeThinkingContent } from "./markdown-copy-html.js";
|
|
15
|
+
async function copyContent(content, options = {}) {
|
|
16
|
+
let capturedText;
|
|
17
|
+
try {
|
|
18
|
+
let payload;
|
|
19
|
+
if (typeof content === "string") {
|
|
20
|
+
const text = options.includeThinking ? content : removeThinkingContent(content);
|
|
21
|
+
payload = { kind: options.isMarkdown ? "markdown" : "text", text };
|
|
22
|
+
if (options.formatJson !== false) {
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(text);
|
|
26
|
+
} catch {
|
|
27
|
+
parsed = void 0;
|
|
28
|
+
}
|
|
29
|
+
if (parsed !== void 0)
|
|
30
|
+
payload = { kind: "json", value: normalizeTransferJson(parsed) };
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
payload = { kind: "json", value: normalizeTransferJson(content) };
|
|
34
|
+
}
|
|
35
|
+
const signal = new AbortController().signal;
|
|
36
|
+
const { snapshot } = await capture(payload, signal);
|
|
37
|
+
const draft = createDraft(snapshot);
|
|
38
|
+
const rich = payload.kind === "markdown" && (options.formatForGoogleDocs || options.formatForWordPress);
|
|
39
|
+
const artifact = rich ? await (await formatAdapter("rich-html").load()).build(
|
|
40
|
+
draft,
|
|
41
|
+
{ profile: options.formatForGoogleDocs ? "google-docs" : "generic" },
|
|
42
|
+
signal
|
|
43
|
+
) : serialize(
|
|
44
|
+
draft,
|
|
45
|
+
payload.kind === "json" ? options.formatJson === false ? "compact-json" : "json" : "plain"
|
|
46
|
+
);
|
|
47
|
+
capturedText = artifact.plainText;
|
|
48
|
+
if (options.showHtmlPreview) {
|
|
49
|
+
if (!options.onShowHtmlPreview || !artifact.html)
|
|
50
|
+
throw new Error(
|
|
51
|
+
"HTML preview requires a formatted Markdown source and a preview callback."
|
|
52
|
+
);
|
|
53
|
+
const previewDelivered = await options.onShowHtmlPreview(artifact.html);
|
|
54
|
+
if (previewDelivered === false) return false;
|
|
55
|
+
options.onSuccess?.();
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const outcome = await createBrowserTransport().copy(artifact, signal);
|
|
59
|
+
if (outcome.status === "success") {
|
|
60
|
+
options.onSuccess?.();
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (outcome.status === "degraded") {
|
|
64
|
+
toast.info(`Copied as plain text: ${outcome.reason}`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (outcome.status === "cancelled") return false;
|
|
68
|
+
throw new Error(outcome.message);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (capturedText !== void 0) {
|
|
71
|
+
showManualCopy({ text: capturedText });
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
options.onError?.(error);
|
|
75
|
+
if (!options.onError)
|
|
76
|
+
toast.error(
|
|
77
|
+
error instanceof Error ? error.message : "Could not copy content"
|
|
78
|
+
);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const COPY_DEFAULT_FLAVOR_KNOB = { feature: "copy", key: "default_flavor" };
|
|
83
|
+
function defaultCopyFlavor() {
|
|
84
|
+
return getSessionKnob(COPY_DEFAULT_FLAVOR_KNOB) === "text" ? "text" : "markdown";
|
|
85
|
+
}
|
|
86
|
+
const FLAVOR_TOAST = {
|
|
87
|
+
default: "Copied",
|
|
88
|
+
markdown: "Markdown copied",
|
|
89
|
+
text: "Text copied"
|
|
90
|
+
};
|
|
91
|
+
function richCopyPlainText(markdown, flavor, defaultFlavor = "markdown") {
|
|
92
|
+
const plainFlavor = flavor === "default" ? defaultFlavor : flavor;
|
|
93
|
+
return plainFlavor === "text" ? markdownToReadableText(markdown) : markdown;
|
|
94
|
+
}
|
|
95
|
+
async function writeClipboardFlavors(plain, html) {
|
|
96
|
+
const landed = html === void 0 ? await copyText(plain) : await copyRich(
|
|
97
|
+
{ text: plain, html },
|
|
98
|
+
{ onDegraded: (reason) => toast.info(`Copied without formatting: ${reason}.`) }
|
|
99
|
+
);
|
|
100
|
+
if (!landed) showManualCopy({ text: plain });
|
|
101
|
+
return landed;
|
|
102
|
+
}
|
|
103
|
+
async function copyRichContent(markdown, flavor = "default", options = {}) {
|
|
104
|
+
const source = options.includeThinking ? markdown : removeThinkingContent(markdown);
|
|
105
|
+
const plain = richCopyPlainText(source, flavor, defaultCopyFlavor());
|
|
106
|
+
const ok = await writeClipboardFlavors(plain, flavor === "default" ? formattedCopyHtml(source) : void 0);
|
|
107
|
+
const label = options.toast === void 0 ? FLAVOR_TOAST[flavor] : options.toast;
|
|
108
|
+
if (ok && label) toast.success(label);
|
|
109
|
+
return ok;
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
COPY_DEFAULT_FLAVOR_KNOB,
|
|
113
|
+
copyContent,
|
|
114
|
+
copyRichContent,
|
|
115
|
+
defaultCopyFlavor,
|
|
116
|
+
markdownToReadableText,
|
|
117
|
+
richCopyPlainText,
|
|
118
|
+
writeClipboardFlavors
|
|
119
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { removeThinkingContent } from "@ai-matrx/print/markdown";
|
|
2
|
+
function formattedCopyHtml(markdown) {
|
|
3
|
+
return import("@ai-matrx/print/markdown").then(({ markdownToHtml }) => markdownToHtml(markdown));
|
|
4
|
+
}
|
|
5
|
+
export {
|
|
6
|
+
formattedCopyHtml,
|
|
7
|
+
removeThinkingContent
|
|
8
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Inline markup → its words. Code spans are kept as their text. */
|
|
2
|
+
export declare function readableInline(line: string): string;
|
|
3
|
+
/** Markdown → readable plain text (envelopes unwrapped first). */
|
|
4
|
+
export declare function markdownToReadableText(markdown: string): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { unwrapKindEnvelopes } from "@ai-matrx/content-ir/source";
|
|
2
|
+
import {
|
|
3
|
+
findTableEnd,
|
|
4
|
+
rowCells,
|
|
5
|
+
tableStartsAt,
|
|
6
|
+
unescapeCellPipes
|
|
7
|
+
} from "../display/markdown-classification/processors/utils/gfm-table-lines.js";
|
|
8
|
+
const FENCE = /^( {0,3})(`{3,}|~{3,})/;
|
|
9
|
+
function readableInline(line) {
|
|
10
|
+
const codes = [];
|
|
11
|
+
let out = line.replace(/(`+)([\s\S]*?[^`])\1(?!`)/g, (_m, _ticks, body) => {
|
|
12
|
+
codes.push(body.replace(/^ (.*) $/, "$1"));
|
|
13
|
+
return `\uE000${codes.length - 1}\uE000`;
|
|
14
|
+
});
|
|
15
|
+
out = out.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_m, alt, url) => alt ? `${alt} (${url})` : url).replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_m, text, url) => {
|
|
16
|
+
const bare = url.replace(/^mailto:/, "");
|
|
17
|
+
return text === url || text === bare ? bare : `${text} (${bare})`;
|
|
18
|
+
}).replace(/<((?:https?:\/\/|mailto:)[^>\s]+)>/g, (_m, url) => url.replace(/^mailto:/, "")).replace(/(\*\*\*|___)(?=\S)([\s\S]*?\S)\1/g, "$2").replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, "$2").replace(/~~(?=\S)([\s\S]*?\S)~~/g, "$1").replace(/(^|[^\w*])\*(?=\S)([^*]*?\S)\*(?!\w)/g, "$1$2").replace(/(^|[^\w])_(?=\S)([^_]*?\S)_(?!\w)/g, "$1$2").replace(/ {2,}$|\\$/g, "").replace(/\\([!-/:-@[-`{-~])/g, "$1");
|
|
19
|
+
return out.replace(/\uE000(\d+)\uE000/g, (_m, i) => codes[Number(i)] ?? "");
|
|
20
|
+
}
|
|
21
|
+
function markdownToReadableText(markdown) {
|
|
22
|
+
const lines = unwrapKindEnvelopes(markdown).replace(/\r\n?/g, "\n").split("\n");
|
|
23
|
+
const out = [];
|
|
24
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
25
|
+
const line = lines[i];
|
|
26
|
+
const fence = FENCE.exec(line);
|
|
27
|
+
if (fence) {
|
|
28
|
+
const marker = fence[2];
|
|
29
|
+
let j = i + 1;
|
|
30
|
+
while (j < lines.length && !new RegExp(`^ {0,3}${marker[0] === "`" ? "`" : "~"}{${marker.length},}\\s*$`).test(lines[j])) {
|
|
31
|
+
out.push(lines[j]);
|
|
32
|
+
j += 1;
|
|
33
|
+
}
|
|
34
|
+
i = j;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (tableStartsAt(lines, i)) {
|
|
38
|
+
const end = findTableEnd(lines, i);
|
|
39
|
+
for (let j = i; j < end; j += 1) {
|
|
40
|
+
if (j === i + 1) continue;
|
|
41
|
+
out.push(rowCells(lines[j]).map((c) => readableInline(unescapeCellPipes(c).trim())).join(" "));
|
|
42
|
+
}
|
|
43
|
+
i = end - 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (/^ {0,3}([-*_])( *\1){2,} *$/.test(line)) {
|
|
47
|
+
out.push("");
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (/^ {0,3}(=+|-+) *$/.test(line) && out.length && out[out.length - 1].trim()) continue;
|
|
51
|
+
let rest = line;
|
|
52
|
+
rest = rest.replace(/^ {0,3}(?:> ?)+/, "");
|
|
53
|
+
rest = rest.replace(/^ {0,3}#{1,6}[ \t]+/, "").replace(/[ \t]+#+[ \t]*$/, "");
|
|
54
|
+
rest = rest.replace(/^(\s*)[-*+][ \t]+\[[ ]\][ \t]+/, "$1\u2610 ").replace(/^(\s*)[-*+][ \t]+\[[xX]\][ \t]+/, "$1\u2611 ").replace(/^(\s*)[-*+][ \t]+/, "$1\u2022 ").replace(/^(\s*)(\d{1,9})[.)][ \t]+/, "$1$2. ");
|
|
55
|
+
out.push(readableInline(rest));
|
|
56
|
+
}
|
|
57
|
+
return out.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
markdownToReadableText,
|
|
61
|
+
readableInline
|
|
62
|
+
};
|
|
@@ -49,16 +49,18 @@ export declare function detectTabular(value: unknown): TabularShape;
|
|
|
49
49
|
* `null` and `undefined` become empty strings (Excel-friendly).
|
|
50
50
|
*/
|
|
51
51
|
export declare function cellToCsv(value: unknown): string;
|
|
52
|
+
/**
|
|
53
|
+
* Rows -> CSV/TSV text through Alchemy's one writer (quoting, line breaks, `'` before a leading
|
|
54
|
+
* = + - @ text; real numbers stay numbers, so -5 is never altered).
|
|
55
|
+
*/
|
|
52
56
|
export declare function rowsToCsv(rows: Record<string, unknown>[], columns: string[], delimiter?: string): string;
|
|
53
57
|
export declare function rowsToNdjson(rows: unknown[]): string;
|
|
54
58
|
/** Browser file-download for a text payload. */
|
|
55
59
|
export declare function downloadText(filename: string, content: string, mime?: string): void;
|
|
56
60
|
/**
|
|
57
|
-
* XLSX export
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* Cell values are pre-stringified for objects/arrays so spreadsheets
|
|
61
|
-
* receive plain text instead of "[object Object]".
|
|
61
|
+
* XLSX export through Alchemy's workbook format (the one spreadsheet engine).
|
|
62
|
+
* Header row = `columns`; object/array cells are pre-stringified so
|
|
63
|
+
* spreadsheets receive plain text instead of "[object Object]".
|
|
62
64
|
*/
|
|
63
65
|
export declare function rowsToXlsx(rows: Record<string, unknown>[], columns: string[], filename: string): Promise<void>;
|
|
64
66
|
/** Default filename helper (no extension). */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { buildFile } from "@ai-matrx/alchemy/operate";
|
|
2
|
+
import { toDelimitedText } from "@ai-matrx/alchemy/operate/read";
|
|
1
3
|
import { downloadFile } from "@ai-matrx/kit/download";
|
|
2
|
-
import { announceBareHost, hostCapability } from "../../../host/registry.js";
|
|
3
4
|
function parseJsonSafe(text) {
|
|
4
5
|
if (!text || !text.trim()) {
|
|
5
6
|
return { ok: false, error: "Empty input" };
|
|
@@ -73,30 +74,38 @@ function cellToCsv(value) {
|
|
|
73
74
|
}
|
|
74
75
|
return String(value);
|
|
75
76
|
}
|
|
76
|
-
function escapeCsv(value, delimiter) {
|
|
77
|
-
const needsQuote = value.includes(delimiter) || value.includes('"') || value.includes("\n") || value.includes("\r");
|
|
78
|
-
if (!needsQuote) return value;
|
|
79
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
80
|
-
}
|
|
81
77
|
function rowsToCsv(rows, columns, delimiter = ",") {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
(row) => columns.map((c) =>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
return toDelimitedText(
|
|
79
|
+
columns,
|
|
80
|
+
rows.map((row) => columns.map((c) => cellToCsvValue(row[c]))),
|
|
81
|
+
{ format: delimiter === " " ? "tsv" : "csv", spreadsheetSafe: true }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
function cellToCsvValue(value) {
|
|
85
|
+
if (value === null || value === void 0) return "";
|
|
86
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
87
|
+
return cellToCsv(value);
|
|
88
88
|
}
|
|
89
89
|
function rowsToNdjson(rows) {
|
|
90
90
|
return rows.map((r) => JSON.stringify(r)).join("\n");
|
|
91
91
|
}
|
|
92
92
|
function downloadText(filename, content, mime = "text/plain") {
|
|
93
|
-
|
|
93
|
+
const blob = new Blob([content], { type: `${mime};charset=utf-8` });
|
|
94
|
+
downloadFile(filename, blob, blob.type);
|
|
94
95
|
}
|
|
95
96
|
async function rowsToXlsx(rows, columns, filename) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
const cell = (v) => v === null || v === void 0 ? null : typeof v === "object" ? JSON.stringify(v) : typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? v : String(v);
|
|
98
|
+
const built = await buildFile(
|
|
99
|
+
{
|
|
100
|
+
kind: "registered",
|
|
101
|
+
format: "workbook",
|
|
102
|
+
value: { sheets: [{ name: "Data", rows: [columns, ...rows.map((row) => columns.map((c) => cell(row[c])))] }] }
|
|
103
|
+
},
|
|
104
|
+
"xlsx-workbook",
|
|
105
|
+
{ filename }
|
|
106
|
+
);
|
|
107
|
+
const blob = built.blob();
|
|
108
|
+
downloadFile(built.filename, blob, blob.type);
|
|
100
109
|
}
|
|
101
110
|
function defaultJsonFilename() {
|
|
102
111
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { downloadFile } from "@ai-matrx/kit/download";
|
|
4
3
|
import { useClipboard } from "@ai-matrx/kit/clipboard";
|
|
5
4
|
import { toast as copyToast } from "../../../host/toast.js";
|
|
6
5
|
import { rewriteTableSource } from "../../../utils/table-source.js";
|
|
@@ -77,6 +76,8 @@ import {
|
|
|
77
76
|
parseMarkdownTable,
|
|
78
77
|
cleanTableHeaderKey
|
|
79
78
|
} from "./parseMarkdownTable.js";
|
|
79
|
+
import { downloadFile } from "@ai-matrx/kit/download";
|
|
80
|
+
import { tableToCsv } from "../../tables/table-csv.js";
|
|
80
81
|
const NO_HIDDEN_COLUMNS = /* @__PURE__ */ new Set();
|
|
81
82
|
const ExportDropdownMenu = ({
|
|
82
83
|
tableData,
|
|
@@ -292,21 +293,13 @@ const StreamingTableRendererCore = ({
|
|
|
292
293
|
};
|
|
293
294
|
const downloadCSV = () => {
|
|
294
295
|
try {
|
|
295
|
-
const csvContent =
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
(row) => row.map((cell) => {
|
|
299
|
-
const escaped = cell.replace(/"/g, '""');
|
|
300
|
-
return cell.includes(",") ? `"${escaped}"` : escaped;
|
|
301
|
-
}).join(",")
|
|
302
|
-
)
|
|
303
|
-
].join("\n");
|
|
304
|
-
const saveCsv = () => downloadFile("table_data.csv", csvContent, "text/csv;charset=utf-8");
|
|
305
|
-
saveCsv();
|
|
296
|
+
const csvContent = tableToCsv(headers, rows);
|
|
297
|
+
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
298
|
+
downloadFile("table_data.csv", blob, blob.type);
|
|
306
299
|
toast.success("Table exported to CSV", {
|
|
307
300
|
action: {
|
|
308
301
|
label: "Download Again",
|
|
309
|
-
onClick:
|
|
302
|
+
onClick: () => downloadFile("table_data.csv", blob, blob.type),
|
|
310
303
|
className: "font-medium"
|
|
311
304
|
}
|
|
312
305
|
});
|
|
@@ -320,12 +313,14 @@ const StreamingTableRendererCore = ({
|
|
|
320
313
|
try {
|
|
321
314
|
const markdownContent = content || "";
|
|
322
315
|
const fileName = headers && headers[0] ? `${headers[0].replace(/[^a-z0-9]/gi, "_").toLowerCase()}.md` : "table_data.md";
|
|
323
|
-
const
|
|
324
|
-
|
|
316
|
+
const blob = new Blob([markdownContent], {
|
|
317
|
+
type: "text/markdown;charset=utf-8"
|
|
318
|
+
});
|
|
319
|
+
downloadFile(fileName, blob, blob.type);
|
|
325
320
|
toast.success("Markdown file downloaded", {
|
|
326
321
|
action: {
|
|
327
322
|
label: "Download Again",
|
|
328
|
-
onClick:
|
|
323
|
+
onClick: () => downloadFile(fileName, blob, blob.type),
|
|
329
324
|
className: "font-medium"
|
|
330
325
|
}
|
|
331
326
|
});
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
reportDelimiterViolations
|
|
8
8
|
} from "@ai-matrx/kit/delimiter-guard";
|
|
9
9
|
import { captureError } from "../../host/diagnostics.js";
|
|
10
|
-
import {
|
|
10
|
+
import { CopyButtons } from "../../host/app-bindings.js";
|
|
11
11
|
import {
|
|
12
12
|
TableRenderPathDiagnostic
|
|
13
13
|
} from "../blocks/table/TableRenderPathDiagnostic.js";
|
|
@@ -102,12 +102,14 @@ const BasicMarkdownContentBody = ({
|
|
|
102
102
|
/* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: PROSE_FRAME_CSS } }),
|
|
103
103
|
/* @__PURE__ */ jsx(MarkdownCore, { preset: "chat", components, children: processedContent }),
|
|
104
104
|
!isStreamActive && isHovering && /* @__PURE__ */ jsx("div", { className: "absolute top-0 right-0 p-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200", children: showCopyButton && /* @__PURE__ */ jsx(
|
|
105
|
-
|
|
105
|
+
CopyButtons,
|
|
106
106
|
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
label: "Content",
|
|
108
|
+
human: content,
|
|
109
|
+
contentFlavor: "markdown",
|
|
110
|
+
stripThinking: true,
|
|
111
|
+
size: "sm",
|
|
112
|
+
className: "absolute top-1 right-1 z-10 mt-1 mr-1"
|
|
111
113
|
}
|
|
112
114
|
) })
|
|
113
115
|
]
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "@ai-matrx/kit/delimiter-guard";
|
|
13
13
|
import { captureError } from "../../host/diagnostics.js";
|
|
14
14
|
import { LinkComponent } from "../blocks/links/LinkComponent.js";
|
|
15
|
-
import {
|
|
15
|
+
import { CopyButtons } from "../../host/app-bindings.js";
|
|
16
16
|
import {
|
|
17
17
|
useMediaBlob,
|
|
18
18
|
useMediaLoadRecovery
|
|
@@ -614,12 +614,14 @@ const ConfigurableMarkdownContentBody = ({
|
|
|
614
614
|
/* @__PURE__ */ jsx(MarkdownCore, { preset: "rich", components, children: processedContent }),
|
|
615
615
|
!isStreamActive && isHovering && /* @__PURE__ */ jsxs("div", { className: "absolute top-0 right-0 p-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200", children: [
|
|
616
616
|
showCopyButton && /* @__PURE__ */ jsx(
|
|
617
|
-
|
|
617
|
+
CopyButtons,
|
|
618
618
|
{
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
619
|
+
label: "Content",
|
|
620
|
+
human: content,
|
|
621
|
+
contentFlavor: "markdown",
|
|
622
|
+
stripThinking: true,
|
|
623
|
+
size: "sm",
|
|
624
|
+
className: "absolute top-1 right-1 z-10 mt-1 mr-1"
|
|
623
625
|
}
|
|
624
626
|
),
|
|
625
627
|
onEditRequest && /* @__PURE__ */ jsx(
|
|
@@ -69,8 +69,10 @@ const BUILTIN_BLOCK_COMPONENTS = {
|
|
|
69
69
|
const BUILTIN_LOADING_COMPONENTS = {
|
|
70
70
|
DiagramLoading: () => /* @__PURE__ */ jsx(LazyBlockWrapper, { children: /* @__PURE__ */ jsx(DiagramLoadingVisualization, {}) })
|
|
71
71
|
};
|
|
72
|
+
import { ensureHostBlockRegistrations } from "../../../host/registry.js";
|
|
72
73
|
const REGISTRY_KEY = /* @__PURE__ */ Symbol.for("ai-matrx.rich-content.block-components");
|
|
73
74
|
function registry() {
|
|
75
|
+
ensureHostBlockRegistrations();
|
|
74
76
|
const g = globalThis;
|
|
75
77
|
return g[REGISTRY_KEY] ??= { blocks: {}, loading: {} };
|
|
76
78
|
}
|
|
@@ -472,6 +472,7 @@ const BLOCK_DISPATCH_CLASSIFICATION = {
|
|
|
472
472
|
const DISPATCH_MAP = new Map(
|
|
473
473
|
Object.entries(BLOCK_DISPATCH)
|
|
474
474
|
);
|
|
475
|
+
import { ensureHostBlockRegistrations } from "../../../host/registry.js";
|
|
475
476
|
const REGISTERED_KEY = /* @__PURE__ */ Symbol.for("ai-matrx.rich-content.block-dispatch");
|
|
476
477
|
function registered() {
|
|
477
478
|
const g = globalThis;
|
|
@@ -481,6 +482,7 @@ function registerBlockDispatch(entries) {
|
|
|
481
482
|
for (const [type, fn] of Object.entries(entries)) if (fn) registered().set(type, fn);
|
|
482
483
|
}
|
|
483
484
|
function resolveBlockDispatch(type) {
|
|
485
|
+
ensureHostBlockRegistrations();
|
|
484
486
|
return registered().get(type) ?? DISPATCH_MAP.get(type) ?? null;
|
|
485
487
|
}
|
|
486
488
|
export {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { downloadFile } from "@ai-matrx/kit/download";
|
|
4
3
|
import { useClipboard } from "@ai-matrx/kit/clipboard";
|
|
5
4
|
import { toast as copyToast } from "../../host/toast.js";
|
|
6
5
|
import { rewriteTableSource } from "../../utils/table-source.js";
|
|
@@ -61,6 +60,8 @@ import {
|
|
|
61
60
|
clearColumn,
|
|
62
61
|
duplicateColumn
|
|
63
62
|
} from "./editing/tableMutations.js";
|
|
63
|
+
import { downloadFile } from "@ai-matrx/kit/download";
|
|
64
|
+
import { tableToCsv } from "./table-csv.js";
|
|
64
65
|
const ExportDropdownMenu = ({
|
|
65
66
|
tableData,
|
|
66
67
|
content,
|
|
@@ -291,21 +292,13 @@ const MarkdownTable = ({
|
|
|
291
292
|
};
|
|
292
293
|
const downloadCSV = () => {
|
|
293
294
|
try {
|
|
294
|
-
const csvContent =
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
(row) => row.map((cell) => {
|
|
298
|
-
const escaped = cell.replace(/"/g, '""');
|
|
299
|
-
return cell.includes(",") ? `"${escaped}"` : escaped;
|
|
300
|
-
}).join(",")
|
|
301
|
-
)
|
|
302
|
-
].join("\n");
|
|
303
|
-
const saveCsv = () => downloadFile("table_data.csv", csvContent, "text/csv;charset=utf-8");
|
|
304
|
-
saveCsv();
|
|
295
|
+
const csvContent = tableToCsv(internalTableData.headers, internalTableData.rows);
|
|
296
|
+
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
297
|
+
downloadFile("table_data.csv", blob, blob.type);
|
|
305
298
|
toast.success("Table exported to CSV", {
|
|
306
299
|
action: {
|
|
307
300
|
label: "Download Again",
|
|
308
|
-
onClick:
|
|
301
|
+
onClick: () => downloadFile("table_data.csv", blob, blob.type),
|
|
309
302
|
className: "font-medium"
|
|
310
303
|
}
|
|
311
304
|
});
|
|
@@ -317,12 +310,14 @@ const MarkdownTable = ({
|
|
|
317
310
|
try {
|
|
318
311
|
const markdownContent = content || "";
|
|
319
312
|
const fileName = internalTableData.headers && internalTableData.headers[0] ? `${internalTableData.headers[0].replace(/[^a-z0-9]/gi, "_").toLowerCase()}.md` : "table_data.md";
|
|
320
|
-
const
|
|
321
|
-
|
|
313
|
+
const blob = new Blob([markdownContent], {
|
|
314
|
+
type: "text/markdown;charset=utf-8"
|
|
315
|
+
});
|
|
316
|
+
downloadFile(fileName, blob, blob.type);
|
|
322
317
|
toast.success("Markdown file downloaded", {
|
|
323
318
|
action: {
|
|
324
319
|
label: "Download Again",
|
|
325
|
-
onClick:
|
|
320
|
+
onClick: () => downloadFile(fileName, blob, blob.type),
|
|
326
321
|
className: "font-medium"
|
|
327
322
|
}
|
|
328
323
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { downloadFile } from "@ai-matrx/kit/download";
|
|
4
3
|
import { useClipboard } from "@ai-matrx/kit/clipboard";
|
|
5
4
|
import { toast as copyToast } from "../../host/toast.js";
|
|
6
5
|
import { rewriteTableSource } from "../../utils/table-source.js";
|
|
@@ -31,6 +30,8 @@ import {
|
|
|
31
30
|
import { useToastManager } from "../../utils/lifted/useToastManager.js";
|
|
32
31
|
import { THEMES } from "../themes.js";
|
|
33
32
|
import { useOpenSaveToTable } from "../../host/tables.js";
|
|
33
|
+
import { downloadFile } from "@ai-matrx/kit/download";
|
|
34
|
+
import { tableToCsv } from "./table-csv.js";
|
|
34
35
|
function useDebounce(value, delay) {
|
|
35
36
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
36
37
|
useEffect(() => {
|
|
@@ -108,21 +109,13 @@ const TableControls = ({
|
|
|
108
109
|
}, [content, copyTableToClipboard, toast]);
|
|
109
110
|
const downloadCSV = useCallback(() => {
|
|
110
111
|
try {
|
|
111
|
-
const csvContent =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
(row) => row.map((cell) => {
|
|
115
|
-
const escaped = cell.replace(/"/g, '""');
|
|
116
|
-
return cell.includes(",") ? `"${escaped}"` : escaped;
|
|
117
|
-
}).join(",")
|
|
118
|
-
)
|
|
119
|
-
].join("\n");
|
|
120
|
-
const saveCsv = () => downloadFile("table_data.csv", csvContent, "text/csv;charset=utf-8");
|
|
121
|
-
saveCsv();
|
|
112
|
+
const csvContent = tableToCsv(tableData.headers, tableData.rows);
|
|
113
|
+
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
114
|
+
downloadFile("table_data.csv", blob, blob.type);
|
|
122
115
|
toast.success("Table exported to CSV", {
|
|
123
116
|
action: {
|
|
124
117
|
label: "Download Again",
|
|
125
|
-
onClick:
|
|
118
|
+
onClick: () => downloadFile("table_data.csv", blob, blob.type),
|
|
126
119
|
className: "font-medium"
|
|
127
120
|
}
|
|
128
121
|
});
|
|
@@ -134,12 +127,14 @@ const TableControls = ({
|
|
|
134
127
|
try {
|
|
135
128
|
const markdownContent = content || "";
|
|
136
129
|
const fileName = tableData.headers && tableData.headers[0] ? `${tableData.headers[0].replace(/[^a-z0-9]/gi, "_").toLowerCase()}.md` : "table_data.md";
|
|
137
|
-
const
|
|
138
|
-
|
|
130
|
+
const blob = new Blob([markdownContent], {
|
|
131
|
+
type: "text/markdown;charset=utf-8"
|
|
132
|
+
});
|
|
133
|
+
downloadFile(fileName, blob, blob.type);
|
|
139
134
|
toast.success("Markdown file downloaded", {
|
|
140
135
|
action: {
|
|
141
136
|
label: "Download Again",
|
|
142
|
-
onClick:
|
|
137
|
+
onClick: () => downloadFile(fileName, blob, blob.type),
|
|
143
138
|
className: "font-medium"
|
|
144
139
|
}
|
|
145
140
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CSV text of a markdown/streaming table, written by Alchemy's one writer. Quotes, commas and
|
|
3
|
+
* line breaks are quoted; text that opens with = + - @ is prefixed `'` so a spreadsheet never runs
|
|
4
|
+
* it (alchemy `spreadsheetSafe`, on). A cell that is a plain number is written as a number, so a
|
|
5
|
+
* negative amount stays `-5`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function tableToCsv(headers: readonly string[], rows: readonly (readonly string[])[]): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { toDelimitedText } from "@ai-matrx/alchemy/operate/read";
|
|
2
|
+
const PLAIN_NUMBER = /^-?\d+(\.\d+)?$/;
|
|
3
|
+
function cellValue(cell) {
|
|
4
|
+
if (PLAIN_NUMBER.test(cell)) {
|
|
5
|
+
const n = Number(cell);
|
|
6
|
+
if (String(n) === cell) return n;
|
|
7
|
+
}
|
|
8
|
+
return cell;
|
|
9
|
+
}
|
|
10
|
+
function tableToCsv(headers, rows) {
|
|
11
|
+
return toDelimitedText(
|
|
12
|
+
headers,
|
|
13
|
+
rows.map((row) => row.map(cellValue)),
|
|
14
|
+
{ spreadsheetSafe: true }
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
tableToCsv
|
|
19
|
+
};
|