myaidev-method 0.2.22 → 0.2.24-1

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 (59) hide show
  1. package/USER_GUIDE.md +453 -48
  2. package/bin/cli.js +236 -38
  3. package/content-rules.example.md +80 -0
  4. package/dist/mcp/mcp-launcher.js +237 -0
  5. package/dist/server/.tsbuildinfo +1 -1
  6. package/dist/server/auth/layers.d.ts +1 -1
  7. package/dist/server/auth/services/AuthService.d.ts +1 -1
  8. package/dist/server/auth/services/TokenService.js.map +1 -1
  9. package/dist/server/auth/services/example.d.ts +5 -5
  10. package/package.json +22 -17
  11. package/src/config/workflows.js +28 -44
  12. package/src/index.js +21 -8
  13. package/src/lib/ascii-banner.js +214 -0
  14. package/src/lib/config-manager.js +470 -0
  15. package/src/lib/content-generator.js +427 -0
  16. package/src/lib/html-conversion-utils.js +843 -0
  17. package/src/lib/seo-optimizer.js +515 -0
  18. package/src/lib/update-manager.js +2 -1
  19. package/src/lib/visual-config-utils.js +321 -295
  20. package/src/lib/visual-generation-utils.js +1000 -811
  21. package/src/lib/wordpress-client.js +633 -0
  22. package/src/lib/workflow-installer.js +3 -3
  23. package/src/scripts/configure-wordpress-mcp.js +8 -3
  24. package/src/scripts/generate-visual-cli.js +365 -235
  25. package/src/scripts/html-conversion-cli.js +526 -0
  26. package/src/scripts/init/configure.js +436 -0
  27. package/src/scripts/init/install.js +460 -0
  28. package/src/scripts/ping.js +250 -0
  29. package/src/scripts/utils/file-utils.js +404 -0
  30. package/src/scripts/utils/logger.js +300 -0
  31. package/src/scripts/utils/write-content.js +293 -0
  32. package/src/scripts/wordpress/publish-to-wordpress.js +165 -0
  33. package/src/server/auth/services/TokenService.ts +1 -1
  34. package/src/templates/claude/agents/content-rules-setup.md +657 -0
  35. package/src/templates/claude/agents/content-writer.md +328 -1
  36. package/src/templates/claude/agents/visual-content-generator.md +311 -8
  37. package/src/templates/claude/commands/myai-configure.md +1 -1
  38. package/src/templates/claude/commands/myai-content-rules-setup.md +204 -0
  39. package/src/templates/claude/commands/myai-convert-html.md +186 -0
  40. package/src/templates/codex/commands/myai-content-rules-setup.md +85 -0
  41. package/src/templates/diagrams/architecture.d2 +52 -0
  42. package/src/templates/diagrams/flowchart.d2 +42 -0
  43. package/src/templates/diagrams/sequence.d2 +47 -0
  44. package/src/templates/docs/content-creation-guide.md +164 -0
  45. package/src/templates/docs/deployment-guide.md +336 -0
  46. package/src/templates/docs/visual-generation-guide.md +248 -0
  47. package/src/templates/docs/wordpress-publishing-guide.md +208 -0
  48. package/src/templates/gemini/commands/myai-content-rules-setup.toml +57 -0
  49. package/src/templates/infographics/comparison-table.html +347 -0
  50. package/src/templates/infographics/data-chart.html +268 -0
  51. package/src/templates/infographics/process-flow.html +365 -0
  52. package/.claude/mcp/sparc-orchestrator-server.js +0 -607
  53. package/.claude/mcp/wordpress-server.js +0 -1277
  54. package/src/agents/content-writer-prompt.md +0 -164
  55. package/src/agents/content-writer.json +0 -70
  56. package/src/templates/claude/mcp_config.json +0 -74
  57. package/src/templates/claude/slash_commands.json +0 -166
  58. package/src/templates/scripts/configure-wordpress-mcp.js +0 -181
  59. /package/src/scripts/{wordpress-health-check.js → wordpress/wordpress-health-check.js} +0 -0
