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.
- package/USER_GUIDE.md +453 -48
- package/bin/cli.js +236 -38
- package/content-rules.example.md +80 -0
- package/dist/mcp/mcp-launcher.js +237 -0
- package/dist/server/.tsbuildinfo +1 -1
- package/dist/server/auth/layers.d.ts +1 -1
- package/dist/server/auth/services/AuthService.d.ts +1 -1
- package/dist/server/auth/services/TokenService.js.map +1 -1
- package/dist/server/auth/services/example.d.ts +5 -5
- package/package.json +22 -17
- package/src/config/workflows.js +28 -44
- package/src/index.js +21 -8
- package/src/lib/ascii-banner.js +214 -0
- package/src/lib/config-manager.js +470 -0
- package/src/lib/content-generator.js +427 -0
- package/src/lib/html-conversion-utils.js +843 -0
- package/src/lib/seo-optimizer.js +515 -0
- package/src/lib/update-manager.js +2 -1
- package/src/lib/visual-config-utils.js +321 -295
- package/src/lib/visual-generation-utils.js +1000 -811
- package/src/lib/wordpress-client.js +633 -0
- package/src/lib/workflow-installer.js +3 -3
- package/src/scripts/configure-wordpress-mcp.js +8 -3
- package/src/scripts/generate-visual-cli.js +365 -235
- package/src/scripts/html-conversion-cli.js +526 -0
- package/src/scripts/init/configure.js +436 -0
- package/src/scripts/init/install.js +460 -0
- package/src/scripts/ping.js +250 -0
- package/src/scripts/utils/file-utils.js +404 -0
- package/src/scripts/utils/logger.js +300 -0
- package/src/scripts/utils/write-content.js +293 -0
- package/src/scripts/wordpress/publish-to-wordpress.js +165 -0
- package/src/server/auth/services/TokenService.ts +1 -1
- package/src/templates/claude/agents/content-rules-setup.md +657 -0
- package/src/templates/claude/agents/content-writer.md +328 -1
- package/src/templates/claude/agents/visual-content-generator.md +311 -8
- package/src/templates/claude/commands/myai-configure.md +1 -1
- package/src/templates/claude/commands/myai-content-rules-setup.md +204 -0
- package/src/templates/claude/commands/myai-convert-html.md +186 -0
- package/src/templates/codex/commands/myai-content-rules-setup.md +85 -0
- package/src/templates/diagrams/architecture.d2 +52 -0
- package/src/templates/diagrams/flowchart.d2 +42 -0
- package/src/templates/diagrams/sequence.d2 +47 -0
- package/src/templates/docs/content-creation-guide.md +164 -0
- package/src/templates/docs/deployment-guide.md +336 -0
- package/src/templates/docs/visual-generation-guide.md +248 -0
- package/src/templates/docs/wordpress-publishing-guide.md +208 -0
- package/src/templates/gemini/commands/myai-content-rules-setup.toml +57 -0
- package/src/templates/infographics/comparison-table.html +347 -0
- package/src/templates/infographics/data-chart.html +268 -0
- package/src/templates/infographics/process-flow.html +365 -0
- package/.claude/mcp/sparc-orchestrator-server.js +0 -607
- package/.claude/mcp/wordpress-server.js +0 -1277
- package/src/agents/content-writer-prompt.md +0 -164
- package/src/agents/content-writer.json +0 -70
- package/src/templates/claude/mcp_config.json +0 -74
- package/src/templates/claude/slash_commands.json +0 -166
- package/src/templates/scripts/configure-wordpress-mcp.js +0 -181
- /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
|
+
};
|