@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,473 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join, resolve } from 'node:path';
5
+
6
+ const require = createRequire(import.meta.url);
7
+ // OHOS: Use system Haitai browser via CDP bridge instead of standalone Chromium
8
+ const { launchBrowserViaCdp } = require('../ohos-browser-bridge.cjs');
9
+
10
+ export const DEFAULT_TLDRAW_WIDTH = 960;
11
+ export const DEFAULT_TLDRAW_HEIGHT = 540;
12
+ export const DEFAULT_TLDRAW_PADDING = 24;
13
+ export const DEFAULT_TLDRAW_OUTPUT = 'diagram.svg';
14
+
15
+ function readOptionValue(args, index, optionName) {
16
+ const next = args[index + 1];
17
+ if (!next || next.startsWith('-')) {
18
+ throw new Error(`Missing value for ${optionName}.`);
19
+ }
20
+ return next;
21
+ }
22
+
23
+ function parsePositiveInteger(value, optionName) {
24
+ const parsed = Number.parseInt(String(value), 10);
25
+ if (!Number.isFinite(parsed) || parsed <= 0) {
26
+ throw new Error(`${optionName} must be a positive integer.`);
27
+ }
28
+ return parsed;
29
+ }
30
+
31
+ function parseNonNegativeInteger(value, optionName) {
32
+ const parsed = Number.parseInt(String(value), 10);
33
+ if (!Number.isFinite(parsed) || parsed < 0) {
34
+ throw new Error(`${optionName} must be zero or a positive integer.`);
35
+ }
36
+ return parsed;
37
+ }
38
+
39
+ function formatNumber(value) {
40
+ return Number.parseFloat(Number(value).toFixed(4)).toString();
41
+ }
42
+
43
+ function escapeAttribute(value) {
44
+ return String(value)
45
+ .replaceAll('&', '&amp;')
46
+ .replaceAll('"', '&quot;')
47
+ .replaceAll('<', '&lt;')
48
+ .replaceAll('>', '&gt;');
49
+ }
50
+
51
+ function extractSvgMarkup(svg) {
52
+ const match = String(svg).match(/<svg\b[^>]*>([\s\S]*?)<\/svg>\s*$/i);
53
+ if (!match) {
54
+ throw new Error('Rendered tldraw output did not contain a root <svg> element.');
55
+ }
56
+ return match[1];
57
+ }
58
+
59
+ function findPackageJsonPath(moduleName) {
60
+ const resolvedEntry = require.resolve(moduleName);
61
+ let currentDir = dirname(resolvedEntry);
62
+
63
+ while (true) {
64
+ const packageJsonPath = join(currentDir, 'package.json');
65
+ if (existsSync(packageJsonPath)) {
66
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
67
+ if (packageJson.name === moduleName) {
68
+ return packageJsonPath;
69
+ }
70
+ }
71
+
72
+ const parentDir = dirname(currentDir);
73
+ if (parentDir === currentDir) {
74
+ break;
75
+ }
76
+ currentDir = parentDir;
77
+ }
78
+
79
+ throw new Error(`Unable to locate package.json for ${moduleName}.`);
80
+ }
81
+
82
+ function getInstalledPackageVersion(moduleName) {
83
+ const packageJsonPath = findPackageJsonPath(moduleName);
84
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
85
+ if (!packageJson.version) {
86
+ throw new Error(`Package ${moduleName} does not declare a version.`);
87
+ }
88
+ return packageJson.version;
89
+ }
90
+
91
+ function isLegacyTldrawPayload(payload) {
92
+ return Boolean(payload && typeof payload === 'object' && payload.document && payload.document.version);
93
+ }
94
+
95
+ export function buildTldrawImportUrl() {
96
+ const tldrawVersion = getInstalledPackageVersion('tldraw');
97
+ const reactVersion = getInstalledPackageVersion('react');
98
+ const reactDomVersion = getInstalledPackageVersion('react-dom');
99
+
100
+ return `https://esm.sh/tldraw@${encodeURIComponent(tldrawVersion)}?deps=react@${encodeURIComponent(reactVersion)},react-dom@${encodeURIComponent(reactDomVersion)}`;
101
+ }
102
+
103
+ export function parseTldrawCliArgs(args = []) {
104
+ const options = {
105
+ input: '',
106
+ output: DEFAULT_TLDRAW_OUTPUT,
107
+ width: DEFAULT_TLDRAW_WIDTH,
108
+ height: DEFAULT_TLDRAW_HEIGHT,
109
+ padding: DEFAULT_TLDRAW_PADDING,
110
+ background: 'transparent',
111
+ pageId: '',
112
+ help: false,
113
+ };
114
+
115
+ for (let index = 0; index < args.length; index += 1) {
116
+ const arg = args[index];
117
+
118
+ if (arg === '-h' || arg === '--help') {
119
+ options.help = true;
120
+ continue;
121
+ }
122
+
123
+ if (arg === '--input') {
124
+ options.input = readOptionValue(args, index, '--input');
125
+ index += 1;
126
+ continue;
127
+ }
128
+
129
+ if (arg.startsWith('--input=')) {
130
+ options.input = arg.slice('--input='.length);
131
+ continue;
132
+ }
133
+
134
+ if (arg === '--output') {
135
+ options.output = readOptionValue(args, index, '--output');
136
+ index += 1;
137
+ continue;
138
+ }
139
+
140
+ if (arg.startsWith('--output=')) {
141
+ options.output = arg.slice('--output='.length);
142
+ continue;
143
+ }
144
+
145
+ if (arg === '--width') {
146
+ options.width = parsePositiveInteger(readOptionValue(args, index, '--width'), '--width');
147
+ index += 1;
148
+ continue;
149
+ }
150
+
151
+ if (arg.startsWith('--width=')) {
152
+ options.width = parsePositiveInteger(arg.slice('--width='.length), '--width');
153
+ continue;
154
+ }
155
+
156
+ if (arg === '--height') {
157
+ options.height = parsePositiveInteger(readOptionValue(args, index, '--height'), '--height');
158
+ index += 1;
159
+ continue;
160
+ }
161
+
162
+ if (arg.startsWith('--height=')) {
163
+ options.height = parsePositiveInteger(arg.slice('--height='.length), '--height');
164
+ continue;
165
+ }
166
+
167
+ if (arg === '--padding') {
168
+ options.padding = parseNonNegativeInteger(readOptionValue(args, index, '--padding'), '--padding');
169
+ index += 1;
170
+ continue;
171
+ }
172
+
173
+ if (arg.startsWith('--padding=')) {
174
+ options.padding = parseNonNegativeInteger(arg.slice('--padding='.length), '--padding');
175
+ continue;
176
+ }
177
+
178
+ if (arg === '--background') {
179
+ options.background = readOptionValue(args, index, '--background').trim() || 'transparent';
180
+ index += 1;
181
+ continue;
182
+ }
183
+
184
+ if (arg.startsWith('--background=')) {
185
+ options.background = arg.slice('--background='.length).trim() || 'transparent';
186
+ continue;
187
+ }
188
+
189
+ if (arg === '--page-id') {
190
+ options.pageId = readOptionValue(args, index, '--page-id').trim();
191
+ index += 1;
192
+ continue;
193
+ }
194
+
195
+ if (arg.startsWith('--page-id=')) {
196
+ options.pageId = arg.slice('--page-id='.length).trim();
197
+ continue;
198
+ }
199
+
200
+ throw new Error(`Unknown option: ${arg}`);
201
+ }
202
+
203
+ if (!options.help && (!options.input || !options.input.trim())) {
204
+ throw new Error('--input must be a non-empty string.');
205
+ }
206
+
207
+ options.input = options.input.trim();
208
+ options.output = String(options.output).trim();
209
+ options.background = options.background || 'transparent';
210
+
211
+ if (!options.help && !options.output) {
212
+ throw new Error('--output must be a non-empty string.');
213
+ }
214
+
215
+ return options;
216
+ }
217
+
218
+ export function getTldrawUsage() {
219
+ return [
220
+ 'Usage: node scripts/render-tldraw.js [options]',
221
+ '',
222
+ 'Options:',
223
+ ' --input <path> Input current-format .tldr or store-snapshot JSON file',
224
+ ` --output <path> Output SVG asset path (default: ${DEFAULT_TLDRAW_OUTPUT})`,
225
+ ` --width <px> Exact output width in CSS pixels (default: ${DEFAULT_TLDRAW_WIDTH})`,
226
+ ` --height <px> Exact output height in CSS pixels (default: ${DEFAULT_TLDRAW_HEIGHT})`,
227
+ ` --padding <px> Inner fit padding in CSS pixels (default: ${DEFAULT_TLDRAW_PADDING})`,
228
+ ' --background <css> Optional wrapper background fill (default: transparent)',
229
+ ' --page-id <id> Optional tldraw page id to export',
230
+ ' -h, --help Show this help message',
231
+ '',
232
+ 'Notes:',
233
+ ' - Legacy pre-records .tldr files are not supported. Open and resave them in a current tldraw build first.',
234
+ '',
235
+ 'Examples:',
236
+ ' node scripts/render-tldraw.js --input slides/assets/diagram.tldr --output slides/assets/diagram.svg',
237
+ ' node scripts/render-tldraw.js --input deck/assets/system.json --output deck/assets/system.svg --width 640 --height 320',
238
+ ].join('\n');
239
+ }
240
+
241
+ export function normalizeTldrawSnapshot(payload) {
242
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
243
+ throw new Error('Expected the tldraw input file to contain a JSON object.');
244
+ }
245
+
246
+ if (isLegacyTldrawPayload(payload)) {
247
+ throw new Error('Legacy pre-records .tldr files are not supported yet. Open the diagram in a current tldraw build and save it again before exporting.');
248
+ }
249
+
250
+ if (payload.store && payload.schema) {
251
+ return payload;
252
+ }
253
+
254
+ if (Array.isArray(payload.records) && payload.schema) {
255
+ const store = Object.fromEntries(
256
+ payload.records.map((record) => {
257
+ if (!record || typeof record !== 'object' || typeof record.id !== 'string') {
258
+ throw new Error('Each tldraw record must be an object with a string id.');
259
+ }
260
+ return [record.id, record];
261
+ }),
262
+ );
263
+
264
+ return {
265
+ store,
266
+ schema: payload.schema,
267
+ };
268
+ }
269
+
270
+ throw new Error('Input JSON must contain either { store, schema } or a current-format { records, schema } tldraw file.');
271
+ }
272
+
273
+ export function buildFixedSizeSvg(
274
+ { svg, width, height },
275
+ {
276
+ targetWidth = DEFAULT_TLDRAW_WIDTH,
277
+ targetHeight = DEFAULT_TLDRAW_HEIGHT,
278
+ padding = DEFAULT_TLDRAW_PADDING,
279
+ background = 'transparent',
280
+ } = {},
281
+ ) {
282
+ const sourceWidth = Number(width);
283
+ const sourceHeight = Number(height);
284
+
285
+ if (!Number.isFinite(sourceWidth) || sourceWidth <= 0 || !Number.isFinite(sourceHeight) || sourceHeight <= 0) {
286
+ throw new Error('Rendered tldraw output must include positive width and height values.');
287
+ }
288
+
289
+ const safePadding = Math.max(0, padding);
290
+ const availableWidth = Math.max(1, targetWidth - safePadding * 2);
291
+ const availableHeight = Math.max(1, targetHeight - safePadding * 2);
292
+ const scale = Math.min(availableWidth / sourceWidth, availableHeight / sourceHeight);
293
+ const fittedWidth = sourceWidth * scale;
294
+ const fittedHeight = sourceHeight * scale;
295
+ const offsetX = (targetWidth - fittedWidth) / 2;
296
+ const offsetY = (targetHeight - fittedHeight) / 2;
297
+ const markup = extractSvgMarkup(svg);
298
+ const backgroundMarkup = background && background !== 'transparent'
299
+ ? `\n <rect x="0" y="0" width="${targetWidth}" height="${targetHeight}" fill="${escapeAttribute(background)}" />`
300
+ : '';
301
+
302
+ return [
303
+ '<?xml version="1.0" encoding="UTF-8"?>',
304
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${targetWidth}" height="${targetHeight}" viewBox="0 0 ${targetWidth} ${targetHeight}" role="img" aria-label="tldraw diagram export">`,
305
+ backgroundMarkup,
306
+ ` <g transform="translate(${formatNumber(offsetX)} ${formatNumber(offsetY)}) scale(${formatNumber(scale)})">`,
307
+ markup
308
+ .split('\n')
309
+ .map((line) => ` ${line}`)
310
+ .join('\n'),
311
+ ' </g>',
312
+ '</svg>',
313
+ '',
314
+ ].join('\n');
315
+ }
316
+
317
+ export async function loadTldrawInput(inputPath) {
318
+ const raw = await readFile(inputPath, 'utf8');
319
+ let parsed;
320
+ try {
321
+ parsed = JSON.parse(raw);
322
+ } catch (error) {
323
+ throw new Error(`Unable to parse tldraw JSON from ${inputPath}: ${error.message}`);
324
+ }
325
+ return normalizeTldrawSnapshot(parsed);
326
+ }
327
+
328
+ export async function renderTldrawSnapshot(snapshot, { pageId = '' } = {}) {
329
+ const browser = process.platform === 'openharmony'
330
+ ? await launchBrowserViaCdp()
331
+ : await require('playwright').chromium.launch({ headless: true });
332
+ const page = await browser.newPage({ viewport: { width: DEFAULT_TLDRAW_WIDTH, height: DEFAULT_TLDRAW_HEIGHT } });
333
+
334
+ try {
335
+ await page.setContent('<!DOCTYPE html><html><head><meta charset="utf-8"></head><body></body></html>', {
336
+ waitUntil: 'load',
337
+ });
338
+
339
+ const moduleUrl = buildTldrawImportUrl();
340
+
341
+ await page.evaluate(
342
+ async ({ snapshot: browserSnapshot, requestedPageId, browserModuleUrl }) => {
343
+ window.__TLDRAW_RENDER_RESULT__ = null;
344
+ window.__TLDRAW_RENDER_ERROR__ = null;
345
+
346
+ try {
347
+ const {
348
+ Editor,
349
+ createTLStore,
350
+ defaultAddFontsFromNode,
351
+ defaultBindingUtils,
352
+ defaultShapeUtils,
353
+ tipTapDefaultExtensions,
354
+ } = await import(browserModuleUrl);
355
+ const container = document.createElement('div');
356
+ container.style.position = 'fixed';
357
+ container.style.inset = '0';
358
+ container.classList.add('tl-container', 'tl-theme__light');
359
+ document.body.appendChild(container);
360
+
361
+ const tempElm = document.createElement('div');
362
+ container.appendChild(tempElm);
363
+
364
+ const store = createTLStore({
365
+ snapshot: browserSnapshot,
366
+ shapeUtils: defaultShapeUtils,
367
+ });
368
+
369
+ const editor = new Editor({
370
+ store,
371
+ shapeUtils: defaultShapeUtils,
372
+ bindingUtils: defaultBindingUtils,
373
+ tools: [],
374
+ getContainer: () => tempElm,
375
+ options: {
376
+ text: {
377
+ tipTapConfig: {
378
+ extensions: tipTapDefaultExtensions,
379
+ },
380
+ addFontsFromNode: defaultAddFontsFromNode,
381
+ },
382
+ },
383
+ });
384
+
385
+ if (requestedPageId) {
386
+ editor.setCurrentPage(requestedPageId);
387
+ }
388
+
389
+ await editor.fonts.loadRequiredFontsForCurrentPage(editor.options.maxFontsToLoadBeforeRender);
390
+ await new Promise((resolveAnimation) => requestAnimationFrame(() => requestAnimationFrame(resolveAnimation)));
391
+
392
+ const shapeIds = Array.from(editor.getCurrentPageShapeIds());
393
+ if (shapeIds.length === 0) {
394
+ throw new Error('The selected tldraw page does not contain any shapes to export.');
395
+ }
396
+
397
+ const result = await editor.getSvgString(shapeIds, {
398
+ background: false,
399
+ padding: 0,
400
+ });
401
+
402
+ editor.dispose();
403
+
404
+ if (!result) {
405
+ throw new Error('tldraw did not return an SVG export.');
406
+ }
407
+
408
+ window.__TLDRAW_RENDER_RESULT__ = result;
409
+ } catch (error) {
410
+ window.__TLDRAW_RENDER_ERROR__ = error instanceof Error ? error.message : String(error);
411
+ }
412
+ },
413
+ {
414
+ snapshot,
415
+ requestedPageId: pageId,
416
+ browserModuleUrl: moduleUrl,
417
+ },
418
+ );
419
+
420
+ await page.waitForFunction(
421
+ () => window.__TLDRAW_RENDER_RESULT__ !== null || window.__TLDRAW_RENDER_ERROR__ !== null,
422
+ null,
423
+ { timeout: 30000 },
424
+ );
425
+
426
+ const errorMessage = await page.evaluate(() => window.__TLDRAW_RENDER_ERROR__);
427
+ if (errorMessage) {
428
+ throw new Error(errorMessage);
429
+ }
430
+
431
+ const result = await page.evaluate(() => window.__TLDRAW_RENDER_RESULT__);
432
+ if (!result) {
433
+ throw new Error('tldraw render completed without producing an SVG export.');
434
+ }
435
+
436
+ return result;
437
+ } finally {
438
+ await browser.close();
439
+ }
440
+ }
441
+
442
+ export async function renderTldrawFile(inputPath, outputPath, options = {}) {
443
+ const snapshot = await loadTldrawInput(inputPath);
444
+ const rendered = await renderTldrawSnapshot(snapshot, { pageId: options.pageId });
445
+ const fittedSvg = buildFixedSizeSvg(rendered, {
446
+ targetWidth: options.width,
447
+ targetHeight: options.height,
448
+ padding: options.padding,
449
+ background: options.background,
450
+ });
451
+
452
+ await mkdir(dirname(outputPath), { recursive: true });
453
+ await writeFile(outputPath, fittedSvg, 'utf8');
454
+ return {
455
+ inputPath,
456
+ outputPath,
457
+ width: options.width,
458
+ height: options.height,
459
+ };
460
+ }
461
+
462
+ export async function main(args = process.argv.slice(2)) {
463
+ const options = parseTldrawCliArgs(args);
464
+ if (options.help) {
465
+ process.stdout.write(`${getTldrawUsage()}\n`);
466
+ return;
467
+ }
468
+
469
+ const inputPath = resolve(process.cwd(), options.input);
470
+ const outputPath = resolve(process.cwd(), options.output);
471
+ const result = await renderTldrawFile(inputPath, outputPath, options);
472
+ process.stdout.write(`Generated tldraw SVG: ${result.outputPath} (${result.width}x${result.height})\n`);
473
+ }
@@ -0,0 +1,120 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const {
5
+ DEFAULT_SLIDE_MODE,
6
+ getSlideModeChoices,
7
+ normalizeSlideMode,
8
+ } = require('../slide-mode.cjs');
9
+
10
+ export const DEFAULT_SLIDES_DIR = 'slides';
11
+ export const DEFAULT_VALIDATE_FORMAT = 'concise';
12
+ export const VALIDATE_FORMATS = ['concise', 'json', 'json-full'];
13
+
14
+ function readOptionValue(args, index, optionName) {
15
+ const next = args[index + 1];
16
+ if (!next || next.startsWith('-')) {
17
+ throw new Error(`Missing value for ${optionName}.`);
18
+ }
19
+ return next;
20
+ }
21
+
22
+ export function parseValidateCliArgs(args) {
23
+ const options = {
24
+ slidesDir: DEFAULT_SLIDES_DIR,
25
+ format: DEFAULT_VALIDATE_FORMAT,
26
+ mode: DEFAULT_SLIDE_MODE,
27
+ help: false,
28
+ slides: [],
29
+ };
30
+
31
+ for (let i = 0; i < args.length; i += 1) {
32
+ const arg = args[i];
33
+
34
+ if (arg === '-h' || arg === '--help') {
35
+ options.help = true;
36
+ continue;
37
+ }
38
+
39
+ if (arg === '--slides-dir') {
40
+ options.slidesDir = readOptionValue(args, i, '--slides-dir');
41
+ i += 1;
42
+ continue;
43
+ }
44
+
45
+ if (arg.startsWith('--slides-dir=')) {
46
+ options.slidesDir = arg.slice('--slides-dir='.length);
47
+ continue;
48
+ }
49
+
50
+ if (arg === '--format') {
51
+ options.format = readOptionValue(args, i, '--format');
52
+ i += 1;
53
+ continue;
54
+ }
55
+
56
+ if (arg.startsWith('--format=')) {
57
+ options.format = arg.slice('--format='.length);
58
+ continue;
59
+ }
60
+
61
+ if (arg === '--mode') {
62
+ options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
63
+ i += 1;
64
+ continue;
65
+ }
66
+
67
+ if (arg.startsWith('--mode=')) {
68
+ options.mode = normalizeSlideMode(arg.slice('--mode='.length));
69
+ continue;
70
+ }
71
+
72
+ if (arg === '--slide') {
73
+ options.slides.push(readOptionValue(args, i, '--slide'));
74
+ i += 1;
75
+ continue;
76
+ }
77
+
78
+ if (arg.startsWith('--slide=')) {
79
+ options.slides.push(arg.slice('--slide='.length));
80
+ continue;
81
+ }
82
+
83
+ throw new Error(`Unknown option: ${arg}`);
84
+ }
85
+
86
+ if (typeof options.slidesDir !== 'string' || options.slidesDir.trim() === '') {
87
+ throw new Error('--slides-dir must be a non-empty string.');
88
+ }
89
+
90
+ if (typeof options.format !== 'string' || options.format.trim() === '') {
91
+ throw new Error('--format must be a non-empty string.');
92
+ }
93
+
94
+ options.slidesDir = options.slidesDir.trim();
95
+ options.format = options.format.trim();
96
+ options.mode = normalizeSlideMode(options.mode);
97
+
98
+ if (!VALIDATE_FORMATS.includes(options.format)) {
99
+ throw new Error(`Unknown --format value: ${options.format}. Expected one of: ${VALIDATE_FORMATS.join(', ')}`);
100
+ }
101
+
102
+ options.slides = options.slides
103
+ .map((slide) => String(slide).trim())
104
+ .filter(Boolean);
105
+
106
+ return options;
107
+ }
108
+
109
+ export function getValidateUsage() {
110
+ return [
111
+ 'Usage: node scripts/validate-slides.js [options]',
112
+ '',
113
+ 'Options:',
114
+ ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
115
+ ` --format <format> Output format: ${VALIDATE_FORMATS.join(', ')} (default: ${DEFAULT_VALIDATE_FORMAT})`,
116
+ ` --mode <mode> Slide mode: ${getSlideModeChoices().join(', ')} (default: ${DEFAULT_SLIDE_MODE})`,
117
+ ' --slide <file> Validate only the named slide file (repeatable)',
118
+ ' -h, --help Show this help message',
119
+ ].join('\n');
120
+ }