@thanh01.pmt/presentation-kit 0.2.18 → 0.3.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 (56) hide show
  1. package/README.md +26 -0
  2. package/dist/ai/index.cjs +16 -15
  3. package/dist/ai/index.cjs.map +1 -1
  4. package/dist/ai/index.d.cts +3 -2
  5. package/dist/ai/index.d.ts +3 -2
  6. package/dist/ai/index.js +2 -1
  7. package/dist/ai/index.js.map +1 -1
  8. package/dist/assets/fonts/NOTICE.md +25 -0
  9. package/dist/assets/fonts/SOURCES.json +47 -0
  10. package/dist/assets/fonts/fira-code.woff2 +0 -0
  11. package/dist/assets/fonts/inter.woff2 +0 -0
  12. package/dist/assets/fonts/jetbrains-mono.woff2 +0 -0
  13. package/dist/assets/fonts/playfair-display.woff2 +0 -0
  14. package/dist/assets/fonts/plus-jakarta-sans.woff2 +0 -0
  15. package/dist/assets/fonts/space-grotesk.woff2 +0 -0
  16. package/dist/{chunk-DDOUFYLK.js → chunk-3DXMAPAY.js} +3 -3
  17. package/dist/{chunk-DDOUFYLK.js.map → chunk-3DXMAPAY.js.map} +1 -1
  18. package/dist/chunk-3Q4RMZVQ.cjs +867 -0
  19. package/dist/chunk-3Q4RMZVQ.cjs.map +1 -0
  20. package/dist/{chunk-JJQDQIRG.js → chunk-5HIDQQHE.js} +288 -855
  21. package/dist/chunk-5HIDQQHE.js.map +1 -0
  22. package/dist/{chunk-SFBEDDGU.cjs → chunk-5WEZOGQA.cjs} +292 -866
  23. package/dist/chunk-5WEZOGQA.cjs.map +1 -0
  24. package/dist/{chunk-DIHFQ5LF.cjs → chunk-CVEC72CT.cjs} +4 -4
  25. package/dist/{chunk-DIHFQ5LF.cjs.map → chunk-CVEC72CT.cjs.map} +1 -1
  26. package/dist/chunk-V6X7D44H.js +854 -0
  27. package/dist/chunk-V6X7D44H.js.map +1 -0
  28. package/dist/{click-css-Bcc1CkTN.d.cts → click-css-C8VciwZ6.d.cts} +1 -1
  29. package/dist/{click-css-B2s9B_uP.d.ts → click-css-COIiFo1I.d.ts} +1 -1
  30. package/dist/index.cjs +90 -69
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +46 -5
  33. package/dist/index.d.ts +46 -5
  34. package/dist/index.js +5 -4
  35. package/dist/index.js.map +1 -1
  36. package/dist/node/index.cjs +117 -0
  37. package/dist/node/index.cjs.map +1 -0
  38. package/dist/node/index.d.cts +36 -0
  39. package/dist/node/index.d.ts +36 -0
  40. package/dist/node/index.js +105 -0
  41. package/dist/node/index.js.map +1 -0
  42. package/dist/{presets-D_hfPJX9.d.ts → presets-DPRGtpO-.d.ts} +1 -1
  43. package/dist/{presets-BYvgAAkJ.d.cts → presets-MIi91wZg.d.cts} +1 -1
  44. package/dist/react/index.cjs +17 -16
  45. package/dist/react/index.cjs.map +1 -1
  46. package/dist/react/index.d.cts +4 -3
  47. package/dist/react/index.d.ts +4 -3
  48. package/dist/react/index.js +4 -3
  49. package/dist/react/index.js.map +1 -1
  50. package/dist/{types-C3d04zzP.d.cts → types-CvxCxWus.d.cts} +1 -67
  51. package/dist/{types-C3d04zzP.d.ts → types-CvxCxWus.d.ts} +1 -67
  52. package/dist/types-k_eae8UV.d.cts +74 -0
  53. package/dist/types-k_eae8UV.d.ts +74 -0
  54. package/package.json +8 -2
  55. package/dist/chunk-JJQDQIRG.js.map +0 -1
  56. package/dist/chunk-SFBEDDGU.cjs.map +0 -1
@@ -1,11 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var yaml = require('js-yaml');
4
-
5
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
-
7
- var yaml__default = /*#__PURE__*/_interopDefault(yaml);
8
-
9
3
  // src/plugins/shiki.ts
10
4
  var highlighterPromise = null;
11
5
  var highlighterFailed = false;
@@ -80,853 +74,6 @@ async function highlightCodeBlocks(html, theme = "dark") {
80
74
  }
81
75
  );
82
76
  }
