@bendyline/squisq-formats 2.5.0 → 2.5.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/NOTICE.md CHANGED
@@ -10,7 +10,7 @@ Third-party components remain under their respective license terms.
10
10
  | ---------------- | ------- | ----------------------- | ---------------------------------- |
11
11
  | @pdf-lib/fontkit | 1.1.1 | MIT | https://github.com/Hopding/fontkit |
12
12
  | @pdf-lib/upng | 1.0.1 | MIT | https://github.com/Hopding/upng |
13
- | @xmldom/xmldom | 0.9.11 | MIT | https://github.com/xmldom/xmldom |
13
+ | @xmldom/xmldom | 0.9.12 | MIT | https://github.com/xmldom/xmldom |
14
14
  | jszip | 3.10.1 | MIT OR GPL-3.0-or-later | https://github.com/Stuk/jszip |
15
15
  | pdf-lib | 1.17.1 | MIT | https://pdf-lib.js.org |
16
16
  | pdfjs-dist | 4.10.38 | Apache-2.0 | https://mozilla.github.io/pdf.js |
@@ -387,6 +387,113 @@ function peelCaptionRow(grid, rect) {
387
387
  return { caption, rect: { ...rect, top: rect.top + 1 } };
388
388
  }
389
389
 
390
+ // src/xlsx/tables.ts
391
+ function columnLetter2(index) {
392
+ let n = index;
393
+ let out = "";
394
+ do {
395
+ out = String.fromCharCode(65 + n % 26) + out;
396
+ n = Math.floor(n / 26) - 1;
397
+ } while (n >= 0);
398
+ return out;
399
+ }
400
+ function a1(row, col) {
401
+ return `${columnLetter2(col)}${row + 1}`;
402
+ }
403
+ function regionHasHeader(cells) {
404
+ if (cells.length < 2) return false;
405
+ const first = cells[0];
406
+ if (!first || first.length === 0) return false;
407
+ return first.every((cell) => cell.kind === "string");
408
+ }
409
+ function cellValue(cell) {
410
+ if (cell.kind === "empty" || cell.kind === "error") return null;
411
+ if (cell.value !== void 0) return cell.value;
412
+ return cell.text === "" ? null : cell.text;
413
+ }
414
+ function dominantKind(kinds) {
415
+ const counts = /* @__PURE__ */ new Map();
416
+ let total = 0;
417
+ for (const kind of kinds) {
418
+ if (kind === "empty") continue;
419
+ counts.set(kind, (counts.get(kind) ?? 0) + 1);
420
+ total += 1;
421
+ }
422
+ if (total === 0) return "empty";
423
+ let best = "string";
424
+ let bestCount = 0;
425
+ for (const [kind, count] of counts) {
426
+ if (count > bestCount) {
427
+ best = kind;
428
+ bestCount = count;
429
+ }
430
+ }
431
+ return bestCount * 2 > total ? best : "mixed";
432
+ }
433
+ function sliceRegion(grid, rect) {
434
+ const out = [];
435
+ for (let r = rect.top; r <= rect.bottom; r++) {
436
+ const row = [];
437
+ for (let c = rect.left; c <= rect.right; c++) {
438
+ row.push(grid[r]?.[c] ?? { text: "", kind: "empty" });
439
+ }
440
+ out.push(row);
441
+ }
442
+ return out;
443
+ }
444
+ function regionToTable(sheet, grid, rect, title, minRows) {
445
+ const cells = sliceRegion(grid, rect);
446
+ if (cells.length === 0) return null;
447
+ const hasHeader = regionHasHeader(cells);
448
+ const body = hasHeader ? cells.slice(1) : cells;
449
+ if (body.length < minRows) return null;
450
+ if (!body.some((row) => row.some(isOccupied))) return null;
451
+ const width = cells.reduce((max, row) => Math.max(max, row.length), 0);
452
+ const columns = [];
453
+ for (let c = 0; c < width; c++) {
454
+ const header = hasHeader ? cells[0]?.[c]?.text ?? "" : "";
455
+ columns.push({
456
+ name: header.trim() || columnLetter2(rect.left + c),
457
+ kind: dominantKind(body.map((row) => row[c]?.kind ?? "empty"))
458
+ });
459
+ }
460
+ return {
461
+ sheet,
462
+ anchor: a1(rect.top, rect.left),
463
+ ...title ? { title } : {},
464
+ columns,
465
+ hasHeader,
466
+ rows: body.map(
467
+ (row) => Array.from({ length: width }, (_, c) => cellValue(row[c] ?? { text: "", kind: "empty" }))
468
+ )
469
+ };
470
+ }
471
+ function gridToTables(sheet, grid, merges = [], options = {}) {
472
+ if (grid.length === 0) return [];
473
+ const minRows = options.minRows ?? 1;
474
+ const plan = detectRegions(grid, merges, {
475
+ ...options.maxRegionsPerSheet !== void 0 ? { maxRegionsPerSheet: options.maxRegionsPerSheet } : {},
476
+ ...options.minRegionCells !== void 0 ? { minRegionCells: options.minRegionCells } : {},
477
+ ...options.signal ? { signal: options.signal } : {}
478
+ });
479
+ if (plan.degraded || plan.regions.length === 0) {
480
+ const rect = {
481
+ top: 0,
482
+ left: 0,
483
+ bottom: grid.length - 1,
484
+ right: grid.reduce((max, row) => Math.max(max, row.length), 1) - 1
485
+ };
486
+ const whole = regionToTable(sheet, grid, rect, void 0, minRows);
487
+ return whole ? [whole] : [];
488
+ }
489
+ const out = [];
490
+ for (const region of plan.regions) {
491
+ const table = regionToTable(sheet, grid, region.rect, region.title, minRows);
492
+ if (table) out.push(table);
493
+ }
494
+ return out;
495
+ }
496
+
390
497
  // src/xlsx/import.ts
