@bendyline/squisq-formats 2.0.1 → 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-CRAVSMPZ.js → chunk-26ISNJ7Y.js} +356 -114
- package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
- package/dist/{chunk-HTW2M27H.js → chunk-3NKXBZSR.js} +193 -42
- package/dist/{chunk-U32AG3G3.js → chunk-4V3KCHAP.js} +3 -4
- package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
- package/dist/{chunk-QRVN6A6E.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-XKUMNGBW.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-ABVI556T.js → chunk-IPN56VLW.js} +83 -58
- package/dist/{chunk-LXYLOOST.js → chunk-JE6LSIHE.js} +83 -22
- 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-2KPARF2P.js → chunk-PU7REGWV.js} +5 -8
- package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
- package/dist/{chunk-VSYHZECT.js → chunk-TAAENIRB.js} +5 -8
- package/dist/{chunk-WC7WULGV.js → chunk-X2DEAXNK.js} +62 -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 -10
- 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-2KPARF2P.js.map +0 -1
- 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-ABVI556T.js.map +0 -1
- package/dist/chunk-CRAVSMPZ.js.map +0 -1
- package/dist/chunk-FE6OJV6O.js.map +0 -1
- package/dist/chunk-GVS2XXV6.js.map +0 -1
- package/dist/chunk-HTW2M27H.js.map +0 -1
- package/dist/chunk-LXYLOOST.js.map +0 -1
- package/dist/chunk-MLX2BOJC.js.map +0 -1
- package/dist/chunk-NKAJPJ4G.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-QRVN6A6E.js.map +0 -1
- package/dist/chunk-RFAPOKHJ.js.map +0 -1
- package/dist/chunk-SSUPBUF5.js.map +0 -1
- package/dist/chunk-U32AG3G3.js.map +0 -1
- package/dist/chunk-U4MRIFKL.js.map +0 -1
- package/dist/chunk-VJJM2SSH.js +0 -275
- package/dist/chunk-VJJM2SSH.js.map +0 -1
- package/dist/chunk-VSYHZECT.js.map +0 -1
- package/dist/chunk-WC7WULGV.js.map +0 -1
- package/dist/chunk-XKUMNGBW.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-H6RXJMHP.js +0 -15
- package/dist/extract-H6RXJMHP.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-QVPK3ZCU.js +0 -12
- package/dist/layouts-QVPK3ZCU.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 -457
- package/src/__tests__/docxImport.test.ts +0 -410
- 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 -435
- 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 -1267
- package/src/docx/import.ts +0 -995
- 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 -196
- 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/shared/fidelity.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared Markdown fidelity reporting for exporters that intentionally support
|
|
3
|
-
* only a subset of the core Markdown AST. Keeping the capability profiles in
|
|
4
|
-
* one exhaustive table prevents each exporter from silently drifting as the
|
|
5
|
-
* core AST grows.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
MarkdownBlockNode,
|
|
10
|
-
MarkdownDocument,
|
|
11
|
-
MarkdownInlineNode,
|
|
12
|
-
} from '@bendyline/squisq/markdown';
|
|
13
|
-
|
|
14
|
-
type ContentNodeType = MarkdownBlockNode['type'] | MarkdownInlineNode['type'];
|
|
15
|
-
export type MarkdownExportProfile = 'docx' | 'pdf' | 'pptx' | 'epub';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Exhaustive by construction: adding a Markdown node type in core makes this
|
|
19
|
-
* package fail typechecking until its export fidelity is considered here.
|
|
20
|
-
*/
|
|
21
|
-
const CONTENT_NODE_TYPES: Record<ContentNodeType, true> = {
|
|
22
|
-
heading: true,
|
|
23
|
-
paragraph: true,
|
|
24
|
-
blockquote: true,
|
|
25
|
-
list: true,
|
|
26
|
-
code: true,
|
|
27
|
-
thematicBreak: true,
|
|
28
|
-
table: true,
|
|
29
|
-
htmlBlock: true,
|
|
30
|
-
math: true,
|
|
31
|
-
definition: true,
|
|
32
|
-
footnoteDefinition: true,
|
|
33
|
-
containerDirective: true,
|
|
34
|
-
leafDirective: true,
|
|
35
|
-
definitionList: true,
|
|
36
|
-
text: true,
|
|
37
|
-
emphasis: true,
|
|
38
|
-
strong: true,
|
|
39
|
-
delete: true,
|
|
40
|
-
inlineCode: true,
|
|
41
|
-
link: true,
|
|
42
|
-
image: true,
|
|
43
|
-
break: true,
|
|
44
|
-
htmlInline: true,
|
|
45
|
-
inlineMath: true,
|
|
46
|
-
footnoteReference: true,
|
|
47
|
-
linkReference: true,
|
|
48
|
-
imageReference: true,
|
|
49
|
-
textDirective: true,
|
|
50
|
-
mention: true,
|
|
51
|
-
inlineIcon: true,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const COMMON_SUPPORTED: readonly ContentNodeType[] = [
|
|
55
|
-
'heading',
|
|
56
|
-
'paragraph',
|
|
57
|
-
'blockquote',
|
|
58
|
-
'list',
|
|
59
|
-
'code',
|
|
60
|
-
'thematicBreak',
|
|
61
|
-
'table',
|
|
62
|
-
'htmlBlock',
|
|
63
|
-
'math',
|
|
64
|
-
'text',
|
|
65
|
-
'emphasis',
|
|
66
|
-
'strong',
|
|
67
|
-
'delete',
|
|
68
|
-
'inlineCode',
|
|
69
|
-
'link',
|
|
70
|
-
'image',
|
|
71
|
-
'break',
|
|
72
|
-
'htmlInline',
|
|
73
|
-
'inlineMath',
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
const PROFILE_SUPPORT: Record<MarkdownExportProfile, ReadonlySet<ContentNodeType>> = {
|
|
77
|
-
docx: new Set([...COMMON_SUPPORTED, 'footnoteDefinition', 'footnoteReference']),
|
|
78
|
-
pdf: new Set([...COMMON_SUPPORTED, 'footnoteDefinition', 'footnoteReference']),
|
|
79
|
-
pptx: new Set(COMMON_SUPPORTED),
|
|
80
|
-
epub: new Set(COMMON_SUPPORTED),
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const PROFILE_LABELS: Record<MarkdownExportProfile, string> = {
|
|
84
|
-
docx: 'DOCX',
|
|
85
|
-
pdf: 'PDF',
|
|
86
|
-
pptx: 'PPTX',
|
|
87
|
-
epub: 'EPUB',
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
interface AstLikeNode {
|
|
91
|
-
type?: string;
|
|
92
|
-
children?: unknown;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Return a user-facing warning when an exporter will omit Markdown nodes. */
|
|
96
|
-
export function markdownFidelityWarnings(
|
|
97
|
-
document: MarkdownDocument,
|
|
98
|
-
profile: MarkdownExportProfile,
|
|
99
|
-
): string[] {
|
|
100
|
-
const supported = PROFILE_SUPPORT[profile];
|
|
101
|
-
const omitted = new Map<ContentNodeType, number>();
|
|
102
|
-
|
|
103
|
-
const visit = (candidate: unknown): void => {
|
|
104
|
-
if (!candidate || typeof candidate !== 'object') return;
|
|
105
|
-
const node = candidate as AstLikeNode;
|
|
106
|
-
const type = node.type as ContentNodeType | undefined;
|
|
107
|
-
|
|
108
|
-
if (type && Object.prototype.hasOwnProperty.call(CONTENT_NODE_TYPES, type)) {
|
|
109
|
-
if (!supported.has(type)) {
|
|
110
|
-
// The exporter drops this whole subtree, so report the highest-fidelity
|
|
111
|
-
// boundary once instead of inflating the count with its descendants.
|
|
112
|
-
omitted.set(type, (omitted.get(type) ?? 0) + 1);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (Array.isArray(node.children)) {
|
|
118
|
-
for (const child of node.children) visit(child);
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
for (const child of document.children) visit(child);
|
|
123
|
-
if (omitted.size === 0) return [];
|
|
124
|
-
|
|
125
|
-
const total = [...omitted.values()].reduce((sum, count) => sum + count, 0);
|
|
126
|
-
const details = [...omitted].map(([type, count]) => `${type} (${count})`).join(', ');
|
|
127
|
-
return [
|
|
128
|
-
`${PROFILE_LABELS[profile]} export omitted ${total} unsupported Markdown node(s): ${details}.`,
|
|
129
|
-
];
|
|
130
|
-
}
|
package/src/shared/images.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared extension → MIME-type mapping for embedded media.
|
|
3
|
-
*
|
|
4
|
-
* Previously triplicated with subtle drift: the docx and pptx importers each
|
|
5
|
-
* carried a byte-identical dotted-key map (png/jpg/.../emf/wmf), while the html
|
|
6
|
-
* exporter's `inferMimeType` used no-dot keys and a different key set (added
|
|
7
|
-
* audio/video/ico/avif, omitted tiff/emf/wmf). This module is the single union
|
|
8
|
-
* of all of them, so a newly-supported type only has to be added once.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/** Union of every extension→MIME entry the format modules need. */
|
|
12
|
-
const EXT_TO_MIME: Record<string, string> = {
|
|
13
|
-
// Raster / vector images
|
|
14
|
-
png: 'image/png',
|
|
15
|
-
jpg: 'image/jpeg',
|
|
16
|
-
jpeg: 'image/jpeg',
|
|
17
|
-
gif: 'image/gif',
|
|
18
|
-
webp: 'image/webp',
|
|
19
|
-
bmp: 'image/bmp',
|
|
20
|
-
svg: 'image/svg+xml',
|
|
21
|
-
tiff: 'image/tiff',
|
|
22
|
-
tif: 'image/tiff',
|
|
23
|
-
ico: 'image/x-icon',
|
|
24
|
-
avif: 'image/avif',
|
|
25
|
-
// Office vector formats (docx/pptx embedded metafiles)
|
|
26
|
-
emf: 'image/emf',
|
|
27
|
-
wmf: 'image/wmf',
|
|
28
|
-
// Audio / video (html export inlines these as data URIs)
|
|
29
|
-
mp3: 'audio/mpeg',
|
|
30
|
-
wav: 'audio/wav',
|
|
31
|
-
ogg: 'audio/ogg',
|
|
32
|
-
mp4: 'video/mp4',
|
|
33
|
-
webm: 'video/webm',
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Map a file extension to its MIME type. Tolerates a leading dot and any
|
|
38
|
-
* casing (`.PNG`, `png`, `.png` all resolve). Returns
|
|
39
|
-
* `application/octet-stream` for unknown extensions.
|
|
40
|
-
*/
|
|
41
|
-
export function extToMime(ext: string): string {
|
|
42
|
-
const key = ext.replace(/^\./, '').toLowerCase();
|
|
43
|
-
return EXT_TO_MIME[key] ?? 'application/octet-stream';
|
|
44
|
-
}
|
package/src/shared/inlineRuns.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared inline walker for the "run-based" exporters.
|
|
3
|
-
*
|
|
4
|
-
* DOCX (`<w:r>`) and PPTX (`<a:r>`) both render inline markdown by threading an
|
|
5
|
-
* active character-format object DOWN through nested nodes (strong → bold,
|
|
6
|
-
* emphasis → italic, …) and emitting flat styled runs at the leaves — as
|
|
7
|
-
* opposed to the HTML exporters, which WRAP children in tags, or the PDF
|
|
8
|
-
* exporter, which accumulates positioned spans. That control flow is identical
|
|
9
|
-
* between DOCX and PPTX; only the leaf serialization differs.
|
|
10
|
-
*
|
|
11
|
-
* This module owns the traversal once. Each exporter supplies the handful of
|
|
12
|
-
* leaf handlers (`run`, `link`, `image`, `lineBreak`, optional `footnoteRef`)
|
|
13
|
-
* via {@link InlineRunHandlers}. Because the handler interface is typed, adding
|
|
14
|
-
* a new inline node type here forces both exporters to be considered — there's
|
|
15
|
-
* no longer a per-exporter `default:` that can silently drift.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type {
|
|
19
|
-
MarkdownInlineNode,
|
|
20
|
-
MarkdownLink,
|
|
21
|
-
MarkdownImage,
|
|
22
|
-
MarkdownFootnoteReference,
|
|
23
|
-
} from '@bendyline/squisq/markdown';
|
|
24
|
-
import { stripHtmlTags } from './text.js';
|
|
25
|
-
|
|
26
|
-
/** Active character formatting threaded down through nested inline nodes. */
|
|
27
|
-
export interface InlineRunFormat {
|
|
28
|
-
bold?: boolean;
|
|
29
|
-
italic?: boolean;
|
|
30
|
-
strike?: boolean;
|
|
31
|
-
code?: boolean;
|
|
32
|
-
/** Explicit run color (hex without `#`); only some exporters use this. */
|
|
33
|
-
color?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Per-format leaf handlers. The walker handles the shared format-threading
|
|
38
|
-
* control flow; these turn the leaves into the target dialect's markup.
|
|
39
|
-
*/
|
|
40
|
-
export interface InlineRunHandlers {
|
|
41
|
-
/** Emit a styled text run for `text`, given the active format. */
|
|
42
|
-
run(text: string, format: InlineRunFormat): string;
|
|
43
|
-
/** Emit a hyperlink (the handler manages its own child rendering). */
|
|
44
|
-
link(node: MarkdownLink, format: InlineRunFormat): string;
|
|
45
|
-
/** Emit an inline image. */
|
|
46
|
-
image(node: MarkdownImage, format: InlineRunFormat): string;
|
|
47
|
-
/** Emit a hard line break. */
|
|
48
|
-
lineBreak(): string;
|
|
49
|
-
/** Emit a footnote reference. Omit to drop footnote refs (PPTX). */
|
|
50
|
-
footnoteRef?(node: MarkdownFootnoteReference): string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Walk a list of inline nodes, concatenating each rendered run. */
|
|
54
|
-
export function inlineNodesToRuns(
|
|
55
|
-
nodes: MarkdownInlineNode[],
|
|
56
|
-
handlers: InlineRunHandlers,
|
|
57
|
-
format: InlineRunFormat = {},
|
|
58
|
-
): string {
|
|
59
|
-
let out = '';
|
|
60
|
-
for (const node of nodes) {
|
|
61
|
-
out += inlineNodeToRuns(node, handlers, format);
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Render a single inline node under the active format. */
|
|
67
|
-
export function inlineNodeToRuns(
|
|
68
|
-
node: MarkdownInlineNode,
|
|
69
|
-
handlers: InlineRunHandlers,
|
|
70
|
-
format: InlineRunFormat,
|
|
71
|
-
): string {
|
|
72
|
-
switch (node.type) {
|
|
73
|
-
case 'text':
|
|
74
|
-
return handlers.run(node.value, format);
|
|
75
|
-
case 'strong':
|
|
76
|
-
return inlineNodesToRuns(node.children, handlers, { ...format, bold: true });
|
|
77
|
-
case 'emphasis':
|
|
78
|
-
return inlineNodesToRuns(node.children, handlers, { ...format, italic: true });
|
|
79
|
-
case 'delete':
|
|
80
|
-
return inlineNodesToRuns(node.children, handlers, { ...format, strike: true });
|
|
81
|
-
case 'inlineCode':
|
|
82
|
-
return handlers.run(node.value, { ...format, code: true });
|
|
83
|
-
case 'inlineMath':
|
|
84
|
-
return handlers.run(node.value, { ...format, code: true });
|
|
85
|
-
case 'htmlInline':
|
|
86
|
-
return handlers.run(stripHtmlTags(node.rawHtml), format);
|
|
87
|
-
case 'link':
|
|
88
|
-
return handlers.link(node, format);
|
|
89
|
-
case 'image':
|
|
90
|
-
return handlers.image(node, format);
|
|
91
|
-
case 'break':
|
|
92
|
-
return handlers.lineBreak();
|
|
93
|
-
case 'footnoteReference':
|
|
94
|
-
return handlers.footnoteRef ? handlers.footnoteRef(node) : '';
|
|
95
|
-
default:
|
|
96
|
-
// linkReference, imageReference, textDirective, inlineIcon, … — skip.
|
|
97
|
-
return '';
|
|
98
|
-
}
|
|
99
|
-
}
|
package/src/shared/text.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared plain-text helpers used by every format exporter.
|
|
3
|
-
*
|
|
4
|
-
* These were previously copy-pasted into the docx / pptx / epub exporters with
|
|
5
|
-
* subtle drift (e.g. some handled `inlineMath`, some handled `break`). Keeping a
|
|
6
|
-
* single implementation here means a fix or a newly-supported inline node type
|
|
7
|
-
* only has to be added in one place.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { MarkdownInlineNode } from '@bendyline/squisq/markdown';
|
|
11
|
-
|
|
12
|
-
/** Strip HTML tags from a string, keeping only the text content. */
|
|
13
|
-
export function stripHtmlTags(html: string): string {
|
|
14
|
-
return html.replace(/<[^>]*>/g, '');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Flatten a single inline node to its plain-text content. */
|
|
18
|
-
export function inlineToPlainText(node: MarkdownInlineNode): string {
|
|
19
|
-
switch (node.type) {
|
|
20
|
-
case 'text':
|
|
21
|
-
case 'inlineCode':
|
|
22
|
-
case 'inlineMath':
|
|
23
|
-
return node.value;
|
|
24
|
-
case 'emphasis':
|
|
25
|
-
case 'strong':
|
|
26
|
-
case 'delete':
|
|
27
|
-
case 'link':
|
|
28
|
-
return extractPlainText(node.children);
|
|
29
|
-
case 'image':
|
|
30
|
-
return node.alt ?? '';
|
|
31
|
-
case 'break':
|
|
32
|
-
return ' ';
|
|
33
|
-
default:
|
|
34
|
-
return '';
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Flatten an array of inline nodes to their concatenated plain-text content. */
|
|
39
|
-
export function extractPlainText(nodes: MarkdownInlineNode[]): string {
|
|
40
|
-
return nodes.map(inlineToPlainText).join('');
|
|
41
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/** Narrow EOCD preflight for rejecting record-count bombs before JSZip allocation. */
|
|
2
|
-
|
|
3
|
-
import { throwIfZipAborted } from './zipLimits.js';
|
|
4
|
-
|
|
5
|
-
const END_OF_CENTRAL_DIRECTORY = 0x06054b50;
|
|
6
|
-
const ZIP64_END_OF_CENTRAL_DIRECTORY = 0x06064b50;
|
|
7
|
-
const ZIP64_END_LOCATOR = 0x07064b50;
|
|
8
|
-
const UINT16_MAX = 0xffff;
|
|
9
|
-
const MIN_EOCD_BYTES = 22;
|
|
10
|
-
const MAX_ZIP_COMMENT_BYTES = 0xffff;
|
|
11
|
-
|
|
12
|
-
export type ZipInput = ArrayBuffer | Uint8Array | Blob;
|
|
13
|
-
|
|
14
|
-
/** Materialize Blob input once when possible so preflight bytes feed JSZip too. */
|
|
15
|
-
export async function prepareZipInput(
|
|
16
|
-
data: ZipInput,
|
|
17
|
-
signal?: AbortSignal,
|
|
18
|
-
): Promise<{ input: ZipInput; bytes?: Uint8Array }> {
|
|
19
|
-
throwIfZipAborted(signal);
|
|
20
|
-
if (ArrayBuffer.isView(data)) {
|
|
21
|
-
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
22
|
-
return { input: bytes, bytes };
|
|
23
|
-
}
|
|
24
|
-
if (!isBlobLike(data)) {
|
|
25
|
-
const bytes = new Uint8Array(data);
|
|
26
|
-
return { input: bytes, bytes };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const bytes = await readBlobBytes(data, signal);
|
|
30
|
-
throwIfZipAborted(signal);
|
|
31
|
-
return bytes ? { input: bytes, bytes } : { input: data };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Read the declared total central-directory record count when unambiguous.
|
|
36
|
-
* Returns undefined for absent, trailing, malformed, or unsupported metadata;
|
|
37
|
-
* JSZip remains authoritative in those cases.
|
|
38
|
-
*/
|
|
39
|
-
export function declaredZipEntryCount(bytes: Uint8Array): number | undefined {
|
|
40
|
-
const eocdOffset = findEocd(bytes);
|
|
41
|
-
if (eocdOffset === undefined) return undefined;
|
|
42
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
43
|
-
const count = view.getUint16(eocdOffset + 10, true);
|
|
44
|
-
if (count !== UINT16_MAX) return count;
|
|
45
|
-
return readZip64EntryCount(view, eocdOffset);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function findEocd(bytes: Uint8Array): number | undefined {
|
|
49
|
-
if (bytes.byteLength < MIN_EOCD_BYTES) return undefined;
|
|
50
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
51
|
-
const earliest = Math.max(0, bytes.byteLength - MIN_EOCD_BYTES - MAX_ZIP_COMMENT_BYTES);
|
|
52
|
-
for (let offset = bytes.byteLength - MIN_EOCD_BYTES; offset >= earliest; offset--) {
|
|
53
|
-
if (view.getUint32(offset, true) !== END_OF_CENTRAL_DIRECTORY) continue;
|
|
54
|
-
const commentLength = view.getUint16(offset + 20, true);
|
|
55
|
-
if (offset + MIN_EOCD_BYTES + commentLength === bytes.byteLength) return offset;
|
|
56
|
-
}
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function readZip64EntryCount(view: DataView, eocdOffset: number): number | undefined {
|
|
61
|
-
const locatorOffset = eocdOffset - 20;
|
|
62
|
-
if (locatorOffset < 0 || view.getUint32(locatorOffset, true) !== ZIP64_END_LOCATOR) {
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
const recordOffset = readSafeUint64(view, locatorOffset + 8);
|
|
66
|
-
if (
|
|
67
|
-
recordOffset === undefined ||
|
|
68
|
-
recordOffset < 0 ||
|
|
69
|
-
recordOffset + 40 > view.byteLength ||
|
|
70
|
-
view.getUint32(recordOffset, true) !== ZIP64_END_OF_CENTRAL_DIRECTORY
|
|
71
|
-
) {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
return readSafeUint64(view, recordOffset + 32);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function readSafeUint64(view: DataView, offset: number): number | undefined {
|
|
78
|
-
if (offset < 0 || offset + 8 > view.byteLength) return undefined;
|
|
79
|
-
const low = view.getUint32(offset, true);
|
|
80
|
-
const high = view.getUint32(offset + 4, true);
|
|
81
|
-
const value = high * 0x1_0000_0000 + low;
|
|
82
|
-
return Number.isSafeInteger(value) ? value : undefined;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function isBlobLike(data: ZipInput): data is Blob {
|
|
86
|
-
const candidate = data as Blob;
|
|
87
|
-
return typeof candidate.size === 'number' && typeof candidate.slice === 'function';
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function readBlobBytes(blob: Blob, signal?: AbortSignal): Promise<Uint8Array | undefined> {
|
|
91
|
-
const arrayBuffer = (blob as Blob & { arrayBuffer?: () => Promise<ArrayBuffer> }).arrayBuffer;
|
|
92
|
-
if (typeof arrayBuffer === 'function') {
|
|
93
|
-
return new Uint8Array(await raceWithAbort(arrayBuffer.call(blob), signal));
|
|
94
|
-
}
|
|
95
|
-
if (typeof FileReader === 'undefined') return undefined;
|
|
96
|
-
return new Promise((resolve, reject) => {
|
|
97
|
-
const reader = new FileReader();
|
|
98
|
-
let settled = false;
|
|
99
|
-
const cleanup = (): void => signal?.removeEventListener('abort', handleAbort);
|
|
100
|
-
const finish = (work: () => void): void => {
|
|
101
|
-
if (settled) return;
|
|
102
|
-
settled = true;
|
|
103
|
-
cleanup();
|
|
104
|
-
work();
|
|
105
|
-
};
|
|
106
|
-
const handleAbort = (): void => {
|
|
107
|
-
if (settled) return;
|
|
108
|
-
const reason = signal?.reason ?? new Error('ZIP operation was cancelled');
|
|
109
|
-
// Mark the promise settled before FileReader.abort(): browsers are
|
|
110
|
-
// allowed to dispatch `abort` synchronously and re-enter this handler.
|
|
111
|
-
finish(() => reject(reason));
|
|
112
|
-
try {
|
|
113
|
-
reader.abort();
|
|
114
|
-
} catch {
|
|
115
|
-
// Preserve the caller's reason if a platform reader is already done.
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
reader.onload = () => finish(() => resolve(new Uint8Array(reader.result as ArrayBuffer)));
|
|
119
|
-
reader.onerror = () =>
|
|
120
|
-
finish(() => reject(reader.error ?? new Error('Could not read ZIP blob.')));
|
|
121
|
-
reader.onabort = handleAbort;
|
|
122
|
-
signal?.addEventListener('abort', handleAbort, { once: true });
|
|
123
|
-
if (signal?.aborted) {
|
|
124
|
-
handleAbort();
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
reader.readAsArrayBuffer(blob);
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function raceWithAbort<T>(work: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
132
|
-
throwIfZipAborted(signal);
|
|
133
|
-
if (!signal) return work;
|
|
134
|
-
return new Promise<T>((resolve, reject) => {
|
|
135
|
-
let settled = false;
|
|
136
|
-
const finish = (complete: () => void): void => {
|
|
137
|
-
if (settled) return;
|
|
138
|
-
settled = true;
|
|
139
|
-
signal.removeEventListener('abort', handleAbort);
|
|
140
|
-
complete();
|
|
141
|
-
};
|
|
142
|
-
const handleAbort = (): void =>
|
|
143
|
-
finish(() => reject(signal.reason ?? new Error('ZIP operation was cancelled')));
|
|
144
|
-
signal.addEventListener('abort', handleAbort, { once: true });
|
|
145
|
-
if (signal.aborted) handleAbort();
|
|
146
|
-
work.then(
|
|
147
|
-
(value) => finish(() => resolve(value)),
|
|
148
|
-
(error: unknown) => finish(() => reject(error)),
|
|
149
|
-
);
|
|
150
|
-
});
|
|
151
|
-
}
|