@ohos-ports/slides-grab 1.5.0-beta.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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README-ko.md +292 -0
  3. package/README.md +301 -0
  4. package/bin/ohos-postinstall.cjs +118 -0
  5. package/bin/ppt-agent.js +620 -0
  6. package/convert.cjs +20 -0
  7. package/native/@img/sharp-libvips-openharmony-arm64/lib/glib-2.0/include/glibconfig.h +220 -0
  8. package/native/@img/sharp-libvips-openharmony-arm64/lib/index.js +1 -0
  9. package/native/@img/sharp-libvips-openharmony-arm64/lib/libexpat.so +0 -0
  10. package/native/@img/sharp-libvips-openharmony-arm64/lib/libffi.so +0 -0
  11. package/native/@img/sharp-libvips-openharmony-arm64/lib/libjpeg.so +0 -0
  12. package/native/@img/sharp-libvips-openharmony-arm64/lib/libpcre2-8.so.0 +0 -0
  13. package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips-cpp.so.42.20.3 +0 -0
  14. package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips.so.42.20.3 +0 -0
  15. package/native/@img/sharp-libvips-openharmony-arm64/package.json +13 -0
  16. package/native/@img/sharp-openharmony-arm64/lib/sharp-openharmony-arm64.node +0 -0
  17. package/native/@img/sharp-openharmony-arm64/package.json +16 -0
  18. package/package.json +95 -0
  19. package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
  20. package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
  21. package/scripts/build-viewer.js +444 -0
  22. package/scripts/design-gate.js +258 -0
  23. package/scripts/download-video.js +213 -0
  24. package/scripts/editor-server.js +1167 -0
  25. package/scripts/figma-export.js +169 -0
  26. package/scripts/generate-image.js +116 -0
  27. package/scripts/generate-images.js +164 -0
  28. package/scripts/html2pdf.js +822 -0
  29. package/scripts/html2png.js +246 -0
  30. package/scripts/html2pptx.js +162 -0
  31. package/scripts/import-template.js +86 -0
  32. package/scripts/install-runtime.js +216 -0
  33. package/scripts/render-tldraw.js +44 -0
  34. package/scripts/validate-slides.js +221 -0
  35. package/skills/slides-grab/SKILL.md +65 -0
  36. package/skills/slides-grab/references/presentation-workflow-reference.md +61 -0
  37. package/skills/slides-grab-card-news/SKILL.md +35 -0
  38. package/skills/slides-grab-design/SKILL.md +88 -0
  39. package/skills/slides-grab-design/references/beautiful-slide-defaults.md +88 -0
  40. package/skills/slides-grab-design/references/design-gate.md +349 -0
  41. package/skills/slides-grab-design/references/design-rules.md +76 -0
  42. package/skills/slides-grab-design/references/design-system-full.md +568 -0
  43. package/skills/slides-grab-design/references/detailed-design-rules.md +70 -0
  44. package/skills/slides-grab-export/SKILL.md +55 -0
  45. package/skills/slides-grab-export/references/export-rules.md +27 -0
  46. package/skills/slides-grab-export/references/html2pptx.md +627 -0
  47. package/skills/slides-grab-export/references/ooxml.md +427 -0
  48. package/skills/slides-grab-export/references/pptx-skill-reference.md +189 -0
  49. package/skills/slides-grab-html/SKILL.md +67 -0
  50. package/skills/slides-grab-image/SKILL.md +82 -0
  51. package/skills/slides-grab-plan/SKILL.md +68 -0
  52. package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
  53. package/skills/slides-grab-plan/references/outline-format.md +47 -0
  54. package/skills/slides-grab-plan/references/plan-workflow-reference.md +140 -0
  55. package/src/codex-imagen.js +182 -0
  56. package/src/design-diversity-data.js +6932 -0
  57. package/src/design-gate-report.js +244 -0
  58. package/src/design-gate-state.js +329 -0
  59. package/src/design-import.js +164 -0
  60. package/src/design-md-parser.js +415 -0
  61. package/src/design-styles-data.js +1928 -0
  62. package/src/design-styles.js +209 -0
  63. package/src/editor/codex-edit.js +584 -0
  64. package/src/editor/edit-subprocess.js +170 -0
  65. package/src/editor/editor-codex-prompt.md +50 -0
  66. package/src/editor/editor.html +1785 -0
  67. package/src/editor/js/editor-bbox.js +332 -0
  68. package/src/editor/js/editor-chat.js +56 -0
  69. package/src/editor/js/editor-direct-edit.js +238 -0
  70. package/src/editor/js/editor-dom.js +59 -0
  71. package/src/editor/js/editor-init.js +405 -0
  72. package/src/editor/js/editor-navigation.js +54 -0
  73. package/src/editor/js/editor-select.js +558 -0
  74. package/src/editor/js/editor-send.js +175 -0
  75. package/src/editor/js/editor-sse.js +163 -0
  76. package/src/editor/js/editor-state.js +41 -0
  77. package/src/editor/js/editor-type.js +71 -0
  78. package/src/editor/js/editor-utils.js +167 -0
  79. package/src/editor/js/model-registry.js +37 -0
  80. package/src/editor/screenshot.js +82 -0
  81. package/src/export-resolution.cjs +68 -0
  82. package/src/figma.js +71 -0
  83. package/src/html2pptx-scale.cjs +120 -0
  84. package/src/html2pptx.cjs +1262 -0
  85. package/src/image-contract.js +329 -0
  86. package/src/image-native.js +468 -0
  87. package/src/nano-banana.js +841 -0
  88. package/src/ohos-browser-bridge.cjs +272 -0
  89. package/src/pptx-raster-export.cjs +299 -0
  90. package/src/resolve.js +110 -0
  91. package/src/slide-mode.cjs +72 -0
  92. package/src/template-fidelity.js +267 -0
  93. package/src/template-import.js +505 -0
  94. package/src/template-layout.js +261 -0
  95. package/src/template-pack.js +255 -0
  96. package/src/tldraw/render.js +473 -0
  97. package/src/validation/cli.js +120 -0
  98. package/src/validation/core.js +1023 -0
  99. package/templates/chart.html +121 -0
  100. package/templates/closing.html +54 -0
  101. package/templates/content.html +50 -0
  102. package/templates/contents.html +60 -0
  103. package/templates/cover.html +64 -0
  104. package/templates/custom/.gitkeep +0 -0
  105. package/templates/custom/README.md +7 -0
  106. package/templates/design-styles/README.md +20 -0
  107. package/templates/design-styles/preview.html +4438 -0
  108. package/templates/diagram-tldraw.html +56 -0
  109. package/templates/diagram.html +98 -0
  110. package/templates/quote.html +31 -0
  111. package/templates/section-divider.html +43 -0
  112. package/templates/split-layout.html +43 -0
  113. package/templates/statistics.html +55 -0
  114. package/templates/team.html +49 -0
  115. package/templates/timeline.html +59 -0
