teamshare-bridge 0.26.25 → 0.27.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 (41) hide show
  1. package/dist/cli/commands/build.js +48 -22
  2. package/dist/cli/commands/build.js.map +1 -1
  3. package/dist/cli/commands/plan.js +40 -2
  4. package/dist/cli/commands/plan.js.map +1 -1
  5. package/dist/cli/commands/planner.js +3 -1
  6. package/dist/cli/commands/planner.js.map +1 -1
  7. package/dist/cli/commands/research.js +38 -1
  8. package/dist/cli/commands/research.js.map +1 -1
  9. package/dist/cli/commands/run.js +54 -3
  10. package/dist/cli/commands/run.js.map +1 -1
  11. package/dist/cli/commands/worker.js +52 -59
  12. package/dist/cli/commands/worker.js.map +1 -1
  13. package/dist/cli/commands/workflow.js +36 -2
  14. package/dist/cli/commands/workflow.js.map +1 -1
  15. package/dist/lib/api-content.d.ts +38 -0
  16. package/dist/lib/api-content.js +14 -0
  17. package/dist/lib/api-content.js.map +1 -1
  18. package/dist/lib/brief.d.ts +12 -0
  19. package/dist/lib/brief.js +10 -7
  20. package/dist/lib/brief.js.map +1 -1
  21. package/dist/lib/git/git-finalize.d.ts +2 -0
  22. package/dist/lib/git/git-finalize.js +32 -5
  23. package/dist/lib/git/git-finalize.js.map +1 -1
  24. package/dist/lib/git/git-pipeline.d.ts +3 -2
  25. package/dist/lib/git/git-pipeline.js.map +1 -1
  26. package/dist/lib/git/git-presence.js +1 -1
  27. package/dist/lib/git/git-presence.js.map +1 -1
  28. package/dist/lib/git/index.d.ts +2 -2
  29. package/dist/lib/git/index.js +4 -1
  30. package/dist/lib/git/index.js.map +1 -1
  31. package/dist/lib/git/pr-token.d.ts +74 -16
  32. package/dist/lib/git/pr-token.js +160 -16
  33. package/dist/lib/git/pr-token.js.map +1 -1
  34. package/dist/lib/run-report.d.ts +38 -11
  35. package/dist/lib/run-report.js +137 -88
  36. package/dist/lib/run-report.js.map +1 -1
  37. package/dist/lib/tools/pptx-deck.d.ts +90 -0
  38. package/dist/lib/tools/pptx-deck.js +1302 -147
  39. package/dist/lib/tools/pptx-deck.js.map +1 -1
  40. package/package.json +1 -1
  41. package/skills/teamshare-cdd/SKILL.md +219 -215
@@ -3,14 +3,264 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.PptxDeckTool = void 0;
6
+ exports.PptxDeckTool = exports.DENSITY_CAPS = exports.FALLBACK_TOKENS = exports.KNOWN_LAYOUTS = void 0;
7
+ exports.loadTemplateMirror = loadTemplateMirror;
8
+ exports.resolveDeckTokens = resolveDeckTokens;
9
+ exports.parseDeckSpec = parseDeckSpec;
10
+ exports.planDeck = planDeck;
7
11
  /**
8
- * Slide Deck Generator tool.
9
- * Creates PowerPoint presentations from markdown outlines using pptxgenjs.
10
- * H1 = document title, H2 = slide title, H3 = bullet points.
12
+ * Slide Deck renderer (node tool `pptx-render`).
13
+ *
14
+ * Chain tool for the `pptx-deck` catalog entry: the harness authors a
15
+ * `deck.json` spec, and this renderer turns it into a themed .pptx via
16
+ * pptxgenjs plus an optional `— Speaker Notes.md` sidecar.
17
+ *
18
+ * deck.json shape (backend contract, see tools registry `briefGuide: 'deck'`):
19
+ * { slides: [{ layout, title?, subtitle?, bullets?, columns?, cards?,
20
+ * stats?, rows?, quote?, notes? }] }
21
+ * layouts: title|agenda|section|bullets|statement|two-column|cards|
22
+ * stats|timeline|quote|table|closing|sources
23
+ *
24
+ * Params: deckJson (text or object), template (id in deck-templates.json),
25
+ * aspect ('16:9'|'4:3'), accentColor (overrides template accent),
26
+ * density ('auto'|'airy'|'normal'|'compact'; 'auto' = template default),
27
+ * outputName (`<Task subject> (YYYY-MM-DD)`), outlineMd (legacy fallback).
11
28
  */
12
29
  const pptxgenjs_1 = __importDefault(require("pptxgenjs"));
30
+ const node_fs_1 = require("node:fs");
31
+ const node_path_1 = require("node:path");
13
32
  const output_name_1 = require("./output-name");