83
- function serializeLayoutToComment(layout) {
84
- if (!layout.boxes || layout.boxes.length === 0) return "";
85
- const cleanData = {
86
- boxes: layout.boxes.map((box) => {
87
- const b = {
88
- id: box.id,
89
- target: box.target || `h1`,
90
- pos: [
91
- Math.round(box.pos[0]),
92
- Math.round(box.pos[1]),
93
- Math.round(box.pos[2]),
94
- Math.round(box.pos[3])
95
- ]
96
- };
97
- if (box.fontSize) b.fontSize = box.fontSize;
98
- if (box.color) b.color = box.color;
99
- if (box.textAlign) b.textAlign = box.textAlign;
100
- if (box.style) b.style = box.style;
101
- return b;
102
- })
103
- };
104
- const yamlStr = yaml__default.default.dump(cleanData, {
105
- indent: 2,
106
- lineWidth: -1,
107
- noRefs: true
108
- }).trim();
109
- return `<!-- layout:
110
- ${yamlStr.split("\n").join("\n ")}
111
- -->`;
112
- }
113
- function splitMarkdownSlides(markdown) {
114
- const lines = markdown.split("\n");
115
- let frontmatter = null;
116
- const slideBuffers = [];
117
- let currentBuffer = [];
118
- let inCodeFence = false;
119
- let lineIdx = 0;
120
- if (lines.length > 0 && lines[0].trim() === "---") {
121
- const fmBuffer = [lines[0]];
122
- lineIdx = 1;
123
- while (lineIdx < lines.length) {
124
- const line = lines[lineIdx];
125
- fmBuffer.push(line);
126
- if (line.trim() === "---") {
127
- frontmatter = fmBuffer.join("\n");
128
- lineIdx++;
129
- break;
130
- }
131
- lineIdx++;
132
- }
133
- }
134
- for (let i = lineIdx; i < lines.length; i++) {
135
- const line = lines[i];
136
- if (/^```/.test(line.trim())) {
137
- inCodeFence = !inCodeFence;
138
- currentBuffer.push(line);
139
- continue;
140
- }
141
- if (!inCodeFence && line.trim() === "---") {
142
- slideBuffers.push(currentBuffer);
143
- currentBuffer = [];
144
- continue;
145
- }
146
- currentBuffer.push(line);
147
- }
148
- slideBuffers.push(currentBuffer);
149
- const slides = slideBuffers.map((b) => b.join("\n"));
150
- return { frontmatter, slides };
151
- }
152
- function updateSlideLayoutInMarkdown(markdown, slideIndex, layout) {
153
- const { frontmatter, slides } = splitMarkdownSlides(markdown);
154
- if (slideIndex < 0 || slideIndex >= slides.length) {
155
- return markdown;
156
- }
157
- let slideContent = slides[slideIndex];
158
- const layoutCommentRegex = /<!--\s*layout:\s*[\s\S]*?-->/i;
159
- const newComment = layout && layout.boxes && layout.boxes.length > 0 ? serializeLayoutToComment(layout) : "";
160
- if (layoutCommentRegex.test(slideContent)) {
161
- if (newComment) {
162
- slideContent = slideContent.replace(layoutCommentRegex, newComment);
163
- } else {
164
- slideContent = slideContent.replace(layoutCommentRegex, "").trim();
165
- }
166
- } else if (newComment) {
167
- const trimmed = slideContent.trimStart();
168
- slideContent = `${newComment}
169
-
170
- ${trimmed}`;
171
- }
172
- slides[slideIndex] = slideContent;
173
- const joinedSlides = slides.join("\n---\n");
174
- if (frontmatter) {
175
- return `${frontmatter}
176
-
177
- ${joinedSlides}`;
178
- }
179
- return joinedSlides;
180
- }
181
- function updateSlideTextInMarkdown(markdown, slideIndex, oldText, newText) {
182
- const { frontmatter, slides } = splitMarkdownSlides(markdown);
183
- if (slideIndex < 0 || slideIndex >= slides.length) {
184
- return markdown;
185
- }
186
- const cleanOld = oldText.trim();
187
- const cleanNew = newText.trim();
188
- if (!cleanOld || cleanOld === cleanNew) return markdown;
189
- let slideContent = slides[slideIndex];
190
- if (slideContent.includes(cleanOld)) {
191
- slideContent = slideContent.replace(cleanOld, cleanNew);
192
- } else {
193
- const lines = slideContent.split("\n");
194
- let replaced = false;
195
- for (let i = 0; i < lines.length; i++) {
196
- const line = lines[i];
197
- const stripped = line.replace(/^[#\s\-\*\>]+/, "").replace(/[*_`]/g, "").trim();
198
- if (stripped === cleanOld) {
199
- const prefixMatch = line.match(/^([#\s\-\*\>]+)/);
200
- const prefix = prefixMatch ? prefixMatch[1] : "";
201
- lines[i] = `${prefix}${cleanNew}`;
202
- replaced = true;
203
- break;
204
- }
205
- }
206
- if (replaced) {
207
- slideContent = lines.join("\n");
208
- } else {
209
- const escaped = cleanOld.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
210
- const regex = new RegExp(escaped, "i");
211
- if (regex.test(slideContent)) {
212
- slideContent = slideContent.replace(regex, cleanNew);
213
- } else {
214
- const words = cleanOld.split(/\s+/).filter((w) => w.length > 2);
215
- if (words.length > 0) {
216
- const pattern = words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\s\\S]*?");
217
- const fuzzyRegex = new RegExp(pattern, "i");
218
- if (fuzzyRegex.test(slideContent)) {
219
- slideContent = slideContent.replace(fuzzyRegex, cleanNew);
220
- }
221
- }
222
- }
223
- }
224
- }
225
- slides[slideIndex] = slideContent;
226
- const joinedSlides = slides.join("\n---\n");
227
- if (frontmatter) {
228
- return `${frontmatter}
229
-
230
- ${joinedSlides}`;
231
- }
232
- return joinedSlides;
233
- }
234
- function removeSlideElementFromMarkdown(markdown, slideIndex, options) {
235
- const { frontmatter, slides } = splitMarkdownSlides(markdown);
236
- if (slideIndex < 0 || slideIndex >= slides.length) {
237
- return markdown;
238
- }
239
- let slideContent = slides[slideIndex];
240
- const cleanContent = options.content?.trim();
241
- if (cleanContent) {
242
- const lines = slideContent.split("\n");
243
- let removed = false;
244
- const updatedLines = lines.filter((line) => {
245
- if (removed) return true;
246
- const stripped = line.replace(/^[#\s\-\*\>]+/, "").replace(/[*_`]/g, "").trim();
247
- if (stripped === cleanContent || line.includes(cleanContent)) {
248
- removed = true;
249
- return false;
250
- }
251
- return true;
252
- });
253
- if (removed) {
254
- slideContent = updatedLines.join("\n");
255
- }
256
- }
257
- const layoutCommentRegex = /<!--\s*layout:\s*([\s\S]*?)-->/i;
258
- const match = slideContent.match(layoutCommentRegex);
259
- if (match && match[1]) {
260
- try {
261
- const parsed = yaml__default.default.load(match[1]);
262
- if (parsed && Array.isArray(parsed.boxes)) {
263
- const remainingBoxes = parsed.boxes.filter((b) => {
264
- if (options.boxId && b.id === options.boxId) return false;
265
- if (options.target && b.target === options.target) return false;
266
- return true;
267
- });
268
- if (remainingBoxes.length > 0) {
269
- const yamlStr = yaml__default.default.dump({ boxes: remainingBoxes }, { indent: 2, lineWidth: -1, noRefs: true }).trim();
270
- slideContent = slideContent.replace(layoutCommentRegex, `<!-- layout:
271
- ${yamlStr.split("\n").join("\n ")}
272
- -->`);
273
- } else {
274
- slideContent = slideContent.replace(layoutCommentRegex, "").trim();
275
- }
276
- }
277
- } catch {
278
- }
279
- }
280
- slides[slideIndex] = slideContent;
281
- const joinedSlides = slides.join("\n---\n");
282
- if (frontmatter) {
283
- return `${frontmatter}
284
-
285
- ${joinedSlides}`;
286
- }
287
- return joinedSlides;
288
- }
289
-
290
- // src/core/layout/presets.ts
291
- var SLIDE_LAYOUT_PRESETS = [
292
- {
293
- id: "title-hero",
294
- name: "Title & Speaker Cover",
295
- category: "cover",
296
- description: "High-impact opening slide with large title, subtitle, and speaker credentials.",
297
- layout: {
298
- boxes: [
299
- {
300
- id: "title",
301
- target: "h1",
302
- pos: [100, 180, 1080, 140],
303
- fontSize: "3rem",
304
- textAlign: "center"
305
- },
306
- {
307
- id: "subtitle",
308
- target: "h3",
309
- pos: [100, 350, 1080, 70],
310
- fontSize: "1.5rem",
311
- textAlign: "center"
312
- },
313
- {
314
- id: "speaker-meta",
315
- target: "p",
316
- pos: [100, 460, 1080, 80],
317
- fontSize: "1.1rem",
318
- textAlign: "center"
319
- }
320
- ]
321
- },
322
- markdownSnippet: `<!-- layout:
323
- boxes:
324
- - id: title
325
- target: h1
326
- pos: [100, 180, 1080, 140]
327
- fontSize: 3rem
328
- textAlign: center
329
- - id: subtitle
330
- target: h3
331
- pos: [100, 350, 1080, 70]
332
- fontSize: 1.5rem
333
- textAlign: center
334
- - id: speaker-meta
335
- target: p
336
- pos: [100, 460, 1080, 80]
337
- fontSize: 1.1rem
338
- textAlign: center
339
- -->
340
-
341
- # \u{1F680} Engineering Scalable Cloud Systems
342
- ### Principles, Bottlenecks, and Real-World Architecture Patterns
343
-
344
- **Presenter:** Senior Systems Architect | **Duration:** 45 mins | **Level:** Intermediate
345
-
346
- <!--
347
- Presenter Notes:
348
- - Welcome the audience and outline today's primary engineering goals.
349
- - Spark curiosity with an opening hook about scaling under high concurrency.
350
- -->`
351
- },
352
- {
353
- id: "two-column-split",
354
- name: "Two-Column Comparison",
355
- category: "split",
356
- description: "Balanced side-by-side comparison for legacy vs modern patterns or pros vs cons.",
357
- layout: {
358
- boxes: [
359
- {
360
- id: "heading",
361
- target: "h2",
362
- pos: [80, 50, 1120, 70],
363
- fontSize: "2rem"
364
- },
365
- {
366
- id: "col-left",
367
- target: ".columns-2 > div:first-child",
368
- pos: [80, 150, 530, 510]
369
- },
370
- {
371
- id: "col-right",
372
- target: ".columns-2 > div:last-child",
373
- pos: [670, 150, 530, 510]
374
- }
375
- ]
376
- },
377
- markdownSnippet: `<!-- layout:
378
- boxes:
379
- - id: heading
380
- target: h2
381
- pos: [80, 50, 1120, 70]
382
- fontSize: 2rem
383
- - id: col-left
384
- target: '.columns-2 > div:first-child'
385
- pos: [80, 150, 530, 510]
386
- - id: col-right
387
- target: '.columns-2 > div:last-child'
388
- pos: [670, 150, 530, 510]
389
- -->
390
-
391
- ## \u2696\uFE0F Monolithic Polling vs Event-Driven Architecture
392
-
393
- <div class="columns-2">
394
- <div>
395
-
396
- ### \u{1F534} Legacy Polling
397
- - Heavy database CPU overhead on idle cycles
398
- - High network latency between updates
399
- - Prone to cascading timeouts during spikes
400
-
401
- </div>
402
- <div>
403
-
404
- ### \u{1F7E2} Event-Driven Streams
405
- - Real-time event propagation via webhooks
406
- - Zero idle compute waste with serverless consumers
407
- - Automatic backpressure and queue isolation
408
-
409
- </div>
410
- </div>
411
-
412
- <!--
413
- Presenter Notes:
414
- - Emphasize the core failure mode of periodic polling under sudden load spikes.
415
- - Ask the room if anyone has experienced database lockups caused by polling loops.
416
- -->`
417
- },
418
- {
419
- id: "code-explainer",
420
- name: "Code Walkthrough & Analysis",
421
- category: "code",
422
- description: "Syntax-highlighted code block on the left with step-by-step key annotations on the right.",
423
- layout: {
424
- boxes: [
425
- {
426
- id: "heading",
427
- target: "h2",
428
- pos: [80, 45, 1120, 65],
429
- fontSize: "1.9rem"
430
- },
431
- {
432
- id: "code-box",
433
- target: ".columns-2 > div:first-child",
434
- pos: [80, 135, 630, 535]
435
- },
436
- {
437
- id: "notes-box",
438
- target: ".columns-2 > div:last-child",
439
- pos: [740, 135, 460, 535]
440
- }
441
- ]
442
- },
443
- markdownSnippet: `<!-- layout:
444
- boxes:
445
- - id: heading
446
- target: h2
447
- pos: [80, 45, 1120, 65]
448
- fontSize: 1.9rem
449
- - id: code-box
450
- target: '.columns-2 > div:first-child'
451
- pos: [80, 135, 630, 535]
452
- - id: notes-box
453
- target: '.columns-2 > div:last-child'
454
- pos: [740, 135, 460, 535]
455
- -->
456
-
457
- ## \u{1F4BB} Robust Async Transaction Pipeline
458
-
459
- <div class="columns-2">
460
- <div>
461
-
462
- \`\`\`ts
463
- // Process payment with idempotency guarantee
464
- export async function processPayment(order: Order) {
465
- const isValid = await validateOrder(order);
466
- if (!isValid) throw new Error("ValidationFailed");
467
-
468
- const receipt = await chargeCard(order);
469
- return sendConfirmation(receipt);
470
- }
471
- \`\`\`
472
-
473
- </div>
474
- <div>
475
-
476
- ### \u{1F50D} Key Engineering Invariants:
477
- 1. **Early Guard Clause:** Validates payload immediately at line 3 before triggering external calls.
478
- 2. **Deterministic Sequence:** Enforces strict order of operations using native \`await\`.
479
- 3. **Audit Trail:** Returns immutable payment receipt upon successful execution.
480
-
481
- </div>
482
- </div>
483
-
484
- <!--
485
- Presenter Notes:
486
- - Walk through lines 1 to 7 sequentially.
487
- - Cold-call check: "What happens if line 6 throws a network timeout?"
488
- - Cognitive scaffolding: Highlight the importance of idempotency keys in payment APIs.
489
- -->`
490
- },
491
- {
492
- id: "metrics-3-card",
493
- name: "3-Metric Key Results Grid",
494
- category: "metrics",
495
- description: "Three high-visibility KPI stat cards for benchmarks, impact, and success criteria.",
496
- layout: {
497
- boxes: [
498
- {
499
- id: "heading",
500
- target: "h2",
501
- pos: [80, 50, 1120, 70],
502
- fontSize: "2rem",
503
- textAlign: "center"
504
- },
505
- {
506
- id: "metric-1",
507
- target: ".columns-3 > div:nth-child(1)",
508
- pos: [80, 165, 340, 485]
509
- },
510
- {
511
- id: "metric-2",
512
- target: ".columns-3 > div:nth-child(2)",
513
- pos: [470, 165, 340, 485]
514
- },
515
- {
516
- id: "metric-3",
517
- target: ".columns-3 > div:nth-child(3)",
518
- pos: [860, 165, 340, 485]
519
- }
520
- ]
521
- },
522
- markdownSnippet: `<!-- layout:
523
- boxes:
524
- - id: heading
525
- target: h2
526
- pos: [80, 50, 1120, 70]
527
- fontSize: 2rem
528
- textAlign: center
529
- - id: metric-1
530
- target: '.columns-3 > div:nth-child(1)'
531
- pos: [80, 165, 340, 485]
532
- - id: metric-2
533
- target: '.columns-3 > div:nth-child(2)'
534
- pos: [470, 165, 340, 485]
535
- - id: metric-3
536
- target: '.columns-3 > div:nth-child(3)'
537
- pos: [860, 165, 340, 485]
538
- -->
539
-
540
- ## \u{1F4CA} Performance & Reliability Benchmarks
541
-
542
- <div class="columns-3">
543
- <div>
544
-
545
- # \u26A1 99.99%
546
- ### SLA Availability
547
- High-availability multi-region cluster with automated failover routing.
548
-
549
- </div>
550
- <div>
551
-
552
- # \u{1F680} 15ms
553
- ### P99 Latency
554
- Sub-20ms roundtrip response time via distributed edge caching.
555
-
556
- </div>
557
- <div>
558
-
559
- # \u{1F6E1}\uFE0F Zero
560
- ### Unhandled Breaches
561
- Full SOC2 Type II compliance and end-to-end payload encryption.
562
-
563
- </div>
564
- </div>
565
-
566
- <!--
567
- Presenter Notes:
568
- - Highlight the 15ms P99 latency target as the primary technical milestone.
569
- - Connect these metrics directly to the architecture decisions discussed next.
570
- -->`
571
- },
572
- {
573
- id: "process-flow",
574
- name: "Architecture & Process Flow",
575
- category: "diagram",
576
- description: "Multi-stage workflow with responsive Mermaid diagram and phase summaries.",
577
- layout: {
578
- boxes: [
579
- {
580
- id: "heading",
581
- target: "h2",
582
- pos: [80, 45, 1120, 65],
583
- fontSize: "1.9rem"
584
- },
585
- {
586
- id: "diagram-box",
587
- target: "pre.mermaid, .mermaid",
588
- pos: [80, 130, 1120, 310]
589
- },
590
- {
591
- id: "summary-box",
592
- target: "ul, p",
593
- pos: [80, 465, 1120, 205]
594
- }
595
- ]
596
- },
597
- markdownSnippet: `<!-- layout:
598
- boxes:
599
- - id: heading
600
- target: h2
601
- pos: [80, 45, 1120, 65]
602
- fontSize: 1.9rem
603
- - id: diagram-box
604
- target: 'pre.mermaid, .mermaid'
605
- pos: [80, 130, 1120, 310]
606
- - id: summary-box
607
- target: 'ul, p'
608
- pos: [80, 465, 1120, 205]
609
- -->
610
-
611
- ## \u{1F504} End-to-End Data Ingestion Pipeline
612
-
613
- \`\`\`mermaid
614
- graph LR
615
- A[1. Client Ingress] --> B[2. API Gateway]
616
- B --> C[3. Auth Verifier]
617
- C --> D[4. Distributed DB]
618
- style A fill:#0284c7,stroke:#38bdf8,stroke-width:2px,color:#fff
619
- style B fill:#1e293b,stroke:#64748b,stroke-width:2px,color:#fff
620
- style C fill:#059669,stroke:#34d399,stroke-width:2px,color:#fff
621
- style D fill:#7c3aed,stroke:#a78bfa,stroke-width:2px,color:#fff
622
- \`\`\`
623
-
624
- - **Ingress & Gateway:** Rate-limits traffic and applies reverse-proxy load balancing.
625
- - **Verification & Storage:** Decodes cryptographic JWT tokens before transactional write commit.
626
-
627
- <!--
628
- Presenter Notes:
629
- - Trace each hop across the diagram from left to right.
630
- - Ask: "Where should rate limiting be applied to prevent DDoS attacks?"
631
- -->`
632
- },
633
- {
634
- id: "quote-highlight",
635
- name: "Core Principle & Quote Highlight",
636
- category: "quote",
637
- description: "Prominent quote callout for fundamental engineering rules or memorable mantras.",
638
- layout: {
639
- boxes: [
640
- {
641
- id: "heading",
642
- target: "h2",
643
- pos: [100, 100, 1080, 80],
644
- fontSize: "2.2rem",
645
- textAlign: "center"
646
- },
647
- {
648
- id: "quote-box",
649
- target: "blockquote",
650
- pos: [140, 230, 1e3, 360],
651
- fontSize: "1.4rem"
652
- }
653
- ]
654
- },
655
- markdownSnippet: `<!-- layout:
656
- boxes:
657
- - id: heading
658
- target: h2
659
- pos: [100, 100, 1080, 80]
660
- fontSize: 2.2rem
661
- textAlign: center
662
- - id: quote-box
663
- target: blockquote
664
- pos: [140, 230, 1000, 360]
665
- fontSize: 1.4rem
666
- -->
667
-
668
- ## \u{1F4A1} Foundational Architecture Principle
669
-
670
- > "Simplicity is prerequisite for reliability. A well-engineered distributed system is one where a new engineer can understand the request flow within their first hour."
671
- >
672
- > \u2014 **Edsger W. Dijkstra**
673
-
674
- <!--
675
- Presenter Notes:
676
- - Pause for 5 seconds of silence to let the quote sink in.
677
- - Ask: "How do our current architectural choices reflect this principle?"
678
- -->`
679
- },
680
- {
681
- id: "quiz-checkpoint",
682
- name: "Interactive Formative Checkpoint",
683
- category: "assessment",
684
- description: "Interactive multiple-choice diagnostic checkpoint with A/B/C/D voting options.",
685
- layout: {
686
- boxes: [
687
- {
688
- id: "heading",
689
- target: "h2",
690
- pos: [80, 45, 1120, 65],
691
- fontSize: "1.9rem"
692
- },
693
- {
694
- id: "question-callout",
695
- target: "blockquote",
696
- pos: [80, 130, 1120, 110],
697
- fontSize: "1.2rem"
698
- },
699
- {
700
- id: "options-list",
701
- target: "ul",
702
- pos: [80, 260, 1120, 390],
703
- fontSize: "1.15rem"
704
- }
705
- ]
706
- },
707
- markdownSnippet: `<!-- layout:
708
- boxes:
709
- - id: heading
710
- target: h2
711
- pos: [80, 45, 1120, 65]
712
- fontSize: 1.9rem
713
- - id: question-callout
714
- target: blockquote
715
- pos: [80, 130, 1120, 110]
716
- fontSize: 1.2rem
717
- - id: options-list
718
- target: ul
719
- pos: [80, 260, 1120, 390]
720
- fontSize: 1.15rem
721
- -->
722
-
723
- ## \u{1F3AF} Formative Checkpoint: Verify Your Understanding
724
-
725
- > **Question:** In an event-driven architecture, which pattern guarantees that messages are never permanently lost if a downstream consumer crashes?
726
-
727
- - **A.** Direct HTTP POST call with a short 2-second timeout
728
- - **B.** Persistent Message Broker with Dead-Letter Queue (DLQ) & retry policy
729
- - **C.** In-memory client RAM cache without disk persistence
730
- - **D.** Discarding failed packets and prompting the user to re-submit
731
-
732
- <!--
733
- Presenter Notes:
734
- - Give students 30 seconds to vote on option A, B, C, or D.
735
- - Correct answer: B.
736
- - Explain why options A and C introduce catastrophic data loss in production.
737
- -->`
738
- },
739
- {
740
- id: "tiered-practice-3cards",
741
- name: "Tiered Differentiation (Bronze / Silver / Gold)",
742
- category: "content",
743
- description: "Three progressive competency tiers allowing self-paced learning acceleration.",
744
- layout: {
745
- boxes: [
746
- {
747
- id: "heading",
748
- target: "h2",
749
- pos: [80, 45, 1120, 65],
750
- fontSize: "1.9rem"
751
- },
752
- {
753
- id: "tier-bronze",
754
- target: ".columns-3 > div:nth-child(1)",
755
- pos: [80, 140, 350, 520]
756
- },
757
- {
758
- id: "tier-silver",
759
- target: ".columns-3 > div:nth-child(2)",
760
- pos: [465, 140, 350, 520]
761
- },
762
- {
763
- id: "tier-gold",
764
- target: ".columns-3 > div:nth-child(3)",
765
- pos: [850, 140, 350, 520]
766
- }
767
- ]
768
- },
769
- markdownSnippet: `<!-- layout:
770
- boxes:
771
- - id: heading
772
- target: h2
773
- pos: [80, 45, 1120, 65]
774
- fontSize: 1.9rem
775
- - id: tier-bronze
776
- target: '.columns-3 > div:nth-child(1)'
777
- pos: [80, 140, 350, 520]
778
- - id: tier-silver
779
- target: '.columns-3 > div:nth-child(2)'
780
- pos: [465, 140, 350, 520]
781
- - id: tier-gold
782
- target: '.columns-3 > div:nth-child(3)'
783
- pos: [850, 140, 350, 520]
784
- -->
785
-
786
- ## \u{1F6E0}\uFE0F Hands-On Challenge: 3-Tier Differentiation
787
-
788
- <div class="columns-3">
789
- <div>
790
-
791
- ### \u{1F949} Bronze Tier
792
- - Implement basic handler logic
793
- - Pass 3 baseline unit tests
794
- - *Goal: Foundational mastery*
795
-
796
- </div>
797
- <div>
798
-
799
- ### \u{1F948} Silver Tier
800
- - Add boundary error handling
801
- - Enforce O(N) memory complexity
802
- - *Goal: Production-ready code*
803
-
804
- </div>
805
- <div>
806
-
807
- ### \u{1F947} Gold Tier
808
- - Design distributed retry queue
809
- - Add 95% automated test coverage
810
- - *Goal: Architectural leadership*
811
-
812
- </div>
813
- </div>
814
-
815
- <!--
816
- Presenter Notes:
817
- - All learners begin at Bronze to establish baseline competency.
818
- - Accelerate self-directed learners to Silver and Gold as they complete each tier.
819
- -->`
820
- },
821
- {
822
- id: "agenda-timeline",
823
- name: "Session Agenda & Milestones",
824
- category: "content",
825
- description: "Structured time-budgeted agenda mapping stages to measurable deliverables.",
826
- layout: {
827
- boxes: [
828
- {
829
- id: "heading",
830
- target: "h2",
831
- pos: [80, 45, 1120, 65],
832
- fontSize: "1.9rem"
833
- },
834
- {
835
- id: "table-box",
836
- target: "table",
837
- pos: [80, 140, 1120, 520]
838
- }
839
- ]
840
- },
841
- markdownSnippet: `<!-- layout:
842
- boxes:
843
- - id: heading
844
- target: h2
845
- pos: [80, 45, 1120, 65]
846
- fontSize: 1.9rem
847
- - id: table-box
848
- target: table
849
- pos: [80, 140, 1120, 520]
850
- -->
851
-
852
- ## \u{1F5FA}\uFE0F Milestone Roadmap & Time Budget
853
-
854
- | Allocated Time | Session Stage | Core Learning Focus | Deliverable Milestone |
855
- |:---:|---|---|---|
856
- | **05 mins** | Warm-Up | Real-World Scenario & Context | Identify system bottleneck |
857
- | **15 mins** | Concept Discovery | Core Architectural Patterns | Deconstruct message queue models |
858
- | **20 mins** | Hands-On Lab | Tiered Code Implementation | Complete Bronze + Silver tasks |
859
- | **05 mins** | Wrap-Up | Formative Checkpoint & Debrief | Consolidate key principles |
860
-
861
- <!--
862
- Presenter Notes:
863
- - Walk through the time budget so students understand expectations and pacing.
864
- -->`
865
- },
866
- {
867
- id: "takeaways-summary",
868
- name: "Key Takeaways & Wrap-up",
869
- category: "content",
870
- description: "Three memorable synthesis takeaways with actionable next steps.",
871
- layout: {
872
- boxes: [
873
- {
874
- id: "heading",
875
- target: "h2",
876
- pos: [80, 50, 1120, 70],
877
- fontSize: "2rem"
878
- },
879
- {
880
- id: "list-box",
881
- target: "ol",
882
- pos: [80, 150, 1120, 340],
883
- fontSize: "1.25rem"
884
- },
885
- {
886
- id: "callout-next",
887
- target: "blockquote",
888
- pos: [80, 520, 1120, 140],
889
- fontSize: "1.1rem"
890
- }
891
- ]
892
- },
893
- markdownSnippet: `<!-- layout:
894
- boxes:
895
- - id: heading
896
- target: h2
897
- pos: [80, 50, 1120, 70]
898
- fontSize: 2rem
899
- - id: list-box
900
- target: ol
901
- pos: [80, 150, 1120, 340]
902
- fontSize: 1.25rem
903
- - id: callout-next
904
- target: blockquote
905
- pos: [80, 520, 1120, 140]
906
- fontSize: 1.1rem
907
- -->
908
-
909
- ## \u{1F3C1} Key Takeaways & Action Items
910
-
911
- 1. **Decouple Before Scaling:** Separate producer rate from consumer processing capacity.
912
- 2. **Design for Failure:** Always configure retries, backoff intervals, and dead-letter queues.
913
- 3. **Measure End-to-End:** Track P99 latency and error rates across all distributed boundaries.
914
-
915
- > \u{1F4DD} **Next Action Item:** Complete the Silver Tier refactoring challenge before our next peer-review session.
916
-
917
- <!--
918
- Presenter Notes:
919
- - Deliver a crisp 2-minute wrap-up reinforcing the 3 core takeaways.
920
- - Acknowledge strong peer collaboration during the hands-on lab.
921
- -->`
922
- }
923
- ];
924
- function getSlideLayoutPresetById(id) {
925
- return SLIDE_LAYOUT_PRESETS.find((p) => p.id === id);
926
- }
927
- function getSlideLayoutPresetsByCategory(category) {
928
- return SLIDE_LAYOUT_PRESETS.filter((p) => p.category === category);
929
- }
930
77
 
931
78
  // src/html-engine/themes.ts
932
79
  var HTML_THEMES = {
@@ -1353,6 +500,133 @@ var HTML_SLIDE_PRESETS = {
1353
500
  }
1354
501
  };
1355
502
 
503
+ // src/html-engine/utilities.ts
504
+ var FONT_SIZES = [
505
+ // class, var, fallback px, line-height
506
+ ["xs", "--font-size-xs", "14px", "1.4"],
507
+ ["sm", "--font-size-sm", "16px", "1.45"],
508
+ ["base", "--font-size-base", "18px", "1.5"],
509
+ ["lg", "--font-size-lg", "20px", "1.5"],
510
+ ["xl", "--font-size-xl", "22px", "1.5"],
511
+ ["2xl", "--font-size-2xl", "26px", "1.4"],
512
+ ["3xl", "--font-size-3xl", "32px", "1.3"],
513
+ ["4xl", "--font-size-4xl", "38px", "1.25"],
514
+ ["6xl", "--font-size-6xl", "58px", "1.15"],
515
+ ["8xl", "--font-size-8xl", "96px", "1.05"]
516
+ ];
517
+ var SPACING = {
518
+ "0.5": "0.125rem",
519
+ "1": "0.25rem",
520
+ "1.5": "0.375rem",
521
+ "2": "0.5rem",
522
+ "2.5": "0.625rem",
523
+ "3": "0.75rem",
524
+ "4": "1rem",
525
+ "5": "1.25rem",
526
+ "6": "1.5rem",
527
+ "7": "1.75rem",
528
+ "8": "2rem",
529
+ "10": "2.5rem",
530
+ "12": "3rem",
531
+ "16": "4rem",
532
+ "20": "5rem",
533
+ "24": "6rem"
534
+ };
535
+ function sp(value) {
536
+ return `calc(${SPACING[value]} * var(--scale-spacing-eff, 1))`;
537
+ }
538
+ function fontSizeRules() {
539
+ return FONT_SIZES.map(
540
+ ([cls, cssVar, fallback, lh]) => `.text-${cls}{font-size:var(${cssVar}, ${fallback});line-height:${lh}}`
541
+ ).join("\n");
542
+ }
543
+ function spacingRules() {
544
+ const out = [];
545
+ for (const value of Object.keys(SPACING)) {
546
+ out.push(`.p-${value}{padding:${sp(value)}}`);
547
+ out.push(`.px-${value}{padding-left:${sp(value)};padding-right:${sp(value)}}`);
548
+ out.push(`.py-${value}{padding-top:${sp(value)};padding-bottom:${sp(value)}}`);
549
+ out.push(`.pt-${value}{padding-top:${sp(value)}}`);
550
+ out.push(`.pb-${value}{padding-bottom:${sp(value)}}`);
551
+ out.push(`.mt-${value}{margin-top:${sp(value)}}`);
552
+ out.push(`.mb-${value}{margin-bottom:${sp(value)}}`);
553
+ out.push(`.my-${value}{margin-top:${sp(value)};margin-bottom:${sp(value)}`);
554
+ out.push(`.gap-${value}{gap:${sp(value)}}`);
555
+ }
556
+ return out.join("\n");
557
+ }
558
+ function sizeRules() {
559
+ const out = [];
560
+ for (const value of Object.keys(SPACING)) {
561
+ out.push(`.w-${value}{width:${sp(value)}}`);
562
+ out.push(`.h-${value}{height:${sp(value)}}`);
563
+ }
564
+ return out.join("\n");
565
+ }
566
+ var SLIDE_UTILITY_CSS = `
567
+ /* preflight \u2014 the presets were authored against Tailwind's reset (no default
568
+ margins, no list markers, border-box everywhere), so it has to come along. */
569
+ .slide-page *,.slide-page *::before,.slide-page *::after{box-sizing:border-box;border-width:0;border-style:solid}
570
+ .slide-page h1,.slide-page h2,.slide-page h3,.slide-page h4,.slide-page h5,.slide-page h6,
571
+ .slide-page p,.slide-page ul,.slide-page ol,.slide-page li,.slide-page figure,.slide-page blockquote,
572
+ .slide-page pre,.slide-page dl,.slide-page dd{margin:0;padding:0}
573
+ .slide-page ul,.slide-page ol{list-style:none}
574
+ .slide-page img,.slide-page svg,.slide-page video{display:block;max-width:100%}
575
+ .slide-page button{font:inherit;background:none;border:0;cursor:pointer;color:inherit}
576
+ .slide-page table{border-collapse:collapse}
577
+ .slide-page a{color:inherit;text-decoration:inherit}
578
+ /* layout */
579
+ .flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.inline-block{display:inline-block}
580
+ .flex-col{flex-direction:column}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.shrink-0{flex-shrink:0}
581
+ .grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}
582
+ .grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}
583
+ .grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}
584
+ .col-span-5{grid-column:span 5/span 5}.col-span-7{grid-column:span 7/span 7}
585
+ .items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}
586
+ .justify-center{justify-content:center}.justify-between{justify-content:space-between}
587
+ .relative{position:relative}.overflow-hidden{overflow:hidden}.z-10{z-index:10}
588
+ .h-full{height:100%}.w-full{width:100%}.min-h-0{min-height:0}.my-auto{margin-top:auto;margin-bottom:auto}
589
+ .text-center{text-align:center}
590
+ /* typography */
591
+ .font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}
592
+ .font-extrabold{font-weight:800}.font-black{font-weight:900}.font-mono{font-family:var(--font-code)}
593
+ .tracking-tighter{letter-spacing:-0.05em}.tracking-tight{letter-spacing:-0.025em}
594
+ .tracking-wider{letter-spacing:0.05em}.tracking-widest{letter-spacing:0.1em}
595
+ .uppercase{text-transform:uppercase}
596
+ .leading-tight{line-height:1.25}.leading-snug{line-height:1.375}.leading-relaxed{line-height:1.625}
597
+ .max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}
598
+ ${fontSizeRules()}
599
+ /* spacing (scaled by the density editor) */
600
+ ${spacingRules()}
601
+ ${sizeRules()}
602
+ .space-y-3 > * + *{margin-top:${sp("3")}}
603
+ .space-y-4 > * + *{margin-top:${sp("4")}}
604
+ .space-y-6 > * + *{margin-top:${sp("6")}}
605
+ /* surfaces & color (theme-variable driven) */
606
+ .rounded{border-radius:0.25rem}.rounded-xl{border-radius:0.75rem}.rounded-full{border-radius:9999px}
607
+ .border{border-width:1px;border-style:solid;border-color:var(--border)}
608
+ .border-t{border-top-width:1px;border-top-style:solid;border-top-color:var(--border)}
609
+ .border-b{border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:var(--border)}
610
+ .border-current{border-color:currentColor}
611
+ .shadow{box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.08)}
612
+ .shadow-sm{box-shadow:0 1px 2px rgba(0,0,0,.08)}
613
+ .shadow-lg{box-shadow:0 10px 15px -3px rgba(0,0,0,.15),0 4px 6px -4px rgba(0,0,0,.1)}
614
+ .opacity-60{opacity:.6}.opacity-65{opacity:.65}.opacity-75{opacity:.75}
615
+ .opacity-80{opacity:.8}.opacity-85{opacity:.85}.opacity-90{opacity:.9}
616
+ .bg-red-500\\/70{background-color:rgba(239,68,68,.7)}
617
+ .bg-yellow-500\\/70{background-color:rgba(234,179,8,.7)}
618
+ .bg-green-500\\/70{background-color:rgba(34,197,94,.7)}
619
+ /* theme color utilities (previously provided by tailwind.config extend) */
620
+ .text-primary{color:var(--primary)}.text-muted{color:var(--text-muted)}.text-themeText{color:var(--text)}
621
+ .bg-themeBg{background-color:var(--bg)}.bg-themeBgSecondary{background-color:var(--bg-secondary)}
622
+ .bg-themePrimary{background-color:var(--primary)}.border-themeBorder{border-color:var(--border)}
623
+ `.trim();
624
+ function listUtilityClasses() {
625
+ return Array.from(SLIDE_UTILITY_CSS.matchAll(/\.([a-zA-Z0-9\\/_.-]+)(?=[{,: >])/g)).map(
626
+ (m) => m[1].replace(/\\\//g, "/")
627
+ );
628
+ }
629
+
1356
630
  // src/html-engine/shell.ts
1357
631
  function generateHtmlShell(options) {
1358
632
  const { title, theme, slidesHtml, slideCount, customHeadTags = "", deckId = "" } = options;
@@ -1364,12 +638,16 @@ function generateHtmlShell(options) {
1364
638
  <title>${title}</title>
1365
639
 
1366
640
  <!-- Fonts -->
641
+ <!-- DECK_FONT_BLOCK_START -->${options?.fontFaceCss ? `
642
+ <style>
643
+ ${options.fontFaceCss}
644
+ </style>` : `
1367
645
  <link rel="preconnect" href="https://fonts.googleapis.com">
1368
646
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1369
- <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@500;700;800&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
647
+ <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@500;700;800&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">`}<!-- DECK_FONT_BLOCK_END -->
1370
648
 
1371
- <!-- Core Presentation Styling & 1920x1080 Scaled Tailwind Configuration -->
1372
- <script src="https://cdn.tailwindcss.com"></script>
649
+ <!-- Core Presentation Styling & 1920x1080 Scaled Layout Utilities (inlined) -->
650
+ <style>${SLIDE_UTILITY_CSS}</style>
1373
651
  <script>
1374
652
  window.tailwind = window.tailwind || {};
1375
653
  tailwind.config = {
@@ -1519,6 +797,59 @@ function generateHtmlShell(options) {
1519
797
  opacity: 1;
1520
798
  }
1521
799
 
800
+ /* \u2500\u2500 Bespoke (authored) slides \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
801
+ The frontend-slides mode hands the model the whole slide surface, so the
802
+ shell owns only what must stay invariant: full-stage sizing, border-box
803
+ maths, theme type defaults, and one orchestrated entrance. */
804
+ .bespoke-slide {
805
+ position: relative;
806
+ width: 100%;
807
+ height: 100%;
808
+ overflow: hidden;
809
+ }
810
+
811
+ .bespoke-slide,
812
+ .bespoke-slide *,
813
+ .bespoke-slide *::before,
814
+ .bespoke-slide *::after {
815
+ box-sizing: border-box;
816
+ }
817
+
818
+ .bespoke-slide h1,
819
+ .bespoke-slide h2 {
820
+ font-family: var(--font-display);
821
+ letter-spacing: -0.02em;
822
+ }
823
+
824
+ .bespoke-slide img,
825
+ .bespoke-slide svg {
826
+ max-width: 100%;
827
+ height: auto;
828
+ }
829
+
830
+ .slide-page.active .bespoke-slide > * {
831
+ animation: bespokeReveal 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
832
+ }
833
+
834
+ .slide-page.active .bespoke-slide > *:nth-child(2) { animation-delay: 0.08s; }
835
+ .slide-page.active .bespoke-slide > *:nth-child(3) { animation-delay: 0.16s; }
836
+ .slide-page.active .bespoke-slide > *:nth-child(4) { animation-delay: 0.24s; }
837
+ .slide-page.active .bespoke-slide > *:nth-child(5) { animation-delay: 0.32s; }
838
+ .slide-page.active .bespoke-slide > *:nth-child(n + 6) { animation-delay: 0.4s; }
839
+
840
+ @keyframes bespokeReveal {
841
+ from { opacity: 0; transform: translateY(14px); }
842
+ to { opacity: 1; transform: none; }
843
+ }
844
+
845
+ @media (prefers-reduced-motion: reduce) {
846
+ .slide-page.active .bespoke-slide > * {
847
+ animation: none;
848
+ opacity: 1;
849
+ transform: none;
850
+ }
851
+ }
852
+
1522
853
  /* Top Progress Bar */
1523
854
  #progress-bar {
1524
855
  position: absolute;
@@ -3661,6 +2992,94 @@ main();`;
3661
2992
  return slots;
3662
2993
  }
3663
2994
 
2995
+ // src/html-engine/sanitize.ts
2996
+ var CONTAINER_TAGS = ["script", "iframe", "object", "embed", "applet", "form", "button", "textarea", "select", "template"];
2997
+ var VOID_TAGS = ["link", "meta", "base", "input"];
2998
+ var CONTAINER_ELEMENT_RE = new RegExp(
2999
+ `<(${CONTAINER_TAGS.join("|")})\\b[\\s\\S]*?<\\/\\1\\s*>`,
3000
+ "gi"
3001
+ );
3002
+ var STRAY_TAG_RE = new RegExp(`<\\/?(?:${[...CONTAINER_TAGS, ...VOID_TAGS].join("|")})\\b[^>]*>`, "gi");
3003
+ var EVENT_ATTR_RE = /\s+on[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi;
3004
+ var EXTERNAL_URL_RE = /^(?:https?:)?\/\//i;
3005
+ var CSS_URL_RE = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)'"\s]*))\s*\)/gi;
3006
+ var CSS_IMPORT_RE = /@import[^;]+;/gi;
3007
+ function isOffBoxUrl(url) {
3008
+ const value = url.trim();
3009
+ if (!value) return false;
3010
+ return EXTERNAL_URL_RE.test(value) || /^javascript:/i.test(value) || /^data:(?!image\/)/i.test(value);
3011
+ }
3012
+ function sanitizeSlideHtml(raw) {
3013
+ const removed = [];
3014
+ let html = String(raw ?? "");
3015
+ html = html.replace(CONTAINER_ELEMENT_RE, (_match, tag) => {
3016
+ removed.push(`<${String(tag).toLowerCase()}>`);
3017
+ return "";
3018
+ });
3019
+ html = html.replace(STRAY_TAG_RE, (match) => {
3020
+ const tag = (match.match(/^<\/?\s*([a-z]+)/i) || [])[1]?.toLowerCase();
3021
+ if (tag) removed.push(`<${tag}>`);
3022
+ return "";
3023
+ });
3024
+ const beforeEvents = html.length;
3025
+ html = html.replace(EVENT_ATTR_RE, "");
3026
+ if (html.length !== beforeEvents) removed.push("inline event handler");
3027
+ html = html.replace(CSS_URL_RE, (match, dq, sq, bare) => {
3028
+ if (!isOffBoxUrl(String(dq ?? sq ?? bare ?? ""))) return match;
3029
+ removed.push("external url()");
3030
+ return "none";
3031
+ });
3032
+ const beforeImports = html.length;
3033
+ html = html.replace(CSS_IMPORT_RE, "");
3034
+ if (html.length !== beforeImports) removed.push("@import");
3035
+ html = html.replace(/\s+(href|src|srcset|poster)\s*=\s*("([^"]*)"|'([^']*)')/gi, (match, attr, _q, dq, sq) => {
3036
+ const value = String(dq ?? sq ?? "").trim();
3037
+ if (!isOffBoxUrl(value)) return match;
3038
+ removed.push(`${attr}="${value.slice(0, 60)}"`);
3039
+ return "";
3040
+ });
3041
+ return { html: html.trim(), removed: Array.from(new Set(removed)) };
3042
+ }
3043
+ function validateBespokeSlideHtml(raw, contract = {}) {
3044
+ const maxChars = contract.maxChars ?? 16e3;
3045
+ const minChars = contract.minChars ?? 120;
3046
+ const issues = [];
3047
+ const html = String(raw ?? "").trim();
3048
+ if (!html) return { valid: false, issues: ["slide html is empty"] };
3049
+ if (html.length > maxChars) issues.push(`slide html exceeds ${maxChars} chars (${html.length})`);
3050
+ if (html.replace(/<[^>]*>/g, "").replace(/\s+/g, "").length < minChars) {
3051
+ issues.push("slide has too little text content to teach anything");
3052
+ }
3053
+ if (!/<h[12]\b/i.test(html)) {
3054
+ issues.push("slide has no <h1>/<h2> \u2014 typographic hierarchy is required");
3055
+ }
3056
+ const { html: clean, removed } = sanitizeSlideHtml(html);
3057
+ if (removed.length > 0) {
3058
+ issues.push(`slide is not self-contained (stripped: ${removed.join(", ")})`);
3059
+ }
3060
+ if (/<script\b/i.test(clean)) issues.push("slide still contains a script tag after sanitizing");
3061
+ if (/\b(todo|tbd|lorem ipsum|<code>)\b/i.test(clean) && !/<pre\b/i.test(clean)) {
3062
+ issues.push("slide contains placeholder text");
3063
+ }
3064
+ if (/<html\b|<body\b|<section\b[^>]*class="[^"]*slide-page/i.test(clean)) {
3065
+ issues.push("slide markup carries deck-level structure (html/body/section.slide-page)");
3066
+ }
3067
+ const pseudoRe = /::(before|after)\s*\{([^}]*)\}/g;
3068
+ for (const [, , body] of clean.matchAll(pseudoRe)) {
3069
+ if (/content:\s*['"]{2}/.test(body) && /position:\s*absolute/.test(body) && /rotate\(|translate\(/.test(body)) {
3070
+ issues.push(
3071
+ "decorative pseudo-element with absolute positioning + rotate/translate overlaps text \u2014 draw the decoration inside a sized box instead"
3072
+ );
3073
+ break;
3074
+ }
3075
+ }
3076
+ return { valid: issues.length === 0, issues };
3077
+ }
3078
+ function buildBespokeSlideMarkup(raw) {
3079
+ const { html } = sanitizeSlideHtml(raw);
3080
+ return `<div class="bespoke-slide">${html}</div>`;
3081
+ }
3082
+
3664
3083
  // src/html-engine/compiler.ts
3665
3084
  var SHIKI_LANGS = /* @__PURE__ */ new Set([
3666
3085
  "javascript",
@@ -3711,6 +3130,16 @@ function compileHtmlDeck(deckData, options) {
3711
3130
  const slides = deckData.slides || [];
3712
3131
  const slideCount = Math.max(slides.length, 1);
3713
3132
  const renderedSlides = slides.map((slide, index) => {
3133
+ if (typeof slide.html === "string" && slide.html.trim()) {
3134
+ const notesAttr2 = slide.notes ? ` data-notes="${encodeURIComponent(slide.notes)}"` : "";
3135
+ const customClass2 = slide.customClass ? ` ${slide.customClass}` : "";
3136
+ return `
3137
+ <!-- Slide ${index + 1}: authored html -->
3138
+ <section class="slide-page${customClass2}" data-index="${index}" data-layout="bespoke"${notesAttr2}>
3139
+ ${buildBespokeSlideMarkup(slide.html)}
3140
+ </section>
3141
+ `;
3142
+ }
3714
3143
  const preset = HTML_SLIDE_PRESETS[slide.layoutId] || HTML_SLIDE_PRESETS["hero-cover"];
3715
3144
  const normalizedSlots = normalizeSlideSlots(slide);
3716
3145
  let innerHtml = preset.template(normalizedSlots, theme);
@@ -3773,21 +3202,18 @@ async function compileHtmlDeckAsync(deckData, options) {
3773
3202
 
3774
3203
  exports.HTML_SLIDE_PRESETS = HTML_SLIDE_PRESETS;
3775
3204
  exports.HTML_THEMES = HTML_THEMES;
3776
- exports.SLIDE_LAYOUT_PRESETS = SLIDE_LAYOUT_PRESETS;
3205
+ exports.SLIDE_UTILITY_CSS = SLIDE_UTILITY_CSS;
3206
+ exports.buildBespokeSlideMarkup = buildBespokeSlideMarkup;
3777
3207
  exports.compileHtmlDeck = compileHtmlDeck;
3778
3208
  exports.compileHtmlDeckAsync = compileHtmlDeckAsync;
3779
3209
  exports.generateHtmlShell = generateHtmlShell;
3780
3210
  exports.getHighlighter = getHighlighter;
3781
- exports.getSlideLayoutPresetById = getSlideLayoutPresetById;
3782
- exports.getSlideLayoutPresetsByCategory = getSlideLayoutPresetsByCategory;
3783
3211
  exports.highlightCodeBlocks = highlightCodeBlocks;
3784
3212
  exports.highlightDeckCodeBlocks = highlightDeckCodeBlocks;
3213
+ exports.listUtilityClasses = listUtilityClasses;
3785
3214
  exports.normalizeSlideSlots = normalizeSlideSlots;
3786
- exports.removeSlideElementFromMarkdown = removeSlideElementFromMarkdown;
3787
3215
  exports.resolveHtmlTheme = resolveHtmlTheme;
3788
- exports.serializeLayoutToComment = serializeLayoutToComment;
3789
- exports.splitMarkdownSlides = splitMarkdownSlides;
3790
- exports.updateSlideLayoutInMarkdown = updateSlideLayoutInMarkdown;
3791
- exports.updateSlideTextInMarkdown = updateSlideTextInMarkdown;
3792
- //# sourceMappingURL=chunk-SFBEDDGU.cjs.map
3793
- //# sourceMappingURL=chunk-SFBEDDGU.cjs.map
3216
+ exports.sanitizeSlideHtml = sanitizeSlideHtml;
3217
+ exports.validateBespokeSlideHtml = validateBespokeSlideHtml;
3218
+ //# sourceMappingURL=chunk-5WEZOGQA.cjs.map
3219
+ //# sourceMappingURL=chunk-5WEZOGQA.cjs.map