@@ -0,0 +1,843 @@
1
+ /**
2
+ * HTML Conversion Utilities
3
+ *
4
+ * Converts HTML/CSS templates and D2 diagrams to PNG, PDF, and PPTX formats.
5
+ * Provides precise control over typography, layout, and data visualization.
6
+ *
7
+ * @module html-conversion-utils
8
+ */
9
+
10
+ import puppeteer from 'puppeteer';
11
+ import PptxGenJS from 'pptxgenjs';
12
+ import { execSync, exec } from 'child_process';
13
+ import { promisify } from 'util';
14
+ import fs from 'fs-extra';
15
+ import path from 'path';
16
+ import os from 'os';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ const execAsync = promisify(exec);
20
+
21
+ // ES Module __dirname equivalent
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+
25
+ // ============================================================================
26
+ // Configuration
27
+ // ============================================================================
28
+
29
+ const DEFAULT_VIEWPORT = { width: 1200, height: 800 };
30
+ const DEFAULT_PDF_FORMAT = 'A4';
31
+ const DEFAULT_D2_THEME = 200; // Neutral theme
32
+
33
+ /**
34
+ * Get the templates directory path
35
+ */
36
+ export function getTemplatesPath() {
37
+ // Check multiple possible locations
38
+ const locations = [
39
+ path.join(process.cwd(), '.myaidev-method', 'templates'),
40
+ path.join(process.cwd(), 'src', 'templates'),
41
+ path.join(__dirname, '..', 'templates'),
42
+ ];
43
+
44
+ for (const loc of locations) {
45
+ if (fs.existsSync(loc)) {
46
+ return loc;
47
+ }
48
+ }
49
+
50
+ // Default to src/templates relative to this file
51
+ return path.join(__dirname, '..', 'templates');
52
+ }
53
+
54
+ /**
55
+ * Get the output assets directory
56
+ */
57
+ export function getAssetsPath() {
58
+ return process.env.VISUAL_ASSETS_PATH || path.join(process.cwd(), 'content-assets');
59
+ }
60
+
61
+ // ============================================================================
62
+ // HTML to PNG Conversion
63
+ // ============================================================================
64
+
65
+ /**
66
+ * Convert HTML string to PNG image
67
+ *
68
+ * @param {string} html - HTML content to render
69
+ * @param {Object} options - Conversion options
70
+ * @param {number} options.width - Viewport width (default: 1200)
71
+ * @param {number} options.height - Viewport height (default: 800)
72
+ * @param {boolean} options.fullPage - Capture full page (default: true)
73
+ * @param {string} options.output - Output file path (optional)
74
+ * @param {number} options.deviceScaleFactor - Scale factor for retina (default: 2)
75
+ * @returns {Promise<{buffer: Buffer, path?: string}>}
76
+ */
77
+ export async function htmlToPng(html, options = {}) {
78
+ const {
79
+ width = DEFAULT_VIEWPORT.width,
80
+ height = DEFAULT_VIEWPORT.height,
81
+ fullPage = true,
82
+ output = null,
83
+ deviceScaleFactor = 2,
84
+ waitForSelector = null,
85
+ waitTime = 100,
86
+ } = options;
87
+
88
+ let browser;
89
+ try {
90
+ browser = await puppeteer.launch({
91
+ headless: 'new',
92
+ args: [
93
+ '--no-sandbox',
94
+ '--disable-setuid-sandbox',
95
+ '--disable-dev-shm-usage',
96
+ '--disable-gpu',
97
+ ],
98
+ });
99
+
100
+ const page = await browser.newPage();
101
+
102
+ await page.setViewport({
103
+ width,
104
+ height,
105
+ deviceScaleFactor,
106
+ });
107
+
108
+ // Set content and wait for rendering
109
+ await page.setContent(html, {
110
+ waitUntil: ['load', 'networkidle0'],
111
+ });
112
+
113
+ // Optional: wait for specific selector
114
+ if (waitForSelector) {
115
+ await page.waitForSelector(waitForSelector, { timeout: 5000 });
116
+ }
117
+
118
+ // Small delay for final rendering
119
+ await new Promise(resolve => setTimeout(resolve, waitTime));
120
+
121
+ // Take screenshot
122
+ const screenshotOptions = {
123
+ type: 'png',
124
+ fullPage,
125
+ omitBackground: false,
126
+ };
127
+
128
+ const buffer = await page.screenshot(screenshotOptions);
129
+
130
+ // Save to file if output path provided
131
+ let outputPath = null;
132
+ if (output) {
133
+ outputPath = output;
134
+ await fs.ensureDir(path.dirname(outputPath));
135
+ await fs.writeFile(outputPath, buffer);
136
+ }
137
+
138
+ return {
139
+ buffer,
140
+ path: outputPath,
141
+ width: fullPage ? undefined : width,
142
+ height: fullPage ? undefined : height,
143
+ };
144
+ } finally {
145
+ if (browser) {
146
+ await browser.close();
147
+ }
148
+ }
149
+ }
150
+
151
+ // ============================================================================
152
+ // HTML to PDF Conversion
153
+ // ============================================================================
154
+
155
+ /**
156
+ * Convert HTML string to PDF document
157
+ *
158
+ * @param {string} html - HTML content to render
159
+ * @param {Object} options - Conversion options
160
+ * @param {string} options.format - PDF format (default: 'A4')
161
+ * @param {boolean} options.landscape - Landscape orientation (default: false)
162
+ * @param {string} options.output - Output file path (optional)
163
+ * @param {Object} options.margin - Page margins
164
+ * @returns {Promise<{buffer: Buffer, path?: string}>}
165
+ */
166
+ export async function htmlToPdf(html, options = {}) {
167
+ const {
168
+ format = DEFAULT_PDF_FORMAT,
169
+ landscape = false,
170
+ output = null,
171
+ margin = { top: '20mm', right: '20mm', bottom: '20mm', left: '20mm' },
172
+ printBackground = true,
173
+ displayHeaderFooter = false,
174
+ headerTemplate = '',
175
+ footerTemplate = '',
176
+ } = options;
177
+
178
+ let browser;
179
+ try {
180
+ browser = await puppeteer.launch({
181
+ headless: 'new',
182
+ args: [
183
+ '--no-sandbox',
184
+ '--disable-setuid-sandbox',
185
+ '--disable-dev-shm-usage',
186
+ ],
187
+ });
188
+
189
+ const page = await browser.newPage();
190
+
191
+ await page.setContent(html, {
192
+ waitUntil: ['load', 'networkidle0'],
193
+ });
194
+
195
+ // Small delay for final rendering
196
+ await new Promise(resolve => setTimeout(resolve, 100));
197
+
198
+ const pdfOptions = {
199
+ format,
200
+ landscape,
201
+ margin,
202
+ printBackground,
203
+ displayHeaderFooter,
204
+ headerTemplate,
205
+ footerTemplate,
206
+ };
207
+
208
+ const buffer = await page.pdf(pdfOptions);
209
+
210
+ // Save to file if output path provided
211
+ let outputPath = null;
212
+ if (output) {
213
+ outputPath = output;
214
+ await fs.ensureDir(path.dirname(outputPath));
215
+ await fs.writeFile(outputPath, buffer);
216
+ }
217
+
218
+ return {
219
+ buffer,
220
+ path: outputPath,
221
+ format,
222
+ landscape,
223
+ };
224
+ } finally {
225
+ if (browser) {
226
+ await browser.close();
227
+ }
228
+ }
229
+ }
230
+
231
+ // ============================================================================
232
+ // D2 Diagram Support
233
+ // ============================================================================
234
+
235
+ /**
236
+ * Check if D2 CLI is installed and available
237
+ * @returns {boolean}
238
+ */
239
+ export function isD2Available() {
240
+ try {
241
+ execSync('d2 --version', { stdio: 'pipe' });
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Get D2 CLI version
250
+ * @returns {string|null}
251
+ */
252
+ export function getD2Version() {
253
+ try {
254
+ const output = execSync('d2 --version', { encoding: 'utf-8' });
255
+ return output.trim();
256
+ } catch {
257
+ return null;
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Convert D2 diagram script to SVG
263
+ *
264
+ * @param {string} d2Script - D2 diagram script
265
+ * @param {Object} options - Conversion options
266
+ * @param {number} options.theme - D2 theme ID (default: 200)
267
+ * @param {string} options.layout - Layout engine (default: 'dagre')
268
+ * @param {string} options.output - Output file path (optional)
269
+ * @returns {Promise<{svg: string, path?: string}>}
270
+ */
271
+ export async function d2ToSvg(d2Script, options = {}) {
272
+ if (!isD2Available()) {
273
+ throw new Error(
274
+ 'D2 CLI is not installed. Install it with:\n' +
275
+ ' macOS: brew install d2\n' +
276
+ ' Linux: curl -fsSL https://d2lang.com/install.sh | sh -s --'
277
+ );
278
+ }
279
+
280
+ const {
281
+ theme = DEFAULT_D2_THEME,
282
+ layout = 'dagre',
283
+ output = null,
284
+ pad = 20,
285
+ } = options;
286
+
287
+ // Create temp files
288
+ const tempDir = os.tmpdir();
289
+ const inputFile = path.join(tempDir, `d2-input-${Date.now()}.d2`);
290
+ const outputFile = path.join(tempDir, `d2-output-${Date.now()}.svg`);
291
+
292
+ try {
293
+ // Write D2 script to temp file
294
+ await fs.writeFile(inputFile, d2Script);
295
+
296
+ // Execute D2 CLI
297
+ const command = `d2 --theme=${theme} --layout=${layout} --pad=${pad} "${inputFile}" "${outputFile}"`;
298
+ await execAsync(command);
299
+
300
+ // Read generated SVG
301
+ const svg = await fs.readFile(outputFile, 'utf-8');
302
+
303
+ // Save to final output if provided
304
+ let finalPath = null;
305
+ if (output) {
306
+ finalPath = output;
307
+ await fs.ensureDir(path.dirname(finalPath));
308
+ await fs.writeFile(finalPath, svg);
309
+ }
310
+
311
+ return {
312
+ svg,
313
+ path: finalPath,
314
+ theme,
315
+ layout,
316
+ };
317
+ } finally {
318
+ // Cleanup temp files
319
+ await fs.remove(inputFile).catch(() => {});
320
+ await fs.remove(outputFile).catch(() => {});
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Convert D2 diagram script to PNG
326
+ *
327
+ * @param {string} d2Script - D2 diagram script
328
+ * @param {Object} options - Conversion options
329
+ * @returns {Promise<{buffer: Buffer, path?: string}>}
330
+ */
331
+ export async function d2ToPng(d2Script, options = {}) {
332
+ const { output = null, ...d2Options } = options;
333
+
334
+ // First convert to SVG
335
+ const { svg } = await d2ToSvg(d2Script, d2Options);
336
+
337
+ // Wrap SVG in HTML for Puppeteer rendering
338
+ const html = `
339
+ <!DOCTYPE html>
340
+ <html>
341
+ <head>
342
+ <style>
343
+ * { margin: 0; padding: 0; }
344
+ body {
345
+ display: flex;
346
+ justify-content: center;
347
+ align-items: center;
348
+ background: white;
349
+ min-height: 100vh;
350
+ }
351
+ svg { max-width: 100%; height: auto; }
352
+ </style>
353
+ </head>
354
+ <body>
355
+ ${svg}
356
+ </body>
357
+ </html>
358
+ `.trim();
359
+
360
+ // Convert to PNG
361
+ return await htmlToPng(html, {
362
+ ...options,
363
+ output,
364
+ fullPage: true,
365
+ });
366
+ }
367
+
368
+ // ============================================================================
369
+ // Template System
370
+ // ============================================================================
371
+
372
+ /**
373
+ * Get list of available templates
374
+ * @returns {Object} Object with html and d2 template arrays
375
+ */
376
+ export function getAvailableTemplates() {
377
+ const templatesPath = getTemplatesPath();
378
+ const templates = { html: [], d2: [] };
379
+
380
+ // HTML templates
381
+ const htmlDir = path.join(templatesPath, 'infographics');
382
+ if (fs.existsSync(htmlDir)) {
383
+ templates.html = fs.readdirSync(htmlDir)
384
+ .filter(f => f.endsWith('.html'))
385
+ .map(f => f.replace('.html', ''));
386
+ }
387
+
388
+ // D2 templates
389
+ const d2Dir = path.join(templatesPath, 'diagrams');
390
+ if (fs.existsSync(d2Dir)) {
391
+ templates.d2 = fs.readdirSync(d2Dir)
392
+ .filter(f => f.endsWith('.d2'))
393
+ .map(f => f.replace('.d2', ''));
394
+ }
395
+
396
+ return templates;
397
+ }
398
+
399
+ /**
400
+ * Render an HTML template with data
401
+ *
402
+ * @param {string} templateName - Name of the template (without extension)
403
+ * @param {Object} data - Data to inject into template
404
+ * @returns {string} Rendered HTML
405
+ */
406
+ export function renderTemplate(templateName, data = {}) {
407
+ const templatesPath = getTemplatesPath();
408
+ const templateFile = path.join(templatesPath, 'infographics', `${templateName}.html`);
409
+
410
+ if (!fs.existsSync(templateFile)) {
411
+ throw new Error(`Template not found: ${templateName}`);
412
+ }
413
+
414
+ let html = fs.readFileSync(templateFile, 'utf-8');
415
+
416
+ // Simple template variable replacement: {{variableName}}
417
+ html = html.replace(/\{\{(\w+)\}\}/g, (match, key) => {
418
+ return data[key] !== undefined ? String(data[key]) : match;
419
+ });
420
+
421
+ // Handle arrays for items: {{#items}}...{{/items}}
422
+ html = html.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (match, key, template) => {
423
+ const items = data[key];
424
+ if (!Array.isArray(items)) return '';
425
+
426
+ return items.map(item => {
427
+ let itemHtml = template;
428
+ for (const [k, v] of Object.entries(item)) {
429
+ itemHtml = itemHtml.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), String(v));
430
+ }
431
+ return itemHtml;
432
+ }).join('');
433
+ });
434
+
435
+ // Handle JSON data injection: {{json:variableName}}
436
+ html = html.replace(/\{\{json:(\w+)\}\}/g, (match, key) => {
437
+ return data[key] !== undefined ? JSON.stringify(data[key]) : 'null';
438
+ });
439
+
440
+ return html;
441
+ }
442
+
443
+ /**
444
+ * Render a D2 template with data
445
+ *
446
+ * @param {string} templateName - Name of the template (without extension)
447
+ * @param {Object} data - Data to inject into template
448
+ * @returns {string} Rendered D2 script
449
+ */
450
+ export function renderD2Template(templateName, data = {}) {
451
+ const templatesPath = getTemplatesPath();
452
+ const templateFile = path.join(templatesPath, 'diagrams', `${templateName}.d2`);
453
+
454
+ if (!fs.existsSync(templateFile)) {
455
+ throw new Error(`D2 template not found: ${templateName}`);
456
+ }
457
+
458
+ let d2Script = fs.readFileSync(templateFile, 'utf-8');
459
+
460
+ // Simple variable replacement: {{variableName}}
461
+ d2Script = d2Script.replace(/\{\{(\w+)\}\}/g, (match, key) => {
462
+ return data[key] !== undefined ? String(data[key]) : match;
463
+ });
464
+
465
+ // Handle components array: {{#components}}...{{/components}}
466
+ d2Script = d2Script.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (match, key, template) => {
467
+ const items = data[key];
468
+ if (!Array.isArray(items)) return '';
469
+
470
+ return items.map(item => {
471
+ let itemD2 = template;
472
+ for (const [k, v] of Object.entries(item)) {
473
+ itemD2 = itemD2.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), String(v));
474
+ }
475
+ return itemD2;
476
+ }).join('\n');
477
+ });
478
+
479
+ return d2Script;
480
+ }
481
+
482
+ /**
483
+ * Convert a template to visual output
484
+ *
485
+ * @param {string} templateName - Name of the template
486
+ * @param {Object} data - Data to inject
487
+ * @param {string} format - Output format: 'png', 'pdf', 'svg'
488
+ * @param {Object} options - Additional options
489
+ * @returns {Promise<{buffer?: Buffer, svg?: string, path?: string}>}
490
+ */
491
+ export async function templateToVisual(templateName, data, format = 'png', options = {}) {
492
+ const html = renderTemplate(templateName, data);
493
+
494
+ switch (format.toLowerCase()) {
495
+ case 'png':
496
+ return await htmlToPng(html, options);
497
+ case 'pdf':
498
+ return await htmlToPdf(html, options);
499
+ default:
500
+ throw new Error(`Unsupported format: ${format}. Use 'png' or 'pdf'.`);
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Convert a D2 template to visual output
506
+ *
507
+ * @param {string} templateName - Name of the D2 template
508
+ * @param {Object} data - Data to inject
509
+ * @param {string} format - Output format: 'png', 'svg'
510
+ * @param {Object} options - Additional options
511
+ * @returns {Promise<{buffer?: Buffer, svg?: string, path?: string}>}
512
+ */
513
+ export async function d2TemplateToVisual(templateName, data, format = 'png', options = {}) {
514
+ const d2Script = renderD2Template(templateName, data);
515
+
516
+ switch (format.toLowerCase()) {
517
+ case 'svg':
518
+ return await d2ToSvg(d2Script, options);
519
+ case 'png':
520
+ return await d2ToPng(d2Script, options);
521
+ default:
522
+ throw new Error(`Unsupported format: ${format}. Use 'png' or 'svg'.`);
523
+ }
524
+ }
525
+
526
+ // ============================================================================
527
+ // PPTX Generation
528
+ // ============================================================================
529
+
530
+ /**
531
+ * Create a PPTX presentation from HTML slides
532
+ *
533
+ * @param {Array<{html: string, title?: string}>} slides - Array of slide objects
534
+ * @param {Object} options - Presentation options
535
+ * @param {string} options.output - Output file path
536
+ * @param {string} options.title - Presentation title
537
+ * @param {string} options.author - Author name
538
+ * @param {Object} options.slideSize - Slide dimensions
539
+ * @returns {Promise<{buffer: Buffer, path?: string}>}
540
+ */
541
+ export async function htmlToPptx(slides, options = {}) {
542
+ const {
543
+ output = null,
544
+ title = 'Presentation',
545
+ author = 'MyAIDev Method',
546
+ subject = '',
547
+ slideSize = { width: 10, height: 7.5 }, // inches (standard 4:3)
548
+ } = options;
549
+
550
+ const pptx = new PptxGenJS();
551
+
552
+ // Set presentation properties
553
+ pptx.title = title;
554
+ pptx.author = author;
555
+ pptx.subject = subject;
556
+ pptx.layout = 'LAYOUT_WIDE'; // 13.33 x 7.5 inches (16:9)
557
+
558
+ // Process each slide
559
+ for (let i = 0; i < slides.length; i++) {
560
+ const slideData = slides[i];
561
+ const { html, title: slideTitle } = slideData;
562
+
563
+ // Render HTML to PNG
564
+ const { buffer } = await htmlToPng(html, {
565
+ width: 1920,
566
+ height: 1080,
567
+ deviceScaleFactor: 2,
568
+ });
569
+
570
+ // Create slide
571
+ const slide = pptx.addSlide();
572
+
573
+ // Add background image (the rendered HTML)
574
+ const base64 = buffer.toString('base64');
575
+ slide.addImage({
576
+ data: `data:image/png;base64,${base64}`,
577
+ x: 0,
578
+ y: 0,
579
+ w: '100%',
580
+ h: '100%',
581
+ });
582
+
583
+ // Optionally add title as text box for accessibility
584
+ if (slideTitle) {
585
+ slide.addText(slideTitle, {
586
+ x: 0.5,
587
+ y: 0.3,
588
+ w: '90%',
589
+ fontSize: 24,
590
+ bold: true,
591
+ color: '363636',
592
+ transparency: 100, // Hidden but accessible
593
+ });
594
+ }
595
+ }
596
+
597
+ // Generate PPTX buffer
598
+ const buffer = await pptx.write({ outputType: 'nodebuffer' });
599
+
600
+ // Save to file if output path provided
601
+ let outputPath = null;
602
+ if (output) {
603
+ outputPath = output;
604
+ await fs.ensureDir(path.dirname(outputPath));
605
+ await fs.writeFile(outputPath, buffer);
606
+ }
607
+
608
+ return {
609
+ buffer,
610
+ path: outputPath,
611
+ slideCount: slides.length,
612
+ };
613
+ }
614
+
615
+ /**
616
+ * Create PPTX from D2 diagrams
617
+ *
618
+ * @param {Array<{d2: string, title?: string}>} slides - Array of D2 slide objects
619
+ * @param {Object} options - Presentation options
620
+ * @returns {Promise<{buffer: Buffer, path?: string}>}
621
+ */
622
+ export async function d2ToPptx(slides, options = {}) {
623
+ // Convert D2 scripts to HTML slides
624
+ const htmlSlides = await Promise.all(
625
+ slides.map(async ({ d2, title }) => {
626
+ const { svg } = await d2ToSvg(d2);
627
+ const html = `
628
+ <!DOCTYPE html>
629
+ <html>
630
+ <head>
631
+ <style>
632
+ * { margin: 0; padding: 0; }
633
+ body {
634
+ display: flex;
635
+ flex-direction: column;
636
+ justify-content: center;
637
+ align-items: center;
638
+ background: white;
639
+ width: 1920px;
640
+ height: 1080px;
641
+ padding: 40px;
642
+ box-sizing: border-box;
643
+ }
644
+ h1 {
645
+ font-family: system-ui, sans-serif;
646
+ font-size: 48px;
647
+ margin-bottom: 40px;
648
+ color: #333;
649
+ }
650
+ .diagram {
651
+ flex: 1;
652
+ display: flex;
653
+ justify-content: center;
654
+ align-items: center;
655
+ width: 100%;
656
+ }
657
+ svg { max-width: 100%; max-height: 100%; }
658
+ </style>
659
+ </head>
660
+ <body>
661
+ ${title ? `<h1>${title}</h1>` : ''}
662
+ <div class="diagram">${svg}</div>
663
+ </body>
664
+ </html>
665
+ `.trim();
666
+ return { html, title };
667
+ })
668
+ );
669
+
670
+ return await htmlToPptx(htmlSlides, options);
671
+ }
672
+
673
+ // ============================================================================
674
+ // Utility Functions
675
+ // ============================================================================
676
+
677
+ /**
678
+ * Validate HTML string
679
+ * @param {string} html - HTML to validate
680
+ * @returns {{valid: boolean, errors: string[]}}
681
+ */
682
+ export function validateHtml(html) {
683
+ const errors = [];
684
+
685
+ if (!html || typeof html !== 'string') {
686
+ errors.push('HTML must be a non-empty string');
687
+ return { valid: false, errors };
688
+ }
689
+
690
+ if (!html.includes('<')) {
691
+ errors.push('HTML appears to have no tags');
692
+ }
693
+
694
+ // Check for common issues
695
+ const openTags = (html.match(/<[a-z][^>]*[^/]>/gi) || []).length;
696
+ const closeTags = (html.match(/<\/[a-z]+>/gi) || []).length;
697
+
698
+ if (openTags > closeTags + 5) {
699
+ errors.push('HTML may have unclosed tags');
700
+ }
701
+
702
+ return {
703
+ valid: errors.length === 0,
704
+ errors,
705
+ };
706
+ }
707
+
708
+ /**
709
+ * Validate D2 script
710
+ * @param {string} d2Script - D2 script to validate
711
+ * @returns {{valid: boolean, errors: string[]}}
712
+ */
713
+ export function validateD2(d2Script) {
714
+ const errors = [];
715
+
716
+ if (!d2Script || typeof d2Script !== 'string') {
717
+ errors.push('D2 script must be a non-empty string');
718
+ return { valid: false, errors };
719
+ }
720
+
721
+ // Basic D2 syntax checks
722
+ if (d2Script.trim().length === 0) {
723
+ errors.push('D2 script is empty');
724
+ }
725
+
726
+ // Check for common syntax patterns
727
+ const hasArrows = d2Script.includes('->') || d2Script.includes('<-');
728
+ const hasColons = d2Script.includes(':');
729
+ const hasNodes = /\w+/.test(d2Script);
730
+
731
+ if (!hasNodes) {
732
+ errors.push('D2 script must define at least one node');
733
+ }
734
+
735
+ return {
736
+ valid: errors.length === 0,
737
+ errors,
738
+ };
739
+ }
740
+
741
+ /**
742
+ * Estimate render time based on HTML complexity
743
+ * @param {string} html - HTML content
744
+ * @returns {number} Estimated time in milliseconds
745
+ */
746
+ export function estimateRenderTime(html) {
747
+ const baseTime = 500; // Base Puppeteer overhead
748
+
749
+ // Add time for content complexity
750
+ const charCount = html.length;
751
+ const tagCount = (html.match(/<[^>]+>/g) || []).length;
752
+ const imageCount = (html.match(/<img/gi) || []).length;
753
+ const svgCount = (html.match(/<svg/gi) || []).length;
754
+
755
+ let estimate = baseTime;
756
+ estimate += Math.min(charCount / 100, 500); // Up to 500ms for large content
757
+ estimate += tagCount * 0.5; // 0.5ms per tag
758
+ estimate += imageCount * 200; // 200ms per image (network)
759
+ estimate += svgCount * 50; // 50ms per SVG
760
+
761
+ return Math.round(estimate);
762
+ }
763
+
764
+ /**
765
+ * Generate a unique filename for output
766
+ * @param {string} type - Type of content (e.g., 'infographic', 'diagram')
767
+ * @param {string} slug - Content slug
768
+ * @param {string} extension - File extension
769
+ * @returns {string} Unique filename
770
+ */
771
+ export function generateFilename(type, slug, extension) {
772
+ const timestamp = Date.now().toString(36);
773
+ const random = Math.random().toString(36).substring(2, 6);
774
+ const safeSlug = slug
775
+ .toLowerCase()
776
+ .replace(/[^a-z0-9]+/g, '-')
777
+ .replace(/^-|-$/g, '')
778
+ .substring(0, 50);
779
+
780
+ return `${type}-${safeSlug}-${timestamp}-${random}.${extension}`;
781
+ }
782
+
783
+ /**
784
+ * Save visual to assets directory with proper organization
785
+ * @param {Buffer} buffer - File buffer
786
+ * @param {Object} metadata - File metadata
787
+ * @returns {Promise<string>} Output file path
788
+ */
789
+ export async function saveToAssets(buffer, metadata = {}) {
790
+ const {
791
+ type = 'visual',
792
+ slug = 'output',
793
+ extension = 'png',
794
+ category = 'images', // images, documents, diagrams
795
+ } = metadata;
796
+
797
+ const assetsPath = getAssetsPath();
798
+ const date = new Date().toISOString().split('T')[0];
799
+ const outputDir = path.join(assetsPath, category, date);
800
+
801
+ await fs.ensureDir(outputDir);
802
+
803
+ const filename = generateFilename(type, slug, extension);
804
+ const outputPath = path.join(outputDir, filename);
805
+
806
+ await fs.writeFile(outputPath, buffer);
807
+
808
+ return outputPath;
809
+ }
810
+
811
+ // ============================================================================
812
+ // Default Export
813
+ // ============================================================================
814
+
815
+ export default {
816
+ // HTML Conversion
817
+ htmlToPng,
818
+ htmlToPdf,
819
+ htmlToPptx,
820
+
821
+ // D2 Conversion
822
+ isD2Available,
823
+ getD2Version,
824
+ d2ToSvg,
825
+ d2ToPng,
826
+ d2ToPptx,
827
+
828
+ // Templates
829
+ getAvailableTemplates,
830
+ renderTemplate,
831
+ renderD2Template,
832
+ templateToVisual,
833
+ d2TemplateToVisual,
834
+
835
+ // Utilities
836
+ validateHtml,
837
+ validateD2,
838
+ estimateRenderTime,
839
+ generateFilename,
840
+ saveToAssets,
841
+ getTemplatesPath,
842
+ getAssetsPath,
843
+ };