33
+ exports.KNOWN_LAYOUTS = [
34
+ 'title',
35
+ 'agenda',
36
+ 'section',
37
+ 'bullets',
38
+ 'statement',
39
+ 'two-column',
40
+ 'cards',
41
+ 'stats',
42
+ 'timeline',
43
+ 'quote',
44
+ 'table',
45
+ 'closing',
46
+ 'sources',
47
+ ];
48
+ /** Built-in corporate-equivalent defaults (used only when the mirror file is unreadable). */
49
+ exports.FALLBACK_TOKENS = {
50
+ accent: '1E3A5F',
51
+ background: 'FFFFFF',
52
+ surface: 'F1F5F9',
53
+ textColor: '0F172A',
54
+ bodyFont: 'Calibri',
55
+ headingFont: 'Calibri',
56
+ coverStyle: 'banner',
57
+ titleStyle: 'rule',
58
+ footerStyle: 'rule',
59
+ cornerStyle: 'square',
60
+ density: 'normal',
61
+ };
62
+ const COVER_STYLES = [
63
+ 'banner',
64
+ 'centered',
65
+ 'split',
66
+ 'minimal',
67
+ 'frame',
68
+ 'band',
69
+ ];
70
+ const TITLE_STYLES = ['rule', 'bar', 'band', 'hairline', 'numbered', 'plain'];
71
+ const FOOTER_STYLES = ['rule', 'dots', 'number', 'plain'];
72
+ const CORNER_STYLES = ['square', 'rounded', 'pill'];
73
+ const DENSITIES = ['airy', 'normal', 'compact'];
74
+ /** Per-density content budgets. Exported for tests. */
75
+ exports.DENSITY_CAPS = {
76
+ airy: { maxBullets: 4, maxWords: 8 },
77
+ normal: { maxBullets: 5, maxWords: 10 },
78
+ compact: { maxBullets: 6, maxWords: 12 },
79
+ };
80
+ function stripMd(raw) {
81
+ return raw
82
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
83
+ .replace(/\*([^*]+)\*/g, '$1')
84
+ .replace(/`([^`]+)`/g, '$1')
85
+ .trim();
86
+ }
87
+ function asStringArray(value) {
88
+ if (!Array.isArray(value))
89
+ return [];
90
+ return value
91
+ .map((v) => (typeof v === 'string' ? stripMd(v) : ''))
92
+ .filter((s) => s.length > 0);
93
+ }
94
+ function normalizeHex(raw, fallback) {
95
+ if (typeof raw !== 'string' || !raw.trim())
96
+ return { color: fallback, changed: false };
97
+ const cleaned = raw.trim().replace(/^#/, '');
98
+ if (/^[0-9a-fA-F]{6}$/.test(cleaned))
99
+ return { color: cleaned.toUpperCase(), changed: true };
100
+ return { color: fallback, changed: false };
101
+ }
102
+ /**
103
+ * Load the template mirror at runtime (never statically imported: tsc does
104
+ * not emit JSON assets into dist, so a static import would break at runtime).
105
+ * Returns null when the file cannot be found or parsed.
106
+ */
107
+ function loadTemplateMirror() {
108
+ const candidates = [
109
+ (0, node_path_1.join)(__dirname, 'deck-templates.json'),
110
+ (0, node_path_1.join)(process.cwd(), 'src', 'lib', 'tools', 'deck-templates.json'),
111
+ (0, node_path_1.join)((0, node_path_1.dirname)(__dirname), '..', '..', 'src', 'lib', 'tools', 'deck-templates.json'),
112
+ ];
113
+ for (const path of candidates) {
114
+ try {
115
+ if (!(0, node_fs_1.existsSync)(path))
116
+ continue;
117
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
118
+ if (parsed &&
119
+ Array.isArray(parsed.templates) &&
120
+ parsed.templates.length > 0) {
121
+ return { templates: parsed.templates };
122
+ }
123
+ }
124
+ catch {
125
+ /* try next candidate */
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+ function coerceDensity(raw, fallback) {
131
+ if (typeof raw === 'string' && DENSITIES.includes(raw)) {
132
+ return { density: raw, changed: raw !== fallback };
133
+ }
134
+ return { density: fallback, changed: false };
135
+ }
136
+ /** Resolve template tokens: mirror lookup + accentColor/density overrides. */
137
+ function resolveDeckTokens(params) {
138
+ const warnings = [];
139
+ const requestedId = typeof params.template === 'string' ? params.template : 'corporate';
140
+ const mirror = loadTemplateMirror();
141
+ const entry = mirror?.templates.find((t) => t.id === requestedId);
142
+ if (requestedId && !entry) {
143
+ warnings.push(`unknown template "${requestedId}", fell back to corporate defaults`);
144
+ }
145
+ const preview = entry?.preview ?? {};
146
+ const base = {
147
+ accent: normalizeHex(preview.accent, exports.FALLBACK_TOKENS.accent).color,
148
+ background: normalizeHex(preview.background, exports.FALLBACK_TOKENS.background)
149
+ .color,
150
+ surface: normalizeHex(preview.surface, exports.FALLBACK_TOKENS.surface).color,
151
+ textColor: normalizeHex(preview.textColor, exports.FALLBACK_TOKENS.textColor).color,
152
+ bodyFont: typeof preview.bodyFont === 'string' && preview.bodyFont
153
+ ? preview.bodyFont
154
+ : exports.FALLBACK_TOKENS.bodyFont,
155
+ headingFont: typeof preview.headingFont === 'string' && preview.headingFont
156
+ ? preview.headingFont
157
+ : exports.FALLBACK_TOKENS.headingFont,
158
+ coverStyle: typeof preview.coverStyle === 'string' &&
159
+ COVER_STYLES.includes(preview.coverStyle)
160
+ ? preview.coverStyle
161
+ : exports.FALLBACK_TOKENS.coverStyle,
162
+ titleStyle: typeof preview.titleStyle === 'string' &&
163
+ TITLE_STYLES.includes(preview.titleStyle)
164
+ ? preview.titleStyle
165
+ : exports.FALLBACK_TOKENS.titleStyle,
166
+ footerStyle: typeof preview.footerStyle === 'string' &&
167
+ FOOTER_STYLES.includes(preview.footerStyle)
168
+ ? preview.footerStyle
169
+ : exports.FALLBACK_TOKENS.footerStyle,
170
+ cornerStyle: typeof preview.cornerStyle === 'string' &&
171
+ CORNER_STYLES.includes(preview.cornerStyle)
172
+ ? preview.cornerStyle
173
+ : exports.FALLBACK_TOKENS.cornerStyle,
174
+ density: typeof preview.density === 'string' &&
175
+ DENSITIES.includes(preview.density)
176
+ ? preview.density
177
+ : exports.FALLBACK_TOKENS.density,
178
+ };
179
+ const accent = normalizeHex(params.accentColor, base.accent);
180
+ if (typeof params.accentColor === 'string' &&
181
+ params.accentColor &&
182
+ !accent.changed) {
183
+ warnings.push(`ignored invalid accentColor "${params.accentColor}"`);
184
+ }
185
+ base.accent = accent.color;
186
+ const densityRaw = params.density;
187
+ if (typeof densityRaw === 'string' && densityRaw !== 'auto') {
188
+ base.density = coerceDensity(densityRaw, base.density).density;
189
+ }
190
+ return { tokens: base, templateId: entry?.id ?? 'corporate', warnings };
191
+ }
192
+ /** Parse deckJson (text or object). Throws on invalid/empty specs. */
193
+ function parseDeckSpec(raw) {
194
+ let parsed = raw;
195
+ if (typeof raw === 'string') {
196
+ const trimmed = raw.trim();
197
+ if (!trimmed)
198
+ throw new Error('deckJson is empty');
199
+ // Harness may wrap the handoff in a code fence.
200
+ const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)```$/);
201
+ parsed = JSON.parse(fenced ? fenced[1] : trimmed);
202
+ }
203
+ if (!parsed || typeof parsed !== 'object')
204
+ throw new Error('deckJson must be an object with a slides array');
205
+ const slides = parsed.slides;
206
+ if (!Array.isArray(slides) || slides.length === 0) {
207
+ throw new Error('deckJson must contain a non-empty slides array');
208
+ }
209
+ const title = parsed.title;
210
+ return {
211
+ title: typeof title === 'string' ? title : undefined,
212
+ slides: slides,
213
+ };
214
+ }
215
+ function trimBullets(bullets, density) {
216
+ const cap = exports.DENSITY_CAPS[density];
217
+ return bullets.slice(0, cap.maxBullets).map((b) => {
218
+ const words = b.split(/\s+/);
219
+ if (words.length <= cap.maxWords)
220
+ return b;
221
+ return `${words.slice(0, cap.maxWords).join(' ')}…`;
222
+ });
223
+ }
224
+ /**
225
+ * Pure layout planner: per-slide sizes/spacing/columns from density + aspect.
226
+ * No pptxgenjs calls — safe to unit-test.
227
+ */
228
+ function planDeck(spec, opts) {
229
+ const base = opts.density === 'airy'
230
+ ? { title: 32, body: 18 }
231
+ : opts.density === 'compact'
232
+ ? { title: 24, body: 14 }
233
+ : { title: 28, body: 16 };
234
+ const shrink = opts.aspect === '4:3' ? 2 : 0;
235
+ return spec.slides.map((slide, index) => {
236
+ const rawLayout = typeof slide.layout === 'string' ? slide.layout : 'bullets';
237
+ const known = exports.KNOWN_LAYOUTS.includes(rawLayout);
238
+ const layout = (known ? rawLayout : 'bullets');
239
+ const bullets = trimBullets(asStringArray(slide.bullets), opts.density);
240
+ let columns = 1;
241
+ if (layout === 'two-column' && Array.isArray(slide.columns))
242
+ columns = Math.min(Math.max(slide.columns.length, 1), 2);
243
+ else if (layout === 'cards' && Array.isArray(slide.cards))
244
+ columns = Math.min(Math.max(slide.cards.length, 1), 4);
245
+ else if (layout === 'stats' && Array.isArray(slide.stats))
246
+ columns = Math.min(Math.max(slide.stats.length, 1), 4);
247
+ const statementBump = layout === 'statement' ||
248
+ layout === 'quote' ||
249
+ layout === 'title' ||
250
+ layout === 'closing'
251
+ ? 6
252
+ : 0;
253
+ return {
254
+ index,
255
+ layout,
256
+ ...(known ? {} : { degradedFrom: rawLayout }),
257
+ titleSize: base.title + statementBump - shrink,
258
+ bodySize: base.body - (shrink > 0 ? 1 : 0),
259
+ columns,
260
+ bullets,
261
+ };
262
+ });
263
+ }
14
264
  function parseOutline(markdown) {
15
265
  const lines = markdown.split(/\r?\n/);
16
266
  let docTitle = 'Presentation';
@@ -26,192 +276,1097 @@ function parseOutline(markdown) {
26
276
  const bullet = trimmed.match(/^[-*+]\s+(.*)$/);
27
277
  const numbered = trimmed.match(/^\d+[.)]\s+(.*)$/);
28
278
  if (h1) {
29
- docTitle = h1[1].replace(/\*\*/g, '');
279
+ docTitle = stripMd(h1[1]);
30
280
  }
31
281
  else if (h2) {
32
282
  if (current)
33
283
  slides.push(current);
34
- current = {
35
- title: h2[1].replace(/\*\*/g, ''),
36
- bullets: [],
37
- };
284
+ current = { title: stripMd(h2[1]), bullets: [] };
38
285
  }
39
286
  else if (h3) {
40
- if (!current) {
287
+ if (!current)
41
288
  current = { title: '', bullets: [] };
42
- }
43
- current.notes = h3[1];
289
+ current.notes = stripMd(h3[1]);
44
290
  }
45
291
  else if ((bullet || numbered) && current) {
46
- const text = (bullet ? bullet[1] : numbered[1])
47
- .replace(/\*\*([^*]+)\*\*/g, '$1')
48
- .replace(/\*([^*]+)\*/g, '$1');
49
- current.bullets.push(text);
292
+ current.bullets.push(stripMd(bullet ? bullet[1] : numbered[1]));
50
293
  }
51
294
  else if (current && !current.title) {
52
- current.title = trimmed.replace(/\*\*/g, '');
295
+ current.title = stripMd(trimmed);
53
296
  }
54
297
  }