391
498
  var XLSX_MAIN_PART = "xl/workbook.xml";
392
499
  function attrNS(el, ns, local, fallback) {
@@ -553,6 +660,16 @@ function excelTimeText(serial, includeSeconds, elapsedHours) {
553
660
  const seconds = totalSeconds % 60;
554
661
  return `${twoDigits(hours)}:${twoDigits(minutes)}${includeSeconds ? `:${twoDigits(seconds)}` : ""}`;
555
662
  }
663
+ function dateValueText(serial, kind, style, date1904) {
664
+ if (!Number.isFinite(serial)) return null;
665
+ const normalized = style ? normalizeFormatCode(style.formatCode) : "";
666
+ if (kind === "time")
667
+ return excelTimeText(serial, /s/.test(normalized), /\[[h]+\]/.test(normalized));
668
+ const date = excelDateText(serial, date1904);
669
+ if (!date) return null;
670
+ if (kind === "datetime") return `${date} ${excelTimeText(serial, /s/.test(normalized), false)}`;
671
+ return date;
672
+ }
556
673
  function formattedNumberText(raw, style, date1904) {
557
674
  if (!style) return raw;
558
675
  const value = Number(raw);
@@ -600,13 +717,14 @@ function readFormula(cell, row, col, ctx) {
600
717
  }
601
718
  function readCell(cell, row, col, ctx) {
602
719
  const formula = readFormula(cell, row, col, ctx);
603
- const withFormula = (text2, kind2) => {
720
+ const withFormula = (text2, kind2, value) => {
604
721
  const out = { text: text2, kind: text2 === "" ? "empty" : kind2 };
605
722
  if (formula !== "") out.formula = formula;
723
+ if (value !== void 0 && out.kind !== "empty") out.value = value;
606
724
  return out;
607
725
  };
608
726
  const stringCell = (item) => {
609
- const out = withFormula(item.text, "string");
727
+ const out = withFormula(item.text, "string", item.text);
610
728
  if (item.rich && out.kind !== "empty") out.richText = item.rich;
611
729
  return out;
612
730
  };
@@ -619,14 +737,19 @@ function readCell(cell, row, col, ctx) {
619
737
  const v = vEls.length ? vEls[0].textContent ?? "" : "";
620
738
  if (v === "") return withFormula("", "empty");
621
739
  if (t === "s") return stringCell(ctx.shared[Number.parseInt(v, 10)] ?? { text: "" });
622
- if (t === "b") return withFormula(v === "1" ? "TRUE" : "FALSE", "bool");
740
+ if (t === "b") return withFormula(v === "1" ? "TRUE" : "FALSE", "bool", v === "1");
623
741
  if (t === "e") return withFormula(v, "error");
624
- if (t === "str") return withFormula(v, "string");
742
+ if (t === "str") return withFormula(v, "string", v);
625
743
  const style = ctx.styles[Number.parseInt(cell.getAttribute("s") ?? "0", 10)];
626
744
  const text = formattedNumberText(v, style, ctx.date1904);
627
745
  const dateLike = style ? numberFormatKind(style.formatCode) : "general";
628
746
  const kind = dateLike === "date" || dateLike === "time" || dateLike === "datetime" ? "date" : "number";
629
- return withFormula(text, kind);
747
+ const numeric = Number(v);
748
+ if (kind === "date") {
749
+ const iso = dateValueText(numeric, dateLike, style, ctx.date1904);
750
+ return withFormula(text, kind, iso ?? text);
751
+ }
752
+ return withFormula(text, kind, Number.isFinite(numeric) ? numeric : v);
630
753
  }
631
754
  async function sheetToCells(pkg, path, shared, styles, date1904) {
632
755
  const doc = await getPartXml(pkg, path);
@@ -723,6 +846,26 @@ function annotatedHeading(depth, text, params) {
723
846
  templateAnnotation: { template: "dataTable", params }
724
847
  };
725
848
  }
849
+ async function xlsxToTables(data, options = {}) {
850
+ const pkg = await openPackage(data, options);
851
+ const mainPart = requireMainPartPath(pkg, XLSX_MAIN_PART, "XLSX");
852
+ const [{ sheets, date1904 }, shared, styles] = await Promise.all([
853
+ readWorkbook(pkg, mainPart),
854
+ readSharedStrings(pkg),
855
+ readCellStyles(pkg)
856
+ ]);
857
+ let selected = sheets;
858
+ if (options.sheet !== void 0) {
859
+ const picked = typeof options.sheet === "number" ? sheets[options.sheet] : sheets.find((s) => s.name === options.sheet);
860
+ selected = picked ? [picked] : [];
861
+ }
862
+ const out = [];
863
+ for (const sheet of selected) {
864
+ const { cells, merges } = await sheetToCells(pkg, sheet.path, shared, styles, date1904);
865
+ out.push(...gridToTables(sheet.name, cells, merges, options));
866
+ }
867
+ return out;
868
+ }
726
869
  async function xlsxToMarkdownDoc(data, options = {}) {
727
870
  const pkg = await openPackage(data, options);
728
871
  const mainPart = requireMainPartPath(pkg, XLSX_MAIN_PART, "XLSX");
@@ -1190,6 +1333,8 @@ async function xlsxToDoc(data, options) {
1190
1333
  }
1191
1334
 
1192
1335
  export {
1336
+ gridToTables,
1337
+ xlsxToTables,
1193
1338
  xlsxToMarkdownDoc,
1194
1339
  markdownDocToXlsx,
1195
1340
  docToXlsx,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  convert,
3
3
  defaultRegistry
4
- } from "./chunk-JTGWQK5V.js";
4
+ } from "./chunk-5JQ5NDRC.js";
5
5
  import {
6
6
  ConversionError
7
7
  } from "./chunk-KXOZMWBS.js";
@@ -0,0 +1,270 @@
1
+ import { MarkdownInlineNode, MarkdownDocument } from '@bendyline/squisq/markdown';
2
+ import { O as OoxmlOpenOptions } from './reader-B_m1aKZC.js';
3
+ import { Doc } from '@bendyline/squisq/schemas';
4
+
5
+ /**
6
+ * Shared SpreadsheetML cell model and A1-reference arithmetic.
7
+ *
8
+ * Import and export both need to move between a zero-based `(row, col)` grid
9
+ * and Excel's `"B7"` addressing. That logic used to exist twice — `colIndex`
10
+ * in `import.ts` and `columnLetter` in `export.ts` — as two halves of the same
11
+ * bijection that never met. It lives here now, together with the richer cell
12
+ * record the region splitter needs.
13
+ *
14
+ * {@link XlsxCell} carries three things the old plain-string grid could not:
15
+ * the cell's *kind* (so a header row can be told apart from a data row, and so
16
+ * export can safely re-emit a number as a number), and its *formula* (so a
17
+ * round trip through markdown keeps `=B2*C2` rather than freezing the cached
18
+ * result).
19
+ */
20
+
21
+ /** What a cell holds, beyond its display text. */
22
+ type XlsxCellKind = 'empty' | 'string' | 'number' | 'bool' | 'date' | 'error';
23
+ /** A single worksheet cell. */
24
+ interface XlsxCell {
25
+ /** Display text — the plain string, with all run formatting flattened out. */
26
+ text: string;
27
+ /** What the cell holds. `empty` iff `text` is `''`. */
28
+ kind: XlsxCellKind;
29
+ /** Formula source WITHOUT the leading `=`, when the cell carries one. */
30
+ formula?: string;
31
+ /**
32
+ * Inline markdown for a cell whose rich text carries formatting worth
33
+ * keeping — today, superscript/subscript runs (`Fresh<sup>1</sup>`).
34
+ *
35
+ * Deliberately additive: `text` remains the flattened string, so region
36
+ * detection, header sniffing, numeric inference and export placement all
37
+ * keep working on exactly the value they saw before. Only the markdown table
38
+ * cell reads this, and only when it is present.
39
+ */
40
+ richText?: MarkdownInlineNode[];
41
+ /**
42
+ * The cell's value as the sheet stores it, before number formatting.
43
+ *
44
+ * `text` is a rendering for people, and rendering destroys information a
45
+ * consumer doing arithmetic needs: a percent-formatted `0.15` renders as
46
+ * `"15.0%"`, a date is a serial rendered as text, and a zero-padded `7`
47
+ * renders as `"007"`. Anything reading a sheet as *data* — rather than as a
48
+ * document — must read this instead.
49
+ *
50
+ * Normalized rather than literally raw, where a literal value would be
51
+ * useless: a date arrives as an ISO `YYYY-MM-DD` (or `YYYY-MM-DD HH:MM`)
52
+ * string rather than an Excel serial, because the serial's meaning depends
53
+ * on a workbook-level 1900/1904 epoch flag that no downstream consumer
54
+ * should have to carry. Numbers, booleans and strings are exact.
55
+ *
56
+ * Absent for `empty` and `error` cells, which have no value to speak of.
57
+ */
58
+ value?: number | boolean | string;
59
+ }
60
+ /** A zero-based, inclusive rectangle of cells. */
61
+ interface CellRect {
62
+ top: number;
63
+ left: number;
64
+ bottom: number;
65
+ right: number;
66
+ }
67
+
68
+ /**
69
+ * XLSX → typed tables, for consumers reading a workbook as **data**.
70
+ *
71
+ * `xlsxToMarkdownDoc` renders a workbook for people: it flattens each cell to
72
+ * the string the sheet displays. That rendering is lossy in exactly the ways
73
+ * arithmetic cares about — a percent-formatted `0.15` becomes `"15.0%"`, a
74
+ * date becomes text, a zero-padded `7` becomes `"007"` — so anything that
75
+ * needs to sum, average or compare must not go through it.
76
+ *
77
+ * This module is the other path. It reuses the same region detection (a sheet
78
+ * is not one table; it is several islands with labels and totals in the gaps)
79
+ * and emits each island's cells as their underlying values, with the type the
80
+ * sheet gave them.
81
+ *
82
+ * Two kinds of region are deliberately excluded from the result. A
83
+ * `formulas` companion is presentation — the same cells again, showing their
84
+ * expressions rather than their results — and a `loose` bucket is stray labels
85
+ * and notes, which have no columns to speak of. Both are useful to a reader
86
+ * and meaningless to a query, so a consumer asking for tables gets neither.
87
+ */
88
+
89
+ /** One column of a detected table. */
90
+ interface XlsxTableColumn {
91
+ /** Header text when the region has a header row; otherwise a column letter. */
92
+ name: string;
93
+ /**
94
+ * The dominant cell kind in the column's body, so a consumer can pick a
95
+ * storage type without re-sniffing. `mixed` when no single kind holds a
96
+ * majority — the honest answer for a column that really is heterogeneous.
97
+ */
98
+ kind: XlsxCellKind | 'mixed';
99
+ }
100
+ /** One data island, as values rather than as display text. */
101
+ interface XlsxTable {
102
+ /** Worksheet name. */
103
+ sheet: string;
104
+ /** A1 address of the region's top-left cell, e.g. `B4`. */
105
+ anchor: string;
106
+ /** Caption absorbed from directly above the region, when there was one. */
107
+ title?: string;
108
+ columns: XlsxTableColumn[];
109
+ /** True when row 0 was read as a header and is therefore not a data row. */
110
+ hasHeader: boolean;
111
+ /**
112
+ * Body rows, header excluded. A cell with no value — blank, or an error —
113
+ * is `null` rather than absent, so every row has the same arity as
114
+ * `columns`.
115
+ */
116
+ rows: (string | number | boolean | null)[][];
117
+ }
118
+ interface XlsxTablesOptions {
119
+ /** Restrict to one sheet, by zero-based index or by name. */
120
+ sheet?: number | string;
121
+ maxRegionsPerSheet?: number;
122
+ minRegionCells?: number;
123
+ /** Skip regions with fewer than this many body rows. Default 1. */
124
+ minRows?: number;
125
+ signal?: AbortSignal;
126
+ }
127
+ /**
128
+ * Split one sheet's grid into typed tables.
129
+ *
130
+ * Exported separately from the workbook entry point so a caller that already
131
+ * has a grid — a test, or a consumer streaming sheets itself — does not have
132
+ * to re-open the package.
133
+ */
134
+ declare function gridToTables(sheet: string, grid: readonly (readonly XlsxCell[])[], merges?: readonly CellRect[], options?: XlsxTablesOptions): XlsxTable[];
135
+
136
+ /**
137
+ * XLSX import — SpreadsheetML (.xlsx) → MarkdownDocument.
138
+ *
139
+ * Reuses the shared ooxml/ reader (zip + DOMParser). Reads the workbook's sheet
140
+ * list, resolves each sheet part via relationships, pulls shared strings, and
141
+ * turns each worksheet into markdown. By default every sheet is imported, each
142
+ * preceded by an H1 of the sheet name; pass `options.sheet` (index or name) to
143
+ * import just one.
144
+ *
145
+ * A sheet is NOT one table. It is usually several tables scattered across the
146
+ * grid with stray labels and notes in the gaps, so by default each worksheet is
147
+ * split into its contiguous data islands (see `regions.ts`) and every island
148
+ * becomes its own block:
149
+ *
150
+ * ```markdown
151
+ * ## Q3 Revenue {[dataTable sheet=Sales anchor=B7]}
152
+ * ```
153
+ *
154
+ * The `sheet`/`anchor` params on the heading annotation are what let
155
+ * `markdownDocToXlsx` put each table back where it came from, so the round trip
156
+ * reproduces addresses rather than piling everything at A1. A region holding
157
+ * formulas additionally emits a `role=formulas` companion table, and every
158
+ * left-over single cell on a sheet collects into one `role=loose` table.
159
+ *
160
+ * Pass `{ regions: false }` for the historical behavior: one table per sheet,
161
+ * spanning the whole used range.
162
+ */
163
+
164
+ interface XlsxImportOptions extends OoxmlOpenOptions {
165
+ /** Which sheet to import (0-based index or sheet name). Default: all sheets. */
166
+ sheet?: number | string;
167
+ /**
168
+ * Split each sheet into its contiguous data islands, one block each, anchored
169
+ * with `{[dataTable sheet=… anchor=…]}`. Default true. Set false for the
170
+ * historical one-table-per-sheet output.
171
+ */
172
+ regions?: boolean;
173
+ /**
174
+ * Emit a `role=formulas` companion table for regions that contain formulas.
175
+ * Default true. Ignored when `regions` is false.
176
+ */
177
+ formulas?: boolean;
178
+ /** Cap on region tables per sheet before the rest fold into loose cells. Default 64. */
179
+ maxRegionsPerSheet?: number;
180
+ /** Smallest island that stays a table of its own. Default 2 — single cells coalesce. */
181
+ minRegionCells?: number;
182
+ }
183
+ /**
184
+ * Read a workbook as typed tables rather than as a document.
185
+ *
186
+ * The data counterpart to {@link xlsxToMarkdownDoc}: same package, same sheet
187
+ * selection, same region detection — but each island's cells arrive as their
188
+ * underlying values, so a consumer can sum a column without first undoing a
189
+ * number format.
190
+ */
191
+ declare function xlsxToTables(data: ArrayBuffer | Blob, options?: XlsxImportOptions & XlsxTablesOptions): Promise<XlsxTable[]>;
192
+ declare function xlsxToMarkdownDoc(data: ArrayBuffer | Blob, options?: XlsxImportOptions): Promise<MarkdownDocument>;
193
+
194
+ /**
195
+ * XLSX export — MarkdownDocument → SpreadsheetML (.xlsx).
196
+ *
197
+ * Tables-only fidelity (honestly documented): every `table` node in the
198
+ * markdown AST becomes worksheet cells; all other content (prose, lists,
199
+ * images, …) is dropped, and headings survive only as sheet names and as the
200
+ * carrier of placement metadata.
201
+ *
202
+ * Placement has two modes, decided per table by `workbookPlan.ts`. A table
203
+ * whose heading carries `{[dataTable sheet=… anchor=…]}` — what
204
+ * `xlsxToMarkdownDoc` emits for every data island it finds — is placed on the
205
+ * named sheet at the named cell, so several mini tables share one worksheet at
206
+ * their original addresses and formulas ride along. A table with no such
207
+ * annotation keeps the historical behavior exactly: its own worksheet, named
208
+ * from the nearest preceding heading, starting at A1.
209
+ *
210
+ * Cells are emitted as inline strings (`t="inlineStr"`) by default so no
211
+ * sharedStrings part is needed and identifier-like numbers remain lossless.
212
+ * Callers can explicitly opt into conservative numeric inference. The package
213
+ * is assembled with the shared ooxml/ writer (auto-generates
214
+ * `[Content_Types].xml` + `_rels`), so only the SpreadsheetML-specific parts
215
+ * (workbook, worksheets, styles) are written here.
216
+ *
217
+ * @example
218
+ * ```ts
219
+ * import { parseMarkdown } from '@bendyline/squisq/markdown';
220
+ * import { markdownDocToXlsx } from '@bendyline/squisq-formats/xlsx';
221
+ *
222
+ * const md = parseMarkdown('# Metrics\n\n| A | B |\n| - | - |\n| 1 | 2 |');
223
+ * const buffer = await markdownDocToXlsx(md);
224
+ * ```
225
+ */
226
+
227
+ /**
228
+ * Options for XLSX export.
229
+ */
230
+ interface XlsxExportOptions {
231
+ /** Cancel at bounded export checkpoints. */
232
+ signal?: AbortSignal;
233
+ /** Maximum cells emitted. Default: 100,000. */
234
+ maxCells?: number;
235
+ /** Workbook title (written to core properties). */
236
+ title?: string;
237
+ /** Workbook author (written to core properties). */
238
+ author?: string;
239
+ /** Prefix used for auto-named sheets when no heading precedes a table. Default: "Sheet". */
240
+ sheetNamePrefix?: string;
241
+ /**
242
+ * Emit canonical, Excel-safe number strings as numeric cells.
243
+ *
244
+ * Defaults to false for hand-authored documents — markdown tables have no
245
+ * column schema, so preserving authored text is the only lossless choice —
246
+ * and to true when the document carries `sheet=` anchors, which only an XLSX
247
+ * import produces. Leading-zero and >15-significant-digit values remain
248
+ * strings either way. Set explicitly to override both defaults.
249
+ */
250
+ inferNumericCells?: boolean;
251
+ /**
252
+ * Called for each non-fatal placement problem (a malformed anchor, an
253
+ * overlapping region, an unusable loose-cell reference). Export never throws
254
+ * for these — a hand-edited markdown file must still convert.
255
+ */
256
+ onWarning?: (message: string) => void;
257
+ }
258
+ /**
259
+ * Convert a MarkdownDocument to a .xlsx file (tables-only fidelity).
260
+ *
261
+ * Each markdown `table` becomes one worksheet; a document with no tables
262
+ * yields a single empty sheet (a valid, openable file — never throws).
263
+ */
264
+ declare function markdownDocToXlsx(doc: MarkdownDocument, options?: XlsxExportOptions): Promise<ArrayBuffer>;
265
+ /**
266
+ * Convert a squisq Doc to a .xlsx file (via the markdown table model).
267
+ */
268
+ declare function docToXlsx(doc: Doc, options?: XlsxExportOptions): Promise<ArrayBuffer>;
269
+
270
+ export { type XlsxExportOptions as X, type XlsxImportOptions as a, type XlsxTable as b, type XlsxTableColumn as c, type XlsxTablesOptions as d, docToXlsx as e, xlsxToTables as f, gridToTables as g, markdownDocToXlsx as m, xlsxToMarkdownDoc as x };
package/dist/index.d.ts CHANGED
@@ -6,13 +6,13 @@ export { PdfExportOptions, PdfImportOptions, configurePdfWorker, docToPdf, markd
6
6
  export { HtmlZipExportOptions, docToHtml, docToHtmlZip } from './html/index.js';
7
7
  export { EpubExportOptions, docToEpub, markdownDocToEpub } from './epub/index.js';
8
8
  export { ExtractedFileTheme, InferSourceFormat, InferThemeOptions, InferredFileTheme, compileExtractedTheme, inferThemeFromFile } from './infer/index.js';
9
- export { B as BUILTIN_FORMAT_IDS, a as BuiltinFormatOptions, C as ConversionLimits, b as ConversionResult, c as ConvertOptions, d as ConvertSource, D as DEFAULT_CONVERSION_LIMITS, e as DbkFormatOptions, F as FormatDefinition, f as FormatId, g as FormatRegistry, M as MarkdownFormatOptions, N as NormalizedInput, P as PreparedConversion, h as PreparedExportOptions, r as resolveConversionLimits } from './types-DByrrXeB.js';
9
+ export { B as BUILTIN_FORMAT_IDS, a as BuiltinFormatOptions, C as ConversionLimits, b as ConversionResult, c as ConvertOptions, d as ConvertSource, D as DEFAULT_CONVERSION_LIMITS, e as DbkFormatOptions, F as FormatDefinition, f as FormatId, g as FormatRegistry, M as MarkdownFormatOptions, N as NormalizedInput, P as PreparedConversion, h as PreparedExportOptions, r as resolveConversionLimits } from './types-bwP9PBSk.js';
10
10
  export { ConversionError, ConversionErrorCode, ConversionErrorOptions, convert, createRegistry, defaultFormats, defaultRegistry, prepareConversion } from './registry/index.js';
11
11
  export { ImportedOutsideInDocument, OUTSIDE_IN_FORMAT_IDS, OUTSIDE_IN_UPDATE_FROM_MARKDOWN_KEY, OutsideInFormatId, OutsideInLayout, OutsideInMetadata, RenderOutsideInOptions, chooseOutsideInMarkdownPath, importOutsideInDocument, isOutsideInMarkdownEditingEnabled, isOutsideInTargetPath, readOutsideInMetadata, renderOutsideInDocument, resolveOutsideInLayout, withOutsideInMarkdownEditing, withOutsideInMetadata } from './outside-in/index.js';
12
12
  export { Z as ZipSafetyError, a as ZipSafetyErrorCode, b as ZipSafetyErrorOptions, c as ZipSafetyLimits } from './zipLimits-BOKCB7qk.js';
13
13
  export { H as HtmlExportOptions, a as HtmlImportOptions, c as collectImagePaths, h as htmlToMarkdown, b as htmlToMarkdownDoc, d as htmlToMarkdownDocSync } from './import-B0gBYUmd.js';
14
14
  export { P as PptxExportOptions, a as PptxImportOptions, d as docToPptx, m as markdownDocToPptx, p as pptxToMarkdownDoc } from './import-C16E8Y4X.js';
15
- export { X as XlsxExportOptions, a as XlsxImportOptions, d as docToXlsx, m as markdownDocToXlsx, x as xlsxToMarkdownDoc } from './export-m0tr9r9d.js';
15
+ export { X as XlsxExportOptions, a as XlsxImportOptions, b as XlsxTable, c as XlsxTableColumn, d as XlsxTablesOptions, e as docToXlsx, g as gridToTables, m as markdownDocToXlsx, x as xlsxToMarkdownDoc, f as xlsxToTables } from './export-Boq78GMq.js';
16
16
  import '@bendyline/squisq/schemas';
17
17
  import '@bendyline/squisq/markdown';
18
18
  import './reader-B_m1aKZC.js';
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  resolveOutsideInLayout,
21
21
  withOutsideInMarkdownEditing,
22
22
  withOutsideInMetadata
23
- } from "./chunk-5PUIFU5I.js";
23
+ } from "./chunk-TVUX3RUC.js";
24
24
  import {
25
25
  DEFAULT_CONVERSION_LIMITS,
26
26
  convert,
@@ -29,7 +29,7 @@ import {
29
29
  defaultRegistry,
30
30
  prepareConversion,
31
31
  resolveConversionLimits
32
- } from "./chunk-JTGWQK5V.js";
32
+ } from "./chunk-5JQ5NDRC.js";
33
33
  import {
34
34
  ConversionError
35
35
  } from "./chunk-KXOZMWBS.js";
@@ -53,10 +53,12 @@ import "./chunk-6N2J7C2B.js";
53
53
  import "./chunk-A6N6IN3I.js";
54
54
  import {
55
55
  docToXlsx,
56
+ gridToTables,
56
57
  markdownDocToXlsx,
57
58
  xlsxToDoc,
58
- xlsxToMarkdownDoc
59
- } from "./chunk-A6LSCIO5.js";
59
+ xlsxToMarkdownDoc,
60
+ xlsxToTables
61
+ } from "./chunk-RX55T5HO.js";
60
62
  import "./chunk-AVOZAKGP.js";
61
63
  import {
62
64
  csvToDoc,
@@ -117,6 +119,7 @@ export {
117
119
  docToXlsx,
118
120
  docxToDoc,
119
121
  docxToMarkdownDoc,
122
+ gridToTables,
120
123
  htmlToMarkdown,
121
124
  htmlToMarkdownDoc,
122
125
  htmlToMarkdownDocSync,
@@ -143,5 +146,6 @@ export {
143
146
  withOutsideInMarkdownEditing,
144
147
  withOutsideInMetadata,
145
148
  xlsxToDoc,
146
- xlsxToMarkdownDoc
149
+ xlsxToMarkdownDoc,
150
+ xlsxToTables
147
151
  };
@@ -1,13 +1,13 @@
1
1
  import { MarkdownDocument } from '@bendyline/squisq/markdown';
2
2
  import { ContentContainer } from '@bendyline/squisq/storage';
3
- import { c as ConvertOptions, b as ConversionResult } from '../types-DByrrXeB.js';
3
+ import { c as ConvertOptions, b as ConversionResult } from '../types-bwP9PBSk.js';
4
4
  import '@bendyline/squisq/schemas';
5
5
  import '@bendyline/squisq/transform';
6
6
  import '../docx/index.js';
7
7
  import '../reader-B_m1aKZC.js';
8
8
  import '../zipLimits-BOKCB7qk.js';
9
9
  import '../import-C16E8Y4X.js';
10
- import '../export-m0tr9r9d.js';
10
+ import '../export-Boq78GMq.js';
11
11
  import '../csv/index.js';
12
12
  import '../pdf/index.js';
13
13
  import '../import-B0gBYUmd.js';
@@ -10,8 +10,8 @@ import {
10
10
  resolveOutsideInLayout,
11
11
  withOutsideInMarkdownEditing,
12
12
  withOutsideInMetadata
13
- } from "../chunk-5PUIFU5I.js";
14
- import "../chunk-JTGWQK5V.js";
13
+ } from "../chunk-TVUX3RUC.js";
14
+ import "../chunk-5JQ5NDRC.js";
15
15
  import "../chunk-KXOZMWBS.js";
16
16
  import "../chunk-7AWFHP5U.js";
17
17
  export {
@@ -1,5 +1,5 @@
1
- import { f as FormatId, g as FormatRegistry, F as FormatDefinition, d as ConvertSource, c as ConvertOptions, b as ConversionResult, P as PreparedConversion } from '../types-DByrrXeB.js';
2
- export { B as BUILTIN_FORMAT_IDS, a as BuiltinFormatOptions, C as ConversionLimits, D as DEFAULT_CONVERSION_LIMITS, e as DbkFormatOptions, M as MarkdownFormatOptions, N as NormalizedInput, h as PreparedExportOptions, r as resolveConversionLimits } from '../types-DByrrXeB.js';
1
+ import { f as FormatId, g as FormatRegistry, F as FormatDefinition, d as ConvertSource, c as ConvertOptions, b as ConversionResult, P as PreparedConversion } from '../types-bwP9PBSk.js';
2
+ export { B as BUILTIN_FORMAT_IDS, a as BuiltinFormatOptions, C as ConversionLimits, D as DEFAULT_CONVERSION_LIMITS, e as DbkFormatOptions, M as MarkdownFormatOptions, N as NormalizedInput, h as PreparedExportOptions, r as resolveConversionLimits } from '../types-bwP9PBSk.js';
3
3
  import '@bendyline/squisq/schemas';
4
4
  import '@bendyline/squisq/transform';
5
5
  import '@bendyline/squisq/markdown';
@@ -8,7 +8,7 @@ import '../docx/index.js';
8
8
  import '../reader-B_m1aKZC.js';
9
9
  import '../zipLimits-BOKCB7qk.js';
10
10
  import '../import-C16E8Y4X.js';
11
- import '../export-m0tr9r9d.js';
11
+ import '../export-Boq78GMq.js';
12
12
  import '../csv/index.js';
13
13
  import '../pdf/index.js';
14
14
  import '../import-B0gBYUmd.js';
@@ -9,7 +9,7 @@ import {
9
9
  defaultRegistry,
10
10
  prepareConversion,
11
11
  resolveConversionLimits
12
- } from "../chunk-JTGWQK5V.js";
12
+ } from "../chunk-5JQ5NDRC.js";
13
13
  import {
14
14
  ConversionError
15
15
  } from "../chunk-KXOZMWBS.js";
@@ -4,7 +4,7 @@ import { ParseOptions, StringifyOptions, MarkdownDocument } from '@bendyline/squ
4
4
  import { ContentContainer } from '@bendyline/squisq/storage';
5
5
  import { DocxImportOptions, DocxExportOptions } from './docx/index.js';
6
6
  import { a as PptxImportOptions, P as PptxExportOptions } from './import-C16E8Y4X.js';
7
- import { a as XlsxImportOptions, X as XlsxExportOptions } from './export-m0tr9r9d.js';
7
+ import { a as XlsxImportOptions, X as XlsxExportOptions } from './export-Boq78GMq.js';
8
8
  import { CsvImportOptions, CsvExportOptions } from './csv/index.js';
9
9
  import { PdfImportOptions, PdfExportOptions } from './pdf/index.js';
10
10
  import { a as HtmlImportOptions, H as HtmlExportOptions } from './import-B0gBYUmd.js';
@@ -1,6 +1,6 @@
1
1
  import { Doc } from '@bendyline/squisq/schemas';
2
- import { a as XlsxImportOptions } from '../export-m0tr9r9d.js';
3
- export { X as XlsxExportOptions, d as docToXlsx, m as markdownDocToXlsx, x as xlsxToMarkdownDoc } from '../export-m0tr9r9d.js';
2
+ import { a as XlsxImportOptions } from '../export-Boq78GMq.js';
3
+ export { X as XlsxExportOptions, b as XlsxTable, c as XlsxTableColumn, d as XlsxTablesOptions, e as docToXlsx, g as gridToTables, m as markdownDocToXlsx, x as xlsxToMarkdownDoc, f as xlsxToTables } from '../export-Boq78GMq.js';
4
4
  import '@bendyline/squisq/markdown';
5
5
  import '../reader-B_m1aKZC.js';
6
6
  import '../zipLimits-BOKCB7qk.js';
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  docToXlsx,
3
+ gridToTables,
3
4
  markdownDocToXlsx,
4
5
  xlsxToDoc,
5
- xlsxToMarkdownDoc
6
- } from "../chunk-A6LSCIO5.js";
6
+ xlsxToMarkdownDoc,
7
+ xlsxToTables
8
+ } from "../chunk-RX55T5HO.js";
7
9
  import "../chunk-AVOZAKGP.js";
8
10
  import "../chunk-ILCJ3WFD.js";
9
11
  import "../chunk-JU2RHXUB.js";
@@ -11,7 +13,9 @@ import "../chunk-S5PCVMKU.js";
11
13
  import "../chunk-7AWFHP5U.js";
12
14
  export {
13
15
  docToXlsx,
16
+ gridToTables,
14
17
  markdownDocToXlsx,
15
18
  xlsxToDoc,
16
- xlsxToMarkdownDoc
19
+ xlsxToMarkdownDoc,
20
+ xlsxToTables
17
21
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-formats",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "Document format converters — DOCX, PDF, OOXML import/export",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -112,7 +112,7 @@
112
112
  "dependencies": {
113
113
  "@pdf-lib/fontkit": "1.1.1",
114
114
  "@pdf-lib/upng": "1.0.1",
115
- "@xmldom/xmldom": "0.9.11",
115
+ "@xmldom/xmldom": "0.9.12",
116
116
  "@bendyline/squisq": "2.10.0",
117
117
  "jszip": "3.10.1",
118
118
  "pdf-lib": "1.17.1",
@@ -1,130 +0,0 @@
1
- import { MarkdownDocument } from '@bendyline/squisq/markdown';
2
- import { O as OoxmlOpenOptions } from './reader-B_m1aKZC.js';
3
- import { Doc } from '@bendyline/squisq/schemas';
4
-
5
- /**
6
- * XLSX import — SpreadsheetML (.xlsx) → MarkdownDocument.
7
- *
8
- * Reuses the shared ooxml/ reader (zip + DOMParser). Reads the workbook's sheet
9
- * list, resolves each sheet part via relationships, pulls shared strings, and
10
- * turns each worksheet into markdown. By default every sheet is imported, each
11
- * preceded by an H1 of the sheet name; pass `options.sheet` (index or name) to
12
- * import just one.
13
- *
14
- * A sheet is NOT one table. It is usually several tables scattered across the
15
- * grid with stray labels and notes in the gaps, so by default each worksheet is
16
- * split into its contiguous data islands (see `regions.ts`) and every island
17
- * becomes its own block:
18
- *
19
- * ```markdown
20
- * ## Q3 Revenue {[dataTable sheet=Sales anchor=B7]}
21
- * ```
22
- *
23
- * The `sheet`/`anchor` params on the heading annotation are what let
24
- * `markdownDocToXlsx` put each table back where it came from, so the round trip
25
- * reproduces addresses rather than piling everything at A1. A region holding
26
- * formulas additionally emits a `role=formulas` companion table, and every
27
- * left-over single cell on a sheet collects into one `role=loose` table.
28
- *
29
- * Pass `{ regions: false }` for the historical behavior: one table per sheet,
30
- * spanning the whole used range.
31
- */
32
-
33
- interface XlsxImportOptions extends OoxmlOpenOptions {
34
- /** Which sheet to import (0-based index or sheet name). Default: all sheets. */
35
- sheet?: number | string;
36
- /**
37
- * Split each sheet into its contiguous data islands, one block each, anchored
38
- * with `{[dataTable sheet=… anchor=…]}`. Default true. Set false for the
39
- * historical one-table-per-sheet output.
40
- */
41
- regions?: boolean;
42
- /**
43
- * Emit a `role=formulas` companion table for regions that contain formulas.
44
- * Default true. Ignored when `regions` is false.
45
- */
46
- formulas?: boolean;
47
- /** Cap on region tables per sheet before the rest fold into loose cells. Default 64. */
48
- maxRegionsPerSheet?: number;
49
- /** Smallest island that stays a table of its own. Default 2 — single cells coalesce. */
50
- minRegionCells?: number;
51
- }
52
- declare function xlsxToMarkdownDoc(data: ArrayBuffer | Blob, options?: XlsxImportOptions): Promise<MarkdownDocument>;
53
-
54
- /**
55
- * XLSX export — MarkdownDocument → SpreadsheetML (.xlsx).
56
- *
57
- * Tables-only fidelity (honestly documented): every `table` node in the
58
- * markdown AST becomes worksheet cells; all other content (prose, lists,
59
- * images, …) is dropped, and headings survive only as sheet names and as the
60
- * carrier of placement metadata.
61
- *
62
- * Placement has two modes, decided per table by `workbookPlan.ts`. A table
63
- * whose heading carries `{[dataTable sheet=… anchor=…]}` — what
64
- * `xlsxToMarkdownDoc` emits for every data island it finds — is placed on the
65
- * named sheet at the named cell, so several mini tables share one worksheet at
66
- * their original addresses and formulas ride along. A table with no such
67
- * annotation keeps the historical behavior exactly: its own worksheet, named
68
- * from the nearest preceding heading, starting at A1.
69
- *
70
- * Cells are emitted as inline strings (`t="inlineStr"`) by default so no
71
- * sharedStrings part is needed and identifier-like numbers remain lossless.
72
- * Callers can explicitly opt into conservative numeric inference. The package
73
- * is assembled with the shared ooxml/ writer (auto-generates
74
- * `[Content_Types].xml` + `_rels`), so only the SpreadsheetML-specific parts
75
- * (workbook, worksheets, styles) are written here.
76
- *
77
- * @example
78
- * ```ts
79
- * import { parseMarkdown } from '@bendyline/squisq/markdown';
80
- * import { markdownDocToXlsx } from '@bendyline/squisq-formats/xlsx';
81
- *
82
- * const md = parseMarkdown('# Metrics\n\n| A | B |\n| - | - |\n| 1 | 2 |');
83
- * const buffer = await markdownDocToXlsx(md);
84
- * ```
85
- */
86
-
87
- /**
88
- * Options for XLSX export.
89
- */
90
- interface XlsxExportOptions {
91
- /** Cancel at bounded export checkpoints. */
92
- signal?: AbortSignal;
93
- /** Maximum cells emitted. Default: 100,000. */
94
- maxCells?: number;
95
- /** Workbook title (written to core properties). */
96
- title?: string;
97
- /** Workbook author (written to core properties). */
98
- author?: string;
99
- /** Prefix used for auto-named sheets when no heading precedes a table. Default: "Sheet". */
100
- sheetNamePrefix?: string;
101
- /**
102
- * Emit canonical, Excel-safe number strings as numeric cells.
103
- *
104
- * Defaults to false for hand-authored documents — markdown tables have no
105
- * column schema, so preserving authored text is the only lossless choice —
106
- * and to true when the document carries `sheet=` anchors, which only an XLSX
107
- * import produces. Leading-zero and >15-significant-digit values remain
108
- * strings either way. Set explicitly to override both defaults.
109
- */
110
- inferNumericCells?: boolean;
111
- /**
112
- * Called for each non-fatal placement problem (a malformed anchor, an
113
- * overlapping region, an unusable loose-cell reference). Export never throws
114
- * for these — a hand-edited markdown file must still convert.
115
- */
116
- onWarning?: (message: string) => void;
117
- }
118
- /**
119
- * Convert a MarkdownDocument to a .xlsx file (tables-only fidelity).
120
- *
121
- * Each markdown `table` becomes one worksheet; a document with no tables
122
- * yields a single empty sheet (a valid, openable file — never throws).
123
- */
124
- declare function markdownDocToXlsx(doc: MarkdownDocument, options?: XlsxExportOptions): Promise<ArrayBuffer>;
125
- /**
126
- * Convert a squisq Doc to a .xlsx file (via the markdown table model).
127
- */
128
- declare function docToXlsx(doc: Doc, options?: XlsxExportOptions): Promise<ArrayBuffer>;
129
-
130
- export { type XlsxExportOptions as X, type XlsxImportOptions as a, docToXlsx as d, markdownDocToXlsx as m, xlsxToMarkdownDoc as x };
File without changes