@bendyline/squisq-formats 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +1 -1
  4. package/dist/{chunk-NNHKUXKA.js → chunk-26ISNJ7Y.js} +85 -65
  5. package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
  6. package/dist/{chunk-WQSHGBLN.js → chunk-3NKXBZSR.js} +193 -42
  7. package/dist/{chunk-KURGXM4I.js → chunk-4V3KCHAP.js} +3 -4
  8. package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
  9. package/dist/{chunk-EW54IRRS.js → chunk-6S6GU3ZG.js} +5 -6
  10. package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
  11. package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
  12. package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
  13. package/dist/{chunk-SC67HYQJ.js → chunk-EJTNGKEA.js} +5 -8
  14. package/dist/chunk-GX7RAUME.js +121 -0
  15. package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
  16. package/dist/{chunk-RLU7UFYU.js → chunk-IPN56VLW.js} +83 -58
  17. package/dist/{chunk-DTDF6QDP.js → chunk-JE6LSIHE.js} +81 -20
  18. package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
  19. package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
  20. package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
  21. package/dist/chunk-OGS5VCGJ.js +446 -0
  22. package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
  23. package/dist/{chunk-74GO3FVS.js → chunk-PU7REGWV.js} +5 -8
  24. package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
  25. package/dist/{chunk-QFLDYKCR.js → chunk-TAAENIRB.js} +5 -8
  26. package/dist/{chunk-7ARKUCQT.js → chunk-X2DEAXNK.js} +54 -2
  27. package/dist/container/index.js +1 -2
  28. package/dist/csv/index.d.ts +27 -2
  29. package/dist/csv/index.js +1 -2
  30. package/dist/docx/index.d.ts +5 -1
  31. package/dist/docx/index.js +9 -11
  32. package/dist/epub/index.d.ts +2 -0
  33. package/dist/epub/index.js +5 -6
  34. package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
  35. package/dist/extract-MN7LA3NL.js +13 -0
  36. package/dist/html/index.d.ts +11 -4
  37. package/dist/html/index.js +3 -4
  38. package/dist/images-ESPQKVTW.js +6 -0
  39. package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
  40. package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
  41. package/dist/index.d.ts +7 -7
  42. package/dist/index.js +28 -26
  43. package/dist/infer/index.d.ts +3 -3
  44. package/dist/infer/index.js +7 -9
  45. package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
  46. package/dist/layouts-DRWZGSPD.js +10 -0
  47. package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
  48. package/dist/ooxml/index.d.ts +3 -3
  49. package/dist/ooxml/index.js +14 -13
  50. package/dist/pdf/index.d.ts +18 -0
  51. package/dist/pdf/index.js +2 -3
  52. package/dist/pptx/index.d.ts +4 -4
  53. package/dist/pptx/index.js +11 -13
  54. package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
  55. package/dist/registry/index.d.ts +21 -5
  56. package/dist/registry/index.js +9 -6
  57. package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
  58. package/dist/xlsx/index.d.ts +3 -3
  59. package/dist/xlsx/index.js +6 -7
  60. package/package.json +6 -3
  61. package/dist/chunk-4VUWTSGM.js.map +0 -1
  62. package/dist/chunk-6M7Z25LA.js +0 -46
  63. package/dist/chunk-6M7Z25LA.js.map +0 -1
  64. package/dist/chunk-74GO3FVS.js.map +0 -1
  65. package/dist/chunk-7ARKUCQT.js.map +0 -1
  66. package/dist/chunk-DTDF6QDP.js.map +0 -1
  67. package/dist/chunk-EW54IRRS.js.map +0 -1
  68. package/dist/chunk-FE6OJV6O.js.map +0 -1
  69. package/dist/chunk-GVS2XXV6.js.map +0 -1
  70. package/dist/chunk-KURGXM4I.js.map +0 -1
  71. package/dist/chunk-MLX2BOJC.js.map +0 -1
  72. package/dist/chunk-NKAJPJ4G.js.map +0 -1
  73. package/dist/chunk-NNHKUXKA.js.map +0 -1
  74. package/dist/chunk-O3GVVND4.js.map +0 -1
  75. package/dist/chunk-ODL3SSPT.js.map +0 -1
  76. package/dist/chunk-PN52A5AA.js.map +0 -1
  77. package/dist/chunk-QFLDYKCR.js.map +0 -1
  78. package/dist/chunk-RFAPOKHJ.js.map +0 -1
  79. package/dist/chunk-RLU7UFYU.js.map +0 -1
  80. package/dist/chunk-SC67HYQJ.js.map +0 -1
  81. package/dist/chunk-SSUPBUF5.js.map +0 -1
  82. package/dist/chunk-U4MRIFKL.js.map +0 -1
  83. package/dist/chunk-UGYF5AZE.js +0 -275
  84. package/dist/chunk-UGYF5AZE.js.map +0 -1
  85. package/dist/chunk-WQSHGBLN.js.map +0 -1
  86. package/dist/chunk-YRT7GQ5Y.js +0 -28
  87. package/dist/chunk-YRT7GQ5Y.js.map +0 -1
  88. package/dist/container/index.js.map +0 -1
  89. package/dist/csv/index.js.map +0 -1
  90. package/dist/docx/index.js.map +0 -1
  91. package/dist/epub/index.js.map +0 -1
  92. package/dist/extract-OJ7ZQV6P.js +0 -15
  93. package/dist/extract-OJ7ZQV6P.js.map +0 -1
  94. package/dist/html/index.js.map +0 -1
  95. package/dist/images-7FBWPKE3.js +0 -7
  96. package/dist/images-7FBWPKE3.js.map +0 -1
  97. package/dist/index.js.map +0 -1
  98. package/dist/infer/index.js.map +0 -1
  99. package/dist/layouts-5VDIRPIJ.js +0 -12
  100. package/dist/layouts-5VDIRPIJ.js.map +0 -1
  101. package/dist/mapTheme-IR27S6IV.js.map +0 -1
  102. package/dist/ooxml/index.js.map +0 -1
  103. package/dist/pdf/index.js.map +0 -1
  104. package/dist/pptx/index.js.map +0 -1
  105. package/dist/registry/index.js.map +0 -1
  106. package/dist/xlsx/index.js.map +0 -1
  107. package/src/__tests__/container.test.ts +0 -230
  108. package/src/__tests__/convert.test.ts +0 -495
  109. package/src/__tests__/csvImport.test.ts +0 -84
  110. package/src/__tests__/docxExport.test.ts +0 -491
  111. package/src/__tests__/docxImport.test.ts +0 -531
  112. package/src/__tests__/epub.test.ts +0 -649
  113. package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
  114. package/src/__tests__/formatRegistry.test.ts +0 -174
  115. package/src/__tests__/html.test.ts +0 -439
  116. package/src/__tests__/htmlImport.test.ts +0 -57
  117. package/src/__tests__/inferTheme.test.ts +0 -135
  118. package/src/__tests__/lossyWarnings.test.ts +0 -146
  119. package/src/__tests__/ooxml.test.ts +0 -271
  120. package/src/__tests__/ooxmlCancellation.test.ts +0 -113
  121. package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
  122. package/src/__tests__/pdfExport.test.ts +0 -322
  123. package/src/__tests__/pdfImport.test.ts +0 -384
  124. package/src/__tests__/plainHtml.test.ts +0 -417
  125. package/src/__tests__/plainHtmlBundle.test.ts +0 -253
  126. package/src/__tests__/pptxExport.test.ts +0 -138
  127. package/src/__tests__/pptxImport.test.ts +0 -145
  128. package/src/__tests__/pptxInferFixtures.ts +0 -314
  129. package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
  130. package/src/__tests__/roundTrip.test.ts +0 -201
  131. package/src/__tests__/roundTripAssets.test.ts +0 -50
  132. package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
  133. package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
  134. package/src/__tests__/roundTripMatrix.test.ts +0 -142
  135. package/src/__tests__/sharedContainer.test.ts +0 -41
  136. package/src/__tests__/sharedImages.test.ts +0 -61
  137. package/src/__tests__/xlsxExport.test.ts +0 -164
  138. package/src/__tests__/xlsxImport.test.ts +0 -80
  139. package/src/__tests__/zipSafety.test.ts +0 -317
  140. package/src/container/index.ts +0 -94
  141. package/src/csv/index.ts +0 -188
  142. package/src/docx/export.ts +0 -1375
  143. package/src/docx/import.ts +0 -1250
  144. package/src/docx/index.ts +0 -26
  145. package/src/docx/styles.ts +0 -145
  146. package/src/epub/export.ts +0 -968
  147. package/src/epub/index.ts +0 -20
  148. package/src/html/docsHtmlBundle.ts +0 -373
  149. package/src/html/htmlTemplate.ts +0 -385
  150. package/src/html/imageUtils.ts +0 -61
  151. package/src/html/import.ts +0 -297
  152. package/src/html/index.ts +0 -212
  153. package/src/html/plainHtml.ts +0 -790
  154. package/src/html/plainHtmlBundle.ts +0 -421
  155. package/src/index.ts +0 -109
  156. package/src/infer/extract.ts +0 -127
  157. package/src/infer/index.ts +0 -199
  158. package/src/infer/mapTheme.ts +0 -176
  159. package/src/infer/types.ts +0 -27
  160. package/src/ooxml/index.ts +0 -111
  161. package/src/ooxml/namespaces.ts +0 -217
  162. package/src/ooxml/readUtils.ts +0 -44
  163. package/src/ooxml/reader.ts +0 -318
  164. package/src/ooxml/themeReader.ts +0 -197
  165. package/src/ooxml/types.ts +0 -103
  166. package/src/ooxml/writer.ts +0 -339
  167. package/src/ooxml/xmlUtils.ts +0 -123
  168. package/src/pdf/export.ts +0 -1084
  169. package/src/pdf/import.ts +0 -1164
  170. package/src/pdf/index.ts +0 -29
  171. package/src/pdf/styles.ts +0 -180
  172. package/src/pptx/export.ts +0 -1184
  173. package/src/pptx/import.ts +0 -455
  174. package/src/pptx/index.ts +0 -52
  175. package/src/pptx/layouts.ts +0 -1222
  176. package/src/pptx/styles.ts +0 -96
  177. package/src/pptx/templates.ts +0 -187
  178. package/src/registry/convert.ts +0 -433
  179. package/src/registry/defaultFormats.ts +0 -413
  180. package/src/registry/errors.ts +0 -46
  181. package/src/registry/index.ts +0 -43
  182. package/src/registry/registry.ts +0 -48
  183. package/src/registry/types.ts +0 -170
  184. package/src/shared/boundedZipArchive.ts +0 -383
  185. package/src/shared/container.ts +0 -28
  186. package/src/shared/fidelity.ts +0 -130
  187. package/src/shared/images.ts +0 -44
  188. package/src/shared/inlineRuns.ts +0 -99
  189. package/src/shared/text.ts +0 -41
  190. package/src/shared/zipEntryCount.ts +0 -151
  191. package/src/shared/zipLimits.ts +0 -296
  192. package/src/shared/zipSafety.ts +0 -19
  193. package/src/xlsx/export.ts +0 -253
  194. package/src/xlsx/import.ts +0 -160
  195. package/src/xlsx/index.ts +0 -35
