@bendyline/squisq-formats 1.3.1 → 1.4.1
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/README.md +180 -43
- package/dist/{chunk-33YRFXZZ.js → chunk-46FMDOWZ.js} +260 -14
- package/dist/chunk-46FMDOWZ.js.map +1 -0
- package/dist/{chunk-ERZ627GR.js → chunk-5LSSWZPU.js} +12 -27
- package/dist/chunk-5LSSWZPU.js.map +1 -0
- package/dist/chunk-6M7Z25LA.js +46 -0
- package/dist/chunk-6M7Z25LA.js.map +1 -0
- package/dist/{chunk-S3Y7H2BK.js → chunk-D7IFPWNZ.js} +95 -50
- package/dist/chunk-D7IFPWNZ.js.map +1 -0
- package/dist/chunk-DQAZR57U.js +33 -0
- package/dist/chunk-DQAZR57U.js.map +1 -0
- package/dist/chunk-EHLGMSTE.js +273 -0
- package/dist/chunk-EHLGMSTE.js.map +1 -0
- package/dist/{chunk-A3FHLTY5.js → chunk-MJGRI6XR.js} +6 -19
- package/dist/chunk-MJGRI6XR.js.map +1 -0
- package/dist/chunk-PN52A5AA.js +32 -0
- package/dist/chunk-PN52A5AA.js.map +1 -0
- package/dist/chunk-RFAPOKHJ.js +123 -0
- package/dist/chunk-RFAPOKHJ.js.map +1 -0
- package/dist/chunk-RTS5XBZ7.js +526 -0
- package/dist/chunk-RTS5XBZ7.js.map +1 -0
- package/dist/{chunk-7DEUGIOO.js → chunk-SHLPSADL.js} +120 -2
- package/dist/chunk-SHLPSADL.js.map +1 -0
- package/dist/chunk-SSUPBUF5.js +15 -0
- package/dist/chunk-SSUPBUF5.js.map +1 -0
- package/dist/{chunk-UDS45KUJ.js → chunk-WWBIKRNP.js} +40 -80
- package/dist/chunk-WWBIKRNP.js.map +1 -0
- package/dist/{chunk-VN2KEOYB.js → chunk-YESS7VY3.js} +379 -87
- package/dist/chunk-YESS7VY3.js.map +1 -0
- package/dist/csv/index.d.ts +50 -0
- package/dist/csv/index.js +13 -0
- package/dist/csv/index.js.map +1 -0
- package/dist/docx/index.js +6 -3
- package/dist/epub/index.js +4 -2
- package/dist/html/index.d.ts +42 -2
- package/dist/html/index.js +9 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +47 -14
- package/dist/ooxml/index.d.ts +5 -1
- package/dist/ooxml/index.js +13 -9
- package/dist/pdf/index.js +2 -1
- package/dist/pptx/index.d.ts +36 -19
- package/dist/pptx/index.js +9 -2
- package/dist/registry/index.d.ts +167 -0
- package/dist/registry/index.js +17 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/xlsx/index.d.ts +60 -29
- package/dist/xlsx/index.js +4 -1
- package/package.json +12 -2
- package/src/__tests__/convert.test.ts +186 -0
- package/src/__tests__/csvImport.test.ts +84 -0
- package/src/__tests__/exportThemeReconciliation.test.ts +87 -0
- package/src/__tests__/formatRegistry.test.ts +174 -0
- package/src/__tests__/htmlImport.test.ts +57 -0
- package/src/__tests__/lossyWarnings.test.ts +105 -0
- package/src/__tests__/pdfImport.test.ts +95 -1
- package/src/__tests__/plainHtml.test.ts +31 -0
- package/src/__tests__/pptxExport.test.ts +138 -0
- package/src/__tests__/pptxImport.test.ts +115 -0
- package/src/__tests__/roundTripAssets.test.ts +50 -0
- package/src/__tests__/roundTripMatrix.fixtures.ts +86 -0
- package/src/__tests__/roundTripMatrix.helpers.ts +154 -0
- package/src/__tests__/roundTripMatrix.test.ts +142 -0
- package/src/__tests__/sharedContainer.test.ts +41 -0
- package/src/__tests__/sharedImages.test.ts +61 -0
- package/src/__tests__/xlsxExport.test.ts +164 -0
- package/src/__tests__/xlsxImport.test.ts +80 -0
- package/src/csv/index.ts +188 -0
- package/src/docx/export.ts +27 -44
- package/src/docx/import.ts +7 -39
- package/src/epub/export.ts +10 -27
- package/src/html/imageUtils.ts +5 -19
- package/src/html/import.ts +297 -0
- package/src/html/index.ts +4 -0
- package/src/html/plainHtml.ts +66 -16
- package/src/index.ts +36 -5
- package/src/ooxml/index.ts +3 -0
- package/src/ooxml/namespaces.ts +13 -0
- package/src/pdf/export.ts +10 -5
- package/src/pdf/import.ts +146 -51
- package/src/pptx/export.ts +225 -85
- package/src/pptx/import.ts +273 -0
- package/src/pptx/index.ts +10 -28
- package/src/registry/convert.ts +318 -0
- package/src/registry/defaultFormats.ts +323 -0
- package/src/registry/errors.ts +46 -0
- package/src/registry/index.ts +38 -0
- package/src/registry/registry.ts +48 -0
- package/src/registry/types.ts +107 -0
- package/src/shared/container.ts +28 -0
- package/src/shared/images.ts +44 -0
- package/src/shared/inlineRuns.ts +99 -0
- package/src/shared/text.ts +41 -0
- package/src/xlsx/export.ts +253 -0
- package/src/xlsx/import.ts +159 -0
- package/src/xlsx/index.ts +18 -61
- package/dist/chunk-33YRFXZZ.js.map +0 -1
- package/dist/chunk-67KIJHV2.js +0 -21
- package/dist/chunk-67KIJHV2.js.map +0 -1
- package/dist/chunk-7DEUGIOO.js.map +0 -1
- package/dist/chunk-A3FHLTY5.js.map +0 -1
- package/dist/chunk-ERZ627GR.js.map +0 -1
- package/dist/chunk-S3Y7H2BK.js.map +0 -1
- package/dist/chunk-UDS45KUJ.js.map +0 -1
- package/dist/chunk-VN2KEOYB.js.map +0 -1
- package/dist/chunk-YN5HFCEW.js +0 -120
- package/dist/chunk-YN5HFCEW.js.map +0 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLSX export — MarkdownDocument → SpreadsheetML (.xlsx).
|
|
3
|
+
*
|
|
4
|
+
* Tables-only fidelity (honestly documented): every `table` node in the
|
|
5
|
+
* markdown AST becomes one worksheet; all other content (headings, prose,
|
|
6
|
+
* lists, images, …) is dropped except that the nearest preceding heading is
|
|
7
|
+
* used to name the sheet. This mirrors the import side (`xlsxToMarkdownDoc`),
|
|
8
|
+
* which turns each worksheet grid back into a markdown table.
|
|
9
|
+
*
|
|
10
|
+
* Cells are emitted as inline strings (`t="inlineStr"`) so no sharedStrings
|
|
11
|
+
* part is needed, except values matching a plain-number pattern, which are
|
|
12
|
+
* emitted as numeric cells. The package is assembled with the shared ooxml/
|
|
13
|
+
* writer (auto-generates `[Content_Types].xml` + `_rels`), so only the
|
|
14
|
+
* SpreadsheetML-specific parts (workbook, worksheets, styles) are written here.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
19
|
+
* import { markdownDocToXlsx } from '@bendyline/squisq-formats/xlsx';
|
|
20
|
+
*
|
|
21
|
+
* const md = parseMarkdown('# Metrics\n\n| A | B |\n| - | - |\n| 1 | 2 |');
|
|
22
|
+
* const buffer = await markdownDocToXlsx(md);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { Doc } from '@bendyline/squisq/schemas';
|
|
27
|
+
import { docToMarkdown } from '@bendyline/squisq/doc';
|
|
28
|
+
import type {
|
|
29
|
+
MarkdownDocument,
|
|
30
|
+
MarkdownBlockNode,
|
|
31
|
+
MarkdownTable,
|
|
32
|
+
MarkdownTableRow,
|
|
33
|
+
} from '@bendyline/squisq/markdown';
|
|
34
|
+
|
|
35
|
+
import { createPackage } from '../ooxml/writer.js';
|
|
36
|
+
import { xmlDeclaration, escapeXml } from '../ooxml/xmlUtils.js';
|
|
37
|
+
import { extractPlainText } from '../shared/text.js';
|
|
38
|
+
import {
|
|
39
|
+
NS_SML,
|
|
40
|
+
NS_R,
|
|
41
|
+
REL_OFFICE_DOCUMENT,
|
|
42
|
+
REL_STYLES,
|
|
43
|
+
REL_WORKSHEET,
|
|
44
|
+
CONTENT_TYPE_XLSX_WORKBOOK,
|
|
45
|
+
CONTENT_TYPE_XLSX_WORKSHEET,
|
|
46
|
+
CONTENT_TYPE_XLSX_STYLES,
|
|
47
|
+
} from '../ooxml/namespaces.js';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Options for XLSX export.
|
|
51
|
+
*/
|
|
52
|
+
export interface XlsxExportOptions {
|
|
53
|
+
/** Workbook title (written to core properties). */
|
|
54
|
+
title?: string;
|
|
55
|
+
/** Workbook author (written to core properties). */
|
|
56
|
+
author?: string;
|
|
57
|
+
/** Prefix used for auto-named sheets when no heading precedes a table. Default: "Sheet". */
|
|
58
|
+
sheetNamePrefix?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface SheetData {
|
|
62
|
+
name: string;
|
|
63
|
+
/** Grid of cell text: first row is the header. */
|
|
64
|
+
grid: string[][];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A plain integer or decimal (optionally negative) — emitted as a numeric cell. */
|
|
68
|
+
const NUMERIC_RE = /^-?\d+(\.\d+)?$/;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Sanitize a candidate name into a valid, unique Excel sheet name.
|
|
72
|
+
* Strips characters Excel forbids (`[]:*?/\`), caps at 31 chars, and
|
|
73
|
+
* de-duplicates against already-used names by appending 2, 3, …
|
|
74
|
+
*/
|
|
75
|
+
function sanitizeSheetName(raw: string, used: Set<string>, fallback: string): string {
|
|
76
|
+
let name = raw.replace(/[[\]:*?/\\]/g, '').trim();
|
|
77
|
+
if (name.length === 0) name = fallback;
|
|
78
|
+
name = name.slice(0, 31);
|
|
79
|
+
|
|
80
|
+
if (!used.has(name)) {
|
|
81
|
+
used.add(name);
|
|
82
|
+
return name;
|
|
83
|
+
}
|
|
84
|
+
// Dedupe: "Name", "Name2", "Name3", … keeping within the 31-char cap.
|
|
85
|
+
for (let n = 2; ; n++) {
|
|
86
|
+
const suffix = String(n);
|
|
87
|
+
const base = name.slice(0, 31 - suffix.length);
|
|
88
|
+
const candidate = `${base}${suffix}`;
|
|
89
|
+
if (!used.has(candidate)) {
|
|
90
|
+
used.add(candidate);
|
|
91
|
+
return candidate;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Convert a table node into a grid of plain-text cells. */
|
|
97
|
+
function tableToGrid(table: MarkdownTable): string[][] {
|
|
98
|
+
return table.children.map((row: MarkdownTableRow) =>
|
|
99
|
+
row.children.map((cell) => extractPlainText(cell.children)),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Walk the document, pairing each table with the nearest preceding heading. */
|
|
104
|
+
function collectSheets(doc: MarkdownDocument, prefix: string): SheetData[] {
|
|
105
|
+
const used = new Set<string>();
|
|
106
|
+
const sheets: SheetData[] = [];
|
|
107
|
+
let pendingHeading: string | null = null;
|
|
108
|
+
|
|
109
|
+
const visit = (nodes: MarkdownBlockNode[]): void => {
|
|
110
|
+
for (const node of nodes) {
|
|
111
|
+
if (node.type === 'heading') {
|
|
112
|
+
pendingHeading = extractPlainText(node.children);
|
|
113
|
+
} else if (node.type === 'table') {
|
|
114
|
+
const fallback = `${prefix}${sheets.length + 1}`;
|
|
115
|
+
const name = sanitizeSheetName(pendingHeading ?? fallback, used, fallback);
|
|
116
|
+
sheets.push({ name, grid: tableToGrid(node) });
|
|
117
|
+
pendingHeading = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
visit(doc.children);
|
|
123
|
+
return sheets;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Column letters for a zero-based index (0 → "A", 26 → "AA"). */
|
|
127
|
+
function columnLetter(index: number): string {
|
|
128
|
+
let n = index + 1;
|
|
129
|
+
let letters = '';
|
|
130
|
+
while (n > 0) {
|
|
131
|
+
const rem = (n - 1) % 26;
|
|
132
|
+
letters = String.fromCharCode(65 + rem) + letters;
|
|
133
|
+
n = Math.floor((n - 1) / 26);
|
|
134
|
+
}
|
|
135
|
+
return letters;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function cellXml(value: string, ref: string): string {
|
|
139
|
+
if (NUMERIC_RE.test(value)) {
|
|
140
|
+
return `<c r="${ref}"><v>${escapeXml(value)}</v></c>`;
|
|
141
|
+
}
|
|
142
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(value)}</t></is></c>`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function worksheetXml(grid: string[][]): string {
|
|
146
|
+
const rows: string[] = [];
|
|
147
|
+
for (let r = 0; r < grid.length; r++) {
|
|
148
|
+
const cells = grid[r]!;
|
|
149
|
+
const cellsXml = cells.map((value, c) => cellXml(value, `${columnLetter(c)}${r + 1}`)).join('');
|
|
150
|
+
rows.push(`<row r="${r + 1}">${cellsXml}</row>`);
|
|
151
|
+
}
|
|
152
|
+
return (
|
|
153
|
+
`${xmlDeclaration()}\n` +
|
|
154
|
+
`<worksheet xmlns="${NS_SML}" xmlns:r="${NS_R}">` +
|
|
155
|
+
`<sheetData>${rows.join('')}</sheetData>` +
|
|
156
|
+
`</worksheet>`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function workbookXml(sheets: SheetData[]): string {
|
|
161
|
+
const sheetEls = sheets
|
|
162
|
+
.map(
|
|
163
|
+
(sheet, i) =>
|
|
164
|
+
`<sheet name="${escapeXml(sheet.name)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`,
|
|
165
|
+
)
|
|
166
|
+
.join('');
|
|
167
|
+
return (
|
|
168
|
+
`${xmlDeclaration()}\n` +
|
|
169
|
+
`<workbook xmlns="${NS_SML}" xmlns:r="${NS_R}">` +
|
|
170
|
+
`<sheets>${sheetEls}</sheets>` +
|
|
171
|
+
`</workbook>`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Minimal but valid styles part (Excel expects the referenced collections to exist). */
|
|
176
|
+
function stylesXml(): string {
|
|
177
|
+
return (
|
|
178
|
+
`${xmlDeclaration()}\n` +
|
|
179
|
+
`<styleSheet xmlns="${NS_SML}">` +
|
|
180
|
+
`<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>` +
|
|
181
|
+
`<fills count="1"><fill><patternFill patternType="none"/></fill></fills>` +
|
|
182
|
+
`<borders count="1"><border/></borders>` +
|
|
183
|
+
`<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>` +
|
|
184
|
+
`<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>` +
|
|
185
|
+
`</styleSheet>`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Convert a MarkdownDocument to a .xlsx file (tables-only fidelity).
|
|
191
|
+
*
|
|
192
|
+
* Each markdown `table` becomes one worksheet; a document with no tables
|
|
193
|
+
* yields a single empty sheet (a valid, openable file — never throws).
|
|
194
|
+
*/
|
|
195
|
+
export async function markdownDocToXlsx(
|
|
196
|
+
doc: MarkdownDocument,
|
|
197
|
+
options: XlsxExportOptions = {},
|
|
198
|
+
): Promise<ArrayBuffer> {
|
|
199
|
+
const prefix = options.sheetNamePrefix ?? 'Sheet';
|
|
200
|
+
let sheets = collectSheets(doc, prefix);
|
|
201
|
+
|
|
202
|
+
// Zero tables → one empty sheet so the file is still valid.
|
|
203
|
+
if (sheets.length === 0) {
|
|
204
|
+
sheets = [{ name: `${prefix}1`, grid: [] }];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const pkg = createPackage();
|
|
208
|
+
|
|
209
|
+
// Worksheets + workbook→worksheet relationships.
|
|
210
|
+
sheets.forEach((sheet, i) => {
|
|
211
|
+
const sheetPath = `xl/worksheets/sheet${i + 1}.xml`;
|
|
212
|
+
pkg.addPart(sheetPath, worksheetXml(sheet.grid), CONTENT_TYPE_XLSX_WORKSHEET);
|
|
213
|
+
pkg.addRelationship('xl/workbook.xml', {
|
|
214
|
+
id: `rId${i + 1}`,
|
|
215
|
+
type: REL_WORKSHEET,
|
|
216
|
+
target: `worksheets/sheet${i + 1}.xml`,
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Styles part (referenced after the sheet relationships).
|
|
221
|
+
pkg.addPart('xl/styles.xml', stylesXml(), CONTENT_TYPE_XLSX_STYLES);
|
|
222
|
+
pkg.addRelationship('xl/workbook.xml', {
|
|
223
|
+
id: `rId${sheets.length + 1}`,
|
|
224
|
+
type: REL_STYLES,
|
|
225
|
+
target: 'styles.xml',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Workbook + root relationship.
|
|
229
|
+
pkg.addPart('xl/workbook.xml', workbookXml(sheets), CONTENT_TYPE_XLSX_WORKBOOK);
|
|
230
|
+
pkg.addRelationship('', {
|
|
231
|
+
id: 'rId1',
|
|
232
|
+
type: REL_OFFICE_DOCUMENT,
|
|
233
|
+
target: 'xl/workbook.xml',
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (options.title || options.author) {
|
|
237
|
+
pkg.setCoreProperties({
|
|
238
|
+
title: options.title,
|
|
239
|
+
creator: options.author,
|
|
240
|
+
created: new Date().toISOString(),
|
|
241
|
+
modified: new Date().toISOString(),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return pkg.toArrayBuffer();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Convert a squisq Doc to a .xlsx file (via the markdown table model).
|
|
250
|
+
*/
|
|
251
|
+
export async function docToXlsx(doc: Doc, options?: XlsxExportOptions): Promise<ArrayBuffer> {
|
|
252
|
+
return markdownDocToXlsx(docToMarkdown(doc), options);
|
|
253
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLSX import — SpreadsheetML (.xlsx) → MarkdownDocument.
|
|
3
|
+
*
|
|
4
|
+
* Reuses the shared ooxml/ reader (zip + DOMParser). Reads the workbook's
|
|
5
|
+
* sheet list, resolves each sheet part via relationships, pulls shared strings,
|
|
6
|
+
* and turns each worksheet's cell grid into a markdown table (first row treated
|
|
7
|
+
* as the header). By default every sheet is imported, each preceded by an H1 of
|
|
8
|
+
* the sheet name; pass `options.sheet` (index or name) to import just one.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
MarkdownBlockNode,
|
|
13
|
+
MarkdownDocument,
|
|
14
|
+
MarkdownTable,
|
|
15
|
+
MarkdownTableCell,
|
|
16
|
+
MarkdownTableRow,
|
|
17
|
+
} from '@bendyline/squisq/markdown';
|
|
18
|
+
import { getPartXml, getPartRelationships, openPackage } from '../ooxml/reader.js';
|
|
19
|
+
import type { OoxmlPackage } from '../ooxml/types.js';
|
|
20
|
+
import { NS_R, NS_SML } from '../ooxml/namespaces.js';
|
|
21
|
+
|
|
22
|
+
export interface XlsxImportOptions {
|
|
23
|
+
/** Which sheet to import (0-based index or sheet name). Default: all sheets. */
|
|
24
|
+
sheet?: number | string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SheetRef {
|
|
28
|
+
name: string;
|
|
29
|
+
path: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function attrNS(el: Element, ns: string, local: string, fallback: string): string | null {
|
|
33
|
+
return el.getAttributeNS(ns, local) ?? el.getAttribute(fallback);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Resolve a relationship target (relative to its part's dir) to a zip path. */
|
|
37
|
+
function resolveTarget(baseDir: string, target: string): string {
|
|
38
|
+
if (target.startsWith('/')) return target.replace(/^\//, '');
|
|
39
|
+
const stack = baseDir ? baseDir.split('/') : [];
|
|
40
|
+
for (const seg of target.split('/')) {
|
|
41
|
+
if (seg === '..') stack.pop();
|
|
42
|
+
else if (seg !== '.') stack.push(seg);
|
|
43
|
+
}
|
|
44
|
+
return stack.join('/');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readSheets(pkg: OoxmlPackage): Promise<SheetRef[]> {
|
|
48
|
+
const wb = await getPartXml(pkg, 'xl/workbook.xml');
|
|
49
|
+
if (!wb) return [];
|
|
50
|
+
const rels = await getPartRelationships(pkg, 'xl/workbook.xml');
|
|
51
|
+
const relById = new Map(rels.map((r) => [r.id, r.target]));
|
|
52
|
+
const out: SheetRef[] = [];
|
|
53
|
+
const sheetEls = wb.getElementsByTagNameNS(NS_SML, 'sheet');
|
|
54
|
+
for (let i = 0; i < sheetEls.length; i++) {
|
|
55
|
+
const el = sheetEls[i]!;
|
|
56
|
+
const name = el.getAttribute('name') ?? `Sheet${i + 1}`;
|
|
57
|
+
const rid = attrNS(el, NS_R, 'id', 'r:id');
|
|
58
|
+
const target = rid ? relById.get(rid) : undefined;
|
|
59
|
+
if (target) out.push({ name, path: resolveTarget('xl', target) });
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readSharedStrings(pkg: OoxmlPackage): Promise<string[]> {
|
|
65
|
+
const doc = await getPartXml(pkg, 'xl/sharedStrings.xml');
|
|
66
|
+
if (!doc) return [];
|
|
67
|
+
const siEls = doc.getElementsByTagNameNS(NS_SML, 'si');
|
|
68
|
+
const out: string[] = [];
|
|
69
|
+
for (let i = 0; i < siEls.length; i++) out.push(siEls[i]!.textContent ?? '');
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Column letters of a cell ref ("B7" → 1, "AA1" → 26). */
|
|
74
|
+
function colIndex(ref: string): number {
|
|
75
|
+
const m = /^([A-Za-z]+)/.exec(ref);
|
|
76
|
+
if (!m) return 0;
|
|
77
|
+
let n = 0;
|
|
78
|
+
for (const ch of m[1]!.toUpperCase()) n = n * 26 + (ch.charCodeAt(0) - 64);
|
|
79
|
+
return n - 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function cellText(cell: Element, shared: string[]): string {
|
|
83
|
+
const t = cell.getAttribute('t');
|
|
84
|
+
if (t === 'inlineStr') {
|
|
85
|
+
const is = cell.getElementsByTagNameNS(NS_SML, 't');
|
|
86
|
+
return is.length ? (is[0]!.textContent ?? '') : '';
|
|
87
|
+
}
|
|
88
|
+
const vEls = cell.getElementsByTagNameNS(NS_SML, 'v');
|
|
89
|
+
const v = vEls.length ? (vEls[0]!.textContent ?? '') : '';
|
|
90
|
+
if (t === 's') return shared[Number.parseInt(v, 10)] ?? '';
|
|
91
|
+
if (t === 'b') return v === '1' ? 'TRUE' : 'FALSE';
|
|
92
|
+
return v;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function sheetToGrid(pkg: OoxmlPackage, path: string, shared: string[]): Promise<string[][]> {
|
|
96
|
+
const doc = await getPartXml(pkg, path);
|
|
97
|
+
if (!doc) return [];
|
|
98
|
+
const rowEls = doc.getElementsByTagNameNS(NS_SML, 'row');
|
|
99
|
+
const grid: string[][] = [];
|
|
100
|
+
for (let r = 0; r < rowEls.length; r++) {
|
|
101
|
+
const cells = rowEls[r]!.getElementsByTagNameNS(NS_SML, 'c');
|
|
102
|
+
const rowArr: string[] = [];
|
|
103
|
+
for (let c = 0; c < cells.length; c++) {
|
|
104
|
+
const cell = cells[c]!;
|
|
105
|
+
const ref = cell.getAttribute('r');
|
|
106
|
+
const idx = ref ? colIndex(ref) : rowArr.length;
|
|
107
|
+
while (rowArr.length < idx) rowArr.push('');
|
|
108
|
+
rowArr[idx] = cellText(cell, shared);
|
|
109
|
+
}
|
|
110
|
+
grid.push(rowArr);
|
|
111
|
+
}
|
|
112
|
+
return grid;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function gridToTable(grid: string[][]): MarkdownTable {
|
|
116
|
+
const maxCols = grid.reduce((m, r) => Math.max(m, r.length), 1);
|
|
117
|
+
const rows: MarkdownTableRow[] = grid.map((cells, rowIdx) => {
|
|
118
|
+
const children: MarkdownTableCell[] = [];
|
|
119
|
+
for (let c = 0; c < maxCols; c++) {
|
|
120
|
+
const value = cells[c] ?? '';
|
|
121
|
+
children.push({
|
|
122
|
+
type: 'tableCell',
|
|
123
|
+
...(rowIdx === 0 ? { isHeader: true } : {}),
|
|
124
|
+
children: value ? [{ type: 'text', value }] : [],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return { type: 'tableRow', children };
|
|
128
|
+
});
|
|
129
|
+
return { type: 'table', children: rows };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function xlsxToMarkdownDoc(
|
|
133
|
+
data: ArrayBuffer | Blob,
|
|
134
|
+
options: XlsxImportOptions = {},
|
|
135
|
+
): Promise<MarkdownDocument> {
|
|
136
|
+
const pkg = await openPackage(data);
|
|
137
|
+
const sheets = await readSheets(pkg);
|
|
138
|
+
const shared = await readSharedStrings(pkg);
|
|
139
|
+
|
|
140
|
+
let selected = sheets;
|
|
141
|
+
if (options.sheet !== undefined) {
|
|
142
|
+
const picked =
|
|
143
|
+
typeof options.sheet === 'number'
|
|
144
|
+
? sheets[options.sheet]
|
|
145
|
+
: sheets.find((s) => s.name === options.sheet);
|
|
146
|
+
selected = picked ? [picked] : [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const children: MarkdownBlockNode[] = [];
|
|
150
|
+
const single = selected.length === 1 && options.sheet !== undefined;
|
|
151
|
+
for (const sheet of selected) {
|
|
152
|
+
const grid = await sheetToGrid(pkg, sheet.path, shared);
|
|
153
|
+
if (!single) {
|
|
154
|
+
children.push({ type: 'heading', depth: 1, children: [{ type: 'text', value: sheet.name }] });
|
|
155
|
+
}
|
|
156
|
+
if (grid.length > 0) children.push(gridToTable(grid));
|
|
157
|
+
}
|
|
158
|
+
return { type: 'document', children };
|
|
159
|
+
}
|
package/src/xlsx/index.ts
CHANGED
|
@@ -1,78 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @bendyline/squisq-formats XLSX Module
|
|
2
|
+
* @bendyline/squisq-formats XLSX Module
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Excel .xlsx support via SpreadsheetML and the shared ooxml/ infrastructure.
|
|
5
|
+
* Both directions are implemented: import (`xlsxToMarkdownDoc` / `xlsxToDoc`)
|
|
6
|
+
* turns each worksheet grid into a markdown table; export (`markdownDocToXlsx`
|
|
7
|
+
* / `docToXlsx`) turns each markdown table into a worksheet. Export is
|
|
8
|
+
* tables-only fidelity — non-table content is dropped (headings only survive
|
|
9
|
+
* as sheet names). See export.ts for details.
|
|
7
10
|
*
|
|
8
11
|
* @example
|
|
9
12
|
* ```ts
|
|
10
|
-
* import { markdownDocToXlsx } from '@bendyline/squisq-formats/xlsx';
|
|
13
|
+
* import { xlsxToMarkdownDoc, markdownDocToXlsx } from '@bendyline/squisq-formats/xlsx';
|
|
11
14
|
* ```
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
|
-
import
|
|
17
|
+
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
15
18
|
import type { Doc } from '@bendyline/squisq/schemas';
|
|
19
|
+
import { type XlsxImportOptions, xlsxToMarkdownDoc } from './import.js';
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*/
|
|
20
|
-
export interface XlsxExportOptions {
|
|
21
|
-
/** Workbook title */
|
|
22
|
-
title?: string;
|
|
23
|
-
/** Workbook author */
|
|
24
|
-
author?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Options for XLSX import (placeholder).
|
|
29
|
-
*/
|
|
30
|
-
export interface XlsxImportOptions {
|
|
31
|
-
/** Which sheet to import (0-based index or name). Default: 0 */
|
|
32
|
-
sheet?: number | string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Convert a MarkdownDocument to a .xlsx Blob.
|
|
37
|
-
*
|
|
38
|
-
* @throws Error — XLSX support is not yet implemented
|
|
39
|
-
*/
|
|
40
|
-
export async function markdownDocToXlsx(
|
|
41
|
-
_doc: MarkdownDocument,
|
|
42
|
-
_options?: XlsxExportOptions,
|
|
43
|
-
): Promise<Blob> {
|
|
44
|
-
throw new Error('XLSX export is not yet implemented');
|
|
45
|
-
}
|
|
21
|
+
export type { XlsxImportOptions } from './import.js';
|
|
22
|
+
export { xlsxToMarkdownDoc } from './import.js';
|
|
46
23
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
* @throws Error — XLSX support is not yet implemented
|
|
51
|
-
*/
|
|
52
|
-
export async function docToXlsx(_doc: Doc, _options?: XlsxExportOptions): Promise<Blob> {
|
|
53
|
-
throw new Error('XLSX export is not yet implemented');
|
|
54
|
-
}
|
|
24
|
+
export type { XlsxExportOptions } from './export.js';
|
|
25
|
+
export { markdownDocToXlsx, docToXlsx } from './export.js';
|
|
55
26
|
|
|
56
27
|
/**
|
|
57
|
-
* Convert a .xlsx file to a
|
|
58
|
-
*
|
|
59
|
-
* @throws Error — XLSX support is not yet implemented
|
|
60
|
-
*/
|
|
61
|
-
export async function xlsxToMarkdownDoc(
|
|
62
|
-
_data: ArrayBuffer | Blob,
|
|
63
|
-
_options?: XlsxImportOptions,
|
|
64
|
-
): Promise<MarkdownDocument> {
|
|
65
|
-
throw new Error('XLSX import is not yet implemented');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Convert a .xlsx file to a squisq Doc.
|
|
70
|
-
*
|
|
71
|
-
* @throws Error — XLSX support is not yet implemented
|
|
28
|
+
* Convert a .xlsx file to a squisq Doc (via the markdown table model).
|
|
72
29
|
*/
|
|
73
30
|
export async function xlsxToDoc(
|
|
74
|
-
|
|
75
|
-
|
|
31
|
+
data: ArrayBuffer | Blob,
|
|
32
|
+
options?: XlsxImportOptions,
|
|
76
33
|
): Promise<Doc> {
|
|
77
|
-
|
|
34
|
+
return markdownToDoc(await xlsxToMarkdownDoc(data, options));
|
|
78
35
|
}
|