@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.
- package/LICENSE +21 -0
- package/NOTICE.md +20 -0
- package/README.md +1 -1
- package/dist/{chunk-NNHKUXKA.js → chunk-26ISNJ7Y.js} +85 -65
- package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
- package/dist/{chunk-WQSHGBLN.js → chunk-3NKXBZSR.js} +193 -42
- package/dist/{chunk-KURGXM4I.js → chunk-4V3KCHAP.js} +3 -4
- package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
- package/dist/{chunk-EW54IRRS.js → chunk-6S6GU3ZG.js} +5 -6
- package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
- package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
- package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
- package/dist/{chunk-SC67HYQJ.js → chunk-EJTNGKEA.js} +5 -8
- package/dist/chunk-GX7RAUME.js +121 -0
- package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
- package/dist/{chunk-RLU7UFYU.js → chunk-IPN56VLW.js} +83 -58
- package/dist/{chunk-DTDF6QDP.js → chunk-JE6LSIHE.js} +81 -20
- package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
- package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
- package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
- package/dist/chunk-OGS5VCGJ.js +446 -0
- package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
- package/dist/{chunk-74GO3FVS.js → chunk-PU7REGWV.js} +5 -8
- package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
- package/dist/{chunk-QFLDYKCR.js → chunk-TAAENIRB.js} +5 -8
- package/dist/{chunk-7ARKUCQT.js → chunk-X2DEAXNK.js} +54 -2
- package/dist/container/index.js +1 -2
- package/dist/csv/index.d.ts +27 -2
- package/dist/csv/index.js +1 -2
- package/dist/docx/index.d.ts +5 -1
- package/dist/docx/index.js +9 -11
- package/dist/epub/index.d.ts +2 -0
- package/dist/epub/index.js +5 -6
- package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
- package/dist/extract-MN7LA3NL.js +13 -0
- package/dist/html/index.d.ts +11 -4
- package/dist/html/index.js +3 -4
- package/dist/images-ESPQKVTW.js +6 -0
- package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
- package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +28 -26
- package/dist/infer/index.d.ts +3 -3
- package/dist/infer/index.js +7 -9
- package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
- package/dist/layouts-DRWZGSPD.js +10 -0
- package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
- package/dist/ooxml/index.d.ts +3 -3
- package/dist/ooxml/index.js +14 -13
- package/dist/pdf/index.d.ts +18 -0
- package/dist/pdf/index.js +2 -3
- package/dist/pptx/index.d.ts +4 -4
- package/dist/pptx/index.js +11 -13
- package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
- package/dist/registry/index.d.ts +21 -5
- package/dist/registry/index.js +9 -6
- package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
- package/dist/xlsx/index.d.ts +3 -3
- package/dist/xlsx/index.js +6 -7
- package/package.json +6 -3
- package/dist/chunk-4VUWTSGM.js.map +0 -1
- package/dist/chunk-6M7Z25LA.js +0 -46
- package/dist/chunk-6M7Z25LA.js.map +0 -1
- package/dist/chunk-74GO3FVS.js.map +0 -1
- package/dist/chunk-7ARKUCQT.js.map +0 -1
- package/dist/chunk-DTDF6QDP.js.map +0 -1
- package/dist/chunk-EW54IRRS.js.map +0 -1
- package/dist/chunk-FE6OJV6O.js.map +0 -1
- package/dist/chunk-GVS2XXV6.js.map +0 -1
- package/dist/chunk-KURGXM4I.js.map +0 -1
- package/dist/chunk-MLX2BOJC.js.map +0 -1
- package/dist/chunk-NKAJPJ4G.js.map +0 -1
- package/dist/chunk-NNHKUXKA.js.map +0 -1
- package/dist/chunk-O3GVVND4.js.map +0 -1
- package/dist/chunk-ODL3SSPT.js.map +0 -1
- package/dist/chunk-PN52A5AA.js.map +0 -1
- package/dist/chunk-QFLDYKCR.js.map +0 -1
- package/dist/chunk-RFAPOKHJ.js.map +0 -1
- package/dist/chunk-RLU7UFYU.js.map +0 -1
- package/dist/chunk-SC67HYQJ.js.map +0 -1
- package/dist/chunk-SSUPBUF5.js.map +0 -1
- package/dist/chunk-U4MRIFKL.js.map +0 -1
- package/dist/chunk-UGYF5AZE.js +0 -275
- package/dist/chunk-UGYF5AZE.js.map +0 -1
- package/dist/chunk-WQSHGBLN.js.map +0 -1
- package/dist/chunk-YRT7GQ5Y.js +0 -28
- package/dist/chunk-YRT7GQ5Y.js.map +0 -1
- package/dist/container/index.js.map +0 -1
- package/dist/csv/index.js.map +0 -1
- package/dist/docx/index.js.map +0 -1
- package/dist/epub/index.js.map +0 -1
- package/dist/extract-OJ7ZQV6P.js +0 -15
- package/dist/extract-OJ7ZQV6P.js.map +0 -1
- package/dist/html/index.js.map +0 -1
- package/dist/images-7FBWPKE3.js +0 -7
- package/dist/images-7FBWPKE3.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infer/index.js.map +0 -1
- package/dist/layouts-5VDIRPIJ.js +0 -12
- package/dist/layouts-5VDIRPIJ.js.map +0 -1
- package/dist/mapTheme-IR27S6IV.js.map +0 -1
- package/dist/ooxml/index.js.map +0 -1
- package/dist/pdf/index.js.map +0 -1
- package/dist/pptx/index.js.map +0 -1
- package/dist/registry/index.js.map +0 -1
- package/dist/xlsx/index.js.map +0 -1
- package/src/__tests__/container.test.ts +0 -230
- package/src/__tests__/convert.test.ts +0 -495
- package/src/__tests__/csvImport.test.ts +0 -84
- package/src/__tests__/docxExport.test.ts +0 -491
- package/src/__tests__/docxImport.test.ts +0 -531
- package/src/__tests__/epub.test.ts +0 -649
- package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
- package/src/__tests__/formatRegistry.test.ts +0 -174
- package/src/__tests__/html.test.ts +0 -439
- package/src/__tests__/htmlImport.test.ts +0 -57
- package/src/__tests__/inferTheme.test.ts +0 -135
- package/src/__tests__/lossyWarnings.test.ts +0 -146
- package/src/__tests__/ooxml.test.ts +0 -271
- package/src/__tests__/ooxmlCancellation.test.ts +0 -113
- package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
- package/src/__tests__/pdfExport.test.ts +0 -322
- package/src/__tests__/pdfImport.test.ts +0 -384
- package/src/__tests__/plainHtml.test.ts +0 -417
- package/src/__tests__/plainHtmlBundle.test.ts +0 -253
- package/src/__tests__/pptxExport.test.ts +0 -138
- package/src/__tests__/pptxImport.test.ts +0 -145
- package/src/__tests__/pptxInferFixtures.ts +0 -314
- package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
- package/src/__tests__/roundTrip.test.ts +0 -201
- package/src/__tests__/roundTripAssets.test.ts +0 -50
- package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
- package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
- package/src/__tests__/roundTripMatrix.test.ts +0 -142
- package/src/__tests__/sharedContainer.test.ts +0 -41
- package/src/__tests__/sharedImages.test.ts +0 -61
- package/src/__tests__/xlsxExport.test.ts +0 -164
- package/src/__tests__/xlsxImport.test.ts +0 -80
- package/src/__tests__/zipSafety.test.ts +0 -317
- package/src/container/index.ts +0 -94
- package/src/csv/index.ts +0 -188
- package/src/docx/export.ts +0 -1375
- package/src/docx/import.ts +0 -1250
- package/src/docx/index.ts +0 -26
- package/src/docx/styles.ts +0 -145
- package/src/epub/export.ts +0 -968
- package/src/epub/index.ts +0 -20
- package/src/html/docsHtmlBundle.ts +0 -373
- package/src/html/htmlTemplate.ts +0 -385
- package/src/html/imageUtils.ts +0 -61
- package/src/html/import.ts +0 -297
- package/src/html/index.ts +0 -212
- package/src/html/plainHtml.ts +0 -790
- package/src/html/plainHtmlBundle.ts +0 -421
- package/src/index.ts +0 -109
- package/src/infer/extract.ts +0 -127
- package/src/infer/index.ts +0 -199
- package/src/infer/mapTheme.ts +0 -176
- package/src/infer/types.ts +0 -27
- package/src/ooxml/index.ts +0 -111
- package/src/ooxml/namespaces.ts +0 -217
- package/src/ooxml/readUtils.ts +0 -44
- package/src/ooxml/reader.ts +0 -318
- package/src/ooxml/themeReader.ts +0 -197
- package/src/ooxml/types.ts +0 -103
- package/src/ooxml/writer.ts +0 -339
- package/src/ooxml/xmlUtils.ts +0 -123
- package/src/pdf/export.ts +0 -1084
- package/src/pdf/import.ts +0 -1164
- package/src/pdf/index.ts +0 -29
- package/src/pdf/styles.ts +0 -180
- package/src/pptx/export.ts +0 -1184
- package/src/pptx/import.ts +0 -455
- package/src/pptx/index.ts +0 -52
- package/src/pptx/layouts.ts +0 -1222
- package/src/pptx/styles.ts +0 -96
- package/src/pptx/templates.ts +0 -187
- package/src/registry/convert.ts +0 -433
- package/src/registry/defaultFormats.ts +0 -413
- package/src/registry/errors.ts +0 -46
- package/src/registry/index.ts +0 -43
- package/src/registry/registry.ts +0 -48
- package/src/registry/types.ts +0 -170
- package/src/shared/boundedZipArchive.ts +0 -383
- package/src/shared/container.ts +0 -28
- package/src/shared/fidelity.ts +0 -130
- package/src/shared/images.ts +0 -44
- package/src/shared/inlineRuns.ts +0 -99
- package/src/shared/text.ts +0 -41
- package/src/shared/zipEntryCount.ts +0 -151
- package/src/shared/zipLimits.ts +0 -296
- package/src/shared/zipSafety.ts +0 -19
- package/src/xlsx/export.ts +0 -253
- package/src/xlsx/import.ts +0 -160
- package/src/xlsx/index.ts +0 -35
package/src/html/import.ts
DELETED
|
@@ -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';
|