@bendyline/squisq-formats 1.4.0 → 1.4.2

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.
Files changed (83) hide show
  1. package/README.md +180 -43
  2. package/dist/{chunk-CRTC6DEX.js → chunk-46FMDOWZ.js} +2 -2
  3. package/dist/{chunk-ON232HJR.js → chunk-5LSSWZPU.js} +2 -2
  4. package/dist/{chunk-APSIAEXL.js → chunk-D7IFPWNZ.js} +87 -43
  5. package/dist/chunk-D7IFPWNZ.js.map +1 -0
  6. package/dist/chunk-DQAZR57U.js +33 -0
  7. package/dist/chunk-DQAZR57U.js.map +1 -0
  8. package/dist/chunk-EHLGMSTE.js +273 -0
  9. package/dist/chunk-EHLGMSTE.js.map +1 -0
  10. package/dist/{chunk-A3FHLTY5.js → chunk-MJGRI6XR.js} +6 -19
  11. package/dist/chunk-MJGRI6XR.js.map +1 -0
  12. package/dist/{chunk-JBNVE2GV.js → chunk-RFAPOKHJ.js} +9 -2
  13. package/dist/chunk-RFAPOKHJ.js.map +1 -0
  14. package/dist/chunk-RTS5XBZ7.js +526 -0
  15. package/dist/chunk-RTS5XBZ7.js.map +1 -0
  16. package/dist/{chunk-FMOIGA5T.js → chunk-SHLPSADL.js} +178 -5
  17. package/dist/chunk-SHLPSADL.js.map +1 -0
  18. package/dist/chunk-SSUPBUF5.js +15 -0
  19. package/dist/chunk-SSUPBUF5.js.map +1 -0
  20. package/dist/{chunk-BJQIJ4S4.js → chunk-WWBIKRNP.js} +17 -41
  21. package/dist/chunk-WWBIKRNP.js.map +1 -0
  22. package/dist/{chunk-I2GFTEOV.js → chunk-YESS7VY3.js} +77 -15
  23. package/dist/{chunk-I2GFTEOV.js.map → chunk-YESS7VY3.js.map} +1 -1
  24. package/dist/csv/index.d.ts +14 -1
  25. package/dist/csv/index.js +1 -1
  26. package/dist/docx/index.js +4 -3
  27. package/dist/epub/index.js +3 -2
  28. package/dist/html/index.d.ts +3 -0
  29. package/dist/html/index.js +3 -2
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +26 -11
  32. package/dist/ooxml/index.d.ts +5 -1
  33. package/dist/ooxml/index.js +16 -12
  34. package/dist/pdf/index.js +2 -1
  35. package/dist/pptx/index.d.ts +18 -3
  36. package/dist/pptx/index.js +7 -4
  37. package/dist/registry/index.d.ts +167 -0
  38. package/dist/registry/index.js +17 -0
  39. package/dist/registry/index.js.map +1 -0
  40. package/dist/xlsx/index.d.ts +44 -14
  41. package/dist/xlsx/index.js +4 -2
  42. package/package.json +7 -2
  43. package/src/__tests__/convert.test.ts +186 -0
  44. package/src/__tests__/csvImport.test.ts +34 -1
  45. package/src/__tests__/formatRegistry.test.ts +174 -0
  46. package/src/__tests__/lossyWarnings.test.ts +105 -0
  47. package/src/__tests__/pdfImport.test.ts +95 -1
  48. package/src/__tests__/pptxImport.test.ts +46 -1
  49. package/src/__tests__/roundTripAssets.test.ts +50 -0
  50. package/src/__tests__/roundTripMatrix.test.ts +10 -4
  51. package/src/__tests__/sharedContainer.test.ts +41 -0
  52. package/src/__tests__/sharedImages.test.ts +61 -0
  53. package/src/__tests__/xlsxExport.test.ts +164 -0
  54. package/src/csv/index.ts +25 -2
  55. package/src/docx/import.ts +6 -37
  56. package/src/html/imageUtils.ts +5 -19
  57. package/src/index.ts +29 -5
  58. package/src/ooxml/index.ts +3 -0
  59. package/src/ooxml/namespaces.ts +10 -0
  60. package/src/pdf/import.ts +145 -50
  61. package/src/pptx/import.ts +127 -10
  62. package/src/pptx/index.ts +1 -1
  63. package/src/registry/convert.ts +318 -0
  64. package/src/registry/defaultFormats.ts +323 -0
  65. package/src/registry/errors.ts +46 -0
  66. package/src/registry/index.ts +38 -0
  67. package/src/registry/registry.ts +48 -0
  68. package/src/registry/types.ts +107 -0
  69. package/src/shared/container.ts +28 -0
  70. package/src/shared/images.ts +44 -0
  71. package/src/xlsx/export.ts +253 -0
  72. package/src/xlsx/index.ts +10 -36
  73. package/dist/chunk-A3FHLTY5.js.map +0 -1
  74. package/dist/chunk-APSIAEXL.js.map +0 -1
  75. package/dist/chunk-BJQIJ4S4.js.map +0 -1
  76. package/dist/chunk-FMOIGA5T.js.map +0 -1
  77. package/dist/chunk-JBNVE2GV.js.map +0 -1
  78. package/dist/chunk-L53YIGYS.js +0 -141
  79. package/dist/chunk-L53YIGYS.js.map +0 -1
  80. package/dist/chunk-QSE7EWE7.js +0 -181
  81. package/dist/chunk-QSE7EWE7.js.map +0 -1
  82. /package/dist/{chunk-CRTC6DEX.js.map → chunk-46FMDOWZ.js.map} +0 -0
  83. /package/dist/{chunk-ON232HJR.js.map → chunk-5LSSWZPU.js.map} +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @bendyline/squisq-formats