@@ -0,0 +1,505 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import { basename, extname, resolve, join } from 'node:path';
3
+ import { deflateSync } from 'node:zlib';
4
+
5
+ import {
6
+ TEMPLATE_PACK_RELATIVE_PATH,
7
+ TEMPLATE_PACK_SUPPORTED_VERSION,
8
+ parseTemplatePack,
9
+ } from './template-pack.js';
10
+
11
+ const TEMPLATE_PREVIEWS_RELATIVE_DIR = '.slides-grab/template-previews';
12
+
13
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.avif']);
14
+ const HTML_EXTENSIONS = new Set(['.html', '.htm']);
15
+
16
+ const DEFAULT_PACK_NAME = 'Imported template pack';
17
+
18
+ /**
19
+ * Error thrown when a template source cannot be classified or imported.
20
+ * The message always starts with a short, stable prefix so CLI callers and
21
+ * tests can match against it without parsing free-form prose.
22
+ */
23
+ export class TemplateImportError extends Error {
24
+ constructor(message, { cause } = {}) {
25
+ super(message, { cause });
26
+ this.name = 'TemplateImportError';
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Parse a flat argv array into a structured import-template options object.
32
+ *
33
+ * Supports repeated `--input`, a single `--slides-dir`, and an optional
34
+ * `--name`. Unknown flags are ignored so the function stays composable. This
35
+ * is a pure parser: it does not touch the filesystem.
36
+ */
37
+ export function parseImportTemplateArgs(argv = []) {
38
+ const inputs = [];
39
+ let slidesDir = null;
40
+ let name = null;
41
+
42
+ for (let i = 0; i < argv.length; i += 1) {
43
+ const flag = argv[i];
44
+ const next = argv[i + 1];
45
+ if (flag === '--input') {
46
+ if (!next || next.startsWith('--')) {
47
+ throw new TemplateImportError('Missing value for --input.');
48
+ }
49
+ inputs.push(next);
50
+ i += 1;
51
+ } else if (flag === '--slides-dir') {
52
+ if (!next || next.startsWith('--')) {
53
+ throw new TemplateImportError('Missing value for --slides-dir.');
54
+ }
55
+ slidesDir = next;
56
+ i += 1;
57
+ } else if (flag === '--name') {
58
+ if (!next || next.startsWith('--')) {
59
+ throw new TemplateImportError('Missing value for --name.');
60
+ }
61
+ name = next;
62
+ i += 1;
63
+ }
64
+ }
65
+
66
+ return { inputs, slidesDir, name };
67
+ }
68
+
69
+ /**
70
+ * Classify a template source file by extension into one of the supported
71
+ * source types: `pptx`, `pdf`, `html`, or `image`. Unsupported extensions
72
+ * throw a {@link TemplateImportError} whose message includes the stable
73
+ * prefix "Unsupported template source type".
74
+ */
75
+ export function detectTemplateSourceType(inputPath) {
76
+ const ext = extname(inputPath).toLowerCase();
77
+ if (ext === '.pptx') return 'pptx';
78
+ if (ext === '.pdf') return 'pdf';
79
+ if (HTML_EXTENSIONS.has(ext)) return 'html';
80
+ if (IMAGE_EXTENSIONS.has(ext)) return 'image';
81
+ throw new TemplateImportError(
82
+ `Unsupported template source type for ${basename(inputPath)} (${ext || 'no extension'}). ` +
83
+ 'Supported types: .pptx, .pdf, .html, .htm, .png, .jpg, .jpeg, .gif, .webp, .bmp, .svg, .avif.',
84
+ );
85
+ }
86
+
87
+ // --- Local, deterministic PNG preview generation ---
88
+
89
+ const CRC_TABLE = (() => {
90
+ const table = new Uint32Array(256);
91
+ for (let n = 0; n < 256; n += 1) {
92
+ let c = n;
93
+ for (let k = 0; k < 8; k += 1) {
94
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
95
+ }
96
+ table[n] = c >>> 0;
97
+ }
98
+ return table;
99
+ })();
100
+
101
+ function crc32(buffer) {
102
+ let c = 0xffffffff;
103
+ for (let i = 0; i < buffer.length; i += 1) {
104
+ c = CRC_TABLE[(c ^ buffer[i]) & 0xff] ^ (c >>> 8);
105
+ }
106
+ return (c ^ 0xffffffff) >>> 0;
107
+ }
108
+
109
+ function pngChunk(type, data) {
110
+ const typeBuffer = Buffer.from(type, 'ascii');
111
+ const lengthBuffer = Buffer.alloc(4);
112
+ lengthBuffer.writeUInt32BE(data.length, 0);
113
+ const crcBuffer = Buffer.alloc(4);
114
+ crcBuffer.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), 0);
115
+ return Buffer.concat([lengthBuffer, typeBuffer, data, crcBuffer]);
116
+ }
117
+
118
+ /**
119
+ * Encode a small solid-color RGB PNG without any native image dependency.
120
+ * Used as a deterministic local preview placeholder. This is intentionally a
121
+ * placeholder render: it does NOT attempt to render the real slide content.
122
+ */
123
+ function encodeSolidColorPng(width, height, [r, g, b]) {
124
+ const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
125
+ const ihdr = Buffer.alloc(13);
126
+ ihdr.writeUInt32BE(width, 0);
127
+ ihdr.writeUInt32BE(height, 4);
128
+ ihdr[8] = 8; // bit depth
129
+ ihdr[9] = 2; // color type: RGB
130
+ ihdr[10] = 0; // compression
131
+ ihdr[11] = 0; // filter
132
+ ihdr[12] = 0; // interlace
133
+
134
+ const rowLength = width * 3 + 1;
135
+ const raw = Buffer.alloc(rowLength * height);
136
+ for (let y = 0; y < height; y += 1) {
137
+ const offset = y * rowLength;
138
+ raw[offset] = 0; // filter: none
139
+ for (let x = 0; x < width; x += 1) {
140
+ raw[offset + 1 + x * 3] = r;
141
+ raw[offset + 1 + x * 3 + 1] = g;
142
+ raw[offset + 1 + x * 3 + 2] = b;
143
+ }
144
+ }
145
+ const idat = deflateSync(raw);
146
+
147
+ return Buffer.concat([
148
+ signature,
149
+ pngChunk('IHDR', ihdr),
150
+ pngChunk('IDAT', idat),
151
+ pngChunk('IEND', Buffer.alloc(0)),
152
+ ]);
153
+ }
154
+
155
+ function hexToRgb(hex) {
156
+ const value = hex.replace('#', '');
157
+ const full = value.length === 3
158
+ ? value.split('').map((c) => c + c).join('')
159
+ : value.slice(0, 6);
160
+ const num = parseInt(full, 16);
161
+ if (!Number.isFinite(num)) return [200, 200, 200];
162
+ return [(num >> 16) & 0xff, (num >> 8) & 0xff, num & 0xff];
163
+ }
164
+
165
+ function normalizeHexColor(raw) {
166
+ const match = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.exec(raw.trim());
167
+ if (!match) return null;
168
+ let value = match[1].toLowerCase();
169
+ if (value.length === 3) {
170
+ value = value.split('').map((c) => c + c).join('');
171
+ }
172
+ if (value.length === 8) {
173
+ value = value.slice(0, 6);
174
+ }
175
+ return `#${value}`;
176
+ }
177
+
178
+ function unique(value, index, array) {
179
+ return array.indexOf(value) === index;
180
+ }
181
+
182
+ function slugify(input) {
183
+ const base = basename(input, extname(input))
184
+ .toLowerCase()
185
+ .replace(/[^a-z0-9]+/g, '-')
186
+ .replace(/^-+|-+$/g, '');
187
+ return base || 'template';
188
+ }
189
+
190
+ // --- Lightweight local HTML analysis (no network, no DOM dependency) ---
191
+
192
+ function extractStyleBlocks(html) {
193
+ const blocks = [];
194
+ const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
195
+ let match;
196
+ while ((match = styleRegex.exec(html)) !== null) {
197
+ blocks.push(match[1]);
198
+ }
199
+ return blocks.join('\n');
200
+ }
201
+
202
+ function extractHexColors(css) {
203
+ const colors = [];
204
+ const regex = /#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
205
+ let match;
206
+ while ((match = regex.exec(css)) !== null) {
207
+ const normalized = normalizeHexColor(match[0]);
208
+ if (normalized) colors.push(normalized);
209
+ }
210
+ return colors.filter(unique);
211
+ }
212
+
213
+ function extractFontFamilies(css) {
214
+ const fonts = [];
215
+ const regex = /font-family\s*:\s*([^;}"']+)/gi;
216
+ let match;
217
+ while ((match = regex.exec(css)) !== null) {
218
+ const first = match[1].split(',')[0].trim().replace(/^["']|["']$/g, '');
219
+ if (first) fonts.push(first);
220
+ }
221
+ return fonts.filter(unique);
222
+ }
223
+
224
+ function extractClassRules(css) {
225
+ const rules = [];
226
+ const ruleRegex = /\.([a-zA-Z0-9_-]+)\s*\{([^}]*)\}/g;
227
+ let match;
228
+ while ((match = ruleRegex.exec(css)) !== null) {
229
+ const cls = match[1];
230
+ const body = match[2];
231
+ const px = (prop) => {
232
+ const re = new RegExp(`${prop}\\s*:\\s*([0-9.]+)px`);
233
+ const m = re.exec(body);
234
+ return m ? Number(m[1]) : null;
235
+ };
236
+ const left = px('left');
237
+ const top = px('top');
238
+ const width = px('width');
239
+ const height = px('height');
240
+ if ([left, top, width, height].every((n) => n !== null)) {
241
+ rules.push({ cls, bbox: { x: left, y: top, width, height } });
242
+ }
243
+ }
244
+ return rules;
245
+ }
246
+
247
+ function roleFromClass(cls) {
248
+ const name = cls.toLowerCase();
249
+ if (/subtitle|sub/.test(name)) return 'subtitle';
250
+ if (/title|headline|heading/.test(name)) return 'title';
251
+ if (/logo|brand/.test(name)) return 'logo';
252
+ if (/footer/.test(name)) return 'footer';
253
+ if (/body|content|text|paragraph/.test(name)) return 'body';
254
+ return cls;
255
+ }
256
+
257
+ function stripTags(html) {
258
+ return html
259
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
260
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
261
+ .replace(/<[^>]+>/g, ' ')
262
+ .replace(/\s+/g, ' ')
263
+ .trim();
264
+ }
265
+
266
+ function analyzeHtml(html) {
267
+ const css = extractStyleBlocks(html);
268
+ const colors = extractHexColors(css);
269
+ const fonts = extractFontFamilies(css);
270
+ const classRules = extractClassRules(css);
271
+ const bodyText = stripTags(html);
272
+ const textDensity = bodyText.length;
273
+ const hasH1 = /<h1[\s>]/i.test(html);
274
+ const hasH2 = /<h2[\s>]/i.test(html);
275
+ const hasList = /<[ou]l[\s>]/i.test(html);
276
+ let textKind = 'content';
277
+ if (hasH1) textKind = 'cover-heading';
278
+ else if (hasList) textKind = 'list';
279
+ else if (hasH2) textKind = 'section-heading';
280
+
281
+ const fields = classRules.map((rule) => ({
282
+ role: roleFromClass(rule.cls),
283
+ bbox: rule.bbox,
284
+ type: 'text',
285
+ }));
286
+
287
+ return { colors, fonts, fields, textDensity, textKind };
288
+ }
289
+
290
+ function buildHtmlLayout({ sourcePath, index, analysis, previewRelativePath }) {
291
+ const slug = slugify(sourcePath);
292
+ const layoutKind = analysis.textKind === 'cover-heading' ? 'cover' : 'content';
293
+ return {
294
+ layoutId: `${slug}-${index + 1}`,
295
+ layoutKind,
296
+ preview: previewRelativePath,
297
+ fields: analysis.fields,
298
+ };
299
+ }
300
+
301
+ function buildImageLayout({ sourcePath, index, previewRelativePath }) {
302
+ const slug = slugify(sourcePath);
303
+ return {
304
+ layoutId: `${slug}-${index + 1}`,
305
+ layoutKind: 'visual-reference',
306
+ preview: previewRelativePath,
307
+ };
308
+ }
309
+
310
+ function buildDocumentLayout({ sourcePath, index, previewRelativePath, sourceType }) {
311
+ const slug = slugify(sourcePath);
312
+ return {
313
+ layoutId: `${slug}-${index + 1}`,
314
+ layoutKind: 'visual-reference',
315
+ preview: previewRelativePath,
316
+ sourceType,
317
+ };
318
+ }
319
+
320
+ function writePreview({ sourcePath, sourceType, analysis, previewPath }) {
321
+ mkdirSync(resolve(previewPath, '..'), { recursive: true });
322
+ if (sourceType === 'image' && extname(sourcePath).toLowerCase() === '.png') {
323
+ // The image itself is the most honest PNG preview we can produce locally.
324
+ copyFileSync(sourcePath, previewPath);
325
+ return;
326
+ }
327
+ const fallbackColor = analysis?.colors?.[0] ?? '#c8c8c8';
328
+ const [r, g, b] = hexToRgb(fallbackColor);
329
+ writeFileSync(previewPath, encodeSolidColorPng(64, 36, [r, g, b]));
330
+ }
331
+
332
+ function expandTemplateInputs(inputs) {
333
+ const expanded = [];
334
+ const warnings = [];
335
+
336
+ for (const input of inputs) {
337
+ const absoluteInput = resolve(input);
338
+ if (!existsSync(absoluteInput)) {
339
+ throw new TemplateImportError(`Template source not found: ${absoluteInput}`);
340
+ }
341
+ const stat = statSync(absoluteInput);
342
+ if (!stat.isDirectory()) {
343
+ expanded.push(absoluteInput);
344
+ continue;
345
+ }
346
+
347
+ const entries = readdirSync(absoluteInput, { withFileTypes: true })
348
+ .filter((entry) => entry.isFile())
349
+ .map((entry) => join(absoluteInput, entry.name))
350
+ .sort((a, b) => basename(a).localeCompare(basename(b)));
351
+
352
+ for (const entryPath of entries) {
353
+ try {
354
+ detectTemplateSourceType(entryPath);
355
+ expanded.push(entryPath);
356
+ } catch (error) {
357
+ if (error instanceof TemplateImportError) {
358
+ warnings.push(`Skipped unsupported template source in directory: ${basename(entryPath)}.`);
359
+ } else {
360
+ throw error;
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ if (expanded.length === 0) {
367
+ throw new TemplateImportError('No supported template sources found in the provided input path(s).');
368
+ }
369
+
370
+ return { inputs: expanded, warnings };
371
+ }
372
+
373
+ /**
374
+ * Import one or more template source files into a loadable template pack.
375
+ *
376
+ * Writes `<slides-dir>/.slides-grab/template-pack.json` plus deterministic PNG
377
+ * previews under `<slides-dir>/.slides-grab/template-previews/`. HTML and
378
+ * image sources are analyzed locally (no network, no LLM). PPTX/PDF sources
379
+ * are accepted as metadata/visual-reference dispatch only: full rendering is
380
+ * not performed locally and a warning is recorded in the pack.
381
+ *
382
+ * @param {object} options
383
+ * @param {string[]} options.inputs Absolute or relative source paths.
384
+ * @param {string} options.slidesDir Target slides directory.
385
+ * @param {string} [options.name] Optional pack display name.
386
+ * @param {() => Date} [options.now] Deterministic clock for importedAt.
387
+ * @returns {Promise<{ packPath: string, previewPaths: string[] }>}
388
+ */
389
+ export async function importTemplatePack({ inputs, slidesDir, name, now = () => new Date() } = {}) {
390
+ if (!Array.isArray(inputs) || inputs.length === 0) {
391
+ throw new TemplateImportError('At least one --input source is required.');
392
+ }
393
+ if (!slidesDir) {
394
+ throw new TemplateImportError('A --slides-dir is required.');
395
+ }
396
+
397
+ const absoluteSlidesDir = resolve(slidesDir);
398
+ const packPath = join(absoluteSlidesDir, TEMPLATE_PACK_RELATIVE_PATH);
399
+ const previewsDir = join(absoluteSlidesDir, TEMPLATE_PREVIEWS_RELATIVE_DIR);
400
+ const previewsRelativeDir = TEMPLATE_PREVIEWS_RELATIVE_DIR;
401
+ mkdirSync(previewsDir, { recursive: true });
402
+
403
+ const layouts = [];
404
+ const previewPaths = [];
405
+ const sourceTypes = [];
406
+ const originalFiles = [];
407
+ const warnings = [];
408
+ const expanded = expandTemplateInputs(inputs);
409
+ warnings.push(...expanded.warnings);
410
+
411
+ const colors = [];
412
+ const fonts = [];
413
+
414
+ const pushUnique = (array, value) => {
415
+ if (value && !array.includes(value)) array.push(value);
416
+ };
417
+
418
+ expanded.inputs.forEach((absoluteInput, index) => {
419
+ const sourceType = detectTemplateSourceType(absoluteInput);
420
+ pushUnique(sourceTypes, sourceType);
421
+ originalFiles.push(basename(absoluteInput));
422
+
423
+ const slug = slugify(absoluteInput);
424
+ const previewFileName = `${slug}-${index + 1}.png`;
425
+ const previewPath = join(previewsDir, previewFileName);
426
+ const previewRelativePath = `${previewsRelativeDir}/${previewFileName}`;
427
+
428
+ let analysis = null;
429
+ if (sourceType === 'html') {
430
+ const html = readFileSync(absoluteInput, 'utf8');
431
+ analysis = analyzeHtml(html);
432
+ analysis.colors.forEach((c) => pushUnique(colors, c));
433
+ analysis.fonts.forEach((f) => pushUnique(fonts, f));
434
+ layouts.push(
435
+ buildHtmlLayout({ sourcePath: absoluteInput, index, analysis, previewRelativePath }),
436
+ );
437
+ } else if (sourceType === 'image') {
438
+ writePreview({ sourcePath: absoluteInput, sourceType, previewPath });
439
+ layouts.push(buildImageLayout({ sourcePath: absoluteInput, index, previewRelativePath }));
440
+ } else if (sourceType === 'pptx' || sourceType === 'pdf') {
441
+ warnings.push(
442
+ `${sourceType.toUpperCase()} rendering is not available locally; preview for ` +
443
+ `${basename(absoluteInput)} is a placeholder, not a real render. Treat the ` +
444
+ `original ${sourceType.toUpperCase()} file as the visual reference.`,
445
+ );
446
+ writePreview({ sourcePath: absoluteInput, sourceType, previewPath });
447
+ layouts.push(
448
+ buildDocumentLayout({ sourcePath: absoluteInput, index, previewRelativePath, sourceType }),
449
+ );
450
+ }
451
+
452
+ if (sourceType !== 'image' && sourceType !== 'pptx' && sourceType !== 'pdf') {
453
+ writePreview({ sourcePath: absoluteInput, sourceType, analysis, previewPath });
454
+ }
455
+ previewPaths.push(previewPath);
456
+ });
457
+
458
+ const designTokens = {
459
+ colors: colors.map((value) => ({ value, kind: 'brand' })),
460
+ fonts: fonts.map((family) => ({ family })),
461
+ };
462
+
463
+ const styleSummary = buildPromptSafeStyleSummary({
464
+ name: name || DEFAULT_PACK_NAME,
465
+ sourceTypes,
466
+ layouts,
467
+ colors,
468
+ fonts,
469
+ });
470
+
471
+ const pack = {
472
+ version: TEMPLATE_PACK_SUPPORTED_VERSION,
473
+ name: name || DEFAULT_PACK_NAME,
474
+ sourceMetadata: {
475
+ originalFiles,
476
+ importedAt: now().toISOString(),
477
+ sourceTypes,
478
+ warnings,
479
+ },
480
+ designTokens,
481
+ layouts,
482
+ promptSafeStyleSummary: styleSummary,
483
+ };
484
+
485
+ // Validate the built pack round-trips through the loader's parser so the
486
+ // written file is guaranteed loadable by loadTemplatePackFromSlidesDir.
487
+ parseTemplatePack(pack, { path: packPath });
488
+
489
+ mkdirSync(resolve(packPath, '..'), { recursive: true });
490
+ writeFileSync(packPath, `${JSON.stringify(pack, null, 2)}\n`, 'utf8');
491
+
492
+ return { packPath, previewPaths };
493
+ }
494
+
495
+ function buildPromptSafeStyleSummary({ name, sourceTypes, layouts, colors, fonts }) {
496
+ const layoutKinds = layouts.map((layout) => layout.layoutKind).filter(unique);
497
+ const parts = [
498
+ `${name}`,
499
+ `${layouts.length} layout(s) imported from ${sourceTypes.join(', ') || 'unknown sources'}.`,
500
+ `Layout kinds: ${layoutKinds.join(', ') || 'none'}.`,
501
+ `Colors: ${colors.join(', ') || 'none detected'}.`,
502
+ `Fonts: ${fonts.join(', ') || 'none detected'}.`,
503
+ ];
504
+ return parts.join(' ');
505
+ }