@bendyline/squisq-formats 2.1.0 → 2.2.0

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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +1 -1
  4. package/dist/{chunk-NNHKUXKA.js → chunk-26ISNJ7Y.js} +85 -65
  5. package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
  6. package/dist/{chunk-WQSHGBLN.js → chunk-3NKXBZSR.js} +193 -42
  7. package/dist/{chunk-KURGXM4I.js → chunk-4V3KCHAP.js} +3 -4
  8. package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
  9. package/dist/{chunk-EW54IRRS.js → chunk-6S6GU3ZG.js} +5 -6
  10. package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
  11. package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
  12. package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
  13. package/dist/{chunk-SC67HYQJ.js → chunk-EJTNGKEA.js} +5 -8
  14. package/dist/chunk-GX7RAUME.js +121 -0
  15. package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
  16. package/dist/{chunk-RLU7UFYU.js → chunk-IPN56VLW.js} +83 -58
  17. package/dist/{chunk-DTDF6QDP.js → chunk-JE6LSIHE.js} +81 -20
  18. package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
  19. package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
  20. package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
  21. package/dist/chunk-OGS5VCGJ.js +446 -0
  22. package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
  23. package/dist/{chunk-74GO3FVS.js → chunk-PU7REGWV.js} +5 -8
  24. package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
  25. package/dist/{chunk-QFLDYKCR.js → chunk-TAAENIRB.js} +5 -8
  26. package/dist/{chunk-7ARKUCQT.js → chunk-X2DEAXNK.js} +54 -2
  27. package/dist/container/index.js +1 -2
  28. package/dist/csv/index.d.ts +27 -2
  29. package/dist/csv/index.js +1 -2
  30. package/dist/docx/index.d.ts +5 -1
  31. package/dist/docx/index.js +9 -11
  32. package/dist/epub/index.d.ts +2 -0
  33. package/dist/epub/index.js +5 -6
  34. package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
  35. package/dist/extract-MN7LA3NL.js +13 -0
  36. package/dist/html/index.d.ts +11 -4
  37. package/dist/html/index.js +3 -4
  38. package/dist/images-ESPQKVTW.js +6 -0
  39. package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
  40. package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
  41. package/dist/index.d.ts +7 -7
  42. package/dist/index.js +28 -26
  43. package/dist/infer/index.d.ts +3 -3
  44. package/dist/infer/index.js +7 -9
  45. package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
  46. package/dist/layouts-DRWZGSPD.js +10 -0
  47. package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
  48. package/dist/ooxml/index.d.ts +3 -3
  49. package/dist/ooxml/index.js +14 -13
  50. package/dist/pdf/index.d.ts +18 -0
  51. package/dist/pdf/index.js +2 -3
  52. package/dist/pptx/index.d.ts +4 -4
  53. package/dist/pptx/index.js +11 -13
  54. package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
  55. package/dist/registry/index.d.ts +21 -5
  56. package/dist/registry/index.js +9 -6
  57. package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
  58. package/dist/xlsx/index.d.ts +3 -3
  59. package/dist/xlsx/index.js +6 -7
  60. package/package.json +6 -3
  61. package/dist/chunk-4VUWTSGM.js.map +0 -1
  62. package/dist/chunk-6M7Z25LA.js +0 -46
  63. package/dist/chunk-6M7Z25LA.js.map +0 -1
  64. package/dist/chunk-74GO3FVS.js.map +0 -1
  65. package/dist/chunk-7ARKUCQT.js.map +0 -1
  66. package/dist/chunk-DTDF6QDP.js.map +0 -1
  67. package/dist/chunk-EW54IRRS.js.map +0 -1
  68. package/dist/chunk-FE6OJV6O.js.map +0 -1
  69. package/dist/chunk-GVS2XXV6.js.map +0 -1
  70. package/dist/chunk-KURGXM4I.js.map +0 -1
  71. package/dist/chunk-MLX2BOJC.js.map +0 -1
  72. package/dist/chunk-NKAJPJ4G.js.map +0 -1
  73. package/dist/chunk-NNHKUXKA.js.map +0 -1
  74. package/dist/chunk-O3GVVND4.js.map +0 -1
  75. package/dist/chunk-ODL3SSPT.js.map +0 -1
  76. package/dist/chunk-PN52A5AA.js.map +0 -1
  77. package/dist/chunk-QFLDYKCR.js.map +0 -1
  78. package/dist/chunk-RFAPOKHJ.js.map +0 -1
  79. package/dist/chunk-RLU7UFYU.js.map +0 -1
  80. package/dist/chunk-SC67HYQJ.js.map +0 -1
  81. package/dist/chunk-SSUPBUF5.js.map +0 -1
  82. package/dist/chunk-U4MRIFKL.js.map +0 -1
  83. package/dist/chunk-UGYF5AZE.js +0 -275
  84. package/dist/chunk-UGYF5AZE.js.map +0 -1
  85. package/dist/chunk-WQSHGBLN.js.map +0 -1
  86. package/dist/chunk-YRT7GQ5Y.js +0 -28
  87. package/dist/chunk-YRT7GQ5Y.js.map +0 -1
  88. package/dist/container/index.js.map +0 -1
  89. package/dist/csv/index.js.map +0 -1
  90. package/dist/docx/index.js.map +0 -1
  91. package/dist/epub/index.js.map +0 -1
  92. package/dist/extract-OJ7ZQV6P.js +0 -15
  93. package/dist/extract-OJ7ZQV6P.js.map +0 -1
  94. package/dist/html/index.js.map +0 -1
  95. package/dist/images-7FBWPKE3.js +0 -7
  96. package/dist/images-7FBWPKE3.js.map +0 -1
  97. package/dist/index.js.map +0 -1
  98. package/dist/infer/index.js.map +0 -1
  99. package/dist/layouts-5VDIRPIJ.js +0 -12
  100. package/dist/layouts-5VDIRPIJ.js.map +0 -1
  101. package/dist/mapTheme-IR27S6IV.js.map +0 -1
  102. package/dist/ooxml/index.js.map +0 -1
  103. package/dist/pdf/index.js.map +0 -1
  104. package/dist/pptx/index.js.map +0 -1
  105. package/dist/registry/index.js.map +0 -1
  106. package/dist/xlsx/index.js.map +0 -1
  107. package/src/__tests__/container.test.ts +0 -230
  108. package/src/__tests__/convert.test.ts +0 -495
  109. package/src/__tests__/csvImport.test.ts +0 -84
  110. package/src/__tests__/docxExport.test.ts +0 -491
  111. package/src/__tests__/docxImport.test.ts +0 -531
  112. package/src/__tests__/epub.test.ts +0 -649
  113. package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
  114. package/src/__tests__/formatRegistry.test.ts +0 -174
  115. package/src/__tests__/html.test.ts +0 -439
  116. package/src/__tests__/htmlImport.test.ts +0 -57
  117. package/src/__tests__/inferTheme.test.ts +0 -135
  118. package/src/__tests__/lossyWarnings.test.ts +0 -146
  119. package/src/__tests__/ooxml.test.ts +0 -271
  120. package/src/__tests__/ooxmlCancellation.test.ts +0 -113
  121. package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
  122. package/src/__tests__/pdfExport.test.ts +0 -322
  123. package/src/__tests__/pdfImport.test.ts +0 -384
  124. package/src/__tests__/plainHtml.test.ts +0 -417
  125. package/src/__tests__/plainHtmlBundle.test.ts +0 -253
  126. package/src/__tests__/pptxExport.test.ts +0 -138
  127. package/src/__tests__/pptxImport.test.ts +0 -145
  128. package/src/__tests__/pptxInferFixtures.ts +0 -314
  129. package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
  130. package/src/__tests__/roundTrip.test.ts +0 -201
  131. package/src/__tests__/roundTripAssets.test.ts +0 -50
  132. package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
  133. package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
  134. package/src/__tests__/roundTripMatrix.test.ts +0 -142
  135. package/src/__tests__/sharedContainer.test.ts +0 -41
  136. package/src/__tests__/sharedImages.test.ts +0 -61
  137. package/src/__tests__/xlsxExport.test.ts +0 -164
  138. package/src/__tests__/xlsxImport.test.ts +0 -80
  139. package/src/__tests__/zipSafety.test.ts +0 -317
  140. package/src/container/index.ts +0 -94
  141. package/src/csv/index.ts +0 -188
  142. package/src/docx/export.ts +0 -1375
  143. package/src/docx/import.ts +0 -1250
  144. package/src/docx/index.ts +0 -26
  145. package/src/docx/styles.ts +0 -145
  146. package/src/epub/export.ts +0 -968
  147. package/src/epub/index.ts +0 -20
  148. package/src/html/docsHtmlBundle.ts +0 -373
  149. package/src/html/htmlTemplate.ts +0 -385
  150. package/src/html/imageUtils.ts +0 -61
  151. package/src/html/import.ts +0 -297
  152. package/src/html/index.ts +0 -212
  153. package/src/html/plainHtml.ts +0 -790
  154. package/src/html/plainHtmlBundle.ts +0 -421
  155. package/src/index.ts +0 -109
  156. package/src/infer/extract.ts +0 -127
  157. package/src/infer/index.ts +0 -199
  158. package/src/infer/mapTheme.ts +0 -176
  159. package/src/infer/types.ts +0 -27
  160. package/src/ooxml/index.ts +0 -111
  161. package/src/ooxml/namespaces.ts +0 -217
  162. package/src/ooxml/readUtils.ts +0 -44
  163. package/src/ooxml/reader.ts +0 -318
  164. package/src/ooxml/themeReader.ts +0 -197
  165. package/src/ooxml/types.ts +0 -103
  166. package/src/ooxml/writer.ts +0 -339
  167. package/src/ooxml/xmlUtils.ts +0 -123
  168. package/src/pdf/export.ts +0 -1084
  169. package/src/pdf/import.ts +0 -1164
  170. package/src/pdf/index.ts +0 -29
  171. package/src/pdf/styles.ts +0 -180
  172. package/src/pptx/export.ts +0 -1184
  173. package/src/pptx/import.ts +0 -455
  174. package/src/pptx/index.ts +0 -52
  175. package/src/pptx/layouts.ts +0 -1222
  176. package/src/pptx/styles.ts +0 -96
  177. package/src/pptx/templates.ts +0 -187
  178. package/src/registry/convert.ts +0 -433
  179. package/src/registry/defaultFormats.ts +0 -413
  180. package/src/registry/errors.ts +0 -46
  181. package/src/registry/index.ts +0 -43
  182. package/src/registry/registry.ts +0 -48
  183. package/src/registry/types.ts +0 -170
  184. package/src/shared/boundedZipArchive.ts +0 -383
  185. package/src/shared/container.ts +0 -28
  186. package/src/shared/fidelity.ts +0 -130
  187. package/src/shared/images.ts +0 -44
  188. package/src/shared/inlineRuns.ts +0 -99
  189. package/src/shared/text.ts +0 -41
  190. package/src/shared/zipEntryCount.ts +0 -151
  191. package/src/shared/zipLimits.ts +0 -296
  192. package/src/shared/zipSafety.ts +0 -19
  193. package/src/xlsx/export.ts +0 -253
  194. package/src/xlsx/import.ts +0 -160
  195. package/src/xlsx/index.ts +0 -35