2
2
 
3
- Document format converters for Squisq. Import and export between Squisq's Markdown-based document model and common file formats — DOCX, PPTX, PDF, and HTML. All converters run entirely in the browser with no server or native binaries required.
3
+ Document format converters for Squisq. Import and export between Squisq's Markdown-based document model and common file formats — DOCX, PDF, HTML, EPUB, PPTX, XLSX, and CSV. All converters run in the browser or Node with no server or native binaries required.
4
4
 
5
5
  Part of the [Squisq](https://github.com/bendyline/squisq) monorepo.
6
6
 
@@ -13,118 +13,255 @@ Part of the [Squisq](https://github.com/bendyline/squisq) monorepo.
13
13
  npm install @bendyline/squisq-formats @bendyline/squisq
14
14
  ```
15
15
 
16
+ ## How Conversion Works
17
+
18
+ All converters use Squisq's `MarkdownDocument` AST (from `@bendyline/squisq/markdown`) as the pivot format:
19
+
20
+ - **Import** parses a file into a `MarkdownDocument`. The `xxxToContainer` variants (DOCX, PDF, PPTX) also extract embedded images into a `ContentContainer` alongside the markdown.
21
+ - **Export** serializes a `MarkdownDocument` out to the target format. Each format also has a `docToXxx` / `xxxToDoc` wrapper that converts through `MarkdownDocument` internally.
22
+
23
+ Conversions preserve document structure and most of the flavor of the source — headings, text formatting, lists, tables, links, images — but they are **not lossless round-trips**. Per-format fidelity limits are listed below.
24
+
25
+ All exports that accept a `themeId` (DOCX, PDF, EPUB, PPTX, plain HTML) apply Squisq theme colors/typography, and fall back to the doc's frontmatter theme (`squisq-theme` / `themeId` / `theme` keys) when the option is omitted.
26
+
27
+ For a uniform, format-agnostic entry point over every converter, use the **format registry** and `convert()` (`@bendyline/squisq-formats/registry`, also re-exported from the root) — see [Programmatic `convert()`](#programmatic-convert) below.
28
+
16
29
  ## Supported Formats
17
30
 
18
- | Format | Import | Export | Theme | Subpath |
19
- | --------------------- | ------- | ------- | ----- | -------------------------------- |
20
- | **DOCX** (Word) | ✅ | ✅ | ✅ | `@bendyline/squisq-formats/docx` |
21
- | **PDF** | ✅ | ✅ | ✅ | `@bendyline/squisq-formats/pdf` |
22
- | **HTML** | | ✅ | ✅ | `@bendyline/squisq-formats/html` |
23
- | **EPUB** (e-book) | — | ✅ | ✅ | `@bendyline/squisq-formats/epub` |
24
- | **PPTX** (PowerPoint) | planned | ✅ | ✅ | `@bendyline/squisq-formats/pptx` |
25
- | **XLSX** (Excel) | planned | planned | — | `@bendyline/squisq-formats/xlsx` |
31
+ | Format | Import | Export | Theme | Subpath |
32
+ | --------------------- | ------ | ------ | ----- | ------------------------------------- |
33
+ | **DOCX** (Word) | ✅ | ✅ | ✅ | `@bendyline/squisq-formats/docx` |
34
+ | **PDF** | ✅ | ✅ | ✅ | `@bendyline/squisq-formats/pdf` |
35
+ | **HTML** | | ✅ | ✅ | `@bendyline/squisq-formats/html` |
36
+ | **EPUB** (e-book) | — | ✅ | ✅ | `@bendyline/squisq-formats/epub` |
37
+ | **PPTX** (PowerPoint) | | ✅ | ✅ | `@bendyline/squisq-formats/pptx` |
38
+ | **XLSX** (Excel) | | ✅¹ | — | `@bendyline/squisq-formats/xlsx` |
39
+ | **CSV** | ✅ | ✅ | — | `@bendyline/squisq-formats/csv` |
40
+ | **Container ZIP** | ✅ | ✅ | — | `@bendyline/squisq-formats/container` |
26
41
 
27
- All export formats accept an optional `themeId` to apply Squisq theme colors and typography.
42
+ ¹ XLSX export is **tables-only** (v1.5) see the [XLSX](#xlsx) section.
28
43
 
29
44
  ## Quick Examples
30
45
 
31
46
  ### DOCX
32
47
 
33
48
  ```ts
34
- import { markdownDocToDocx, docxToMarkdownDoc } from '@bendyline/squisq-formats/docx';
49
+ import {
50
+ markdownDocToDocx,
51
+ docxToMarkdownDoc,
52
+ docxToContainer,
53
+ } from '@bendyline/squisq-formats/docx';
35
54
 
36
- // Export: MarkdownDocument → DOCX (Uint8Array)
37
- const docxBytes = await markdownDocToDocx(markdownDoc);
55
+ // Export: MarkdownDocument → DOCX (ArrayBuffer)
56
+ const docxBytes = await markdownDocToDocx(markdownDoc, { title: 'Report', themeId: 'documentary' });
38
57
 
39
- // Import: DOCX (ArrayBuffer) → MarkdownDocument
40
- const markdownDoc = await docxToMarkdownDoc(docxBuffer);
58
+ // Import: DOCX (ArrayBuffer | Blob) → MarkdownDocument
59
+ const imported = await docxToMarkdownDoc(docxBuffer, { extractImages: true });
60
+
61
+ // Import with assets: DOCX → ContentContainer (markdown + images/ files)
62
+ const container = await docxToContainer(docxBuffer);
41
63
  ```
42
64
 
65
+ **Fidelity:** import covers headings, paragraphs, inline formatting (bold, italic, strikethrough, inline code), hyperlinks, lists, tables, blockquotes, code blocks, footnotes, and embedded images (opt-in via `extractImages`; always on for `docxToContainer`). Export embeds images only when provided via `options.images`; otherwise they appear as placeholder text.
66
+
43
67
  ### PDF
44
68
 
45
69
  ```ts
46
70
  import {
47
71
  markdownDocToPdf,
48
72
  pdfToMarkdownDoc,
73
+ pdfToContainer,
49
74
  configurePdfWorker,
50
75
  } from '@bendyline/squisq-formats/pdf';
51
76
 
52
- // Configure the PDF.js worker (required for import)
77
+ // Configure the PDF.js worker (needed for import in some environments)
53
78
  configurePdfWorker('/pdf.worker.min.mjs');
54
79
 
55
- // Export: MarkdownDocument → PDF (Uint8Array)
56
- const pdfBytes = await markdownDocToPdf(markdownDoc);
80
+ // Export: MarkdownDocument → PDF (ArrayBuffer)
81
+ const pdfBytes = await markdownDocToPdf(markdownDoc, { pageSize: 'a4' });
57
82
 
58
- // Import: PDF (ArrayBuffer) MarkdownDocument
59
- const markdownDoc = await pdfToMarkdownDoc(pdfBuffer);
83
+ // Import: PDF → MarkdownDocument (heuristic structure detection)
84
+ const imported = await pdfToMarkdownDoc(pdfBuffer);
85
+
86
+ // Import with assets: PDF → ContentContainer (markdown + extracted images)
87
+ const container = await pdfToContainer(pdfBuffer);
60
88
  ```
61
89
 
90
+ **Fidelity:** export uses pdf-lib's standard 14 fonts (`themeId` affects colors only). PDF has no semantic structure, so import is heuristic and best-effort: headings are detected by font size, and tables / code blocks / blockquotes / links via the `detectTables` / `detectCodeBlocks` / `detectBlockquotes` / `detectLinks` options (all default true). `pdfToMarkdownDoc` is text-only; `pdfToContainer` also extracts embedded images, placed **by page** — each image is inserted after the last content block from its page (image-only pages fall back to the nearest preceding page with content, else the document end). Placement is page-level only. Image extraction needs a browser canvas to encode PNG — **under Node it is skipped** (with a `console.warn`) and no images are emitted.
91
+
62
92
  ### HTML
63
93
 
64
94
  ```ts
65
- import { docToHtml, docToHtmlZip } from '@bendyline/squisq-formats/html';
95
+ import { PLAYER_BUNDLE } from '@bendyline/squisq-react/standalone-source';
96
+ import {
97
+ docToHtml,
98
+ docToHtmlZip,
99
+ markdownDocToPlainHtml,
100
+ htmlToMarkdownDoc,
101
+ } from '@bendyline/squisq-formats/html';
102
+
103
+ // Interactive player export: Doc → single self-contained HTML string
104
+ // (player inlined, images as data URIs, timer-based playback — no audio)
105
+ const html = docToHtml(doc, { playerScript: PLAYER_BUNDLE, images });
106
+
107
+ // Interactive player export: Doc → ZIP Blob (external assets + optional audio)
108
+ const zipBlob = await docToHtmlZip(doc, { playerScript: PLAYER_BUNDLE, images, audio });
66
109
 
67
- // Export: Docstandalone HTML string
68
- const html = docToHtml(doc);
110
+ // Static export: MarkdownDocumentsemantic HTML page (no player)
111
+ const page = markdownDocToPlainHtml(markdownDoc, { themeId: 'warm-earth' });
69
112
 
70
- // Export: DocZIP with HTML + images
71
- const zipBytes = await docToHtmlZip(doc);
113
+ // Import: HTMLMarkdownDocument (sanitized by default)
114
+ const imported = await htmlToMarkdownDoc(htmlString);
72
115
  ```
73
116
 
117
+ The subpath also exports `markdownDocsToPlainHtmlBundle` / `markdownDocsToHtmlBundle` (recursive multi-doc ZIP bundles with `.md` links rewritten to `.html`), `htmlToMarkdownDocSync` / `htmlToMarkdown`, and helpers (`collectImagePaths`, `collectLinkRefs`, `inferMimeType`).
118
+
74
119
  ### EPUB
75
120
 
76
121
  ```ts
77
122
  import { markdownDocToEpub } from '@bendyline/squisq-formats/epub';
78
123
 
79
- // Export: MarkdownDocument → EPUB (ArrayBuffer)
124
+ // Export: MarkdownDocument → EPUB 3 (ArrayBuffer). No import path.
80
125
  const epubBytes = await markdownDocToEpub(markdownDoc, {
81
126
  title: 'My Book',
82
127
  author: 'Jane Doe',
128
+ images, // Map<string, ArrayBuffer> — embedded when provided
129
+ coverImage, // optional JPEG/PNG
83
130
  });
84
131
  ```
85
132
 
133
+ Chapters split at H1/H2 boundaries. Providing `audio` + `audioSegments` generates EPUB 3 Media Overlays (SMIL) for synchronized narration.
134
+
86
135
  ### PPTX
87
136
 
88
137
  ```ts
89
- import { markdownDocToPptx } from '@bendyline/squisq-formats/pptx';
138
+ import {
139
+ markdownDocToPptx,
140
+ pptxToMarkdownDoc,
141
+ pptxToContainer,
142
+ } from '@bendyline/squisq-formats/pptx';
90
143
 
91
144
  // Export: MarkdownDocument → PPTX (ArrayBuffer)
92
- // Each H1/H2 heading starts a new slide
93
- const pptxBytes = await markdownDocToPptx(markdownDoc);
145
+ // slideBreak: 'h1' | 'h2' (default — H1 and H2 both break) | 'heading'
146
+ const pptxBytes = await markdownDocToPptx(markdownDoc, { slideBreak: 'h2', images });
147
+
148
+ // Import: PPTX → MarkdownDocument (add { extractImages: true } to reference slide images)
149
+ const imported = await pptxToMarkdownDoc(pptxBuffer);
150
+
151
+ // Import with assets: PPTX → ContentContainer (markdown + extracted slide images)
152
+ const container = await pptxToContainer(pptxBuffer);
153
+ ```
154
+
155
+ **Fidelity:** export preserves inline formatting as DrawingML runs and embeds images when provided via `options.images`. Import reads slide order from `ppt/presentation.xml` and converts each slide's title (→ H2), body text (→ bullet list), and tables. **Slide-image extraction (v1.5):** import can now extract slide-level `<p:pic>` bitmaps into `images/` — `pptxToContainer` returns a container with those files and forces `extractImages: true`, while `pptxToMarkdownDoc` leaves it off by default. Honest limit: only slide-level `<p:pic>` bitmaps are extracted — layout/master images, charts, SmartArt, and picture-fills are **not**.
156
+
157
+ ### CSV
158
+
159
+ ```ts
160
+ import { csvToMarkdownDoc, markdownDocToCsv, parseCsv } from '@bendyline/squisq-formats/csv';
161
+
162
+ // Import: CSV (string | ArrayBuffer | Blob) → single-table MarkdownDocument
163
+ const tableDoc = await csvToMarkdownDoc(csvText, { delimiter: ',', hasHeader: true });
164
+
165
+ // Export: serializes one table node to CSV text (the first by default)
166
+ const csv = markdownDocToCsv(markdownDoc);
167
+ // Pick another table in a multi-table document:
168
+ const second = markdownDocToCsv(markdownDoc, { tableIndex: 1 });
169
+ ```
170
+
171
+ Self-contained RFC-4180 parser/serializer (not OOXML). Cell formatting is flattened to plain text on export. Export emits a single table — the first, or `tableIndex` (zero-based). An out-of-range `tableIndex` throws; a table-less document exports to an empty string.
172
+
173
+ ### XLSX
174
+
175
+ ```ts
176
+ import { xlsxToMarkdownDoc, markdownDocToXlsx } from '@bendyline/squisq-formats/xlsx';
177
+
178
+ // Import: XLSX → MarkdownDocument (one table per sheet; or pick one via `sheet`)
179
+ const imported = await xlsxToMarkdownDoc(xlsxBuffer, { sheet: 0 });
180
+
181
+ // Export: MarkdownDocument → XLSX (ArrayBuffer) — one worksheet per markdown table
182
+ const xlsxBytes = await markdownDocToXlsx(markdownDoc);
183
+ ```
184
+
185
+ **Export is implemented (v1.5) with tables-only fidelity:** every markdown `table` becomes one worksheet, named after the nearest preceding heading (auto-named `Sheet1`, `Sheet2`, … otherwise). All non-table content is dropped. Numeric-looking cells are written as numbers; everything else as inline strings. A document with no tables yields a single empty (but valid) sheet — export never throws. `markdownDocToXlsx` / `docToXlsx` now return `Promise<ArrayBuffer>` (previously threw / returned `Blob`).
186
+
187
+ ### Container ZIP
188
+
189
+ ```ts
190
+ import { containerToZip, zipToContainer } from '@bendyline/squisq-formats/container';
191
+
192
+ // ContentContainer → ZIP Blob (paths preserved)
193
+ const zipBlob = await containerToZip(container);
194
+
195
+ // ZIP → MemoryContentContainer (directories skipped, path traversal rejected)
196
+ const container2 = await zipToContainer(zipData);
94
197
  ```
95
198
 
96
199
  ### Doc-level Convenience Functions
97
200
 
98
- Each format also exports `Doc`-level wrappers that handle the Markdown↔Doc conversion internally:
201
+ Each format also exports `Doc`-level wrappers that handle the Markdown Doc conversion internally:
99
202
 
100
203
  ```ts
101
204
  import { docToDocx, docxToDoc } from '@bendyline/squisq-formats/docx';
102
205
  import { docToPdf, pdfToDoc } from '@bendyline/squisq-formats/pdf';
103
- import { docToPptx } from '@bendyline/squisq-formats/pptx';
206
+ import { docToPptx, pptxToDoc } from '@bendyline/squisq-formats/pptx';
207
+ import { docToEpub } from '@bendyline/squisq-formats/epub';
208
+ import { csvToDoc } from '@bendyline/squisq-formats/csv';
209
+ import { xlsxToDoc, docToXlsx } from '@bendyline/squisq-formats/xlsx';
104
210
  ```
105
211
 
212
+ ## Programmatic `convert()`
213
+
214
+ `@bendyline/squisq-formats/registry` (also re-exported from the package root) is a uniform, format-agnostic front door over every converter. Each format is a `FormatDefinition` that knows how to import raw bytes into the `MarkdownDocument` pivot and/or export back out; `convert()` normalizes any source into a `Doc`, optionally applies a theme/transform, and hands off to the target exporter. Every result comes back as the same `ConversionResult` (`bytes` + `mimeType` + `suggestedFilename` + `warnings`). Converter modules load lazily, so importing the registry never eagerly bundles a heavy converter.
215
+
216
+ ```ts
217
+ import { convert } from '@bendyline/squisq-formats/registry';
218
+
219
+ const result = await convert(
220
+ { kind: 'markdown', markdown: '# Hello', baseName: 'greeting' },
221
+ 'docx',
222
+ );
223
+ // result.bytes, result.mimeType, result.suggestedFilename === 'greeting.docx'
224
+ ```
225
+
226
+ Sources are `{ kind: 'bytes' | 'markdown' | 'doc', … }`; byte sources are format-sniffed by magic bytes + extension (pass `from` to skip). Failures throw a structured `ConversionError` with a stable `code` (`unknown-format`, `unsupported-input`, `unsupported-output`, `invalid-input`, `missing-dependency`, `conversion-failed`) plus a `hint`.
227
+
228
+ **Player-embedding HTML export (`html` / `htmlzip`)** needs the standalone player bundle. Pass `resolvePlayerScript`, or you'll get a `missing-dependency` error:
229
+
230
+ ```ts
231
+ const html = await convert({ kind: 'markdown', markdown: src }, 'html', {
232
+ resolvePlayerScript: () =>
233
+ import('@bendyline/squisq-react/standalone-source').then((m) => m.PLAYER_BUNDLE),
234
+ });
235
+ ```
236
+
237
+ Also exported: `createRegistry` / `defaultRegistry` / `defaultFormats`, `BUILTIN_FORMAT_IDS`, `ConversionError`, and the `ConvertSource` / `ConvertOptions` / `ConversionResult` / `FormatDefinition` / `FormatRegistry` types.
238
+
106
239
  ## Subpath Exports
107
240
 
108
- | Subpath | Contents |
109
- | --------------------------------- | -------------------------------------------------------------- |
110
- | `@bendyline/squisq-formats/docx` | DOCX import/export |
111
- | `@bendyline/squisq-formats/pdf` | PDF import/export + worker config |
112
- | `@bendyline/squisq-formats/html` | HTML export |
113
- | `@bendyline/squisq-formats/epub` | EPUB 3 e-book export |
114
- | `@bendyline/squisq-formats/ooxml` | Shared OOXML infrastructure (ZIP reader/writer, XML utilities) |
115
- | `@bendyline/squisq-formats/pptx` | PPTX export (import planned) |
116
- | `@bendyline/squisq-formats/xlsx` | XLSX stubs (not yet implemented) |
241
+ | Subpath | Contents |
242
+ | ------------------------------------- | ------------------------------------------------------------------------------ |
243
+ | `@bendyline/squisq-formats/docx` | DOCX import/export (+ `docxToContainer`) |
244
+ | `@bendyline/squisq-formats/pdf` | PDF import/export (+ `pdfToContainer`, `configurePdfWorker`) |
245
+ | `@bendyline/squisq-formats/html` | Player HTML export, static plain-HTML export + bundles, HTML import |
246
+ | `@bendyline/squisq-formats/epub` | EPUB 3 e-book export (with optional Media Overlays) |
247
+ | `@bendyline/squisq-formats/pptx` | PPTX export + import (text/lists/tables + slide images) |
248
+ | `@bendyline/squisq-formats/xlsx` | XLSX import + tables-only export |
249
+ | `@bendyline/squisq-formats/csv` | CSV import/export (RFC 4180) |
250
+ | `@bendyline/squisq-formats/ooxml` | Shared OOXML infrastructure (package reader/writer, XML utilities, namespaces) |
251
+ | `@bendyline/squisq-formats/container` | `ContentContainer` ↔ ZIP serialization |
252
+ | `@bendyline/squisq-formats/registry` | Format registry + programmatic `convert()` (also re-exported from the root) |
117
253
 
118
- ## Architecture
254
+ The package root re-exports the common converters; `./container`, the plain-HTML/bundle functions, `docxToContainer`, `pdfToContainer`, `PdfPageSize`, and the image utilities are subpath-only.
119
255
 
120
- All converters use Squisq's `MarkdownDocument` AST as the pivot format. Importing a file parses it into a `MarkdownDocument`; exporting serializes from one. The OOXML subpath provides shared infrastructure for reading and writing Office Open XML packages (used by DOCX and PPTX, and eventually XLSX).
256
+ See the full [API Reference](../../docs/API.md#bendylinesquisq-formats) for every signature and options interface.
121
257
 
122
258
  ## Related Packages
123
259
 
124
260
  | Package | Description |
125
261
  | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
126
262
  | [@bendyline/squisq](https://www.npmjs.com/package/@bendyline/squisq) | Headless core — schemas, templates, spatial, markdown, storage |
127
- | [@bendyline/squisq-react](https://www.npmjs.com/package/@bendyline/squisq-react) | React components for rendering docs |
263
+ | [@bendyline/squisq-react](https://www.npmjs.com/package/@bendyline/squisq-react) | React components for rendering docs (+ `PLAYER_BUNDLE`) |
264
+ | [@bendyline/squisq-cli](https://www.npmjs.com/package/@bendyline/squisq-cli) | `squisq` CLI — batch conversion + MP4 rendering |
128
265
  | [@bendyline/squisq-editor-react](https://www.npmjs.com/package/@bendyline/squisq-editor-react) | React editor with raw/WYSIWYG/preview modes |
129
266
 
130
267
  ## License
@@ -2,7 +2,7 @@ import {
2
2
  arrayBufferToBase64DataUrl,
3
3
  extractFilename,
4
4
  inferMimeType
5
- } from "./chunk-A3FHLTY5.js";
5
+ } from "./chunk-MJGRI6XR.js";
6
6
 
7
7
  // src/html/index.ts
8
8
  import JSZip3 from "jszip";
@@ -1430,4 +1430,4 @@ export {
1430
1430
  docToHtml,
1431
1431
  docToHtmlZip
1432
1432
  };
1433
- //# sourceMappingURL=chunk-CRTC6DEX.js.map
1433
+ //# sourceMappingURL=chunk-46FMDOWZ.js.map
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  extractFilename,
9
9
  inferMimeType
10
- } from "./chunk-A3FHLTY5.js";
10
+ } from "./chunk-MJGRI6XR.js";
11
11
 
12
12
  // src/epub/export.ts
13
13
  import JSZip from "jszip";
@@ -629,4 +629,4 @@ export {
629
629
  markdownDocToEpub,
630
630
  docToEpub
631
631
  };
632
- //# sourceMappingURL=chunk-ON232HJR.js.map
632
+ //# sourceMappingURL=chunk-5LSSWZPU.js.map
@@ -1,3 +1,7 @@
1
+ import {
2
+ buildContainer
3
+ } from "./chunk-SSUPBUF5.js";
4
+
1
5
  // src/pdf/export.ts
2
6
  import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
3
7
  import { docToMarkdown, resolveThemeForDoc } from "@bendyline/squisq/doc";
@@ -713,7 +717,6 @@ function hexToRgb(hex) {
713
717
  // src/pdf/import.ts
714
718
  import { markdownToDoc } from "@bendyline/squisq/doc";
715
719
  import { stringifyMarkdown } from "@bendyline/squisq/markdown";
716
- import { MemoryContentContainer } from "@bendyline/squisq/storage";
717
720
  async function pdfToMarkdownDoc(data, options = {}) {
718
721
  const bytes = data instanceof Blob ? new Uint8Array(await data.arrayBuffer()) : data instanceof ArrayBuffer ? new Uint8Array(data) : data;
719
722
  const textLines = await extractTextLines(bytes);
@@ -733,18 +736,16 @@ async function pdfToContainer(data, options = {}) {
733
736
  const textLines = await extractTextLines(bytes);
734
737
  const images = await extractImages(bytes);
735
738
  const bodySize = options.bodyFontSize ?? detectBodyFontSize(textLines);
736
- let blocks = classifyLines(textLines, bodySize, options);
739
+ const blockPages = [];
740
+ let blocks = classifyLines(textLines, bodySize, options, blockPages);
737
741
  if (images.length > 0) {
738
- blocks = insertImageBlocks(blocks, images);
742
+ blocks = insertImageBlocks(blocks, blockPages, images);
739
743
  }
740
744
  const markdownDoc = { type: "document", children: blocks };
741
- const markdown = stringifyMarkdown(markdownDoc);
742
- const container = new MemoryContentContainer();
743
- await container.writeDocument(markdown);
744
- for (const img of images) {
745
- await container.writeFile(img.path, new Uint8Array(img.data), "image/png");
746
- }
747
- return container;
745
+ return buildContainer(
746
+ stringifyMarkdown(markdownDoc),
747
+ images.map((img) => [img.path, { data: img.data, mimeType: "image/png" }])
748
+ );
748
749
  }
749
750
  async function extractImages(data) {
750
751
  if (typeof document === "undefined") return [];
@@ -836,20 +837,47 @@ function imageDataToPng(img) {
836
837
  return null;
837
838
  }
838
839
  }
839
- function insertImageBlocks(blocks, images) {
840
+ function imageParagraph(img) {
841
+ const imgNode = {
842
+ type: "image",
843
+ url: img.path,
844
+ alt: `Image ${img.path.replace("images/image", "").replace(".png", "")}`
845
+ };
846
+ return { type: "paragraph", children: [imgNode] };
847
+ }
848
+ function insertImageBlocks(blocks, blockPages, images) {
840
849
  if (images.length === 0) return blocks;
841
- const result = [...blocks];
850
+ if (blocks.length === 0) {
851
+ return images.map(imageParagraph);
852
+ }
853
+ const lastBlockIndexByPage = /* @__PURE__ */ new Map();
854
+ for (let i = 0; i < blocks.length; i++) {
855
+ lastBlockIndexByPage.set(blockPages[i], i);
856
+ }
857
+ const pagesWithBlocks = [...lastBlockIndexByPage.keys()].sort((a, b) => a - b);
858
+ const lastIndex = blocks.length - 1;
859
+ const anchorFor = (page) => {
860
+ const direct = lastBlockIndexByPage.get(page);
861
+ if (direct !== void 0) return direct;
862
+ let anchor = -1;
863
+ for (const p of pagesWithBlocks) {
864
+ if (p < page) anchor = lastBlockIndexByPage.get(p);
865
+ else break;
866
+ }
867
+ return anchor === -1 ? lastIndex : anchor;
868
+ };
869
+ const insertAfter = /* @__PURE__ */ new Map();
842
870
  for (const img of images) {
843
- const imgNode = {
844
- type: "image",
845
- url: img.path,
846
- alt: `Image ${img.path.replace("images/image", "").replace(".png", "")}`
847
- };
848
- const para = {
849
- type: "paragraph",
850
- children: [imgNode]
851
- };
852
- result.push(para);
871
+ const anchor = anchorFor(img.page);
872
+ const group = insertAfter.get(anchor);
873
+ if (group) group.push(imageParagraph(img));
874
+ else insertAfter.set(anchor, [imageParagraph(img)]);
875
+ }
876
+ const result = [];
877
+ for (let i = 0; i < blocks.length; i++) {
878
+ result.push(blocks[i]);
879
+ const imgs = insertAfter.get(i);
880
+ if (imgs) result.push(...imgs);
853
881
  }
854
882
  return result;
855
883
  }
@@ -966,8 +994,12 @@ function modeStr(arr) {
966
994
  }
967
995
  return maxVal;
968
996
  }
969
- function classifyLines(lines, bodySize, options) {
997
+ function classifyLines(lines, bodySize, options, blockPages) {
970
998
  const blocks = [];
999
+ const pushBlock = (block, page) => {
1000
+ blocks.push(block);
1001
+ if (blockPages) blockPages.push(page);
1002
+ };
971
1003
  const detectTables = options.detectTables !== false;
972
1004
  const detectCodeBlocks = options.detectCodeBlocks !== false;
973
1005
  const detectBlockquotes = options.detectBlockquotes !== false;
@@ -979,11 +1011,14 @@ function classifyLines(lines, bodySize, options) {
979
1011
  const line = lines[i];
980
1012
  if (line.fontSize >= IMPORT_HEADING_MIN_SIZE && line.fontSize > bodySize + 1) {
981
1013
  const depth = sizeToHeadingDepth(line.fontSize);
982
- blocks.push({
983
- type: "heading",
984
- depth,
985
- children: buildInlineNodes(line, options)
986
- });
1014
+ pushBlock(
1015
+ {
1016
+ type: "heading",
1017
+ depth,
1018
+ children: buildInlineNodes(line, options)
1019
+ },
1020
+ line.page
1021
+ );
987
1022
  i++;
988
1023
  continue;
989
1024
  }
@@ -993,10 +1028,13 @@ function classifyLines(lines, bodySize, options) {
993
1028
  codeLines.push(lines[i].text);
994
1029
  i++;
995
1030
  }
996
- blocks.push({
997
- type: "code",
998
- value: codeLines.join("\n")
999
- });
1031
+ pushBlock(
1032
+ {
1033
+ type: "code",
1034
+ value: codeLines.join("\n")
1035
+ },
1036
+ line.page
1037
+ );
1000
1038
  continue;
1001
1039
  }
1002
1040
  if (detectTables && i + 1 < lines.length) {
@@ -1004,7 +1042,7 @@ function classifyLines(lines, bodySize, options) {
1004
1042
  if (tableLines > 0) {
1005
1043
  const table = buildTable(lines.slice(i, i + tableLines), options);
1006
1044
  if (table) {
1007
- blocks.push(table);
1045
+ pushBlock(table, line.page);
1008
1046
  i += tableLines;
1009
1047
  continue;
1010
1048
  }
@@ -1014,7 +1052,7 @@ function classifyLines(lines, bodySize, options) {
1014
1052
  const orderedMatch = line.text.match(IMPORT_ORDERED_PREFIX);
1015
1053
  if (bulletMatch || orderedMatch) {
1016
1054
  const listResult = consumeList(lines, i, typicalLeftMargin, bodySize, options);
1017
- blocks.push(listResult.list);
1055
+ pushBlock(listResult.list, line.page);
1018
1056
  i = listResult.nextIndex;
1019
1057
  continue;
1020
1058
  }
@@ -1030,10 +1068,13 @@ function classifyLines(lines, bodySize, options) {
1030
1068
  children: buildInlineNodes(ql, options)
1031
1069
  })
1032
1070
  );
1033
- blocks.push({
1034
- type: "blockquote",
1035
- children: quoteBlocks
1036
- });
1071
+ pushBlock(
1072
+ {
1073
+ type: "blockquote",
1074
+ children: quoteBlocks
1075
+ },
1076
+ line.page
1077
+ );
1037
1078
  continue;
1038
1079
  }
1039
1080
  const paraLines = [line];
@@ -1061,10 +1102,13 @@ function classifyLines(lines, bodySize, options) {
1061
1102
  allInlines.push(...buildInlineNodes(paraLines[j], options));
1062
1103
  }
1063
1104
  if (allInlines.length > 0) {
1064
- blocks.push({
1065
- type: "paragraph",
1066
- children: mergeAdjacentText(allInlines)
1067
- });
1105
+ pushBlock(
1106
+ {
1107
+ type: "paragraph",
1108
+ children: mergeAdjacentText(allInlines)
1109
+ },
1110
+ line.page
1111
+ );
1068
1112
  }
1069
1113
  }
1070
1114
  return blocks;
@@ -1284,4 +1328,4 @@ export {
1284
1328
  pdfToContainer,
1285
1329
  configurePdfWorker
1286
1330
  };
1287
- //# sourceMappingURL=chunk-APSIAEXL.js.map
1331
+ //# sourceMappingURL=chunk-D7IFPWNZ.js.map