@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.
- package/LICENSE +21 -0
- package/README-ko.md +292 -0
- package/README.md +301 -0
- package/bin/ohos-postinstall.cjs +118 -0
- package/bin/ppt-agent.js +620 -0
- package/convert.cjs +20 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/glib-2.0/include/glibconfig.h +220 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/index.js +1 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libexpat.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libffi.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libjpeg.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libpcre2-8.so.0 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips-cpp.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/package.json +13 -0
- package/native/@img/sharp-openharmony-arm64/lib/sharp-openharmony-arm64.node +0 -0
- package/native/@img/sharp-openharmony-arm64/package.json +16 -0
- package/package.json +95 -0
- package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
- package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
- package/scripts/build-viewer.js +444 -0
- package/scripts/design-gate.js +258 -0
- package/scripts/download-video.js +213 -0
- package/scripts/editor-server.js +1167 -0
- package/scripts/figma-export.js +169 -0
- package/scripts/generate-image.js +116 -0
- package/scripts/generate-images.js +164 -0
- package/scripts/html2pdf.js +822 -0
- package/scripts/html2png.js +246 -0
- package/scripts/html2pptx.js +162 -0
- package/scripts/import-template.js +86 -0
- package/scripts/install-runtime.js +216 -0
- package/scripts/render-tldraw.js +44 -0
- package/scripts/validate-slides.js +221 -0
- package/skills/slides-grab/SKILL.md +65 -0
- package/skills/slides-grab/references/presentation-workflow-reference.md +61 -0
- package/skills/slides-grab-card-news/SKILL.md +35 -0
- package/skills/slides-grab-design/SKILL.md +88 -0
- package/skills/slides-grab-design/references/beautiful-slide-defaults.md +88 -0
- package/skills/slides-grab-design/references/design-gate.md +349 -0
- package/skills/slides-grab-design/references/design-rules.md +76 -0
- package/skills/slides-grab-design/references/design-system-full.md +568 -0
- package/skills/slides-grab-design/references/detailed-design-rules.md +70 -0
- package/skills/slides-grab-export/SKILL.md +55 -0
- package/skills/slides-grab-export/references/export-rules.md +27 -0
- package/skills/slides-grab-export/references/html2pptx.md +627 -0
- package/skills/slides-grab-export/references/ooxml.md +427 -0
- package/skills/slides-grab-export/references/pptx-skill-reference.md +189 -0
- package/skills/slides-grab-html/SKILL.md +67 -0
- package/skills/slides-grab-image/SKILL.md +82 -0
- package/skills/slides-grab-plan/SKILL.md +68 -0
- package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
- package/skills/slides-grab-plan/references/outline-format.md +47 -0
- package/skills/slides-grab-plan/references/plan-workflow-reference.md +140 -0
- package/src/codex-imagen.js +182 -0
- package/src/design-diversity-data.js +6932 -0
- package/src/design-gate-report.js +244 -0
- package/src/design-gate-state.js +329 -0
- package/src/design-import.js +164 -0
- package/src/design-md-parser.js +415 -0
- package/src/design-styles-data.js +1928 -0
- package/src/design-styles.js +209 -0
- package/src/editor/codex-edit.js +584 -0
- package/src/editor/edit-subprocess.js +170 -0
- package/src/editor/editor-codex-prompt.md +50 -0
- package/src/editor/editor.html +1785 -0
- package/src/editor/js/editor-bbox.js +332 -0
- package/src/editor/js/editor-chat.js +56 -0
- package/src/editor/js/editor-direct-edit.js +238 -0
- package/src/editor/js/editor-dom.js +59 -0
- package/src/editor/js/editor-init.js +405 -0
- package/src/editor/js/editor-navigation.js +54 -0
- package/src/editor/js/editor-select.js +558 -0
- package/src/editor/js/editor-send.js +175 -0
- package/src/editor/js/editor-sse.js +163 -0
- package/src/editor/js/editor-state.js +41 -0
- package/src/editor/js/editor-type.js +71 -0
- package/src/editor/js/editor-utils.js +167 -0
- package/src/editor/js/model-registry.js +37 -0
- package/src/editor/screenshot.js +82 -0
- package/src/export-resolution.cjs +68 -0
- package/src/figma.js +71 -0
- package/src/html2pptx-scale.cjs +120 -0
- package/src/html2pptx.cjs +1262 -0
- package/src/image-contract.js +329 -0
- package/src/image-native.js +468 -0
- package/src/nano-banana.js +841 -0
- package/src/ohos-browser-bridge.cjs +272 -0
- package/src/pptx-raster-export.cjs +299 -0
- package/src/resolve.js +110 -0
- package/src/slide-mode.cjs +72 -0
- package/src/template-fidelity.js +267 -0
- package/src/template-import.js +505 -0
- package/src/template-layout.js +261 -0
- package/src/template-pack.js +255 -0
- package/src/tldraw/render.js +473 -0
- package/src/validation/cli.js +120 -0
- package/src/validation/core.js +1023 -0
- package/templates/chart.html +121 -0
- package/templates/closing.html +54 -0
- package/templates/content.html +50 -0
- package/templates/contents.html +60 -0
- package/templates/cover.html +64 -0
- package/templates/custom/.gitkeep +0 -0
- package/templates/custom/README.md +7 -0
- package/templates/design-styles/README.md +20 -0
- package/templates/design-styles/preview.html +4438 -0
- package/templates/diagram-tldraw.html +56 -0
- package/templates/diagram.html +98 -0
- package/templates/quote.html +31 -0
- package/templates/section-divider.html +43 -0
- package/templates/split-layout.html +43 -0
- package/templates/statistics.html +55 -0
- package/templates/team.html +49 -0
- 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
|
+
}
|