@bendyline/squisq-formats 1.4.1 → 2.0.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/README.md +48 -2
- package/dist/{chunk-WWBIKRNP.js → chunk-2CSZJEAL.js} +13 -11
- package/dist/chunk-2CSZJEAL.js.map +1 -0
- package/dist/{chunk-RTS5XBZ7.js → chunk-2EFDWKC6.js} +262 -84
- package/dist/chunk-2EFDWKC6.js.map +1 -0
- package/dist/chunk-2PRVLAU3.js +740 -0
- package/dist/chunk-2PRVLAU3.js.map +1 -0
- package/dist/{chunk-SHLPSADL.js → chunk-4HJ6AK2Q.js} +54 -193
- package/dist/chunk-4HJ6AK2Q.js.map +1 -0
- package/dist/{chunk-5LSSWZPU.js → chunk-4VUWTSGM.js} +72 -25
- package/dist/chunk-4VUWTSGM.js.map +1 -0
- package/dist/{chunk-YESS7VY3.js → chunk-CVNVD7VH.js} +154 -39
- package/dist/chunk-CVNVD7VH.js.map +1 -0
- package/dist/chunk-CX3LH47J.js +99 -0
- package/dist/chunk-CX3LH47J.js.map +1 -0
- package/dist/{chunk-D7IFPWNZ.js → chunk-GVS2XXV6.js} +27 -26
- package/dist/chunk-GVS2XXV6.js.map +1 -0
- package/dist/chunk-H7T5DXUU.js +479 -0
- package/dist/chunk-H7T5DXUU.js.map +1 -0
- package/dist/{chunk-EHLGMSTE.js → chunk-HBSCRS5P.js} +10 -8
- package/dist/chunk-HBSCRS5P.js.map +1 -0
- package/dist/{chunk-46FMDOWZ.js → chunk-LXYLOOST.js} +49 -17
- package/dist/chunk-LXYLOOST.js.map +1 -0
- package/dist/{chunk-MJGRI6XR.js → chunk-MLX2BOJC.js} +12 -7
- package/dist/chunk-MLX2BOJC.js.map +1 -0
- package/dist/chunk-NKAJPJ4G.js +106 -0
- package/dist/chunk-NKAJPJ4G.js.map +1 -0
- package/dist/{chunk-DQAZR57U.js → chunk-O3GVVND4.js} +1 -1
- package/dist/chunk-ODL3SSPT.js +18 -0
- package/dist/chunk-ODL3SSPT.js.map +1 -0
- package/dist/chunk-X4USWOQ2.js +131 -0
- package/dist/chunk-X4USWOQ2.js.map +1 -0
- package/dist/chunk-XTLQXR4U.js +195 -0
- package/dist/chunk-XTLQXR4U.js.map +1 -0
- package/dist/chunk-YRT7GQ5Y.js +28 -0
- package/dist/chunk-YRT7GQ5Y.js.map +1 -0
- package/dist/chunk-ZZ7D4DN6.js +97 -0
- package/dist/chunk-ZZ7D4DN6.js.map +1 -0
- package/dist/container/index.d.ts +6 -2
- package/dist/container/index.js +15 -17
- package/dist/container/index.js.map +1 -1
- package/dist/docx/index.d.ts +6 -2
- package/dist/docx/index.js +5 -3
- package/dist/epub/index.d.ts +3 -1
- package/dist/epub/index.js +3 -3
- package/dist/export-Djev9BLU.d.ts +69 -0
- package/dist/extract-LNOA7DBK.js +15 -0
- package/dist/extract-LNOA7DBK.js.map +1 -0
- package/dist/html/index.d.ts +15 -85
- package/dist/html/index.js +3 -3
- package/dist/images-7FBWPKE3.js +7 -0
- package/dist/images-7FBWPKE3.js.map +1 -0
- package/dist/import-DTkDxHmZ.d.ts +84 -0
- package/dist/import-Yivc3kZM.d.ts +136 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +30 -11
- package/dist/infer/index.d.ts +138 -0
- package/dist/infer/index.js +28 -0
- package/dist/infer/index.js.map +1 -0
- package/dist/layouts-7OUUVAGU.js +12 -0
- package/dist/layouts-7OUUVAGU.js.map +1 -0
- package/dist/layouts-CZ0bdsNL.d.ts +150 -0
- package/dist/mapTheme-IR27S6IV.js +11 -0
- package/dist/mapTheme-IR27S6IV.js.map +1 -0
- package/dist/ooxml/index.d.ts +21 -127
- package/dist/ooxml/index.js +31 -9
- package/dist/pdf/index.d.ts +3 -1
- package/dist/pdf/index.js +1 -1
- package/dist/pptx/index.d.ts +8 -108
- package/dist/pptx/index.js +13 -4
- package/dist/reader-Gof6OrJX.d.ts +140 -0
- package/dist/registry/index.d.ts +46 -8
- package/dist/registry/index.js +5 -2
- package/dist/themeReader-DPw4J5Ss.d.ts +56 -0
- package/dist/xlsx/index.d.ts +6 -66
- package/dist/xlsx/index.js +4 -2
- package/dist/zipLimits-bGYMS9cI.d.ts +29 -0
- package/package.json +8 -2
- package/src/__tests__/container.test.ts +41 -0
- package/src/__tests__/convert.test.ts +167 -0
- package/src/__tests__/epub.test.ts +102 -1
- package/src/__tests__/html.test.ts +62 -0
- package/src/__tests__/inferTheme.test.ts +135 -0
- package/src/__tests__/lossyWarnings.test.ts +41 -0
- package/src/__tests__/ooxml.test.ts +59 -2
- package/src/__tests__/ooxmlThemeReader.test.ts +92 -0
- package/src/__tests__/plainHtml.test.ts +15 -1
- package/src/__tests__/plainHtmlBundle.test.ts +18 -0
- package/src/__tests__/pptxImport.test.ts +30 -0
- package/src/__tests__/pptxInferFixtures.ts +314 -0
- package/src/__tests__/pptxLayoutInfer.test.ts +395 -0
- package/src/__tests__/zipSafety.test.ts +317 -0
- package/src/container/index.ts +20 -29
- package/src/docx/export.ts +4 -2
- package/src/docx/import.ts +4 -3
- package/src/epub/export.ts +94 -25
- package/src/html/docsHtmlBundle.ts +6 -2
- package/src/html/htmlTemplate.ts +31 -6
- package/src/html/imageUtils.ts +14 -5
- package/src/html/index.ts +12 -1
- package/src/html/plainHtml.ts +7 -3
- package/src/html/plainHtmlBundle.ts +11 -9
- package/src/index.ts +20 -0
- package/src/infer/extract.ts +127 -0
- package/src/infer/index.ts +168 -0
- package/src/infer/mapTheme.ts +176 -0
- package/src/infer/types.ts +27 -0
- package/src/ooxml/index.ts +14 -0
- package/src/ooxml/readUtils.ts +44 -0
- package/src/ooxml/reader.ts +87 -28
- package/src/ooxml/themeReader.ts +197 -0
- package/src/ooxml/types.ts +8 -9
- package/src/ooxml/writer.ts +28 -10
- package/src/pdf/export.ts +4 -2
- package/src/pdf/import.ts +40 -34
- package/src/pptx/export.ts +4 -2
- package/src/pptx/import.ts +214 -32
- package/src/pptx/index.ts +14 -0
- package/src/pptx/layouts.ts +1168 -0
- package/src/registry/convert.ts +54 -23
- package/src/registry/defaultFormats.ts +131 -44
- package/src/registry/index.ts +3 -0
- package/src/registry/types.ts +52 -6
- package/src/shared/boundedZipArchive.ts +342 -0
- package/src/shared/fidelity.ts +130 -0
- package/src/shared/zipEntryCount.ts +97 -0
- package/src/shared/zipLimits.ts +286 -0
- package/src/shared/zipSafety.ts +19 -0
- package/src/xlsx/import.ts +3 -2
- package/dist/chunk-46FMDOWZ.js.map +0 -1
- package/dist/chunk-5LSSWZPU.js.map +0 -1
- package/dist/chunk-D7IFPWNZ.js.map +0 -1
- package/dist/chunk-EHLGMSTE.js.map +0 -1
- package/dist/chunk-MJGRI6XR.js.map +0 -1
- package/dist/chunk-RTS5XBZ7.js.map +0 -1
- package/dist/chunk-SHLPSADL.js.map +0 -1
- package/dist/chunk-WWBIKRNP.js.map +0 -1
- package/dist/chunk-YESS7VY3.js.map +0 -1
- /package/dist/{chunk-DQAZR57U.js.map → chunk-O3GVVND4.js.map} +0 -0
|
@@ -25,7 +25,7 @@ import type {
|
|
|
25
25
|
HtmlNode,
|
|
26
26
|
} from '@bendyline/squisq/markdown';
|
|
27
27
|
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
28
|
-
import type { Block, Doc } from '@bendyline/squisq/schemas';
|
|
28
|
+
import type { Block, Doc, ThemeRegistry } from '@bendyline/squisq/schemas';
|
|
29
29
|
import { generateExternalHtml } from './htmlTemplate.js';
|
|
30
30
|
import {
|
|
31
31
|
collectLinkRefs,
|
|
@@ -51,6 +51,8 @@ export interface HtmlBundleOptions {
|
|
|
51
51
|
title?: string;
|
|
52
52
|
/** Theme id applied uniformly to every page. */
|
|
53
53
|
themeId?: string;
|
|
54
|
+
/** Explicit caller-owned registry for non-document custom themes. */
|
|
55
|
+
themeRegistry?: ThemeRegistry;
|
|
54
56
|
/** Rendering mode for every page (default: 'static' — scrollable, link-friendly). */
|
|
55
57
|
mode?: 'slideshow' | 'static';
|
|
56
58
|
/** Maximum recursion depth (default: unlimited; cycles always handled). */
|
|
@@ -83,6 +85,7 @@ export async function markdownDocsToHtmlBundle(options: HtmlBundleOptions): Prom
|
|
|
83
85
|
playerScript,
|
|
84
86
|
title,
|
|
85
87
|
themeId,
|
|
88
|
+
themeRegistry,
|
|
86
89
|
mode = 'static',
|
|
87
90
|
maxDepth = Infinity,
|
|
88
91
|
entryAsIndex = false,
|
|
@@ -161,7 +164,6 @@ export async function markdownDocsToHtmlBundle(options: HtmlBundleOptions): Prom
|
|
|
161
164
|
// link rewrite must walk that tree on every Doc before we serialize
|
|
162
165
|
// it into the rendered HTML.
|
|
163
166
|
let doc = markdownToDoc(mdDoc);
|
|
164
|
-
if (themeId) doc = { ...doc, themeId };
|
|
165
167
|
if (linkMap.size > 0) {
|
|
166
168
|
doc = rewriteDocLinks(doc, linkMap);
|
|
167
169
|
}
|
|
@@ -179,6 +181,8 @@ export async function markdownDocsToHtmlBundle(options: HtmlBundleOptions): Prom
|
|
|
179
181
|
imageRewriteMap.size > 0 ? Object.fromEntries(imageRewriteMap.entries()) : undefined,
|
|
180
182
|
mode,
|
|
181
183
|
title: pageTitle,
|
|
184
|
+
themeId,
|
|
185
|
+
themeRegistry,
|
|
182
186
|
});
|
|
183
187
|
zip.file(htmlPath, html);
|
|
184
188
|
}
|
package/src/html/htmlTemplate.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* 2. **External** — JS is referenced via `<script src>`, images via relative paths.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { Doc, Layer, Block } from '@bendyline/squisq/schemas';
|
|
13
|
+
import type { Doc, Layer, Block, ThemeRegistry } from '@bendyline/squisq/schemas';
|
|
14
|
+
import { readFrontmatterThemeId } from '@bendyline/squisq/markdown';
|
|
14
15
|
import { arrayBufferToBase64DataUrl, inferMimeType } from './imageUtils.js';
|
|
15
16
|
|
|
16
17
|
// ── Types ──────────────────────────────────────────────────────────
|
|
@@ -48,6 +49,8 @@ export interface HtmlExportOptions {
|
|
|
48
49
|
* so the SquisqPlayer renders with that theme's colors and typography.
|
|
49
50
|
*/
|
|
50
51
|
themeId?: string;
|
|
52
|
+
/** Explicit caller-owned registry for non-document custom themes. */
|
|
53
|
+
themeRegistry?: ThemeRegistry;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
// ── Image Path Collection ──────────────────────────────────────────
|
|
@@ -230,12 +233,10 @@ export function generateInlineHtml(doc: Doc, options: HtmlExportOptions): string
|
|
|
230
233
|
title = 'Squisq Document',
|
|
231
234
|
autoPlay = false,
|
|
232
235
|
themeId,
|
|
236
|
+
themeRegistry,
|
|
233
237
|
} = options;
|
|
234
238
|
|
|
235
|
-
|
|
236
|
-
if (themeId) {
|
|
237
|
-
doc = { ...doc, themeId };
|
|
238
|
-
}
|
|
239
|
+
doc = applyThemeSelection(doc, themeId, themeRegistry);
|
|
239
240
|
|
|
240
241
|
// Build base64 image map
|
|
241
242
|
const imageMap: Record<string, string> = {};
|
|
@@ -291,7 +292,7 @@ ${mode === 'static' ? '#squisq-root{align-items:flex-start;overflow-y:auto;backg
|
|
|
291
292
|
*/
|
|
292
293
|
export function generateExternalHtml(
|
|
293
294
|
doc: Doc,
|
|
294
|
-
options: Pick<HtmlExportOptions, 'mode' | 'title' | 'autoPlay'> & {
|
|
295
|
+
options: Pick<HtmlExportOptions, 'mode' | 'title' | 'autoPlay' | 'themeId' | 'themeRegistry'> & {
|
|
295
296
|
/** Relative path to the player JS file (e.g., 'squisq-player.js') */
|
|
296
297
|
playerScriptPath: string;
|
|
297
298
|
/** Map of original image paths to their rewritten relative paths in the ZIP */
|
|
@@ -307,8 +308,12 @@ export function generateExternalHtml(
|
|
|
307
308
|
mode = 'slideshow',
|
|
308
309
|
title = 'Squisq Document',
|
|
309
310
|
autoPlay = false,
|
|
311
|
+
themeId,
|
|
312
|
+
themeRegistry,
|
|
310
313
|
} = options;
|
|
311
314
|
|
|
315
|
+
doc = applyThemeSelection(doc, themeId, themeRegistry);
|
|
316
|
+
|
|
312
317
|
const docJson = escapeForScript(JSON.stringify(doc));
|
|
313
318
|
const imageMapJson = imagePathMap ? escapeForScript(JSON.stringify(imagePathMap)) : '{}';
|
|
314
319
|
const audioMapJson = audioPathMap ? escapeForScript(JSON.stringify(audioPathMap)) : 'null';
|
|
@@ -347,6 +352,26 @@ ${mode === 'static' ? '#squisq-root{align-items:flex-start;overflow-y:auto;backg
|
|
|
347
352
|
</html>`;
|
|
348
353
|
}
|
|
349
354
|
|
|
355
|
+
function applyThemeSelection(
|
|
356
|
+
doc: Doc,
|
|
357
|
+
themeId: string | undefined,
|
|
358
|
+
registry: ThemeRegistry | undefined,
|
|
359
|
+
): Doc {
|
|
360
|
+
const selectedId = themeId ?? doc.themeId ?? readFrontmatterThemeId(doc.frontmatter);
|
|
361
|
+
if (!selectedId) return doc;
|
|
362
|
+
const selectedDocTheme = doc.customThemes?.some((theme) => theme.id === selectedId);
|
|
363
|
+
if (selectedDocTheme || !registry) {
|
|
364
|
+
return selectedId === doc.themeId ? doc : { ...doc, themeId: selectedId };
|
|
365
|
+
}
|
|
366
|
+
const registered = registry.get(selectedId);
|
|
367
|
+
if (!registered) return selectedId === doc.themeId ? doc : { ...doc, themeId: selectedId };
|
|
368
|
+
return {
|
|
369
|
+
...doc,
|
|
370
|
+
themeId: selectedId,
|
|
371
|
+
customThemes: [...(doc.customThemes ?? []), registered],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
350
375
|
/**
|
|
351
376
|
* Escape HTML special characters to prevent injection.
|
|
352
377
|
*/
|
package/src/html/imageUtils.ts
CHANGED
|
@@ -28,12 +28,21 @@ export function inferMimeType(filename: string): string {
|
|
|
28
28
|
*/
|
|
29
29
|
export function arrayBufferToBase64DataUrl(buffer: ArrayBuffer, mimeType: string): string {
|
|
30
30
|
const bytes = new Uint8Array(buffer);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// Keep chunks divisible by three so concatenating their independently
|
|
32
|
+
// encoded results cannot introduce padding in the middle of the stream.
|
|
33
|
+
// This avoids both quadratic string concatenation and one image-sized
|
|
34
|
+
// intermediate binary string for large embedded assets.
|
|
35
|
+
const chunkSize = 3 * 8192;
|
|
36
|
+
const chunks: string[] = [];
|
|
37
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
38
|
+
const end = Math.min(offset + chunkSize, bytes.length);
|
|
39
|
+
let binary = '';
|
|
40
|
+
for (let i = offset; i < end; i++) {
|
|
41
|
+
binary += String.fromCharCode(bytes[i]);
|
|
42
|
+
}
|
|
43
|
+
chunks.push(btoa(binary));
|
|
34
44
|
}
|
|
35
|
-
|
|
36
|
-
return `data:${mimeType};base64,${base64}`;
|
|
45
|
+
return `data:${mimeType};base64,${chunks.join('')}`;
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
/**
|
package/src/html/index.ts
CHANGED
|
@@ -127,7 +127,16 @@ export function docToHtml(doc: Doc, options: HtmlExportOptions): string {
|
|
|
127
127
|
* ```
|
|
128
128
|
*/
|
|
129
129
|
export async function docToHtmlZip(doc: Doc, options: HtmlZipExportOptions): Promise<Blob> {
|
|
130
|
-
const {
|
|
130
|
+
const {
|
|
131
|
+
playerScript,
|
|
132
|
+
images,
|
|
133
|
+
audio,
|
|
134
|
+
mode = 'slideshow',
|
|
135
|
+
title,
|
|
136
|
+
autoPlay,
|
|
137
|
+
themeId,
|
|
138
|
+
themeRegistry,
|
|
139
|
+
} = options;
|
|
131
140
|
|
|
132
141
|
const zip = new JSZip();
|
|
133
142
|
|
|
@@ -174,6 +183,8 @@ export async function docToHtmlZip(doc: Doc, options: HtmlZipExportOptions): Pro
|
|
|
174
183
|
mode,
|
|
175
184
|
title,
|
|
176
185
|
autoPlay,
|
|
186
|
+
themeId,
|
|
187
|
+
themeRegistry,
|
|
177
188
|
});
|
|
178
189
|
zip.file('index.html', html);
|
|
179
190
|
|
package/src/html/plainHtml.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
type HtmlNode,
|
|
24
24
|
readFrontmatterThemeId,
|
|
25
25
|
} from '@bendyline/squisq/markdown';
|
|
26
|
-
import type { Theme } from '@bendyline/squisq/schemas';
|
|
26
|
+
import type { Theme, ThemeRegistry } from '@bendyline/squisq/schemas';
|
|
27
27
|
import { resolveFontFamily, buildGoogleFontsUrl } from '@bendyline/squisq/schemas';
|
|
28
28
|
import { resolveThemeForDoc } from '@bendyline/squisq/doc';
|
|
29
29
|
|
|
@@ -68,6 +68,8 @@ export interface PlainHtmlExportOptions {
|
|
|
68
68
|
* When both `theme` and `themeId` are provided, `theme` wins.
|
|
69
69
|
*/
|
|
70
70
|
themeId?: string;
|
|
71
|
+
/** Explicit caller-owned registry for non-document custom themes. */
|
|
72
|
+
themeRegistry?: ThemeRegistry;
|
|
71
73
|
/**
|
|
72
74
|
* Optional FontAwesome CSS text to inline into the rendered page,
|
|
73
75
|
* replacing the default cross-origin `<link>` to cdnjs. Required for
|
|
@@ -124,7 +126,9 @@ export function markdownDocToPlainHtml(
|
|
|
124
126
|
// `squisq-theme` key. Stays undefined when nothing selects a theme so
|
|
125
127
|
// un-themed exports render unstyled (unchanged behavior).
|
|
126
128
|
const resolveId = themeId ?? readFrontmatterThemeId(doc.frontmatter);
|
|
127
|
-
const theme =
|
|
129
|
+
const theme =
|
|
130
|
+
options.theme ??
|
|
131
|
+
(resolveId ? resolveThemeForDoc(doc, resolveId, options.themeRegistry) : undefined);
|
|
128
132
|
const ctx: RenderCtx = { images, links, htmlPolicy };
|
|
129
133
|
const body = renderTopLevel(doc.children, ctx);
|
|
130
134
|
const fontsLink = theme ? renderFontsLink(theme) : '';
|
|
@@ -187,7 +191,7 @@ function docUsesIcons(doc: MarkdownDocument): boolean {
|
|
|
187
191
|
* blocks that follow them (up to the next sibling-or-higher heading).
|
|
188
192
|
* Each group renders as a single `<section class="squisq-feature ...">`
|
|
189
193
|
* with a media column and a text column — the plain-HTML analogue of
|
|
190
|
-
* the SVG layer layout produced by `
|
|
194
|
+
* the SVG layer layout produced by `materializeBlockLayers` for the same templates.
|
|
191
195
|
*/
|
|
192
196
|
function renderTopLevel(children: MarkdownNode[], ctx: RenderCtx | undefined): string {
|
|
193
197
|
const out: string[] = [];
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
import JSZip from 'jszip';
|
|
19
19
|
import { parseMarkdown, inferDocumentTitle } from '@bendyline/squisq/markdown';
|
|
20
20
|
import type { MarkdownDocument, HtmlNode } from '@bendyline/squisq/markdown';
|
|
21
|
-
import type { Theme } from '@bendyline/squisq/schemas';
|
|
22
|
-
import { resolveTheme } from '@bendyline/squisq/schemas';
|
|
21
|
+
import type { Theme, ThemeRegistry } from '@bendyline/squisq/schemas';
|
|
23
22
|
import { markdownDocToPlainHtml } from './plainHtml.js';
|
|
24
23
|
|
|
25
24
|
// ── Public Types ───────────────────────────────────────────────────
|
|
@@ -36,13 +35,14 @@ export interface PlainHtmlBundleOptions {
|
|
|
36
35
|
/** Optional theme applied uniformly to every page. Overrides {@link themeId}. */
|
|
37
36
|
theme?: Theme;
|
|
38
37
|
/**
|
|
39
|
-
* Optional theme id (e.g. `'warm-earth'`, `'gezellig'`)
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* `themeId` are supplied, `theme` wins.
|
|
38
|
+
* Optional theme id (e.g. `'warm-earth'`, `'gezellig'`) applied to every
|
|
39
|
+
* page. Each document resolves its own inline definition before consulting
|
|
40
|
+
* {@link themeRegistry}. When both `theme` and `themeId` are supplied,
|
|
41
|
+
* `theme` wins.
|
|
44
42
|
*/
|
|
45
43
|
themeId?: string;
|
|
44
|
+
/** Explicit caller-owned registry for non-document custom themes. */
|
|
45
|
+
themeRegistry?: ThemeRegistry;
|
|
46
46
|
/** Maximum recursion depth (default: unlimited; cycles always handled). */
|
|
47
47
|
maxDepth?: number;
|
|
48
48
|
/**
|
|
@@ -74,10 +74,10 @@ export async function markdownDocsToPlainHtmlBundle(
|
|
|
74
74
|
title,
|
|
75
75
|
theme,
|
|
76
76
|
themeId,
|
|
77
|
+
themeRegistry,
|
|
77
78
|
maxDepth = Infinity,
|
|
78
79
|
entryAsIndex = false,
|
|
79
80
|
} = options;
|
|
80
|
-
const resolvedTheme = theme ?? (themeId ? resolveTheme(themeId) : undefined);
|
|
81
81
|
const entry = normalizePath(entryPath);
|
|
82
82
|
if (!entry) {
|
|
83
83
|
throw new Error('markdownDocsToPlainHtmlBundle: entryPath is required');
|
|
@@ -165,7 +165,9 @@ export async function markdownDocsToPlainHtmlBundle(
|
|
|
165
165
|
title: docTitle ?? titleForFilename(path, mdDoc),
|
|
166
166
|
images: imageRewriteMap,
|
|
167
167
|
links: linkMap,
|
|
168
|
-
theme
|
|
168
|
+
theme,
|
|
169
|
+
themeId,
|
|
170
|
+
themeRegistry,
|
|
169
171
|
});
|
|
170
172
|
|
|
171
173
|
const htmlPath = htmlPathFor(path);
|
package/src/index.ts
CHANGED
|
@@ -64,6 +64,15 @@ export type { HtmlImportOptions } from './html/index.js';
|
|
|
64
64
|
export { markdownDocToEpub, docToEpub } from './epub/index.js';
|
|
65
65
|
export type { EpubExportOptions } from './epub/index.js';
|
|
66
66
|
|
|
67
|
+
// Theme inference from file imports (DOCX/PPTX/XLSX theme1.xml → Squisq Theme)
|
|
68
|
+
export { inferThemeFromFile, compileExtractedTheme } from './infer/index.js';
|
|
69
|
+
export type {
|
|
70
|
+
InferThemeOptions,
|
|
71
|
+
InferredFileTheme,
|
|
72
|
+
ExtractedFileTheme,
|
|
73
|
+
InferSourceFormat,
|
|
74
|
+
} from './infer/index.js';
|
|
75
|
+
|
|
67
76
|
// Format registry + programmatic convert()
|
|
68
77
|
export {
|
|
69
78
|
convert,
|
|
@@ -73,6 +82,14 @@ export {
|
|
|
73
82
|
ConversionError,
|
|
74
83
|
BUILTIN_FORMAT_IDS,
|
|
75
84
|
} from './registry/index.js';
|
|
85
|
+
|
|
86
|
+
// Shared bounded-decompression errors/options used by DBK and OOXML imports.
|
|
87
|
+
export { ZipSafetyError } from './shared/zipSafety.js';
|
|
88
|
+
export type {
|
|
89
|
+
ZipSafetyLimits,
|
|
90
|
+
ZipSafetyErrorCode,
|
|
91
|
+
ZipSafetyErrorOptions,
|
|
92
|
+
} from './shared/zipSafety.js';
|
|
76
93
|
export type {
|
|
77
94
|
FormatId,
|
|
78
95
|
ConversionResult,
|
|
@@ -83,4 +100,7 @@ export type {
|
|
|
83
100
|
ConvertSource,
|
|
84
101
|
ConversionErrorCode,
|
|
85
102
|
ConversionErrorOptions,
|
|
103
|
+
BuiltinFormatOptions,
|
|
104
|
+
MarkdownFormatOptions,
|
|
105
|
+
DbkFormatOptions,
|
|
86
106
|
} from './registry/index.js';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-format theme extraction: locate the theme part of an already-opened
|
|
3
|
+
* OOXML package, parse it, and resolve the document's background/text color
|
|
4
|
+
* mapping. Returns null when the file has no theme part at all.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { OoxmlPackage } from '../ooxml/types.js';
|
|
8
|
+
import { getPartRelationships, getPartXml } from '../ooxml/reader.js';
|
|
9
|
+
import { CONTENT_TYPE_PPTX_THEME, NS_PML, NS_WML, REL_SLIDE_MASTER } from '../ooxml/namespaces.js';
|
|
10
|
+
import { findRelByType, resolveTarget } from '../ooxml/readUtils.js';
|
|
11
|
+
import type { OoxmlTheme } from '../ooxml/themeReader.js';
|
|
12
|
+
import { parseThemeXml, readThemePart } from '../ooxml/themeReader.js';
|
|
13
|
+
import type { ExtractedFileTheme, SchemeSlot } from './types.js';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_COLOR_MAP: { bg1: SchemeSlot; tx1: SchemeSlot } = { bg1: 'lt1', tx1: 'dk1' };
|
|
16
|
+
|
|
17
|
+
const SCHEME_SLOTS: readonly SchemeSlot[] = ['dk1', 'lt1', 'dk2', 'lt2'];
|
|
18
|
+
|
|
19
|
+
function asSchemeSlot(value: string | null | undefined): SchemeSlot | undefined {
|
|
20
|
+
return SCHEME_SLOTS.includes(value as SchemeSlot) ? (value as SchemeSlot) : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fallback theme lookup for packages whose theme relationship is missing or
|
|
25
|
+
* unconventional: scan `[Content_Types].xml` overrides for the theme content
|
|
26
|
+
* type (shared by all three formats) and parse the first match.
|
|
27
|
+
*/
|
|
28
|
+
async function readThemeByContentType(pkg: OoxmlPackage): Promise<OoxmlTheme | null> {
|
|
29
|
+
for (const [partPath, contentType] of pkg.contentTypes.overrides) {
|
|
30
|
+
if (contentType !== CONTENT_TYPE_PPTX_THEME) continue;
|
|
31
|
+
const doc = await getPartXml(pkg, partPath);
|
|
32
|
+
if (doc) return parseThemeXml(doc);
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toExtracted(
|
|
38
|
+
sourceFormat: ExtractedFileTheme['sourceFormat'],
|
|
39
|
+
theme: OoxmlTheme,
|
|
40
|
+
colorMap: { bg1: SchemeSlot; tx1: SchemeSlot },
|
|
41
|
+
extraWarnings: string[] = [],
|
|
42
|
+
): ExtractedFileTheme {
|
|
43
|
+
return {
|
|
44
|
+
sourceFormat,
|
|
45
|
+
...(theme.name ? { themeName: theme.name } : {}),
|
|
46
|
+
...(theme.colors ? { colors: theme.colors } : {}),
|
|
47
|
+
colorMap,
|
|
48
|
+
...(theme.fonts ? { fonts: theme.fonts } : {}),
|
|
49
|
+
warnings: [...theme.warnings, ...extraWarnings],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* DOCX: theme hangs off `word/document.xml`; an optional
|
|
55
|
+
* `w:clrSchemeMapping` in `word/settings.xml` remaps bg1/t1 slots
|
|
56
|
+
* (values `light1`/`dark1`/`light2`/`dark2`).
|
|
57
|
+
*/
|
|
58
|
+
export async function extractDocxTheme(pkg: OoxmlPackage): Promise<ExtractedFileTheme | null> {
|
|
59
|
+
const theme =
|
|
60
|
+
(await readThemePart(pkg, 'word/document.xml')) ?? (await readThemeByContentType(pkg));
|
|
61
|
+
if (!theme) return null;
|
|
62
|
+
|
|
63
|
+
let colorMap = DEFAULT_COLOR_MAP;
|
|
64
|
+
const settings = await getPartXml(pkg, 'word/settings.xml');
|
|
65
|
+
const mappingEls = settings?.getElementsByTagNameNS(NS_WML, 'clrSchemeMapping');
|
|
66
|
+
const mapping = mappingEls && mappingEls.length > 0 ? mappingEls[0]! : null;
|
|
67
|
+
if (mapping) {
|
|
68
|
+
const wordSlotToScheme: Record<string, SchemeSlot> = {
|
|
69
|
+
light1: 'lt1',
|
|
70
|
+
dark1: 'dk1',
|
|
71
|
+
light2: 'lt2',
|
|
72
|
+
dark2: 'dk2',
|
|
73
|
+
};
|
|
74
|
+
const bg1 =
|
|
75
|
+
wordSlotToScheme[
|
|
76
|
+
mapping.getAttributeNS(NS_WML, 'bg1') ?? mapping.getAttribute('w:bg1') ?? ''
|
|
77
|
+
];
|
|
78
|
+
const tx1 =
|
|
79
|
+
wordSlotToScheme[mapping.getAttributeNS(NS_WML, 't1') ?? mapping.getAttribute('w:t1') ?? ''];
|
|
80
|
+
if (bg1 && tx1) colorMap = { bg1, tx1 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return toExtracted('docx', theme, colorMap);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* PPTX: theme hangs off the first slide master, whose `<p:clrMap>` records
|
|
88
|
+
* which scheme slots back the deck's background/text (dark decks set
|
|
89
|
+
* `bg1="dk1" tx1="lt1"`).
|
|
90
|
+
*/
|
|
91
|
+
export async function extractPptxTheme(pkg: OoxmlPackage): Promise<ExtractedFileTheme | null> {
|
|
92
|
+
const presRels = await getPartRelationships(pkg, 'ppt/presentation.xml');
|
|
93
|
+
const masterRel = findRelByType(presRels, REL_SLIDE_MASTER);
|
|
94
|
+
const masterPath = masterRel ? resolveTarget('ppt', masterRel.target) : undefined;
|
|
95
|
+
|
|
96
|
+
const warnings: string[] = [];
|
|
97
|
+
let theme: OoxmlTheme | null = null;
|
|
98
|
+
let colorMap = DEFAULT_COLOR_MAP;
|
|
99
|
+
|
|
100
|
+
if (masterPath) {
|
|
101
|
+
theme = await readThemePart(pkg, masterPath);
|
|
102
|
+
const masterDoc = await getPartXml(pkg, masterPath);
|
|
103
|
+
const clrMaps = masterDoc?.getElementsByTagNameNS(NS_PML, 'clrMap');
|
|
104
|
+
const clrMap = clrMaps && clrMaps.length > 0 ? clrMaps[0]! : null;
|
|
105
|
+
if (clrMap) {
|
|
106
|
+
const bg1 = asSchemeSlot(clrMap.getAttribute('bg1'));
|
|
107
|
+
const tx1 = asSchemeSlot(clrMap.getAttribute('tx1'));
|
|
108
|
+
if (bg1 && tx1) {
|
|
109
|
+
colorMap = { bg1, tx1 };
|
|
110
|
+
} else if (clrMap.getAttribute('bg1') || clrMap.getAttribute('tx1')) {
|
|
111
|
+
warnings.push('theme: unsupported clrMap slot mapping; using default bg1/tx1');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!theme) theme = await readThemeByContentType(pkg);
|
|
116
|
+
if (!theme) return null;
|
|
117
|
+
|
|
118
|
+
return toExtracted('pptx', theme, colorMap, warnings);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** XLSX: theme hangs off `xl/workbook.xml`; no color remapping exists. */
|
|
122
|
+
export async function extractXlsxTheme(pkg: OoxmlPackage): Promise<ExtractedFileTheme | null> {
|
|
123
|
+
const theme =
|
|
124
|
+
(await readThemePart(pkg, 'xl/workbook.xml')) ?? (await readThemeByContentType(pkg));
|
|
125
|
+
if (!theme) return null;
|
|
126
|
+
return toExtracted('xlsx', theme, DEFAULT_COLOR_MAP);
|
|
127
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bendyline/squisq-formats Infer Module
|
|
3
|
+
*
|
|
4
|
+
* "Infer theme and layouts from a file import": read the OOXML theme
|
|
5
|
+
* (colors + fonts) out of a DOCX/PPTX/XLSX file and compile it into a
|
|
6
|
+
* Squisq `Theme`; for PPTX, optionally derive custom layout templates
|
|
7
|
+
* from the deck's slide layouts/masters.
|
|
8
|
+
*
|
|
9
|
+
* Consumers:
|
|
10
|
+
* - the editor's theme customizer surfaces (upload / drag-drop a file)
|
|
11
|
+
* - the PPTX importer (theme + layouts ride along as frontmatter)
|
|
12
|
+
*
|
|
13
|
+
* PDF is deliberately unsupported: PDF files carry no theme tables, so
|
|
14
|
+
* inference would produce a mostly-default theme. `inferThemeFromFile`
|
|
15
|
+
* rejects PDF bytes with a clear `ConversionError`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { CustomTemplateDefinition, Theme } from '@bendyline/squisq/schemas';
|
|
19
|
+
import { openPackage } from '../ooxml/reader.js';
|
|
20
|
+
import type { OoxmlOpenOptions } from '../ooxml/reader.js';
|
|
21
|
+
import type { OoxmlPackage } from '../ooxml/types.js';
|
|
22
|
+
import { ConversionError } from '../registry/errors.js';
|
|
23
|
+
import type { ExtractedFileTheme, InferSourceFormat } from './types.js';
|
|
24
|
+
import { extractDocxTheme, extractPptxTheme, extractXlsxTheme } from './extract.js';
|
|
25
|
+
import { colorHintsFromExtraction, compileExtractedTheme } from './mapTheme.js';
|
|
26
|
+
|
|
27
|
+
export type { ExtractedFileTheme, InferSourceFormat, SchemeSlot } from './types.js';
|
|
28
|
+
export { extractDocxTheme, extractPptxTheme, extractXlsxTheme } from './extract.js';
|
|
29
|
+
export type { CompileExtractedOptions, MappedThemePartial } from './mapTheme.js';
|
|
30
|
+
export {
|
|
31
|
+
colorHintsFromExtraction,
|
|
32
|
+
compileExtractedTheme,
|
|
33
|
+
extractedThemeToPartial,
|
|
34
|
+
} from './mapTheme.js';
|
|
35
|
+
|
|
36
|
+
export interface InferThemeOptions extends OoxmlOpenOptions {
|
|
37
|
+
/** Skip sniffing when the caller already knows the format (e.g. from the extension). */
|
|
38
|
+
format?: InferSourceFormat;
|
|
39
|
+
/** PPTX only: also derive custom layout templates from slide layouts/masters. */
|
|
40
|
+
inferLayouts?: boolean;
|
|
41
|
+
/** Preferred display name for the compiled theme (e.g. the file's basename). */
|
|
42
|
+
nameHint?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface InferredFileTheme {
|
|
46
|
+
/** Compiled, validated Squisq theme (`custom-<slug>` id, seeds recorded). */
|
|
47
|
+
theme: Theme;
|
|
48
|
+
/** Raw extraction, for callers mapping into their own editing model. */
|
|
49
|
+
extraction: ExtractedFileTheme;
|
|
50
|
+
/** Present only for PPTX with `inferLayouts` and ≥1 non-redundant layout. */
|
|
51
|
+
layouts?: CustomTemplateDefinition[];
|
|
52
|
+
/** Non-fatal notes: sysClr fallbacks, dropped accents, skipped layouts, … */
|
|
53
|
+
warnings: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function looksLikePdf(data: ArrayBuffer | Blob): Promise<boolean> {
|
|
57
|
+
// Duck-typed (not instanceof): buffers routinely cross realms in test
|
|
58
|
+
// environments and web workers. Blobs carry `size`; ArrayBuffers carry
|
|
59
|
+
// `byteLength`.
|
|
60
|
+
let head: Uint8Array | null = null;
|
|
61
|
+
const blob = data as Blob;
|
|
62
|
+
if (
|
|
63
|
+
typeof blob.size === 'number' &&
|
|
64
|
+
typeof blob.slice === 'function' &&
|
|
65
|
+
typeof blob.arrayBuffer === 'function'
|
|
66
|
+
) {
|
|
67
|
+
head = new Uint8Array(await blob.slice(0, 5).arrayBuffer());
|
|
68
|
+
} else if (typeof (data as ArrayBuffer).byteLength === 'number') {
|
|
69
|
+
const buf = data as ArrayBuffer;
|
|
70
|
+
head = new Uint8Array(buf, 0, Math.min(5, buf.byteLength));
|
|
71
|
+
}
|
|
72
|
+
if (!head || head.length < 4) return false;
|
|
73
|
+
// '%PDF'
|
|
74
|
+
return head[0] === 0x25 && head[1] === 0x50 && head[2] === 0x44 && head[3] === 0x46;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Classify an opened package from its already-parsed content types.
|
|
79
|
+
* (Deliberately not the registry's byte sniffer — that re-parses the zip
|
|
80
|
+
* and carries `dbk` semantics this path doesn't want.)
|
|
81
|
+
*/
|
|
82
|
+
function sniffOoxmlFormat(pkg: OoxmlPackage): InferSourceFormat | undefined {
|
|
83
|
+
const values = [...pkg.contentTypes.overrides.values(), ...pkg.contentTypes.defaults.values()];
|
|
84
|
+
for (const ct of values) {
|
|
85
|
+
if (ct.includes('wordprocessingml')) return 'docx';
|
|
86
|
+
if (ct.includes('presentationml')) return 'pptx';
|
|
87
|
+
if (ct.includes('spreadsheetml')) return 'xlsx';
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const EXTRACTORS: Record<
|
|
93
|
+
InferSourceFormat,
|
|
94
|
+
(pkg: OoxmlPackage) => Promise<ExtractedFileTheme | null>
|
|
95
|
+
> = {
|
|
96
|
+
docx: extractDocxTheme,
|
|
97
|
+
pptx: extractPptxTheme,
|
|
98
|
+
xlsx: extractXlsxTheme,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Infer a Squisq theme (and, for PPTX, optionally custom layout templates)
|
|
103
|
+
* from an office file's bytes.
|
|
104
|
+
*
|
|
105
|
+
* Throws `ConversionError`:
|
|
106
|
+
* - `unsupported-input` for PDF bytes (no theme tables to read)
|
|
107
|
+
* - `invalid-input` for non-OOXML data or files with no theme part
|
|
108
|
+
*/
|
|
109
|
+
export async function inferThemeFromFile(
|
|
110
|
+
data: ArrayBuffer | Blob,
|
|
111
|
+
options: InferThemeOptions = {},
|
|
112
|
+
): Promise<InferredFileTheme> {
|
|
113
|
+
if (await looksLikePdf(data)) {
|
|
114
|
+
throw new ConversionError(
|
|
115
|
+
'unsupported-input',
|
|
116
|
+
'PDF theme inference is not supported — PDF files carry no theme color/font tables.',
|
|
117
|
+
{ format: 'pdf' },
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let pkg: OoxmlPackage;
|
|
122
|
+
try {
|
|
123
|
+
pkg = await openPackage(data, options);
|
|
124
|
+
} catch (err: unknown) {
|
|
125
|
+
throw new ConversionError(
|
|
126
|
+
'invalid-input',
|
|
127
|
+
'Could not read this file as a Word, PowerPoint, or Excel document.',
|
|
128
|
+
{ cause: err },
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const format = options.format ?? sniffOoxmlFormat(pkg);
|
|
133
|
+
if (!format) {
|
|
134
|
+
throw new ConversionError(
|
|
135
|
+
'invalid-input',
|
|
136
|
+
'Not a Word, PowerPoint, or Excel document — no theme to infer.',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const extraction = await EXTRACTORS[format](pkg);
|
|
141
|
+
if (!extraction) {
|
|
142
|
+
throw new ConversionError('invalid-input', 'No theme part found in this file.', { format });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { theme, warnings: mapWarnings } = compileExtractedTheme(extraction, {
|
|
146
|
+
nameHint: options.nameHint,
|
|
147
|
+
});
|
|
148
|
+
const warnings = [...extraction.warnings, ...mapWarnings];
|
|
149
|
+
|
|
150
|
+
let layouts: CustomTemplateDefinition[] | undefined;
|
|
151
|
+
if (options.inferLayouts) {
|
|
152
|
+
if (format !== 'pptx') {
|
|
153
|
+
warnings.push('Layout inference is only available for PowerPoint files; skipped.');
|
|
154
|
+
} else {
|
|
155
|
+
const { analyzePptxLayouts } = await import('../pptx/layouts.js');
|
|
156
|
+
const analysis = await analyzePptxLayouts(pkg, {
|
|
157
|
+
colors: colorHintsFromExtraction(extraction),
|
|
158
|
+
});
|
|
159
|
+
warnings.push(...analysis.warnings);
|
|
160
|
+
const defs = analysis.layouts
|
|
161
|
+
.map((l) => (l.verdict.kind === 'custom' ? l.verdict.def : null))
|
|
162
|
+
.filter((d): d is CustomTemplateDefinition => d !== null);
|
|
163
|
+
if (defs.length > 0) layouts = defs;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { theme, extraction, ...(layouts ? { layouts } : {}), warnings };
|
|
168
|
+
}
|