55
298
  if (current)
56
299
  slides.push(current);
57
300
  return { docTitle, slides };
58
301
  }
59
- const THEMES = {
60
- default: {
61
- bg: 'FFFFFF',
62
- color: '374151',
63
- accent: '16a34a',
64
- titleColor: '111827',
65
- },
66
- dark: {
67
- bg: '1f2937',
68
- color: 'e5e7eb',
69
- accent: '3b82f6',
70
- titleColor: 'f9fafb',
71
- },
72
- ocean: {
73
- bg: 'F0F9FF',
74
- color: '1e3a5f',
75
- accent: '0284c7',
76
- titleColor: '0c4a6e',
77
- },
78
- forest: {
79
- bg: 'F0FDF4',
80
- color: '14532d',
81
- accent: '16a34a',
82
- titleColor: '166534',
83
- },
84
- sunset: {
85
- bg: 'FFF7ED',
86
- color: '7c2d12',
87
- accent: 'ea580c',
88
- titleColor: '9a3412',
89
- },
90
- };
302
+ function shapeFor(tokens, pptx) {
303
+ if (tokens.cornerStyle === 'square')
304
+ return pptx.ShapeType.rect;
305
+ return pptx.ShapeType.roundRect;
306
+ }
307
+ function addTitleBlock(slide, pptx, tokens, title, slideNo, plan) {
308
+ const accent = tokens.accent;
309
+ const heading = tokens.headingFont;
310
+ switch (tokens.titleStyle) {
311
+ case 'bar':
312
+ slide.addShape(pptx.ShapeType.rect, {
313
+ x: '7%',
314
+ y: '7%',
315
+ w: '1.2%',
316
+ h: '12%',
317
+ fill: { color: accent },
318
+ });
319
+ slide.addText(title, {
320
+ x: '10%',
321
+ y: '7%',
322
+ w: '82%',
323
+ h: '12%',
324
+ fontSize: plan.titleSize,
325
+ fontFace: heading,
326
+ color: tokens.textColor,
327
+ bold: true,
328
+ });
329
+ break;
330
+ case 'band':
331
+ slide.addShape(pptx.ShapeType.rect, {
332
+ x: '0%',
333
+ y: '5%',
334
+ w: '100%',
335
+ h: '16%',
336
+ fill: { color: tokens.surface },
337
+ });
338
+ slide.addText(title, {
339
+ x: '8%',
340
+ y: '6%',
341
+ w: '84%',
342
+ h: '14%',
343
+ fontSize: plan.titleSize,
344
+ fontFace: heading,
345
+ color: accent,
346
+ bold: true,
347
+ });
348
+ break;
349
+ case 'hairline':
350
+ slide.addText(title, {
351
+ x: '8%',
352
+ y: '7%',
353
+ w: '84%',
354
+ h: '12%',
355
+ fontSize: plan.titleSize,
356
+ fontFace: heading,
357
+ color: tokens.textColor,
358
+ bold: false,
359
+ });
360
+ slide.addShape(pptx.ShapeType.rect, {
361
+ x: '8%',
362
+ y: '20%',
363
+ w: '84%',
364
+ h: '0.3%',
365
+ fill: { color: accent, transparency: 60 },
366
+ });
367
+ break;
368
+ case 'numbered':
369
+ slide.addText(`${slideNo}`, {
370
+ x: '8%',
371
+ y: '6%',
372
+ w: '10%',
373
+ h: '14%',
374
+ fontSize: plan.titleSize + 6,
375
+ fontFace: heading,
376
+ color: accent,
377
+ bold: true,
378
+ });
379
+ slide.addText(title, {
380
+ x: '16%',
381
+ y: '6%',
382
+ w: '76%',
383
+ h: '14%',
384
+ fontSize: plan.titleSize,
385
+ fontFace: heading,
386
+ color: tokens.textColor,
387
+ bold: true,
388
+ });
389
+ break;
390
+ case 'plain':
391
+ slide.addText(title, {
392
+ x: '8%',
393
+ y: '7%',
394
+ w: '84%',
395
+ h: '12%',
396
+ fontSize: plan.titleSize,
397
+ fontFace: heading,
398
+ color: tokens.textColor,
399
+ bold: true,
400
+ });
401
+ break;
402
+ case 'rule':
403
+ default:
404
+ slide.addText(title, {
405
+ x: '8%',
406
+ y: '7%',
407
+ w: '84%',
408
+ h: '12%',
409
+ fontSize: plan.titleSize,
410
+ fontFace: heading,
411
+ color: tokens.textColor,
412
+ bold: true,
413
+ });
414
+ slide.addShape(pptx.ShapeType.rect, {
415
+ x: '8%',
416
+ y: '20%',
417
+ w: '15%',
418
+ h: '0.6%',
419
+ fill: { color: accent },
420
+ });
421
+ break;
422
+ }
423
+ }
424
+ function addFooter(slide, pptx, tokens, slideNo) {
425
+ const accent = tokens.accent;
426
+ switch (tokens.footerStyle) {
427
+ case 'rule':
428
+ slide.addShape(pptx.ShapeType.rect, {
429
+ x: '8%',
430
+ y: '92%',
431
+ w: '84%',
432
+ h: '0.3%',
433
+ fill: { color: tokens.textColor, transparency: 80 },
434
+ });
435
+ slide.addText(`${slideNo}`, {
436
+ x: '88%',
437
+ y: '93%',
438
+ w: '4%',
439
+ h: '4%',
440
+ fontSize: 9,
441
+ fontFace: tokens.bodyFont,
442
+ color: tokens.textColor,
443
+ transparency: 50,
444
+ align: 'right',
445
+ });
446
+ break;
447
+ case 'dots':
448
+ slide.addShape(pptx.ShapeType.ellipse, {
449
+ x: '8%',
450
+ y: '93.5%',
451
+ w: '1%',
452
+ h: '1.6%',
453
+ fill: { color: accent },
454
+ });
455
+ slide.addShape(pptx.ShapeType.ellipse, {
456
+ x: '9.6%',
457
+ y: '93.5%',
458
+ w: '1%',
459
+ h: '1.6%',
460
+ fill: { color: accent, transparency: 65 },
461
+ });
462
+ slide.addText(`${slideNo}`, {
463
+ x: '88%',
464
+ y: '93%',
465
+ w: '4%',
466
+ h: '4%',
467
+ fontSize: 9,
468
+ fontFace: tokens.bodyFont,
469
+ color: tokens.textColor,
470
+ transparency: 50,
471
+ align: 'right',
472
+ });
473
+ break;
474
+ case 'number':
475
+ slide.addText(`${slideNo}`, {
476
+ x: '88%',
477
+ y: '93%',
478
+ w: '4%',
479
+ h: '4%',
480
+ fontSize: 10,
481
+ fontFace: tokens.bodyFont,
482
+ color: accent,
483
+ bold: true,
484
+ align: 'right',
485
+ });
486
+ break;
487
+ case 'plain':
488
+ default:
489
+ break;
490
+ }
491
+ }
492
+ function bulletRuns(bullets, tokens, fontSize) {
493
+ return bullets.map((b) => ({
494
+ text: b,
495
+ options: {
496
+ fontSize,
497
+ fontFace: tokens.bodyFont,
498
+ color: tokens.textColor,
499
+ bullet: { code: '2022' },
500
+ paraSpaceAfter: 8,
501
+ },
502
+ }));
503
+ }
504
+ function addBullets(slide, tokens, bullets, plan, opts) {
505
+ if (bullets.length === 0)
506
+ return;
507
+ slide.addText(bulletRuns(bullets, tokens, opts?.fontSize ?? plan.bodySize), {
508
+ x: opts?.x ?? '8%',
509
+ y: opts?.y ?? '26%',
510
+ w: opts?.w ?? '84%',
511
+ h: opts?.h ?? '62%',
512
+ valign: 'top',
513
+ lineSpacingMultiple: 1.2,
514
+ });
515
+ }
516
+ function renderCover(slide, pptx, tokens, title, subtitle, notes) {
517
+ const accent = tokens.accent;
518
+ const heading = tokens.headingFont;
519
+ const body = tokens.bodyFont;
520
+ const onAccent = 'FFFFFF';
521
+ switch (tokens.coverStyle) {
522
+ case 'split':
523
+ slide.addShape(pptx.ShapeType.rect, {
524
+ x: '0%',
525
+ y: '0%',
526
+ w: '42%',
527
+ h: '100%',
528
+ fill: { color: accent },
529
+ });
530
+ slide.addText(title, {
531
+ x: '5%',
532
+ y: '30%',
533
+ w: '33%',
534
+ h: '35%',
535
+ fontSize: 34,
536
+ fontFace: heading,
537
+ color: onAccent,
538
+ bold: true,
539
+ valign: 'middle',
540
+ });
541
+ if (subtitle) {
542
+ slide.addText(subtitle, {
543
+ x: '48%',
544
+ y: '35%',
545
+ w: '46%',
546
+ h: '30%',
547
+ fontSize: 18,
548
+ fontFace: body,
549
+ color: tokens.textColor,
550
+ valign: 'middle',
551
+ });
552
+ }
553
+ else if (notes) {
554
+ slide.addText(notes, {
555
+ x: '48%',
556
+ y: '35%',
557
+ w: '46%',
558
+ h: '30%',
559
+ fontSize: 14,
560
+ fontFace: body,
561
+ color: tokens.textColor,
562
+ transparency: 30,
563
+ valign: 'middle',
564
+ });
565
+ }
566
+ break;
567
+ case 'centered':
568
+ slide.addText(title, {
569
+ x: '10%',
570
+ y: '30%',
571
+ w: '80%',
572
+ h: '25%',
573
+ fontSize: 38,
574
+ fontFace: heading,
575
+ color: tokens.textColor,
576
+ bold: true,
577
+ align: 'center',
578
+ valign: 'middle',
579
+ });
580
+ slide.addShape(pptx.ShapeType.rect, {
581
+ x: '45%',
582
+ y: '57%',
583
+ w: '10%',
584
+ h: '0.8%',
585
+ fill: { color: accent },
586
+ });
587
+ if (subtitle) {
588
+ slide.addText(subtitle, {
589
+ x: '15%',
590
+ y: '60%',
591
+ w: '70%',
592
+ h: '15%',
593
+ fontSize: 18,
594
+ fontFace: body,
595
+ color: tokens.textColor,
596
+ transparency: 30,
597
+ align: 'center',
598
+ });
599
+ }
600
+ break;
601
+ case 'minimal':
602
+ slide.addText(title, {
603
+ x: '12%',
604
+ y: '38%',
605
+ w: '76%',
606
+ h: '20%',
607
+ fontSize: 34,
608
+ fontFace: heading,
609
+ color: tokens.textColor,
610
+ bold: false,
611
+ });
612
+ if (subtitle) {
613
+ slide.addText(subtitle, {
614
+ x: '12%',
615
+ y: '58%',
616
+ w: '76%',
617
+ h: '12%',
618
+ fontSize: 16,
619
+ fontFace: body,
620
+ color: tokens.textColor,
621
+ transparency: 40,
622
+ });
623
+ }
624
+ break;
625
+ case 'frame':
626
+ slide.addShape(pptx.ShapeType.rect, {
627
+ x: '5%',
628
+ y: '8%',
629
+ w: '90%',
630
+ h: '84%',
631
+ fill: { color: tokens.background },
632
+ line: { color: accent, width: 3 },
633
+ });
634
+ slide.addText(title, {
635
+ x: '12%',
636
+ y: '32%',
637
+ w: '76%',
638
+ h: '25%',
639
+ fontSize: 34,
640
+ fontFace: heading,
641
+ color: tokens.textColor,
642
+ bold: true,
643
+ align: 'center',
644
+ valign: 'middle',
645
+ });
646
+ if (subtitle) {
647
+ slide.addText(subtitle, {
648
+ x: '15%',
649
+ y: '58%',
650
+ w: '70%',
651
+ h: '15%',
652
+ fontSize: 16,
653
+ fontFace: body,
654
+ color: tokens.textColor,
655
+ transparency: 30,
656
+ align: 'center',
657
+ });
658
+ }
659
+ break;
660
+ case 'band':
661
+ slide.addShape(pptx.ShapeType.rect, {
662
+ x: '0%',
663
+ y: '0%',
664
+ w: '100%',
665
+ h: '14%',
666
+ fill: { color: accent },
667
+ });
668
+ slide.addText(title, {
669
+ x: '8%',
670
+ y: '28%',
671
+ w: '84%',
672
+ h: '30%',
673
+ fontSize: 36,
674
+ fontFace: heading,
675
+ color: tokens.textColor,
676
+ bold: true,
677
+ valign: 'middle',
678
+ });
679
+ if (subtitle) {
680
+ slide.addText(subtitle, {
681
+ x: '8%',
682
+ y: '58%',
683
+ w: '84%',
684
+ h: '15%',
685
+ fontSize: 18,
686
+ fontFace: body,
687
+ color: tokens.textColor,
688
+ transparency: 30,
689
+ });
690
+ }
691
+ break;
692
+ case 'banner':
693
+ default:
694
+ slide.background = { color: accent };
695
+ slide.addText(title, {
696
+ x: '10%',
697
+ y: '28%',
698
+ w: '80%',
699
+ h: '30%',
700
+ fontSize: 36,
701
+ fontFace: heading,
702
+ color: onAccent,
703
+ bold: true,
704
+ align: 'center',
705
+ valign: 'middle',
706
+ });
707
+ if (subtitle) {
708
+ slide.addText(subtitle, {
709
+ x: '12%',
710
+ y: '58%',
711
+ w: '76%',
712
+ h: '15%',
713
+ fontSize: 18,
714
+ fontFace: body,
715
+ color: onAccent,
716
+ transparency: 20,
717
+ align: 'center',
718
+ });
719
+ }
720
+ break;
721
+ }
722
+ }
723
+ function renderDivider(slide, pptx, tokens, title, subtitle, plan) {
724
+ slide.background = { color: tokens.accent };
725
+ slide.addText(title, {
726
+ x: '10%',
727
+ y: '34%',
728
+ w: '80%',
729
+ h: '20%',
730
+ fontSize: plan.titleSize + 4,
731
+ fontFace: tokens.headingFont,
732
+ color: 'FFFFFF',
733
+ bold: true,
734
+ align: 'center',
735
+ valign: 'middle',
736
+ });
737
+ if (subtitle) {
738
+ slide.addText(subtitle, {
739
+ x: '15%',
740
+ y: '56%',
741
+ w: '70%',
742
+ h: '12%',
743
+ fontSize: plan.bodySize + 2,
744
+ fontFace: tokens.bodyFont,
745
+ color: 'FFFFFF',
746
+ transparency: 20,
747
+ align: 'center',
748
+ });
749
+ }
750
+ }
751
+ function renderCards(slide, pptx, tokens, cards, plan) {
752
+ const count = Math.min(Math.max(cards.length, 1), 4);
753
+ const gap = 3;
754
+ const left = 8;
755
+ const width = (84 - gap * (count - 1)) / count;
756
+ const shape = shapeFor(tokens, pptx);
757
+ cards.slice(0, 4).forEach((card, i) => {
758
+ const x = left + i * (width + gap);
759
+ slide.addShape(shape, {
760
+ x: `${x}%`,
761
+ y: '27%',
762
+ w: `${width}%`,
763
+ h: '60%',
764
+ fill: { color: tokens.surface },
765
+ ...(shape === pptx.ShapeType.roundRect
766
+ ? { rectRadius: 0.08 }
767
+ : {}),
768
+ });
769
+ if (card.title) {
770
+ slide.addText(stripMd(String(card.title)), {
771
+ x: `${x + 1.5}%`,
772
+ y: '31%',
773
+ w: `${width - 3}%`,
774
+ h: '14%',
775
+ fontSize: plan.bodySize + 1,
776
+ fontFace: tokens.headingFont,
777
+ color: tokens.accent,
778
+ bold: true,
779
+ });
780
+ }
781
+ if (card.text) {
782
+ slide.addText(stripMd(String(card.text)), {
783
+ x: `${x + 1.5}%`,
784
+ y: card.title ? '45%' : '31%',
785
+ w: `${width - 3}%`,
786
+ h: '36%',
787
+ fontSize: plan.bodySize - 1,
788
+ fontFace: tokens.bodyFont,
789
+ color: tokens.textColor,
790
+ valign: 'top',
791
+ lineSpacingMultiple: 1.25,
792
+ });
793
+ }
794
+ });
795
+ }
796
+ function renderStats(slide, pptx, tokens, stats, plan) {
797
+ const count = Math.min(Math.max(stats.length, 1), 4);
798
+ const gap = 4;
799
+ const left = 8;
800
+ const width = (84 - gap * (count - 1)) / count;
801
+ const shape = shapeFor(tokens, pptx);
802
+ stats.slice(0, 4).forEach((stat, i) => {
803
+ const x = left + i * (width + gap);
804
+ slide.addShape(shape, {
805
+ x: `${x}%`,
806
+ y: '30%',
807
+ w: `${width}%`,
808
+ h: '52%',
809
+ fill: { color: tokens.surface },
810
+ ...(shape === pptx.ShapeType.roundRect
811
+ ? { rectRadius: 0.1 }
812
+ : {}),
813
+ });
814
+ slide.addText(stripMd(String(stat.value ?? '')), {
815
+ x: `${x}%`,
816
+ y: '36%',
817
+ w: `${width}%`,
818
+ h: '20%',
819
+ fontSize: plan.titleSize + 4,
820
+ fontFace: tokens.headingFont,
821
+ color: tokens.accent,
822
+ bold: true,
823
+ align: 'center',
824
+ });
825
+ if (stat.label) {
826
+ slide.addText(stripMd(String(stat.label)), {
827
+ x: `${x}%`,
828
+ y: '56%',
829
+ w: `${width}%`,
830
+ h: '20%',
831
+ fontSize: plan.bodySize,
832
+ fontFace: tokens.bodyFont,
833
+ color: tokens.textColor,
834
+ align: 'center',
835
+ valign: 'top',
836
+ });
837
+ }
838
+ });
839
+ }
840
+ function renderTable(slide, tokens, rows, plan) {
841
+ const clean = rows
842
+ .map((r) => Array.isArray(r) ? r.map((c) => stripMd(String(c ?? ''))) : [])
843
+ .filter((r) => r.length > 0);
844
+ if (clean.length === 0)
845
+ return false;
846
+ const colCount = Math.max(...clean.map((r) => r.length));
847
+ const tableRows = clean.map((cells, rowIdx) => Array.from({ length: colCount }, (_, c) => ({
848
+ text: cells[c] ?? '',
849
+ options: {
850
+ fontSize: rowIdx === 0 ? plan.bodySize : plan.bodySize - 1,
851
+ fontFace: rowIdx === 0 ? tokens.headingFont : tokens.bodyFont,
852
+ bold: rowIdx === 0,
853
+ color: rowIdx === 0 ? 'FFFFFF' : tokens.textColor,
854
+ fill: {
855
+ color: rowIdx === 0
856
+ ? tokens.accent
857
+ : rowIdx % 2 === 0
858
+ ? tokens.surface
859
+ : tokens.background,
860
+ },
861
+ valign: 'middle',
862
+ margin: 4,
863
+ },
864
+ })));
865
+ slide.addTable(tableRows, {
866
+ x: '8%',
867
+ y: '26%',
868
+ w: '84%',
869
+ border: { color: tokens.textColor, pt: 0.5 },
870
+ });
871
+ return true;
872
+ }
91
873
  class PptxDeckTool {
92
874
  key = 'pptx-render';
93
875
  async run(params) {
94
- const outlineMd = String(params.outlineMd ?? '');
95
- const themeName = String(params.theme ?? 'default');
96
- const aspect = String(params.aspect ?? '16:9');
97
- if (!outlineMd.trim()) {
98
- return { outputs: [], error: 'No outline provided' };
876
+ const aspect = params.aspect === '4:3' ? '4:3' : '16:9';
877
+ const deckJsonRaw = params.deckJson ?? params.deck_spec ?? params.spec;
878
+ const outlineMd = typeof params.outlineMd === 'string' ? params.outlineMd : '';
879
+ const { tokens, templateId, warnings } = resolveDeckTokens(params);
880
+ const fallbacks = [...warnings];
881
+ const log = [];
882
+ // -- legacy path: markdown outline, no deck.json -------------------------
883
+ if (deckJsonRaw === undefined ||
884
+ deckJsonRaw === null ||
885
+ deckJsonRaw === '') {
886
+ if (!outlineMd.trim()) {
887
+ return {
888
+ outputs: [],
889
+ error: 'No deck provided (deckJson or legacy outlineMd)',
890
+ };
891
+ }
892
+ return this.runLegacy(params, outlineMd, tokens, templateId, aspect, fallbacks, log);
99
893
  }
100
- const theme = THEMES[themeName] ?? THEMES.default;
101
- const { docTitle, slides: slideData } = parseOutline(outlineMd);
102
- if (slideData.length === 0) {
894
+ let spec;
895
+ try {
896
+ spec = parseDeckSpec(deckJsonRaw);
897
+ }
898
+ catch (err) {
103
899
  return {
104
900
  outputs: [],
105
- error: 'No slides found in outline (use ## for slide titles)',
901
+ error: err instanceof Error ? err.message : String(err),
106
902
  };
107
903
  }
904
+ const plan = planDeck(spec, { density: tokens.density, aspect });
905
+ const layoutsUsed = [];
906
+ for (const p of plan) {
907
+ layoutsUsed.push(p.layout);
908
+ if (p.degradedFrom)
909
+ fallbacks.push(`slide ${p.index + 1}: unknown layout "${p.degradedFrom}" degraded to bullets`);
910
+ }
108
911
  const pptx = new pptxgenjs_1.default();
109
912
  pptx.layout = aspect === '4:3' ? 'LAYOUT_4x3' : 'LAYOUT_16x9';
110
913
  pptx.author = 'TeamShare';
914
+ const docTitle = (typeof spec.title === 'string' && spec.title) ||
915
+ this.inferTitle(spec) ||
916
+ 'Presentation';
111
917
  pptx.title = docTitle;
112
- // Title slide
113
- const titleSlide = pptx.addSlide();
114
- titleSlide.background = { color: theme.accent };
115
- titleSlide.addText(docTitle, {
116
- x: '10%',
117
- y: '30%',
118
- w: '80%',
119
- h: '30%',
120
- fontSize: 36,
121
- fontFace: 'Arial',
122
- color: 'FFFFFF',
123
- bold: true,
124
- align: 'center',
125
- valign: 'middle',
126
- });
127
- titleSlide.addText(`Generated by TeamShare`, {
128
- x: '10%',
129
- y: '65%',
130
- w: '80%',
131
- h: '10%',
132
- fontSize: 14,
133
- fontFace: 'Arial',
134
- color: 'FFFFFFCC',
135
- align: 'center',
136
- });
137
- // Content slides
138
- for (const slide of slideData) {
918
+ spec.slides.forEach((slide, idx) => {
919
+ const p = plan[idx];
139
920
  const s = pptx.addSlide();
140
- s.background = { color: theme.bg };
141
- // Slide number
142
- s.addText(`${slideData.indexOf(slide) + 1}`, {
143
- x: '92%',
144
- y: '92%',
145
- w: '6%',
146
- h: '5%',
147
- fontSize: 10,
148
- color: theme.color + '80',
149
- align: 'center',
150
- });
151
- // Title
152
- s.addText(slide.title, {
153
- x: '8%',
154
- y: '8%',
155
- w: '84%',
156
- h: '15%',
157
- fontSize: 28,
158
- fontFace: 'Arial',
159
- color: theme.titleColor,
160
- bold: true,
921
+ s.background = { color: tokens.background };
922
+ const title = typeof slide.title === 'string' ? stripMd(slide.title) : '';
923
+ const subtitle = typeof slide.subtitle === 'string'
924
+ ? stripMd(slide.subtitle)
925
+ : undefined;
926
+ const notes = typeof slide.notes === 'string' && slide.notes.trim()
927
+ ? slide.notes.trim()
928
+ : undefined;
929
+ const slideNo = idx + 1;
930
+ switch (p.layout) {
931
+ case 'title':
932
+ renderCover(s, pptx, tokens, title || docTitle, subtitle ?? this.firstBullets(slide), notes);
933
+ break;
934
+ case 'section':
935
+ renderDivider(s, pptx, tokens, title || `Section ${slideNo}`, subtitle, p);
936
+ break;
937
+ case 'closing':
938
+ renderCover(s, pptx, tokens, title || 'Thank you', subtitle, notes);
939
+ break;
940
+ case 'agenda':
941
+ addTitleBlock(s, pptx, tokens, title || 'Agenda', slideNo, p);
942
+ addBullets(s, tokens, p.bullets.length > 0 ? p.bullets : asStringArray(slide.bullets), p);
943
+ addFooter(s, pptx, tokens, slideNo);
944
+ break;
945
+ case 'statement':
946
+ s.addText(title || this.firstBullets(slide) || docTitle, {
947
+ x: '10%',
948
+ y: '32%',
949
+ w: '80%',
950
+ h: '30%',
951
+ fontSize: p.titleSize,
952
+ fontFace: tokens.headingFont,
953
+ color: tokens.textColor,
954
+ bold: true,
955
+ align: 'center',
956
+ valign: 'middle',
957
+ });
958
+ if (subtitle) {
959
+ s.addText(subtitle, {
960
+ x: '15%',
961
+ y: '62%',
962
+ w: '70%',
963
+ h: '12%',
964
+ fontSize: p.bodySize,
965
+ fontFace: tokens.bodyFont,
966
+ color: tokens.textColor,
967
+ transparency: 30,
968
+ align: 'center',
969
+ });
970
+ }
971
+ addFooter(s, pptx, tokens, slideNo);
972
+ break;
973
+ case 'two-column': {
974
+ addTitleBlock(s, pptx, tokens, title || `Slide ${slideNo}`, slideNo, p);
975
+ const cols = this.coerceColumns(slide.columns);
976
+ const left = cols[0];
977
+ const right = cols[1];
978
+ if (left) {
979
+ if (left.heading) {
980
+ s.addText(left.heading, {
981
+ x: '8%',
982
+ y: '27%',
983
+ w: '38%',
984
+ h: '10%',
985
+ fontSize: p.bodySize + 1,
986
+ fontFace: tokens.headingFont,
987
+ color: tokens.accent,
988
+ bold: true,
989
+ });
990
+ }
991
+ addBullets(s, tokens, trimBullets(left.bullets, tokens.density), p, {
992
+ x: '8%',
993
+ y: left.heading ? '37%' : '27%',
994
+ w: '38%',
995
+ h: '55%',
996
+ });
997
+ }
998
+ if (right) {
999
+ if (right.heading) {
1000
+ s.addText(right.heading, {
1001
+ x: '54%',
1002
+ y: '27%',
1003
+ w: '38%',
1004
+ h: '10%',
1005
+ fontSize: p.bodySize + 1,
1006
+ fontFace: tokens.headingFont,
1007
+ color: tokens.accent,
1008
+ bold: true,
1009
+ });
1010
+ }
1011
+ addBullets(s, tokens, trimBullets(right.bullets, tokens.density), p, {
1012
+ x: '54%',
1013
+ y: right.heading ? '37%' : '27%',
1014
+ w: '38%',
1015
+ h: '55%',
1016
+ });
1017
+ }
1018
+ if (!left && !right)
1019
+ addBullets(s, tokens, p.bullets, p);
1020
+ addFooter(s, pptx, tokens, slideNo);
1021
+ break;
1022
+ }
1023
+ case 'cards': {
1024
+ addTitleBlock(s, pptx, tokens, title || `Slide ${slideNo}`, slideNo, p);
1025
+ const cards = this.coerceCards(slide.cards);
1026
+ if (cards.length > 0)
1027
+ renderCards(s, pptx, tokens, cards, p);
1028
+ else
1029
+ addBullets(s, tokens, p.bullets, p);
1030
+ addFooter(s, pptx, tokens, slideNo);
1031
+ break;
1032
+ }
1033
+ case 'stats': {
1034
+ addTitleBlock(s, pptx, tokens, title || `Slide ${slideNo}`, slideNo, p);
1035
+ const stats = this.coerceStats(slide.stats);
1036
+ if (stats.length > 0)
1037
+ renderStats(s, pptx, tokens, stats, p);
1038
+ else
1039
+ addBullets(s, tokens, p.bullets, p);
1040
+ addFooter(s, pptx, tokens, slideNo);
1041
+ break;
1042
+ }
1043
+ case 'timeline': {
1044
+ addTitleBlock(s, pptx, tokens, title || 'Timeline', slideNo, p);
1045
+ const entries = this.coerceTimeline(slide.rows);
1046
+ if (entries.length > 0) {
1047
+ const rowH = Math.min(14, Math.floor(58 / Math.max(entries.length, 1)));
1048
+ entries.slice(0, 6).forEach((entry, i) => {
1049
+ const y = 28 + i * (rowH + 2);
1050
+ s.addShape(pptx.ShapeType.ellipse, {
1051
+ x: '8%',
1052
+ y: `${y + 1}%`,
1053
+ w: '1.6%',
1054
+ h: '2.6%',
1055
+ fill: { color: tokens.accent },
1056
+ });
1057
+ if (i < entries.length - 1) {
1058
+ s.addShape(pptx.ShapeType.rect, {
1059
+ x: '8.7%',
1060
+ y: `${y + 3.6}%`,
1061
+ w: '0.2%',
1062
+ h: `${rowH - 1}%`,
1063
+ fill: { color: tokens.accent, transparency: 65 },
1064
+ });
1065
+ }
1066
+ const head = [entry.date, entry.title]
1067
+ .filter(Boolean)
1068
+ .join(' — ');
1069
+ s.addText(head, {
1070
+ x: '11%',
1071
+ y: `${y}%`,
1072
+ w: '81%',
1073
+ h: '5%',
1074
+ fontSize: p.bodySize,
1075
+ fontFace: tokens.headingFont,
1076
+ color: tokens.accent,
1077
+ bold: true,
1078
+ });
1079
+ if (entry.text) {
1080
+ s.addText(entry.text, {
1081
+ x: '11%',
1082
+ y: `${y + 5}%`,
1083
+ w: '81%',
1084
+ h: `${Math.max(rowH - 5, 4)}%`,
1085
+ fontSize: p.bodySize - 1,
1086
+ fontFace: tokens.bodyFont,
1087
+ color: tokens.textColor,
1088
+ valign: 'top',
1089
+ });
1090
+ }
1091
+ });
1092
+ }
1093
+ else {
1094
+ addBullets(s, tokens, p.bullets, p);
1095
+ }
1096
+ addFooter(s, pptx, tokens, slideNo);
1097
+ break;
1098
+ }
1099
+ case 'quote': {
1100
+ const q = this.coerceQuote(slide.quote) ?? { text: title || this.firstBullets(slide) || docTitle };
1101
+ s.addText(`“${q.text}”`, {
1102
+ x: '12%',
1103
+ y: '30%',
1104
+ w: '76%',
1105
+ h: '32%',
1106
+ fontSize: p.titleSize - 4,
1107
+ fontFace: tokens.headingFont,
1108
+ color: tokens.textColor,
1109
+ italic: true,
1110
+ align: 'center',
1111
+ valign: 'middle',
1112
+ });
1113
+ if (q.attribution) {
1114
+ s.addText(`— ${q.attribution}`, {
1115
+ x: '12%',
1116
+ y: '62%',
1117
+ w: '76%',
1118
+ h: '10%',
1119
+ fontSize: p.bodySize,
1120
+ fontFace: tokens.bodyFont,
1121
+ color: tokens.accent,
1122
+ align: 'center',
1123
+ });
1124
+ }
1125
+ addFooter(s, pptx, tokens, slideNo);
1126
+ break;
1127
+ }
1128
+ case 'table': {
1129
+ addTitleBlock(s, pptx, tokens, title || `Slide ${slideNo}`, slideNo, p);
1130
+ const rows = this.coerceTableRows(slide.rows);
1131
+ if (!renderTable(s, tokens, rows, p)) {
1132
+ fallbacks.push(`slide ${slideNo}: table has no rows, rendered bullets instead`);
1133
+ addBullets(s, tokens, p.bullets, p);
1134
+ }
1135
+ addFooter(s, pptx, tokens, slideNo);
1136
+ break;
1137
+ }
1138
+ case 'sources':
1139
+ addTitleBlock(s, pptx, tokens, title || 'Sources', slideNo, p);
1140
+ addBullets(s, tokens, p.bullets, p, {
1141
+ fontSize: Math.max(planBodyFloor(p.bodySize) - 2, 10),
1142
+ });
1143
+ addFooter(s, pptx, tokens, slideNo);
1144
+ break;
1145
+ case 'bullets':
1146
+ default:
1147
+ addTitleBlock(s, pptx, tokens, title || `Slide ${slideNo}`, slideNo, p);
1148
+ addBullets(s, tokens, p.bullets, p);
1149
+ addFooter(s, pptx, tokens, slideNo);
1150
+ break;
1151
+ }
1152
+ if (notes)
1153
+ s.addNotes(notes);
1154
+ });
1155
+ const buffer = (await pptx.write({ outputType: 'nodebuffer' }));
1156
+ const base = (0, output_name_1.outputBaseName)(params);
1157
+ const safeName = docTitle.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
1158
+ const pptxName = base ? `${base} — Deck.pptx` : `${safeName}.pptx`;
1159
+ const outputs = [
1160
+ {
1161
+ buffer,
1162
+ fileName: pptxName,
1163
+ mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
1164
+ },
1165
+ ];
1166
+ const notesMd = this.buildSpeakerNotes(spec, docTitle);
1167
+ if (notesMd) {
1168
+ outputs.push({
1169
+ buffer: Buffer.from(notesMd, 'utf8'),
1170
+ fileName: base
1171
+ ? `${base} — Speaker Notes.md`
1172
+ : `${safeName} — Speaker Notes.md`,
1173
+ mime: 'text/markdown',
161
1174
  });
162
- // Accent bar under title
163
- s.addShape(pptx.ShapeType.rect, {
164
- x: '8%',
165
- y: '23%',
166
- w: '15%',
167
- h: '0.5%',
168
- fill: { color: theme.accent },
169
- });
170
- // Bullets
171
- if (slide.bullets.length > 0) {
172
- const bulletText = slide.bullets.map((b) => ({
173
- text: b,
174
- options: {
175
- fontSize: 16,
176
- fontFace: 'Arial',
177
- color: theme.color,
178
- bullet: { code: '2022' },
179
- paraSpaceAfter: 8,
180
- },
181
- }));
182
- s.addText(bulletText, {
183
- x: '8%',
184
- y: '28%',
185
- w: '84%',
186
- h: '60%',
187
- valign: 'top',
188
- lineSpacingMultiple: 1.2,
189
- });
1175
+ }
1176
+ log.push(`Template: ${templateId}`, `Slides: ${spec.slides.length}`, `Density: ${tokens.density}, Aspect: ${aspect}`, `Layouts: ${layoutsUsed.join(', ')}`, ...fallbacks.map((f) => `Fallback: ${f}`), `Generated PPTX: ${pptxName} (${buffer.length} bytes)`);
1177
+ return { outputs, log };
1178
+ }
1179
+ inferTitle(spec) {
1180
+ const first = spec.slides[0];
1181
+ if (first && typeof first.title === 'string' && first.title.trim()) {
1182
+ return stripMd(first.title);
1183
+ }
1184
+ return undefined;
1185
+ }
1186
+ firstBullets(slide) {
1187
+ const bullets = asStringArray(slide.bullets);
1188
+ return bullets[0];
1189
+ }
1190
+ coerceColumns(value) {
1191
+ if (!Array.isArray(value))
1192
+ return [];
1193
+ return value.slice(0, 2).map((col) => {
1194
+ if (typeof col === 'string')
1195
+ return { bullets: [stripMd(col)] };
1196
+ if (col && typeof col === 'object') {
1197
+ const c = col;
1198
+ const heading = typeof c.heading === 'string' ? stripMd(c.heading) : undefined;
1199
+ const bullets = asStringArray(c.bullets ?? c.body ?? c.text);
1200
+ const single = typeof c.body === 'string' && !Array.isArray(c.bullets)
1201
+ ? [stripMd(c.body)]
1202
+ : bullets;
1203
+ return { heading, bullets: single };
190
1204
  }
191
- // Speaker notes
192
- if (slide.notes) {
193
- s.addNotes(slide.notes);
1205
+ return { bullets: [] };
1206
+ });
1207
+ }
1208
+ coerceCards(value) {
1209
+ if (!Array.isArray(value))
1210
+ return [];
1211
+ return value.slice(0, 4).map((card) => {
1212
+ if (typeof card === 'string')
1213
+ return { text: stripMd(card) };
1214
+ if (card && typeof card === 'object') {
1215
+ const c = card;
1216
+ return {
1217
+ title: typeof c.title === 'string' ? stripMd(c.title) : undefined,
1218
+ text: typeof c.text === 'string' ? stripMd(c.text) : undefined,
1219
+ };
1220
+ }
1221
+ return {};
1222
+ });
1223
+ }
1224
+ coerceStats(value) {
1225
+ if (!Array.isArray(value))
1226
+ return [];
1227
+ return value.slice(0, 4).map((stat) => {
1228
+ if (typeof stat === 'string')
1229
+ return { value: stripMd(stat) };
1230
+ if (stat && typeof stat === 'object') {
1231
+ const c = stat;
1232
+ return {
1233
+ value: typeof c.value === 'string' ? stripMd(c.value) : undefined,
1234
+ label: typeof c.label === 'string' ? stripMd(c.label) : undefined,
1235
+ };
1236
+ }
1237
+ return {};
1238
+ });
1239
+ }
1240
+ coerceTimeline(value) {
1241
+ if (!Array.isArray(value))
1242
+ return [];
1243
+ return value.slice(0, 6).map((row) => {
1244
+ if (typeof row === 'string')
1245
+ return { title: stripMd(row) };
1246
+ if (Array.isArray(row)) {
1247
+ const [date, title, text] = row.map((c) => typeof c === 'string' ? stripMd(c) : '');
1248
+ return {
1249
+ date: date || undefined,
1250
+ title: title || undefined,
1251
+ text: text || undefined,
1252
+ };
1253
+ }
1254
+ if (row && typeof row === 'object') {
1255
+ const c = row;
1256
+ return {
1257
+ date: typeof c.date === 'string' ? stripMd(c.date) : undefined,
1258
+ title: typeof c.title === 'string' ? stripMd(c.title) : undefined,
1259
+ text: typeof c.text === 'string' ? stripMd(c.text) : undefined,
1260
+ };
1261
+ }
1262
+ return {};
1263
+ });
1264
+ }
1265
+ coerceQuote(value) {
1266
+ if (typeof value === 'string' && value.trim())
1267
+ return { text: stripMd(value) };
1268
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
1269
+ const c = value;
1270
+ if (typeof c.text === 'string' && c.text.trim()) {
1271
+ return {
1272
+ text: stripMd(c.text),
1273
+ attribution: typeof c.attribution === 'string'
1274
+ ? stripMd(c.attribution)
1275
+ : undefined,
1276
+ };
194
1277
  }
195
1278
  }
196
- const buffer = await pptx.write({ outputType: 'nodebuffer' });
197
- const safeName = docTitle.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
1279
+ return null;
1280
+ }
1281
+ coerceTableRows(value) {
1282
+ if (!Array.isArray(value))
1283
+ return [];
1284
+ return value.map((row) => {
1285
+ if (Array.isArray(row))
1286
+ return row.map((c) => (typeof c === 'string' ? c : String(c ?? '')));
1287
+ if (row && typeof row === 'object')
1288
+ return Object.values(row).map((c) => typeof c === 'string'
1289
+ ? c
1290
+ : typeof c === 'number' || typeof c === 'boolean'
1291
+ ? String(c)
1292
+ : '');
1293
+ return [typeof row === 'string' ? row : String(row ?? '')];
1294
+ });
1295
+ }
1296
+ buildSpeakerNotes(spec, docTitle) {
1297
+ const withNotes = spec.slides
1298
+ .map((slide, idx) => ({ slide, idx }))
1299
+ .filter(({ slide }) => typeof slide.notes === 'string' && slide.notes.trim());
1300
+ if (withNotes.length === 0)
1301
+ return null;
1302
+ const lines = [`# ${docTitle}`, ''];
1303
+ for (const { slide, idx } of withNotes) {
1304
+ const title = typeof slide.title === 'string' && slide.title.trim()
1305
+ ? stripMd(slide.title)
1306
+ : `Slide ${idx + 1}`;
1307
+ lines.push(`## Slide ${idx + 1}: ${title}`, String(slide.notes).trim(), '');
1308
+ }
1309
+ return lines.join('\n');
1310
+ }
1311
+ async runLegacy(params, outlineMd, tokens, templateId, aspect, fallbacks, log) {
1312
+ const { docTitle, slides: slideData } = parseOutline(outlineMd);
1313
+ if (slideData.length === 0) {
1314
+ return {
1315
+ outputs: [],
1316
+ error: 'No slides found in outline (use ## for slide titles)',
1317
+ };
1318
+ }
1319
+ fallbacks.push('legacy outline');
1320
+ const pptx = new pptxgenjs_1.default();
1321
+ pptx.layout = aspect === '4:3' ? 'LAYOUT_4x3' : 'LAYOUT_16x9';
1322
+ pptx.author = 'TeamShare';
1323
+ pptx.title = docTitle;
1324
+ const cover = pptx.addSlide();
1325
+ renderCover(cover, pptx, tokens, docTitle, undefined, undefined);
1326
+ slideData.forEach((slide, i) => {
1327
+ const s = pptx.addSlide();
1328
+ s.background = { color: tokens.background };
1329
+ const plan = {
1330
+ index: i,
1331
+ layout: 'bullets',
1332
+ titleSize: tokens.density === 'airy'
1333
+ ? 32
1334
+ : tokens.density === 'compact'
1335
+ ? 24
1336
+ : 28,
1337
+ bodySize: tokens.density === 'airy'
1338
+ ? 18
1339
+ : tokens.density === 'compact'
1340
+ ? 14
1341
+ : 16,
1342
+ columns: 1,
1343
+ bullets: trimBullets(slide.bullets, tokens.density),
1344
+ };
1345
+ addTitleBlock(s, pptx, tokens, slide.title || `Slide ${i + 1}`, i + 2, plan);
1346
+ addBullets(s, tokens, plan.bullets, plan);
1347
+ if (slide.notes)
1348
+ s.addNotes(slide.notes);
1349
+ addFooter(s, pptx, tokens, i + 2);
1350
+ });
1351
+ const buffer = (await pptx.write({ outputType: 'nodebuffer' }));
198
1352
  const base = (0, output_name_1.outputBaseName)(params);
1353
+ const safeName = docTitle.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
1354
+ const pptxName = base ? `${base} — Deck.pptx` : `${safeName}.pptx`;
1355
+ log.push(`Template: ${templateId}`, `Slides: ${slideData.length + 1} (1 title + ${slideData.length} content)`, `Density: ${tokens.density}, Aspect: ${aspect}`, 'Layouts: title, bullets', ...fallbacks.map((f) => `Fallback: ${f}`), `Generated PPTX: ${pptxName} (${buffer.length} bytes)`);
199
1356
  return {
200
1357
  outputs: [
201
1358
  {
202
- buffer: buffer,
203
- fileName: base ? `${base} — Deck.pptx` : `${safeName}.pptx`,
1359
+ buffer,
1360
+ fileName: pptxName,
204
1361
  mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
205
1362
  },
206
1363
  ],
207
- log: [
208
- `Generated PPTX: ${safeName}.pptx`,
209
- `Title: ${docTitle}`,
210
- `Slides: ${slideData.length + 1} (1 title + ${slideData.length} content)`,
211
- `Theme: ${themeName}, Aspect: ${aspect}`,
212
- ],
1364
+ log,
213
1365
  };
214
1366
  }
215
1367
  }
216
1368
  exports.PptxDeckTool = PptxDeckTool;
1369
+ function planBodyFloor(bodySize) {
1370
+ return Math.max(Math.min(bodySize, 18), 12);
1371
+ }
217
1372
  //# sourceMappingURL=pptx-deck.js.map