@@ -1,297 +0,0 @@
1
- /**
2
- * HTML Import Module — @bendyline/squisq-formats/html
3
- *
4
- * Converts an HTML document (or fragment) into a squisq `MarkdownDocument`.
5
- * Built on the existing `parseHtmlToNodes` (parse5-backed) + `sanitizeHtmlNodes`
6
- * from `@bendyline/squisq/markdown`, so no new dependency is introduced.
7
- *
8
- * The primary consumer is email: HTML mail bodies are hostile input, so the
9
- * parse is sanitized by default (scripts/styles/event handlers/dangerous URLs
10
- * stripped) before the tree is walked into markdown nodes.
11
- *
12
- * Two entry points mirror the docx/pdf importers:
13
- * - `htmlToMarkdownDoc(data, options?)` → `MarkdownDocument`
14
- * - `htmlToMarkdown(html, options?)` → markdown string (convenience for the
15
- * string-in/string-out path email body conversion needs)
16
- */
17
-
18
- import {
19
- type MarkdownBlockNode,
20
- type MarkdownDocument,
21
- type MarkdownInlineNode,
22
- type MarkdownListItem,
23
- type MarkdownTableCell,
24
- type MarkdownTableRow,
25
- type HtmlElement,
26
- type HtmlNode,
27
- parseHtmlToNodes,
28
- sanitizeHtmlNodes,
29
- stringifyMarkdown,
30
- } from '@bendyline/squisq/markdown';
31
-
32
- export interface HtmlImportOptions {
33
- /**
34
- * Strip scripts / styles / event handlers / dangerous URLs before walking
35
- * the tree. Default `true` — HTML email is untrusted; only disable for
36
- * trusted input where you want raw fidelity.
37
- */
38
- sanitize?: boolean;
39
- }
40
-
41
- // ── tag classification ──────────────────────────────────────────────
42
-
43
- const HEADINGS: Record<string, 1 | 2 | 3 | 4 | 5 | 6> = {
44
- h1: 1,
45
- h2: 2,
46
- h3: 3,
47
- h4: 4,
48
- h5: 5,
49
- h6: 6,
50
- };
51
-
52
- // Containers with no semantic block meaning — unwrap and walk their children.
53
- const TRANSPARENT_BLOCK = new Set([
54
- 'div',
55
- 'section',
56
- 'article',
57
- 'header',
58
- 'footer',
59
- 'main',
60
- 'aside',
61
- 'nav',
62
- 'figure',
63
- 'figcaption',
64
- 'form',
65
- 'fieldset',
66
- 'body',
67
- 'html',
68
- 'center',
69
- ]);
70
-
71
- // Elements that carry no content we want (and whose text must not leak).
72
- const DROP = new Set(['script', 'style', 'head', 'title', 'noscript', 'template', 'svg']);
73
-
74
- const INLINE_STRONG = new Set(['strong', 'b']);
75
- const INLINE_EM = new Set(['em', 'i']);
76
- const INLINE_DEL = new Set(['del', 's', 'strike']);
77
- const INLINE_TRANSPARENT = new Set(['span', 'font', 'abbr', 'mark', 'small', 'sub', 'sup', 'u']);
78
-
79
- const isElement = (n: HtmlNode): n is HtmlElement => n.type === 'htmlElement';
80
- const isText = (n: HtmlNode): n is { type: 'htmlText'; value: string } => n.type === 'htmlText';
81
-
82
- const collapseWs = (s: string): string => s.replace(/\s+/g, ' ');
83
-
84
- // ── inline conversion ───────────────────────────────────────────────
85
-
86
- function inlinesFromNodes(nodes: HtmlNode[]): MarkdownInlineNode[] {
87
- const out: MarkdownInlineNode[] = [];
88
- for (const node of nodes) {
89
- if (isText(node)) {
90
- const value = collapseWs(node.value);
91
- if (value) out.push({ type: 'text', value });
92
- continue;
93
- }
94
- if (!isElement(node)) continue; // comment
95
- const tag = node.tagName.toLowerCase();
96
- if (DROP.has(tag)) continue;
97
-
98
- if (tag === 'br') {
99
- out.push({ type: 'break' });
100
- } else if (INLINE_STRONG.has(tag)) {
101
- out.push({ type: 'strong', children: inlinesFromNodes(node.children) });
102
- } else if (INLINE_EM.has(tag)) {
103
- out.push({ type: 'emphasis', children: inlinesFromNodes(node.children) });
104
- } else if (INLINE_DEL.has(tag)) {
105
- out.push({ type: 'delete', children: inlinesFromNodes(node.children) });
106
- } else if (tag === 'code' || tag === 'kbd' || tag === 'samp' || tag === 'tt') {
107
- out.push({ type: 'inlineCode', value: textContent(node) });
108
- } else if (tag === 'a') {
109
- const url = node.attributes.href ?? '';
110
- const children = inlinesFromNodes(node.children);
111
- out.push({
112
- type: 'link',
113
- url,
114
- children: children.length > 0 ? children : [{ type: 'text', value: url }],
115
- });
116
- } else if (tag === 'img') {
117
- const url = node.attributes.src ?? '';
118
- const alt = node.attributes.alt;
119
- if (url) out.push({ type: 'image', url, ...(alt ? { alt } : {}) });
120
- } else {
121
- // Unknown / transparent inline (span, font, …): flatten children.
122
- out.push(...inlinesFromNodes(node.children));
123
- }
124
- }
125
- return out;
126
- }
127
-
128
- /** Flatten an element's descendant text (for code blocks / inline code). */
129
- function textContent(node: HtmlNode): string {
130
- if (isText(node)) return node.value;
131
- if (isElement(node)) return node.children.map(textContent).join('');
132
- return '';
133
- }
134
-
135
- const onlyWhitespace = (inlines: MarkdownInlineNode[]): boolean =>
136
- inlines.every((n) => n.type === 'text' && n.value.trim() === '');
137
-
138
- // ── block conversion ────────────────────────────────────────────────
139
-
140
- function blocksFromNodes(nodes: HtmlNode[]): MarkdownBlockNode[] {
141
- const out: MarkdownBlockNode[] = [];
142
- let inlineBuffer: HtmlNode[] = [];
143
-
144
- const flush = () => {
145
- if (inlineBuffer.length === 0) return;
146
- const inlines = inlinesFromNodes(inlineBuffer);
147
- inlineBuffer = [];
148
- if (inlines.length > 0 && !onlyWhitespace(inlines)) {
149
- out.push({ type: 'paragraph', children: inlines });
150
- }
151
- };
152
-
153
- for (const node of nodes) {
154
- if (isText(node)) {
155
- inlineBuffer.push(node);
156
- continue;
157
- }
158
- if (!isElement(node)) continue;
159
- const tag = node.tagName.toLowerCase();
160
- if (DROP.has(tag)) continue;
161
-
162
- const block = blockForElement(node, tag);
163
- if (block === 'inline') {
164
- inlineBuffer.push(node);
165
- } else if (block) {
166
- flush();
167
- out.push(...block);
168
- }
169
- }
170
- flush();
171
- return out;
172
- }
173
-
174
- /** Returns the block node(s) for a block element, `'inline'` for inline ones. */
175
- function blockForElement(node: HtmlElement, tag: string): MarkdownBlockNode[] | 'inline' | null {
176
- if (tag in HEADINGS) {
177
- const children = inlinesFromNodes(node.children);
178
- return [{ type: 'heading', depth: HEADINGS[tag]!, children }];
179
- }
180
- if (tag === 'p') {
181
- const children = inlinesFromNodes(node.children);
182
- return children.length > 0 && !onlyWhitespace(children)
183
- ? [{ type: 'paragraph', children }]
184
- : [];
185
- }
186
- if (tag === 'br') return 'inline';
187
- if (tag === 'hr') return [{ type: 'thematicBreak' }];
188
- if (tag === 'blockquote') {
189
- return [{ type: 'blockquote', children: blocksFromNodes(node.children) }];
190
- }
191
- if (tag === 'pre') {
192
- return [{ type: 'code', value: stripTrailingNewline(textContent(node)) }];
193
- }
194
- if (tag === 'ul' || tag === 'ol') {
195
- return [listFromElement(node, tag === 'ol')];
196
- }
197
- if (tag === 'table') {
198
- const table = tableFromElement(node);
199
- return table ? [table] : [];
200
- }
201
- if (TRANSPARENT_BLOCK.has(tag)) {
202
- return blocksFromNodes(node.children);
203
- }
204
- if (
205
- INLINE_STRONG.has(tag) ||
206
- INLINE_EM.has(tag) ||
207
- INLINE_DEL.has(tag) ||
208
- INLINE_TRANSPARENT.has(tag) ||
209
- tag === 'a' ||
210
- tag === 'img' ||
211
- tag === 'code' ||
212
- tag === 'kbd' ||
213
- tag === 'samp'
214
- ) {
215
- return 'inline';
216
- }
217
- // Unknown element: treat as a transparent container so its content survives.
218
- return blocksFromNodes(node.children);
219
- }
220
-
221
- function listFromElement(node: HtmlElement, ordered: boolean): MarkdownBlockNode {
222
- const items: MarkdownListItem[] = [];
223
- for (const child of node.children) {
224
- if (isElement(child) && child.tagName.toLowerCase() === 'li') {
225
- const blocks = blocksFromNodes(child.children);
226
- items.push({ type: 'listItem', children: blocks });
227
- }
228
- }
229
- const startAttr = node.attributes.start;
230
- const start = ordered && startAttr ? Number.parseInt(startAttr, 10) : undefined;
231
- return {
232
- type: 'list',
233
- ordered,
234
- ...(start !== undefined && Number.isFinite(start) ? { start } : {}),
235
- children: items,
236
- };
237
- }
238
-
239
- function tableFromElement(node: HtmlElement): MarkdownBlockNode | null {
240
- const rows: MarkdownTableRow[] = [];
241
- const collectRows = (n: HtmlElement) => {
242
- for (const child of n.children) {
243
- if (!isElement(child)) continue;
244
- const t = child.tagName.toLowerCase();
245
- if (t === 'tr') {
246
- const cells: MarkdownTableCell[] = [];
247
- for (const cell of child.children) {
248
- if (
249
- isElement(cell) &&
250
- (cell.tagName.toLowerCase() === 'td' || cell.tagName.toLowerCase() === 'th')
251
- ) {
252
- cells.push({ type: 'tableCell', children: inlinesFromNodes(cell.children) });
253
- }
254
- }
255
- if (cells.length > 0) rows.push({ type: 'tableRow', children: cells });
256
- } else if (t === 'thead' || t === 'tbody' || t === 'tfoot') {
257
- collectRows(child);
258
- }
259
- }
260
- };
261
- collectRows(node);
262
- if (rows.length === 0) return null;
263
- return { type: 'table', children: rows };
264
- }
265
-
266
- const stripTrailingNewline = (s: string): string => s.replace(/\n+$/, '');
267
-
268
- // ── public API ──────────────────────────────────────────────────────
269
-
270
- function toHtmlString(data: ArrayBuffer | Uint8Array | string): string {
271
- if (typeof data === 'string') return data;
272
- const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
273
- return new TextDecoder('utf-8').decode(bytes);
274
- }
275
-
276
- /** Parse HTML into a squisq `MarkdownDocument`. */
277
- export function htmlToMarkdownDocSync(
278
- html: string,
279
- options: HtmlImportOptions = {},
280
- ): MarkdownDocument {
281
- let nodes = parseHtmlToNodes(html);
282
- if (options.sanitize !== false) nodes = sanitizeHtmlNodes(nodes);
283
- return { type: 'document', children: blocksFromNodes(nodes) };
284
- }
285
-
286
- /** Async, ArrayBuffer-accepting entry mirroring docx/pdf importers. */
287
- export async function htmlToMarkdownDoc(
288
- data: ArrayBuffer | Uint8Array | string,
289
- options: HtmlImportOptions = {},
290
- ): Promise<MarkdownDocument> {
291
- return htmlToMarkdownDocSync(toHtmlString(data), options);
292
- }
293
-
294
- /** Convenience string→string conversion (used for email HTML bodies). */
295
- export function htmlToMarkdown(html: string, options: HtmlImportOptions = {}): string {
296
- return stringifyMarkdown(htmlToMarkdownDocSync(html, options));
297
- }
package/src/html/index.ts DELETED
@@ -1,212 +0,0 @@
1
- /**
2
- * HTML Export Module — @bendyline/squisq-formats/html
3
- *
4
- * Exports squisq documents as self-contained HTML files or ZIP archives.
5
- *
6
- * Two export modes:
7
- *
8
- * 1. **Single HTML** (`docToHtml`) — Everything embedded in one file:
9
- * - SquisqPlayer IIFE bundle inlined in `<script>`
10
- * - Images base64-encoded as data URIs
11
- * - Timer-based playback (no audio — too large for inline)
12
- *
13
- * 2. **ZIP Archive** (`docToHtmlZip`) — Multi-file package:
14
- * - `index.html` referencing external `squisq-player.js`
15
- * - `squisq-player.js` — the IIFE bundle
16
- * - `images/` — extracted image files
17
- * - `audio/` — optional audio segment files (enables full playback)
18
- *
19
- * @example
20
- * ```ts
21
- * import { PLAYER_BUNDLE } from '@bendyline/squisq-react/standalone-source';
22
- * import { docToHtml, docToHtmlZip } from '@bendyline/squisq-formats/html';
23
- *
24
- * // Single HTML file
25
- * const html = docToHtml(doc, { playerScript: PLAYER_BUNDLE, images });
26
- *
27
- * // ZIP archive
28
- * const zipBlob = await docToHtmlZip(doc, { playerScript: PLAYER_BUNDLE, images, audio });
29
- * ```
30
- */
31
-
32
- import type { Doc } from '@bendyline/squisq/schemas';
33
- import JSZip from 'jszip';
34
- import {
35
- generateInlineHtml,
36
- generateExternalHtml,
37
- type HtmlExportOptions,
38
- } from './htmlTemplate.js';
39
- import { extractFilename } from './imageUtils.js';
40
-
41
- // ── Public Types ───────────────────────────────────────────────────
42
-
43
- export type { HtmlExportOptions };
44
-
45
- /**
46
- * Normalize a doc-referenced path for use as a zip entry name. Strips
47
- * leading slashes and rejects any path that tries to escape the archive
48
- * root via `..` segments (a malicious or malformed doc shouldn't be able
49
- * to write outside the zip when extracted). Backslashes are folded to
50
- * forward slashes so Windows-authored paths still produce a consistent
51
- * tree across extractors.
52
- */
53
- function sanitizeZipPath(path: string): string | null {
54
- const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
55
- if (!normalized) return null;
56
- if (normalized.split('/').some((seg) => seg === '..')) return null;
57
- return normalized;
58
- }
59
-
60
- export interface HtmlZipExportOptions extends HtmlExportOptions {
61
- /**
62
- * Map of audio segment identifiers to binary audio data.
63
- * Keys should match the audio segment `name` or `url` fields in the Doc.
64
- * When provided, audio files are included in the ZIP and full playback is enabled.
65
- */
66
- audio?: Map<string, ArrayBuffer>;
67
- }
68
-
69
- // ── Single HTML Export ─────────────────────────────────────────────
70
-
71
- /**
72
- * Export a Doc as a single, self-contained HTML file.
73
- *
74
- * The player JS is embedded inline, images are base64-encoded as data URIs,
75
- * and playback uses a timer (no audio) for compact file size.
76
- *
77
- * @param doc - The Doc to export
78
- * @param options - Export options (must include `playerScript`)
79
- * @returns Complete HTML document as a string
80
- *
81
- * @example
82
- * ```ts
83
- * const html = docToHtml(myDoc, {
84
- * playerScript: PLAYER_BUNDLE,
85
- * images: imageMap,
86
- * title: 'My Document',
87
- * mode: 'slideshow',
88
- * });
89
- * const blob = new Blob([html], { type: 'text/html' });
90
- * ```
91
- */
92
- export function docToHtml(doc: Doc, options: HtmlExportOptions): string {
93
- return generateInlineHtml(doc, options);
94
- }
95
-
96
- // ── ZIP Archive Export ─────────────────────────────────────────────
97
-
98
- /**
99
- * Export a Doc as a ZIP archive containing HTML, JS, images, and optionally audio.
100
- *
101
- * The archive structure:
102
- * ```
103
- * document.zip
104
- * ├── index.html # HTML page referencing squisq-player.js
105
- * ├── squisq-player.js # Standalone IIFE bundle
106
- * ├── images/ # Extracted image files
107
- * │ ├── hero.jpg
108
- * │ └── ...
109
- * └── audio/ # Optional audio segment files
110
- * ├── intro.mp3
111
- * └── ...
112
- * ```
113
- *
114
- * @param doc - The Doc to export
115
- * @param options - Export options (must include `playerScript`)
116
- * @returns A Promise resolving to a ZIP Blob
117
- *
118
- * @example
119
- * ```ts
120
- * const blob = await docToHtmlZip(myDoc, {
121
- * playerScript: PLAYER_BUNDLE,
122
- * images: imageMap,
123
- * audio: audioMap,
124
- * });
125
- * // Trigger browser download
126
- * const url = URL.createObjectURL(blob);
127
- * ```
128
- */
129
- export async function docToHtmlZip(doc: Doc, options: HtmlZipExportOptions): Promise<Blob> {
130
- const {
131
- playerScript,
132
- images,
133
- audio,
134
- mode = 'slideshow',
135
- title,
136
- autoPlay,
137
- themeId,
138
- themeRegistry,
139
- } = options;
140
-
141
- const zip = new JSZip();
142
-
143
- // 1. Add player JS as a separate file
144
- zip.file('squisq-player.js', playerScript);
145
-
146
- // 2. Add images preserving their original relative paths.
147
- //
148
- // We used to flatten everything into `images/<basename>`, but that
149
- // requires the player to rewrite every `<img src>` at render time using
150
- // the path map — and the static (non-slideshow) renderer doesn't do
151
- // that, so its emitted `<img>` tags 404'd against the renamed assets.
152
- // Keeping the original layout means direct `src="folder/file.png"`
153
- // references just work, and the exported zip mirrors how the source
154
- // folder is organized (e.g. pandoc-style `notes_files/` sidecars).
155
- const imagePathMap: Record<string, string> = {};
156
- if (images) {
157
- for (const [originalPath, buffer] of images.entries()) {
158
- const zipPath = sanitizeZipPath(originalPath);
159
- if (!zipPath) continue;
160
- zip.file(zipPath, buffer);
161
- imagePathMap[originalPath] = zipPath;
162
- }
163
- }
164
-
165
- // 3. Add audio to audio/ folder and build path mapping
166
- const audioPathMap: Record<string, string> = {};
167
- if (audio) {
168
- for (const [segmentKey, buffer] of audio.entries()) {
169
- const filename = extractFilename(segmentKey);
170
- // Ensure .mp3 extension
171
- const finalName = filename.includes('.') ? filename : `${filename}.mp3`;
172
- const zipPath = `audio/${finalName}`;
173
- zip.file(zipPath, buffer);
174
- audioPathMap[segmentKey] = zipPath;
175
- }
176
- }
177
-
178
- // 4. Generate HTML that references external files
179
- const html = generateExternalHtml(doc, {
180
- playerScriptPath: 'squisq-player.js',
181
- imagePathMap: Object.keys(imagePathMap).length > 0 ? imagePathMap : undefined,
182
- audioPathMap: Object.keys(audioPathMap).length > 0 ? audioPathMap : undefined,
183
- mode,
184
- title,
185
- autoPlay,
186
- themeId,
187
- themeRegistry,
188
- });
189
- zip.file('index.html', html);
190
-
191
- // 5. Generate ZIP blob
192
- return zip.generateAsync({
193
- type: 'blob',
194
- compression: 'DEFLATE',
195
- compressionOptions: { level: 6 },
196
- });
197
- }
198
-
199
- // ── Re-exports ─────────────────────────────────────────────────────
200
-
201
- export { collectImagePaths } from './htmlTemplate.js';
202
- export { inferMimeType, arrayBufferToBase64DataUrl, extractFilename } from './imageUtils.js';
203
- export { markdownDocToPlainHtml } from './plainHtml.js';
204
- export type { PlainHtmlExportOptions } from './plainHtml.js';
205
- export { markdownDocsToPlainHtmlBundle, collectLinkRefs } from './plainHtmlBundle.js';
206
- export type { PlainHtmlBundleOptions } from './plainHtmlBundle.js';
207
- export { markdownDocsToHtmlBundle } from './docsHtmlBundle.js';
208
- export type { HtmlBundleOptions } from './docsHtmlBundle.js';
209
-
210
- // ── Import (HTML → MarkdownDocument) ────────────────────────────────
211
- export { htmlToMarkdown, htmlToMarkdownDoc, htmlToMarkdownDocSync } from './import.js';
212
- export type { HtmlImportOptions } from './import.js';