@@ -1,199 +0,0 @@
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, throwIfOoxmlAborted } 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, signal?: AbortSignal): Promise<boolean> {
57
- throwIfOoxmlAborted(signal);
58
- // Duck-typed (not instanceof): buffers routinely cross realms in test
59
- // environments and web workers. Blobs carry `size`; ArrayBuffers carry
60
- // `byteLength`.
61
- let head: Uint8Array | null = null;
62
- const blob = data as Blob;
63
- if (
64
- typeof blob.size === 'number' &&
65
- typeof blob.slice === 'function' &&
66
- typeof blob.arrayBuffer === 'function'
67
- ) {
68
- head = new Uint8Array(await waitForAbortable(blob.slice(0, 5).arrayBuffer(), signal));
69
- } else if (typeof (data as ArrayBuffer).byteLength === 'number') {
70
- const buf = data as ArrayBuffer;
71
- head = new Uint8Array(buf, 0, Math.min(5, buf.byteLength));
72
- }
73
- throwIfOoxmlAborted(signal);
74
- if (!head || head.length < 4) return false;
75
- // '%PDF'
76
- return head[0] === 0x25 && head[1] === 0x50 && head[2] === 0x44 && head[3] === 0x46;
77
- }
78
-
79
- function waitForAbortable<T>(work: Promise<T>, signal?: AbortSignal): Promise<T> {
80
- throwIfOoxmlAborted(signal);
81
- if (!signal) return work;
82
- return new Promise<T>((resolve, reject) => {
83
- let settled = false;
84
- const finish = (complete: () => void): void => {
85
- if (settled) return;
86
- settled = true;
87
- signal.removeEventListener('abort', handleAbort);
88
- complete();
89
- };
90
- const handleAbort = (): void =>
91
- finish(() => reject(signal.reason ?? new Error('OOXML operation was cancelled')));
92
- signal.addEventListener('abort', handleAbort, { once: true });
93
- if (signal.aborted) handleAbort();
94
- work.then(
95
- (value) => finish(() => resolve(value)),
96
- (error: unknown) => finish(() => reject(error)),
97
- );
98
- });
99
- }
100
-
101
- /**
102
- * Classify an opened package from its already-parsed content types.
103
- * (Deliberately not the registry's byte sniffer — that re-parses the zip
104
- * and carries `dbk` semantics this path doesn't want.)
105
- */
106
- function sniffOoxmlFormat(pkg: OoxmlPackage): InferSourceFormat | undefined {
107
- const values = [...pkg.contentTypes.overrides.values(), ...pkg.contentTypes.defaults.values()];
108
- for (const ct of values) {
109
- if (ct.includes('wordprocessingml')) return 'docx';
110
- if (ct.includes('presentationml')) return 'pptx';
111
- if (ct.includes('spreadsheetml')) return 'xlsx';
112
- }
113
- return undefined;
114
- }
115
-
116
- const EXTRACTORS: Record<
117
- InferSourceFormat,
118
- (pkg: OoxmlPackage) => Promise<ExtractedFileTheme | null>
119
- > = {
120
- docx: extractDocxTheme,
121
- pptx: extractPptxTheme,
122
- xlsx: extractXlsxTheme,
123
- };
124
-
125
- /**
126
- * Infer a Squisq theme (and, for PPTX, optionally custom layout templates)
127
- * from an office file's bytes.
128
- *
129
- * Throws `ConversionError`:
130
- * - `unsupported-input` for PDF bytes (no theme tables to read)
131
- * - `invalid-input` for non-OOXML data or files with no theme part
132
- */
133
- export async function inferThemeFromFile(
134
- data: ArrayBuffer | Blob,
135
- options: InferThemeOptions = {},
136
- ): Promise<InferredFileTheme> {
137
- throwIfOoxmlAborted(options.signal);
138
- if (await looksLikePdf(data, options.signal)) {
139
- throw new ConversionError(
140
- 'unsupported-input',
141
- 'PDF theme inference is not supported — PDF files carry no theme color/font tables.',
142
- { format: 'pdf' },
143
- );
144
- }
145
-
146
- let pkg: OoxmlPackage;
147
- try {
148
- pkg = await openPackage(data, options);
149
- } catch (err: unknown) {
150
- if (options.signal?.aborted) throw options.signal.reason ?? err;
151
- throw new ConversionError(
152
- 'invalid-input',
153
- 'Could not read this file as a Word, PowerPoint, or Excel document.',
154
- { cause: err },
155
- );
156
- }
157
-
158
- const format = options.format ?? sniffOoxmlFormat(pkg);
159
- throwIfOoxmlAborted(options.signal);
160
- if (!format) {
161
- throw new ConversionError(
162
- 'invalid-input',
163
- 'Not a Word, PowerPoint, or Excel document — no theme to infer.',
164
- );
165
- }
166
-
167
- const extraction = await EXTRACTORS[format](pkg);
168
- throwIfOoxmlAborted(options.signal);
169
- if (!extraction) {
170
- throw new ConversionError('invalid-input', 'No theme part found in this file.', { format });
171
- }
172
-
173
- const { theme, warnings: mapWarnings } = compileExtractedTheme(extraction, {
174
- nameHint: options.nameHint,
175
- });
176
- throwIfOoxmlAborted(options.signal);
177
- const warnings = [...extraction.warnings, ...mapWarnings];
178
-
179
- let layouts: CustomTemplateDefinition[] | undefined;
180
- if (options.inferLayouts) {
181
- if (format !== 'pptx') {
182
- warnings.push('Layout inference is only available for PowerPoint files; skipped.');
183
- } else {
184
- const { analyzePptxLayouts } = await import('../pptx/layouts.js');
185
- const analysis = await analyzePptxLayouts(pkg, {
186
- colors: colorHintsFromExtraction(extraction),
187
- signal: options.signal,
188
- });
189
- warnings.push(...analysis.warnings);
190
- const defs = analysis.layouts
191
- .map((l) => (l.verdict.kind === 'custom' ? l.verdict.def : null))
192
- .filter((d): d is CustomTemplateDefinition => d !== null);
193
- if (defs.length > 0) layouts = defs;
194
- }
195
- }
196
-
197
- throwIfOoxmlAborted(options.signal);
198
- return { theme, extraction, ...(layouts ? { layouts } : {}), warnings };
199
- }
@@ -1,176 +0,0 @@
1
- /**
2
- * Map a raw OOXML theme extraction onto a compiled Squisq `Theme`.
3
- *
4
- * Only a handful of seeds are taken from the file (lead accents, background,
5
- * text, major/minor fonts); `compileTheme` derives the rest of the palette
6
- * via OKLCh so the result is always a complete, validated theme that stays
7
- * re-editable in the customizer (seeds are recorded on `Theme.seedColors`).
8
- */
9
-
10
- import type {
11
- DeepPartial,
12
- Theme,
13
- ThemeColorScheme,
14
- ThemeSeedColors,
15
- } from '@bendyline/squisq/schemas';
16
- import {
17
- accentToColorScheme,
18
- compileTheme,
19
- contrastRatio,
20
- isHex,
21
- matchFontFamily,
22
- relativeLuminance,
23
- } from '@bendyline/squisq/schemas';
24
- import type { PptxColorHints } from '../pptx/layouts.js';
25
- import type { ExtractedFileTheme } from './types.js';
26
-
27
- export interface CompileExtractedOptions {
28
- /** Preferred display name for the compiled theme (e.g. the file's basename). */
29
- nameHint?: string;
30
- }
31
-
32
- export interface MappedThemePartial {
33
- partial: DeepPartial<Theme>;
34
- warnings: string[];
35
- }
36
-
37
- /** Neutral primary used when a theme resolves no usable accent slots. */
38
- const FALLBACK_PRIMARY = '#3182ce';
39
-
40
- /** Accents closer to the background than this contrast are dropped (near-invisible). */
41
- const MIN_ACCENT_CONTRAST = 1.3;
42
-
43
- const ACCENT_SLOTS = ['accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6'] as const;
44
-
45
- /** Companion slot providing `backgroundLight` for each background slot. */
46
- const COMPANION_SLOT = { lt1: 'lt2', dk1: 'dk2', lt2: 'lt1', dk2: 'dk1' } as const;
47
-
48
- function slugify(name: string): string {
49
- const slug = name
50
- .toLowerCase()
51
- .replace(/[^a-z0-9]+/g, '-')
52
- .replace(/^-+|-+$/g, '');
53
- return slug || 'imported';
54
- }
55
-
56
- /**
57
- * Build the `DeepPartial<Theme>` for an extraction. Exported for tests; most
58
- * callers want {@link compileExtractedTheme}.
59
- */
60
- export function extractedThemeToPartial(
61
- extraction: ExtractedFileTheme,
62
- opts: CompileExtractedOptions = {},
63
- ): MappedThemePartial {
64
- const warnings: string[] = [];
65
- const { colors, colorMap, fonts } = extraction;
66
-
67
- const name = opts.nameHint?.trim() || extraction.themeName || 'Imported Theme';
68
- const partial: DeepPartial<Theme> = {
69
- id: `custom-${slugify(name)}`,
70
- name,
71
- description: `Imported from a ${extraction.sourceFormat} file${
72
- extraction.themeName ? ` (theme "${extraction.themeName}")` : ''
73
- }.`,
74
- };
75
-
76
- if (colors) {
77
- const primary = colors.accent1 ?? colors.accent2;
78
- if (!primary) {
79
- warnings.push('theme: no usable accent colors; primary defaults to Squisq blue');
80
- }
81
-
82
- let background: string | undefined = colors[colorMap.bg1];
83
- let text: string | undefined = colors[colorMap.tx1];
84
- // Pathological color maps can land background and text on similar
85
- // surfaces; when they don't contrast, let the compiler derive both
86
- // from the primary instead of shipping unreadable seeds.
87
- if (
88
- background &&
89
- text &&
90
- Math.abs(relativeLuminance(background) - relativeLuminance(text)) < 0.2
91
- ) {
92
- warnings.push('theme: background and text colors are too close; deriving surfaces instead');
93
- background = undefined;
94
- text = undefined;
95
- }
96
-
97
- const seeds: ThemeSeedColors = {
98
- primary: primary ?? FALLBACK_PRIMARY,
99
- ...(colors.accent2 ? { secondary: colors.accent2 } : {}),
100
- ...(colors.accent3 ? { accent: colors.accent3 } : {}),
101
- ...(background ? { background } : {}),
102
- ...(text ? { text } : {}),
103
- };
104
- partial.seedColors = seeds;
105
-
106
- const explicitColors: DeepPartial<Theme>['colors'] = {};
107
- const companion = background ? colors[COMPANION_SLOT[colorMap.bg1]] : undefined;
108
- if (background && companion && companion !== background) {
109
- explicitColors.backgroundLight = companion;
110
- }
111
- if (colors.hlink && isHex(colors.hlink)) {
112
- explicitColors.highlight = colors.hlink;
113
- }
114
- if (Object.keys(explicitColors).length > 0) partial.colors = explicitColors;
115
-
116
- const schemes: Record<string, ThemeColorScheme> = {};
117
- const seenHexes = new Set<string>();
118
- const schemeBackground = background ?? colors[colorMap.bg1];
119
- for (const slot of ACCENT_SLOTS) {
120
- const hex = colors[slot];
121
- if (!hex || seenHexes.has(hex)) continue;
122
- if (schemeBackground && contrastRatio(hex, schemeBackground) < MIN_ACCENT_CONTRAST) {
123
- warnings.push(`theme: ${slot} is too close to the background; dropped from accents`);
124
- continue;
125
- }
126
- seenHexes.add(hex);
127
- schemes[slot] = accentToColorScheme(hex);
128
- }
129
- if (Object.keys(schemes).length > 0) partial.colorSchemes = schemes;
130
- }
131
-
132
- if (fonts?.majorLatin || fonts?.minorLatin) {
133
- partial.typography = {
134
- ...(fonts.majorLatin ? { titleFont: matchFontFamily(fonts.majorLatin) } : {}),
135
- ...(fonts.minorLatin ? { bodyFont: matchFontFamily(fonts.minorLatin) } : {}),
136
- };
137
- }
138
-
139
- return { partial, warnings };
140
- }
141
-
142
- /**
143
- * Compile an extraction into a full validated Squisq theme. Mapping warnings
144
- * (dropped accents, low-contrast surfaces) are returned alongside; extraction
145
- * warnings stay on `extraction.warnings`.
146
- */
147
- export function compileExtractedTheme(
148
- extraction: ExtractedFileTheme,
149
- opts: CompileExtractedOptions = {},
150
- ): { theme: Theme; warnings: string[] } {
151
- const { partial, warnings } = extractedThemeToPartial(extraction, opts);
152
- return { theme: compileTheme(partial), warnings };
153
- }
154
-
155
- /**
156
- * Build the color hints the PPTX layout generator uses to resolve scheme
157
- * color references (`a:schemeClr val="accent1"`, `bg1`, `tx1`, …) on
158
- * decorative shapes.
159
- */
160
- export function colorHintsFromExtraction(extraction: ExtractedFileTheme): PptxColorHints {
161
- const { colors, colorMap } = extraction;
162
- if (!colors) return {};
163
- const schemeColors: Record<string, string> = {};
164
- for (const [slot, hex] of Object.entries(colors)) {
165
- if (typeof hex === 'string') schemeColors[slot] = hex;
166
- }
167
- const background = colors[colorMap.bg1];
168
- const text = colors[colorMap.tx1];
169
- if (background) schemeColors['bg1'] = background;
170
- if (text) schemeColors['tx1'] = text;
171
- return {
172
- ...(background ? { background } : {}),
173
- ...(text ? { text } : {}),
174
- schemeColors,
175
- };
176
- }
@@ -1,27 +0,0 @@
1
- /**
2
- * Shared types for file-import theme inference.
3
- */
4
-
5
- import type { OoxmlColorScheme, OoxmlFontScheme } from '../ooxml/themeReader.js';
6
-
7
- /** Source formats theme inference supports. PDF carries no theme tables. */
8
- export type InferSourceFormat = 'docx' | 'pptx' | 'xlsx';
9
-
10
- /** The four surface/text scheme slots a color map can point at. */
11
- export type SchemeSlot = 'dk1' | 'lt1' | 'dk2' | 'lt2';
12
-
13
- /**
14
- * Raw theme extraction from one office file, before mapping onto a Squisq
15
- * `Theme`. `colorMap` records which scheme slots back the document's
16
- * background/text — PPTX masters carry `<p:clrMap bg1=… tx1=…>` and DOCX may
17
- * carry `w:clrSchemeMapping`; dark decks map `bg1="dk1"` and invert here.
18
- */
19
- export interface ExtractedFileTheme {
20
- sourceFormat: InferSourceFormat;
21
- /** `a:theme@name` when present (e.g. "Office Theme", "Ion"). */
22
- themeName?: string;
23
- colors?: OoxmlColorScheme;
24
- colorMap: { bg1: SchemeSlot; tx1: SchemeSlot };
25
- fonts?: OoxmlFontScheme;
26
- warnings: string[];
27
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * @bendyline/squisq-formats OOXML Module
3
- *
4
- * Shared infrastructure for reading and writing Office Open XML packages
5
- * (DOCX, PPTX, XLSX). Provides ZIP archive handling, relationship management,
6
- * content type assembly, XML utilities, and core property parsing.
7
- *
8
- * This module is consumed by the format-specific modules (docx/, pptx/, xlsx/)
9
- * and can also be imported directly by advanced consumers.
10
- *
11
- * @example
12
- * ```ts
13
- * import { createPackage, openPackage } from '@bendyline/squisq-formats/ooxml';
14
- * ```
15
- */
16
-
17
- // Types
18
- export type {
19
- OoxmlPackage,
20
- ContentTypeMap,
21
- Relationship,
22
- CoreProperties,
23
- PackagePart,
24
- PendingRelationship,
25
- } from './types.js';
26
-
27
- // Reader
28
- export type { OoxmlOpenOptions } from './reader.js';
29
- export { ZipSafetyError } from '../shared/zipSafety.js';
30
- export type {
31
- ZipSafetyLimits,
32
- ZipSafetyErrorCode,
33
- ZipSafetyErrorOptions,
34
- } from '../shared/zipSafety.js';
35
- export {
36
- openPackage,
37
- getPartRelationships,
38
- getPartXml,
39
- getPartBinary,
40
- getCoreProperties,
41
- } from './reader.js';
42
-
43
- // Read utilities
44
- export { attrNS, baseDirOf, resolveTarget, findRelByType } from './readUtils.js';
45
-
46
- // Theme reader
47
- export type { OoxmlColorScheme, OoxmlFontScheme, OoxmlTheme } from './themeReader.js';
48
- export { parseThemeXml, readThemePart } from './themeReader.js';
49
-
50
- // Writer
51
- export type { OoxmlPackageBuilder } from './writer.js';
52
- export { createPackage } from './writer.js';
53
-
54
- // XML Utilities
55
- export {
56
- xmlDeclaration,
57
- escapeXml,
58
- attrString,
59
- selfClosingElement,
60
- xmlElement,
61
- textElement,
62
- } from './xmlUtils.js';
63
-
64
- // Namespace constants
65
- export {
66
- NS_RELATIONSHIPS,
67
- NS_CONTENT_TYPES,
68
- REL_OFFICE_DOCUMENT,
69
- REL_CORE_PROPERTIES,
70
- REL_EXTENDED_PROPERTIES,
71
- REL_STYLES,
72
- REL_NUMBERING,
73
- REL_FONT_TABLE,
74
- REL_SETTINGS,
75
- REL_HYPERLINK,
76
- REL_IMAGE,
77
- REL_FOOTNOTES,
78
- REL_THEME,
79
- NS_WML,
80
- NS_PML,
81
- NS_SML,
82
- NS_DRAWINGML,
83
- NS_WP_DRAWING,
84
- NS_PICTURE,
85
- NS_DC,
86
- NS_DCTERMS,
87
- NS_CORE_PROPERTIES,
88
- NS_XSI,
89
- NS_MC,
90
- NS_R,
91
- CONTENT_TYPE_RELATIONSHIPS,
92
- CONTENT_TYPE_CORE_PROPERTIES,
93
- CONTENT_TYPE_DOCX_DOCUMENT,
94
- CONTENT_TYPE_DOCX_STYLES,
95
- CONTENT_TYPE_DOCX_NUMBERING,
96
- CONTENT_TYPE_DOCX_SETTINGS,
97
- CONTENT_TYPE_DOCX_FONT_TABLE,
98
- CONTENT_TYPE_DOCX_FOOTNOTES,
99
- CONTENT_TYPE_PPTX_PRESENTATION,
100
- CONTENT_TYPE_PPTX_SLIDE,
101
- CONTENT_TYPE_PPTX_SLIDE_LAYOUT,
102
- CONTENT_TYPE_PPTX_SLIDE_MASTER,
103
- CONTENT_TYPE_PPTX_THEME,
104
- REL_SLIDE,
105
- REL_SLIDE_LAYOUT,
106
- REL_SLIDE_MASTER,
107
- CONTENT_TYPE_XLSX_WORKBOOK,
108
- CONTENT_TYPE_XLSX_WORKSHEET,
109
- CONTENT_TYPE_XLSX_STYLES,
110
- REL_WORKSHEET,
111
- } from './namespaces.js';