@thanh01.pmt/presentation-kit 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- var chunk2FJGU32M_cjs = require('./chunk-2FJGU32M.cjs');
3
+ var chunkWKTPYYO2_cjs = require('./chunk-WKTPYYO2.cjs');
4
4
  var chunk3Q4RMZVQ_cjs = require('./chunk-3Q4RMZVQ.cjs');
5
- var chunkYO6TN5TA_cjs = require('./chunk-YO6TN5TA.cjs');
5
+ var chunkKXX5PGN7_cjs = require('./chunk-KXX5PGN7.cjs');
6
6
  var chunkEAPUC5BA_cjs = require('./chunk-EAPUC5BA.cjs');
7
7
 
8
8
  // src/engine.ts
@@ -20,14 +20,14 @@ function createEngine(config = {}) {
20
20
  }
21
21
  return {
22
22
  render: (markdown, options) => {
23
- return chunk2FJGU32M_cjs.renderSlide(markdown, mergeOptions(options));
23
+ return chunkWKTPYYO2_cjs.renderSlide(markdown, mergeOptions(options));
24
24
  },
25
25
  renderAsync: async (markdown, options) => {
26
- return chunk2FJGU32M_cjs.renderSlideAsync(markdown, mergeOptions(options));
26
+ return chunkWKTPYYO2_cjs.renderSlideAsync(markdown, mergeOptions(options));
27
27
  },
28
28
  getThemeCss: (theme, containerId) => {
29
- const resolved = chunk2FJGU32M_cjs.resolveTheme(theme);
30
- return chunk2FJGU32M_cjs.generateScopedCss2(resolved, containerId);
29
+ const resolved = chunkWKTPYYO2_cjs.resolveTheme(theme);
30
+ return chunkWKTPYYO2_cjs.generateScopedCss2(resolved, containerId);
31
31
  }
32
32
  };
33
33
  }
@@ -103,7 +103,7 @@ async function convertMarkdownToPptx(markdown, options) {
103
103
  pptx.layout = "LAYOUT_WIDE";
104
104
  if (options?.title) pptx.title = options.title;
105
105
  if (options?.author) pptx.author = options.author;
106
- const renderResult = chunk2FJGU32M_cjs.renderSlide(markdown);
106
+ const renderResult = chunkWKTPYYO2_cjs.renderSlide(markdown);
107
107
  const { slides } = chunk3Q4RMZVQ_cjs.splitMarkdownSlides(markdown);
108
108
  const isDark = options?.theme !== "light";
109
109
  const defaultBg = isDark ? "14151B" : "FFFFFF";
@@ -203,107 +203,107 @@ async function convertMarkdownToPptx(markdown, options) {
203
203
 
204
204
  Object.defineProperty(exports, "convertTextArrows", {
205
205
  enumerable: true,
206
- get: function () { return chunk2FJGU32M_cjs.convertTextArrows; }
206
+ get: function () { return chunkWKTPYYO2_cjs.convertTextArrows; }
207
207
  });
208
208
  Object.defineProperty(exports, "countSlides", {
209
209
  enumerable: true,
210
- get: function () { return chunk2FJGU32M_cjs.countSlides; }
210
+ get: function () { return chunkWKTPYYO2_cjs.countSlides; }
211
211
  });
212
212
  Object.defineProperty(exports, "darkThemeTokens", {
213
213
  enumerable: true,
214
- get: function () { return chunk2FJGU32M_cjs.darkThemeTokens; }
214
+ get: function () { return chunkWKTPYYO2_cjs.darkThemeTokens; }
215
215
  });
216
216
  Object.defineProperty(exports, "defineTheme", {
217
217
  enumerable: true,
218
- get: function () { return chunk2FJGU32M_cjs.defineTheme; }
218
+ get: function () { return chunkWKTPYYO2_cjs.defineTheme; }
219
219
  });
220
220
  Object.defineProperty(exports, "detectMermaidBlocks", {
221
221
  enumerable: true,
222
- get: function () { return chunk2FJGU32M_cjs.detectMermaidBlocks; }
222
+ get: function () { return chunkWKTPYYO2_cjs.detectMermaidBlocks; }
223
223
  });
224
224
  Object.defineProperty(exports, "extractLayoutsAndNotes", {
225
225
  enumerable: true,
226
- get: function () { return chunk2FJGU32M_cjs.extractLayoutsAndNotes; }
226
+ get: function () { return chunkWKTPYYO2_cjs.extractLayoutsAndNotes; }
227
227
  });
228
228
  Object.defineProperty(exports, "extractMarpSource", {
229
229
  enumerable: true,
230
- get: function () { return chunk2FJGU32M_cjs.extractMarpSource; }
230
+ get: function () { return chunkWKTPYYO2_cjs.extractMarpSource; }
231
231
  });
232
232
  Object.defineProperty(exports, "extractMaxClicks", {
233
233
  enumerable: true,
234
- get: function () { return chunk2FJGU32M_cjs.extractMaxClicks; }
234
+ get: function () { return chunkWKTPYYO2_cjs.extractMaxClicks; }
235
235
  });
236
236
  Object.defineProperty(exports, "filterPresenterNotes", {
237
237
  enumerable: true,
238
- get: function () { return chunk2FJGU32M_cjs.filterPresenterNotes; }
238
+ get: function () { return chunkWKTPYYO2_cjs.filterPresenterNotes; }
239
239
  });
240
240
  Object.defineProperty(exports, "generateClickCss", {
241
241
  enumerable: true,
242
- get: function () { return chunk2FJGU32M_cjs.generateClickCss; }
242
+ get: function () { return chunkWKTPYYO2_cjs.generateClickCss; }
243
243
  });
244
244
  Object.defineProperty(exports, "generateLayoutCss", {
245
245
  enumerable: true,
246
- get: function () { return chunk2FJGU32M_cjs.generateLayoutCss; }
246
+ get: function () { return chunkWKTPYYO2_cjs.generateLayoutCss; }
247
247
  });
248
248
  Object.defineProperty(exports, "generateScopedCss", {
249
249
  enumerable: true,
250
- get: function () { return chunk2FJGU32M_cjs.generateScopedCss2; }
250
+ get: function () { return chunkWKTPYYO2_cjs.generateScopedCss2; }
251
251
  });
252
252
  Object.defineProperty(exports, "generateScopedCssFromTokens", {
253
253
  enumerable: true,
254
- get: function () { return chunk2FJGU32M_cjs.generateScopedCss; }
254
+ get: function () { return chunkWKTPYYO2_cjs.generateScopedCss; }
255
255
  });
256
256
  Object.defineProperty(exports, "getThemeTokens", {
257
257
  enumerable: true,
258
- get: function () { return chunk2FJGU32M_cjs.getThemeTokens; }
258
+ get: function () { return chunkWKTPYYO2_cjs.getThemeTokens; }
259
259
  });
260
260
  Object.defineProperty(exports, "isLayoutComment", {
261
261
  enumerable: true,
262
- get: function () { return chunk2FJGU32M_cjs.isLayoutComment; }
262
+ get: function () { return chunkWKTPYYO2_cjs.isLayoutComment; }
263
263
  });
264
264
  Object.defineProperty(exports, "lightThemeTokens", {
265
265
  enumerable: true,
266
- get: function () { return chunk2FJGU32M_cjs.lightThemeTokens; }
266
+ get: function () { return chunkWKTPYYO2_cjs.lightThemeTokens; }
267
267
  });
268
268
  Object.defineProperty(exports, "navyThemeTokens", {
269
269
  enumerable: true,
270
- get: function () { return chunk2FJGU32M_cjs.navyThemeTokens; }
270
+ get: function () { return chunkWKTPYYO2_cjs.navyThemeTokens; }
271
271
  });
272
272
  Object.defineProperty(exports, "parseLayoutFromComment", {
273
273
  enumerable: true,
274
- get: function () { return chunk2FJGU32M_cjs.parseLayoutFromComment; }
274
+ get: function () { return chunkWKTPYYO2_cjs.parseLayoutFromComment; }
275
275
  });
276
276
  Object.defineProperty(exports, "postprocessClicks", {
277
277
  enumerable: true,
278
- get: function () { return chunk2FJGU32M_cjs.postprocessClicks; }
278
+ get: function () { return chunkWKTPYYO2_cjs.postprocessClicks; }
279
279
  });
280
280
  Object.defineProperty(exports, "preprocessClicks", {
281
281
  enumerable: true,
282
- get: function () { return chunk2FJGU32M_cjs.preprocessClicks; }
282
+ get: function () { return chunkWKTPYYO2_cjs.preprocessClicks; }
283
283
  });
284
284
  Object.defineProperty(exports, "renderMarp", {
285
285
  enumerable: true,
286
- get: function () { return chunk2FJGU32M_cjs.renderMarp; }
286
+ get: function () { return chunkWKTPYYO2_cjs.renderMarp; }
287
287
  });
288
288
  Object.defineProperty(exports, "renderMermaidDiagrams", {
289
289
  enumerable: true,
290
- get: function () { return chunk2FJGU32M_cjs.renderMermaidDiagrams; }
290
+ get: function () { return chunkWKTPYYO2_cjs.renderMermaidDiagrams; }
291
291
  });
292
292
  Object.defineProperty(exports, "renderSlide", {
293
293
  enumerable: true,
294
- get: function () { return chunk2FJGU32M_cjs.renderSlide; }
294
+ get: function () { return chunkWKTPYYO2_cjs.renderSlide; }
295
295
  });
296
296
  Object.defineProperty(exports, "renderSlideAsync", {
297
297
  enumerable: true,
298
- get: function () { return chunk2FJGU32M_cjs.renderSlideAsync; }
298
+ get: function () { return chunkWKTPYYO2_cjs.renderSlideAsync; }
299
299
  });
300
300
  Object.defineProperty(exports, "resolveTheme", {
301
301
  enumerable: true,
302
- get: function () { return chunk2FJGU32M_cjs.resolveTheme; }
302
+ get: function () { return chunkWKTPYYO2_cjs.resolveTheme; }
303
303
  });
304
304
  Object.defineProperty(exports, "sanitizeMermaidCode", {
305
305
  enumerable: true,
306
- get: function () { return chunk2FJGU32M_cjs.sanitizeMermaidCode; }
306
+ get: function () { return chunkWKTPYYO2_cjs.sanitizeMermaidCode; }
307
307
  });
308
308
  Object.defineProperty(exports, "SLIDE_LAYOUT_PRESETS", {
309
309
  enumerable: true,
@@ -339,63 +339,63 @@ Object.defineProperty(exports, "updateSlideTextInMarkdown", {
339
339
  });
340
340
  Object.defineProperty(exports, "HTML_SLIDE_PRESETS", {
341
341
  enumerable: true,
342
- get: function () { return chunkYO6TN5TA_cjs.HTML_SLIDE_PRESETS; }
342
+ get: function () { return chunkKXX5PGN7_cjs.HTML_SLIDE_PRESETS; }
343
343
  });
344
344
  Object.defineProperty(exports, "HTML_THEMES", {
345
345
  enumerable: true,
346
- get: function () { return chunkYO6TN5TA_cjs.HTML_THEMES; }
346
+ get: function () { return chunkKXX5PGN7_cjs.HTML_THEMES; }
347
347
  });
348
348
  Object.defineProperty(exports, "SLIDE_UTILITY_CSS", {
349
349
  enumerable: true,
350
- get: function () { return chunkYO6TN5TA_cjs.SLIDE_UTILITY_CSS; }
350
+ get: function () { return chunkKXX5PGN7_cjs.SLIDE_UTILITY_CSS; }
351
351
  });
352
352
  Object.defineProperty(exports, "buildBespokeSlideMarkup", {
353
353
  enumerable: true,
354
- get: function () { return chunkYO6TN5TA_cjs.buildBespokeSlideMarkup; }
354
+ get: function () { return chunkKXX5PGN7_cjs.buildBespokeSlideMarkup; }
355
355
  });
356
356
  Object.defineProperty(exports, "compileHtmlDeck", {
357
357
  enumerable: true,
358
- get: function () { return chunkYO6TN5TA_cjs.compileHtmlDeck; }
358
+ get: function () { return chunkKXX5PGN7_cjs.compileHtmlDeck; }
359
359
  });
360
360
  Object.defineProperty(exports, "compileHtmlDeckAsync", {
361
361
  enumerable: true,
362
- get: function () { return chunkYO6TN5TA_cjs.compileHtmlDeckAsync; }
362
+ get: function () { return chunkKXX5PGN7_cjs.compileHtmlDeckAsync; }
363
363
  });
364
364
  Object.defineProperty(exports, "generateHtmlShell", {
365
365
  enumerable: true,
366
- get: function () { return chunkYO6TN5TA_cjs.generateHtmlShell; }
366
+ get: function () { return chunkKXX5PGN7_cjs.generateHtmlShell; }
367
367
  });
368
368
  Object.defineProperty(exports, "getHighlighter", {
369
369
  enumerable: true,
370
- get: function () { return chunkYO6TN5TA_cjs.getHighlighter; }
370
+ get: function () { return chunkKXX5PGN7_cjs.getHighlighter; }
371
371
  });
372
372
  Object.defineProperty(exports, "highlightCodeBlocks", {
373
373
  enumerable: true,
374
- get: function () { return chunkYO6TN5TA_cjs.highlightCodeBlocks; }
374
+ get: function () { return chunkKXX5PGN7_cjs.highlightCodeBlocks; }
375
375
  });
376
376
  Object.defineProperty(exports, "highlightDeckCodeBlocks", {
377
377
  enumerable: true,
378
- get: function () { return chunkYO6TN5TA_cjs.highlightDeckCodeBlocks; }
378
+ get: function () { return chunkKXX5PGN7_cjs.highlightDeckCodeBlocks; }
379
379
  });
380
380
  Object.defineProperty(exports, "listUtilityClasses", {
381
381
  enumerable: true,
382
- get: function () { return chunkYO6TN5TA_cjs.listUtilityClasses; }
382
+ get: function () { return chunkKXX5PGN7_cjs.listUtilityClasses; }
383
383
  });
384
384
  Object.defineProperty(exports, "normalizeSlideSlots", {
385
385
  enumerable: true,
386
- get: function () { return chunkYO6TN5TA_cjs.normalizeSlideSlots; }
386
+ get: function () { return chunkKXX5PGN7_cjs.normalizeSlideSlots; }
387
387
  });
388
388
  Object.defineProperty(exports, "resolveHtmlTheme", {
389
389
  enumerable: true,
390
- get: function () { return chunkYO6TN5TA_cjs.resolveHtmlTheme; }
390
+ get: function () { return chunkKXX5PGN7_cjs.resolveHtmlTheme; }
391
391
  });
392
392
  Object.defineProperty(exports, "sanitizeSlideHtml", {
393
393
  enumerable: true,
394
- get: function () { return chunkYO6TN5TA_cjs.sanitizeSlideHtml; }
394
+ get: function () { return chunkKXX5PGN7_cjs.sanitizeSlideHtml; }
395
395
  });
396
396
  Object.defineProperty(exports, "validateBespokeSlideHtml", {
397
397
  enumerable: true,
398
- get: function () { return chunkYO6TN5TA_cjs.validateBespokeSlideHtml; }
398
+ get: function () { return chunkKXX5PGN7_cjs.validateBespokeSlideHtml; }
399
399
  });
400
400
  Object.defineProperty(exports, "FRONTEND_SLIDES_CORE_RULES", {
401
401
  enumerable: true,
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { resolveTheme, generateScopedCss2, renderSlideAsync, renderSlide } from './chunk-FIBXWRWQ.js';
2
- export { convertTextArrows, countSlides, darkThemeTokens, defineTheme, detectMermaidBlocks, extractLayoutsAndNotes, extractMarpSource, extractMaxClicks, filterPresenterNotes, generateClickCss, generateLayoutCss, generateScopedCss2 as generateScopedCss, generateScopedCss as generateScopedCssFromTokens, getThemeTokens, isLayoutComment, lightThemeTokens, navyThemeTokens, parseLayoutFromComment, postprocessClicks, preprocessClicks, renderMarp, renderMermaidDiagrams, renderSlide, renderSlideAsync, resolveTheme, sanitizeMermaidCode } from './chunk-FIBXWRWQ.js';
1
+ import { resolveTheme, generateScopedCss2, renderSlideAsync, renderSlide } from './chunk-UOWSDZEF.js';
2
+ export { convertTextArrows, countSlides, darkThemeTokens, defineTheme, detectMermaidBlocks, extractLayoutsAndNotes, extractMarpSource, extractMaxClicks, filterPresenterNotes, generateClickCss, generateLayoutCss, generateScopedCss2 as generateScopedCss, generateScopedCss as generateScopedCssFromTokens, getThemeTokens, isLayoutComment, lightThemeTokens, navyThemeTokens, parseLayoutFromComment, postprocessClicks, preprocessClicks, renderMarp, renderMermaidDiagrams, renderSlide, renderSlideAsync, resolveTheme, sanitizeMermaidCode } from './chunk-UOWSDZEF.js';
3
3
  import { splitMarkdownSlides } from './chunk-V6X7D44H.js';
4
4
  export { SLIDE_LAYOUT_PRESETS, getSlideLayoutPresetById, getSlideLayoutPresetsByCategory, removeSlideElementFromMarkdown, serializeLayoutToComment, splitMarkdownSlides, updateSlideLayoutInMarkdown, updateSlideTextInMarkdown } from './chunk-V6X7D44H.js';
5
- export { HTML_SLIDE_PRESETS, HTML_THEMES, SLIDE_UTILITY_CSS, buildBespokeSlideMarkup, compileHtmlDeck, compileHtmlDeckAsync, generateHtmlShell, getHighlighter, highlightCodeBlocks, highlightDeckCodeBlocks, listUtilityClasses, normalizeSlideSlots, resolveHtmlTheme, sanitizeSlideHtml, validateBespokeSlideHtml } from './chunk-LVSQN2VI.js';
5
+ export { HTML_SLIDE_PRESETS, HTML_THEMES, SLIDE_UTILITY_CSS, buildBespokeSlideMarkup, compileHtmlDeck, compileHtmlDeckAsync, generateHtmlShell, getHighlighter, highlightCodeBlocks, highlightDeckCodeBlocks, listUtilityClasses, normalizeSlideSlots, resolveHtmlTheme, sanitizeSlideHtml, validateBespokeSlideHtml } from './chunk-7ZQHP7OD.js';
6
6
  export { FRONTEND_SLIDES_CORE_RULES, STYLE_PRESETS, TEMPLATES_CATALOG, VIEWPORT_BASE_CSS, frontendSlidesTools, getFrontendSlidesSkillPrompt, getSlideStylePresetSchema, getStylePreset, getStylePresetById, getTemplatesCatalog, querySlideTemplatesSchema, searchTemplates } from './chunk-WNPKREEW.js';
7
7
 
8
8
  // src/engine.ts
@@ -1,32 +1,193 @@
1
1
  'use strict';
2
2
 
3
- var chunkYO6TN5TA_cjs = require('../chunk-YO6TN5TA.cjs');
3
+ var chunkKXX5PGN7_cjs = require('../chunk-KXX5PGN7.cjs');
4
4
  var fs = require('fs');
5
- var path = require('path');
5
+ var path2 = require('path');
6
6
  var url = require('url');
7
+ var os = require('os');
8
+ var child_process = require('child_process');
7
9
 
8
10
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
9
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
12
 
11
13
  var fs__default = /*#__PURE__*/_interopDefault(fs);
12
- var path__default = /*#__PURE__*/_interopDefault(path);
14
+ var path2__default = /*#__PURE__*/_interopDefault(path2);
15
+ var os__default = /*#__PURE__*/_interopDefault(os);
13
16
 
17
+ function chromeCandidates() {
18
+ if (process.env.LAYOUT_QA_CHROME) return [process.env.LAYOUT_QA_CHROME];
19
+ const linux = ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome"];
20
+ const mac = [
21
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
22
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
23
+ ];
24
+ return process.platform === "darwin" ? [...mac, ...linux] : [...linux, ...mac];
25
+ }
26
+ function findChrome() {
27
+ for (const c of chromeCandidates()) {
28
+ try {
29
+ if (c && fs__default.default.existsSync(c)) return c;
30
+ } catch {
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+ var PROBE = `(function(){
36
+ var H = 1080, ROW = 10, ROWS = Math.ceil(H / ROW);
37
+ var pages = Array.prototype.slice.call(document.querySelectorAll('.slide-page'));
38
+ var wrap = document.createElement('div');
39
+ wrap.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;overflow:hidden;';
40
+ var stage = document.createElement('div');
41
+ stage.className = 'slide-canvas';
42
+ stage.style.cssText = 'position:absolute;left:0;top:0;width:1920px;height:1080px;';
43
+ var inner = document.createElement('div');
44
+ inner.style.cssText = 'position:absolute;inset:0;';
45
+ stage.appendChild(inner);
46
+ wrap.appendChild(stage);
47
+ document.body.appendChild(wrap);
48
+ function hasDirectText(el) {
49
+ for (var n = el.firstChild; n; n = n.nextSibling) {
50
+ if (n.nodeType === 3 && n.textContent.trim().length > 0) return true;
51
+ }
52
+ return false;
53
+ }
54
+ var out = [];
55
+ for (var i = 0; i < pages.length; i++) {
56
+ var src = pages[i].querySelector('.bespoke-slide');
57
+ if (!src) { out.push(null); continue; }
58
+ inner.innerHTML = '';
59
+ inner.appendChild(src.cloneNode(true));
60
+ var all = stage.querySelectorAll('*');
61
+ for (var j = 0; j < all.length; j++) {
62
+ all[j].style.animation = 'none';
63
+ all[j].style.transition = 'none';
64
+ all[j].style.opacity = '1';
65
+ all[j].style.visibility = 'visible';
66
+ if (!/^(SVG|CANVAS|IMG|VIDEO)$/.test(all[j].tagName)) all[j].style.transform = 'none';
67
+ }
68
+ var stageTop = stage.getBoundingClientRect().top;
69
+ var rows = new Array(ROWS);
70
+ for (var k = 0; k < ROWS; k++) rows[k] = false;
71
+ var els = inner.querySelectorAll('*');
72
+ for (var e = 0; e < els.length; e++) {
73
+ var el = els[e];
74
+ var isVisual = /^(IMG|SVG|CANVAS|VIDEO)$/.test(el.tagName);
75
+ if (!isVisual && !hasDirectText(el)) continue;
76
+ var cs = getComputedStyle(el);
77
+ if (cs.display === 'none' || cs.visibility === 'hidden') continue;
78
+ var r = el.getBoundingClientRect();
79
+ if (r.width < 4 || r.height < 4) continue;
80
+ var t = r.top - stageTop;
81
+ var b = r.bottom - stageTop;
82
+ if (b <= 0 || t >= H) continue;
83
+ var k0 = Math.max(0, Math.floor(t / ROW));
84
+ var k1 = Math.min(ROWS, Math.ceil(b / ROW));
85
+ for (var k = k0; k < k1; k++) rows[k] = true;
86
+ }
87
+ var best = 0, bestStart = 0, cur = 0, curStart = 0;
88
+ for (var k = 2; k < ROWS - 2; k++) {
89
+ if (!rows[k]) {
90
+ if (cur === 0) curStart = k;
91
+ cur++;
92
+ if (cur > best) { best = cur; bestStart = curStart; }
93
+ } else cur = 0;
94
+ }
95
+ out.push({ deadBand: best * ROW, atY: bestStart * ROW });
96
+ }
97
+ wrap.remove();
98
+ document.title = 'QA_RESULT:' + JSON.stringify(out);
99
+ })();`;
100
+ async function runSlideLayoutQa(slides, options = {}) {
101
+ const threshold = options.threshold ?? 400;
102
+ const timeoutMs = options.timeoutMs ?? 12e4;
103
+ const chrome = findChrome();
104
+ if (!chrome) return { available: false, issues: [], error: "no chrome binary found" };
105
+ const themeCss = ":root{--bg:#ffffff;--bg-secondary:#f5f5f5;--primary:#1d4ed8;--accent:#f59e0b;--text:#111111;--text-muted:#555555;--border:#e5e7eb;--font-display:'Space Grotesk',sans-serif;--font-body:'Inter',sans-serif;--font-code:'JetBrains Mono',monospace;}";
106
+ const pagesHtml = slides.map((s) => `<div class="slide-page"><div class="bespoke-slide">${s.html}</div></div>`).join("\n");
107
+ const probeSafe = PROBE.replace(/<\/script/gi, "<\\/script");
108
+ const html = `<!doctype html><html><head><meta charset="utf-8"><style>
109
+ ${themeCss}
110
+ .slide-canvas{position:relative;width:1920px;height:1080px;overflow:hidden;background:var(--bg);}
111
+ .slide-page{position:absolute;inset:0;width:100%;height:100%;display:none;opacity:0;}
112
+ .slide-page.active{display:block;opacity:1;}
113
+ </style></head><body><div class="slide-canvas">
114
+ ${pagesHtml}
115
+ </div><script>${probeSafe}</script></body></html>`;
116
+ const dir = fs__default.default.mkdtempSync(path2__default.default.join(os__default.default.tmpdir(), "layout-qa-"));
117
+ const file = path2__default.default.join(dir, "deck.html");
118
+ try {
119
+ fs__default.default.writeFileSync(file, html, "utf-8");
120
+ const args = [
121
+ "--headless=new",
122
+ "--no-sandbox",
123
+ "--disable-gpu",
124
+ "--disable-dev-shm-usage",
125
+ "--hide-scrollbars",
126
+ "--virtual-time-budget=5000",
127
+ "--window-size=1920,1200",
128
+ "--dump-dom",
129
+ `file://${file}`
130
+ ];
131
+ const stdout = await new Promise((resolve, reject) => {
132
+ child_process.execFile(
133
+ chrome,
134
+ args,
135
+ { timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 },
136
+ (err, stdout2) => {
137
+ if (err && !stdout2) return reject(err);
138
+ resolve(String(stdout2 || ""));
139
+ }
140
+ );
141
+ });
142
+ const marker = stdout.match(/QA_RESULT:(\[[\s\S]*?\])<\/title>/);
143
+ if (!marker) {
144
+ return {
145
+ available: true,
146
+ issues: slides.map((_, i) => ({ slideIndex: i + 1, deadBand: threshold, atY: 0 })),
147
+ error: "probe produced no result (chrome render failed?)"
148
+ };
149
+ }
150
+ const parsed = JSON.parse(marker[1]);
151
+ const issues = [];
152
+ for (let i = 0; i < parsed.length; i++) {
153
+ const row = parsed[i];
154
+ if (!row) continue;
155
+ if (row.deadBand >= threshold) {
156
+ issues.push({ slideIndex: i + 1, deadBand: row.deadBand, atY: row.atY });
157
+ }
158
+ }
159
+ return { available: true, issues };
160
+ } catch (err) {
161
+ return {
162
+ available: true,
163
+ issues: slides.map((_, i) => ({ slideIndex: i + 1, deadBand: threshold, atY: 0 })),
164
+ error: err?.message || String(err)
165
+ };
166
+ } finally {
167
+ try {
168
+ fs__default.default.rmSync(dir, { recursive: true, force: true });
169
+ } catch {
170
+ }
171
+ }
172
+ }
173
+
174
+ // src/node/index.ts
14
175
  var fontManifest = null;
15
176
  var fontFaceCache = /* @__PURE__ */ new Map();
16
177
  function fontDirCandidates() {
17
- const here = path__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
178
+ const here = path2__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
18
179
  return [
19
180
  process.env.PRESENTATION_KIT_FONT_DIR || "",
20
181
  // dist/node/index.js → dist/assets/fonts
21
- path__default.default.resolve(here, "../assets/fonts"),
22
- path__default.default.resolve(here, "../..", "assets/fonts"),
182
+ path2__default.default.resolve(here, "../assets/fonts"),
183
+ path2__default.default.resolve(here, "../..", "assets/fonts"),
23
184
  // src/node/index.ts (tsx/vitest) → <pkg>/assets/fonts
24
- path__default.default.resolve(here, "../../assets/fonts")
185
+ path2__default.default.resolve(here, "../../assets/fonts")
25
186
  ].filter(Boolean);
26
187
  }
27
188
  function resolveFontDir() {
28
189
  for (const dir of fontDirCandidates()) {
29
- if (fs__default.default.existsSync(path__default.default.join(dir, "SOURCES.json"))) return dir;
190
+ if (fs__default.default.existsSync(path2__default.default.join(dir, "SOURCES.json"))) return dir;
30
191
  }
31
192
  return null;
32
193
  }
@@ -39,11 +200,11 @@ function loadManifest() {
39
200
  );
40
201
  return fontManifest = [];
41
202
  }
42
- const parsed = JSON.parse(fs__default.default.readFileSync(path__default.default.join(dir, "SOURCES.json"), "utf-8"));
203
+ const parsed = JSON.parse(fs__default.default.readFileSync(path2__default.default.join(dir, "SOURCES.json"), "utf-8"));
43
204
  return fontManifest = parsed.fonts;
44
205
  }
45
206
  function base64(file, dir) {
46
- return fs__default.default.readFileSync(path__default.default.join(dir, file)).toString("base64");
207
+ return fs__default.default.readFileSync(path2__default.default.join(dir, file)).toString("base64");
47
208
  }
48
209
  function loadEmbeddedFontFaceCss(families) {
49
210
  const dir = resolveFontDir();
@@ -82,7 +243,7 @@ function collectDeckFontFamilies(html, theme) {
82
243
  return Array.from(found);
83
244
  }
84
245
  function compileStandaloneHtmlDeck(deckData, options = {}) {
85
- const base = chunkYO6TN5TA_cjs.compileHtmlDeck(deckData, options);
246
+ const base = chunkKXX5PGN7_cjs.compileHtmlDeck(deckData, options);
86
247
  if (options.embedFonts === false) return { ...base, embeddedFonts: [] };
87
248
  const families = collectDeckFontFamilies(base.html, base.theme);
88
249
  const fontFaceCss = loadEmbeddedFontFaceCss(families);
@@ -113,5 +274,6 @@ exports.compileStandaloneHtmlDeck = compileStandaloneHtmlDeck;
113
274
  exports.findExternalReferences = findExternalReferences;
114
275
  exports.loadEmbeddedFontFaceCss = loadEmbeddedFontFaceCss;
115
276
  exports.resolveFontDir = resolveFontDir;
277
+ exports.runSlideLayoutQa = runSlideLayoutQa;
116
278
  //# sourceMappingURL=index.cjs.map
117
279
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/node/index.ts"],"names":["path","fileURLToPath","fs","compileHtmlDeck"],"mappings":"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","file":"index.cjs","sourcesContent":["// ═══════════════════════════════════════════════════════════════════════════════\n// @learnwell/presentation-kit/node — Self-contained deck assembly (Node only)\n//\n// Kept OUT of the main entry on purpose: the browser entry (`compileHtmlDeck`,\n// `renderSlide`) must never pull `node:fs`. Server-side callers (curriculum-kit's\n// slide workflow, the app's artifact pipeline) use this module to produce a deck\n// that renders with typography intact and WITHOUT a single outbound request.\n//\n// What \"self-contained\" has to cover (measured on the old artifacts):\n// 1. fonts — were `<link>` to fonts.googleapis.com (gone offline → system fonts)\n// 2. Tailwind — was `<script src=\"https://cdn.tailwindcss.com\">` (gone offline →\n// every slot-layout slide rendered unstyled)\n// 3. anything the AUTHORED slides bring in (handled by the sanitizer)\n// ═══════════════════════════════════════════════════════════════════════════════\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { compileHtmlDeck } from '../html-engine/compiler.js';\nimport type { HtmlCompileOptions, HtmlDeckData } from '../html-engine/types.js';\n\ninterface FontSource {\n family: string;\n file: string;\n weights: string;\n license: string;\n source: string;\n}\n\nlet fontManifest: FontSource[] | null = null;\nlet fontFaceCache = new Map<string, string>();\n\n/** Candidate asset dirs: published `dist/assets/fonts`, then the repo's `assets/fonts`. */\nfunction fontDirCandidates(): string[] {\n const here = path.dirname(fileURLToPath(import.meta.url));\n return [\n process.env.PRESENTATION_KIT_FONT_DIR || '',\n // dist/node/index.js → dist/assets/fonts\n path.resolve(here, '../assets/fonts'),\n path.resolve(here, '../..', 'assets/fonts'),\n // src/node/index.ts (tsx/vitest) → <pkg>/assets/fonts\n path.resolve(here, '../../assets/fonts'),\n ].filter(Boolean) as string[];\n}\n\nexport function resolveFontDir(): string | null {\n for (const dir of fontDirCandidates()) {\n if (fs.existsSync(path.join(dir, 'SOURCES.json'))) return dir;\n }\n return null;\n}\n\nfunction loadManifest(): FontSource[] {\n if (fontManifest) return fontManifest;\n const dir = resolveFontDir();\n if (!dir) {\n console.warn(\n '[presentation-kit/node] vendored fonts not found (assets/fonts/SOURCES.json) — ' +\n 'run `node scripts/vendor-fonts.mjs`. Decks will fall back to external font links.'\n );\n return (fontManifest = []);\n }\n const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'SOURCES.json'), 'utf-8'));\n return (fontManifest = parsed.fonts as FontSource[]);\n}\n\nfunction base64(file: string, dir: string): string {\n return fs.readFileSync(path.join(dir, file)).toString('base64');\n}\n\n/**\n * `@font-face` CSS with base64 woff2 for the requested families (all vendored\n * families when omitted). Returns '' when nothing is vendored, so the shell\n * keeps its (external) fallback instead of silently rendering with no fonts.\n */\nexport function loadEmbeddedFontFaceCss(families?: string[]): string {\n const dir = resolveFontDir();\n const manifest = loadManifest();\n if (!dir || manifest.length === 0) return '';\n\n const wanted = families && families.length > 0 ? families : manifest.map((f) => f.family);\n const cacheKey = [...wanted].sort().join('|');\n const cached = fontFaceCache.get(cacheKey);\n if (cached !== undefined) return cached;\n\n const blocks: string[] = [];\n for (const font of manifest) {\n if (!wanted.includes(font.family)) continue;\n const data = base64(font.file, dir);\n blocks.push(\n `@font-face{font-family:'${font.family}';font-style:normal;font-weight:${font.weights};` +\n `font-display:swap;src:url(data:font/woff2;base64,${data}) format('woff2');}`\n );\n }\n const css = blocks.join('\\n');\n fontFaceCache.set(cacheKey, css);\n return css;\n}\n\n/**\n * Which vendored families a compiled deck actually needs: the active theme's\n * display/body/code pairing, plus any vendored family named in the markup\n * (authored slides may reference one directly).\n */\nexport function collectDeckFontFamilies(html: string, theme: { typography?: Record<string, string> }): string[] {\n const manifest = loadManifest();\n const vendored = new Set(manifest.map((f) => f.family));\n const found = new Set<string>();\n\n // Only the PRIMARY family of each stack needs embedding: fallbacks exist for\n // when the primary is unavailable, and an embedded primary never is. (The old\n // code also pulled every fallback, e.g. Fira Code behind JetBrains Mono → +35KB\n // per deck for a font that can never be reached.)\n for (const value of Object.values(theme.typography || {})) {\n const primary = String(value).split(',')[0].replace(/[\"']/g, '').trim();\n if (vendored.has(primary)) found.add(primary);\n }\n // Authored slides may name a family directly (`font-family: 'Fira Code'`).\n // The theme's own variable declarations are excluded: they legitimately list\n // fallbacks, which must not drag a whole extra font into every deck.\n const authoredOnly = html.replace(/--font-(?:display|body|code)\\s*:[^;]+;/g, '');\n for (const family of vendored) {\n if (new RegExp(`font-family[^;}]*${family.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`, 'i').test(authoredOnly)) {\n found.add(family);\n }\n }\n return Array.from(found);\n}\n\nexport interface StandaloneCompileOptions extends HtmlCompileOptions {\n /** Skip embedding fonts (returns the deck with external font links). */\n embedFonts?: boolean;\n}\n\n/**\n * Compiles a deck that needs no network at all: fonts embedded, utilities\n * inlined (the shell does that unconditionally now), markup sanitized.\n */\nexport function compileStandaloneHtmlDeck(\n deckData: HtmlDeckData,\n options: StandaloneCompileOptions = {}\n): { html: string; slideCount: number; theme: unknown; title: string; embeddedFonts: string[] } {\n const base = compileHtmlDeck(deckData, options);\n if (options.embedFonts === false) return { ...base, embeddedFonts: [] };\n\n const families = collectDeckFontFamilies(base.html, base.theme as any);\n const fontFaceCss = loadEmbeddedFontFaceCss(families);\n if (!fontFaceCss) return { ...base, embeddedFonts: [] };\n\n // The shell marks the block so this is an exact, single replacement — no\n // regex surgery on the rest of the document.\n const html = base.html.replace(\n /<!-- DECK_FONT_BLOCK_START -->[\\s\\S]*?<!-- DECK_FONT_BLOCK_END -->/,\n `<!-- DECK_FONT_BLOCK_START -->\\n <style>\\n${fontFaceCss}\\n </style>\\n <!-- DECK_FONT_BLOCK_END -->`\n );\n\n return { ...base, html, embeddedFonts: families };\n}\n\n/** Off-box references a compiled deck still carries — must be empty to ship. */\nexport function findExternalReferences(html: string): string[] {\n const refs: string[] = [];\n for (const match of html.matchAll(/(?:https?:)?\\/\\/[a-z0-9.-]+\\.[a-z]{2,}[^\\s\"'<>)]*/gi)) {\n const url = match[0];\n // Namespaced identifiers (xmlns, schema URLs) are values, not fetches.\n if (/^\\/\\/(www\\.)?w3\\.org/i.test(url)) continue;\n if (/xmlns/i.test(html.slice(Math.max(0, (match.index ?? 0) - 24), match.index ?? 0))) continue;\n refs.push(url);\n }\n return Array.from(new Set(refs));\n}\n"]}
1
+ {"version":3,"sources":["../../src/node/layoutQa.ts","../../src/node/index.ts"],"names":["fs","path","os","execFile","stdout","fileURLToPath","compileHtmlDeck"],"mappings":"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","file":"index.cjs","sourcesContent":["// ═══════════════════════════════════════════════════════════════════════════════\n// @learnwell/presentation-kit/node — post-compile layout QA (headless scanline)\n//\n// Closes the gap ADR-0009 documented as the static lint's ceiling: authored\n// slides whose geometry hides a ≥400px vertical dead band (multi-column\n// layouts, footer bands, non-space-between holes) pass the HTML contract but\n// render with a hole. This harness drives a real browser, lays each slide out\n// at the REAL 1920×1080 stage, and applies the scanline test: a horizontal\n// band is \"occupied\" when any glyph, image or vector touches it.\n//\n// Optional capability, same contract family as validateBespokeSlideHtml:\n// - no Chrome binary → returns { available: false }, caller keeps the deck\n// - any harness error → every slide counts as flagged (fail-closed)\n//\n// ponytail: system Chromium only (no puppeteer bundle — deployment weight).\n// Upgrade path: real interaction QA (step-reveal walking) once needed.\n// ═══════════════════════════════════════════════════════════════════════════════\n\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { execFile } from 'node:child_process';\n\nfunction chromeCandidates(): string[] {\n if (process.env.LAYOUT_QA_CHROME) return [process.env.LAYOUT_QA_CHROME];\n const linux = ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome'];\n const mac = [\n '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',\n '/Applications/Chromium.app/Contents/MacOS/Chromium',\n ];\n return process.platform === 'darwin' ? [...mac, ...linux] : [...linux, ...mac];\n}\n\nfunction findChrome(): string | null {\n for (const c of chromeCandidates()) {\n try {\n if (c && fs.existsSync(c)) return c;\n } catch {\n /* unreadable path — skip */\n }\n }\n return null;\n}\n\nexport interface LayoutQaIssue {\n slideIndex: number;\n /** Largest fully-empty horizontal band, in stage px. */\n deadBand: number;\n /** Top edge of the dead band, in stage px. */\n atY: number;\n}\n\nexport interface LayoutQaResult {\n /** false = harness unavailable (no Chrome) — QA skipped, NOT failed. */\n available: boolean;\n issues: LayoutQaIssue[];\n error?: string;\n}\n\nexport interface LayoutQaOptions {\n /** Band height (stage px) that flags a slide. Default 400. */\n threshold?: number;\n /** Wall-clock budget for the whole headless run. Default 120s. */\n timeoutMs?: number;\n}\n\n/** Runs inside headless Chrome, writes results into document.title. */\nconst PROBE = `(function(){\n var H = 1080, ROW = 10, ROWS = Math.ceil(H / ROW);\n var pages = Array.prototype.slice.call(document.querySelectorAll('.slide-page'));\n var wrap = document.createElement('div');\n wrap.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;overflow:hidden;';\n var stage = document.createElement('div');\n stage.className = 'slide-canvas';\n stage.style.cssText = 'position:absolute;left:0;top:0;width:1920px;height:1080px;';\n var inner = document.createElement('div');\n inner.style.cssText = 'position:absolute;inset:0;';\n stage.appendChild(inner);\n wrap.appendChild(stage);\n document.body.appendChild(wrap);\n function hasDirectText(el) {\n for (var n = el.firstChild; n; n = n.nextSibling) {\n if (n.nodeType === 3 && n.textContent.trim().length > 0) return true;\n }\n return false;\n }\n var out = [];\n for (var i = 0; i < pages.length; i++) {\n var src = pages[i].querySelector('.bespoke-slide');\n if (!src) { out.push(null); continue; }\n inner.innerHTML = '';\n inner.appendChild(src.cloneNode(true));\n var all = stage.querySelectorAll('*');\n for (var j = 0; j < all.length; j++) {\n all[j].style.animation = 'none';\n all[j].style.transition = 'none';\n all[j].style.opacity = '1';\n all[j].style.visibility = 'visible';\n if (!/^(SVG|CANVAS|IMG|VIDEO)$/.test(all[j].tagName)) all[j].style.transform = 'none';\n }\n var stageTop = stage.getBoundingClientRect().top;\n var rows = new Array(ROWS);\n for (var k = 0; k < ROWS; k++) rows[k] = false;\n var els = inner.querySelectorAll('*');\n for (var e = 0; e < els.length; e++) {\n var el = els[e];\n var isVisual = /^(IMG|SVG|CANVAS|VIDEO)$/.test(el.tagName);\n if (!isVisual && !hasDirectText(el)) continue;\n var cs = getComputedStyle(el);\n if (cs.display === 'none' || cs.visibility === 'hidden') continue;\n var r = el.getBoundingClientRect();\n if (r.width < 4 || r.height < 4) continue;\n var t = r.top - stageTop;\n var b = r.bottom - stageTop;\n if (b <= 0 || t >= H) continue;\n var k0 = Math.max(0, Math.floor(t / ROW));\n var k1 = Math.min(ROWS, Math.ceil(b / ROW));\n for (var k = k0; k < k1; k++) rows[k] = true;\n }\n var best = 0, bestStart = 0, cur = 0, curStart = 0;\n for (var k = 2; k < ROWS - 2; k++) {\n if (!rows[k]) {\n if (cur === 0) curStart = k;\n cur++;\n if (cur > best) { best = cur; bestStart = curStart; }\n } else cur = 0;\n }\n out.push({ deadBand: best * ROW, atY: bestStart * ROW });\n }\n wrap.remove();\n document.title = 'QA_RESULT:' + JSON.stringify(out);\n})();`;\n\nexport interface LayoutQaSlidePage {\n /** Authored HTML for one slide (the `.bespoke-slide` inner markup). */\n html: string;\n}\n\n/**\n * Scanline QA over authored slide HTML: wraps each slide in a minimal deck\n * shell (same class names the compiler emits), renders in headless Chromium,\n * and flags slides whose largest content-free horizontal band ≥ threshold.\n */\nexport async function runSlideLayoutQa(\n slides: LayoutQaSlidePage[],\n options: LayoutQaOptions = {}\n): Promise<LayoutQaResult> {\n const threshold = options.threshold ?? 400;\n const timeoutMs = options.timeoutMs ?? 120_000;\n const chrome = findChrome();\n if (!chrome) return { available: false, issues: [], error: 'no chrome binary found' };\n\n // Minimal shell — theme vars mirror the compiled defaults so authored\n // markup lays out with real colors/fonts; the probe only reads geometry.\n const themeCss =\n ':root{--bg:#ffffff;--bg-secondary:#f5f5f5;--primary:#1d4ed8;--accent:#f59e0b;--text:#111111;--text-muted:#555555;--border:#e5e7eb;' +\n \"--font-display:'Space Grotesk',sans-serif;--font-body:'Inter',sans-serif;--font-code:'JetBrains Mono',monospace;}\";\n\n const pagesHtml = slides\n .map((s) => `<div class=\"slide-page\"><div class=\"bespoke-slide\">${s.html}</div></div>`)\n .join('\\n');\n\n const probeSafe = PROBE.replace(/<\\/script/gi, '<\\\\/script');\n const html = `<!doctype html><html><head><meta charset=\"utf-8\"><style>\n${themeCss}\n.slide-canvas{position:relative;width:1920px;height:1080px;overflow:hidden;background:var(--bg);}\n.slide-page{position:absolute;inset:0;width:100%;height:100%;display:none;opacity:0;}\n.slide-page.active{display:block;opacity:1;}\n</style></head><body><div class=\"slide-canvas\">\n${pagesHtml}\n</div><script>${probeSafe}</script></body></html>`;\n\n const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'layout-qa-'));\n const file = path.join(dir, 'deck.html');\n try {\n fs.writeFileSync(file, html, 'utf-8');\n const args = [\n '--headless=new',\n '--no-sandbox',\n '--disable-gpu',\n '--disable-dev-shm-usage',\n '--hide-scrollbars',\n '--virtual-time-budget=5000',\n '--window-size=1920,1200',\n '--dump-dom',\n `file://${file}`,\n ];\n const stdout = await new Promise<string>((resolve, reject) => {\n execFile(\n chrome,\n args,\n { timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 },\n (err, stdout) => {\n if (err && !stdout) return reject(err);\n resolve(String(stdout || ''));\n }\n );\n });\n const marker = stdout.match(/QA_RESULT:(\\[[\\s\\S]*?\\])<\\/title>/);\n if (!marker) {\n // Fail-closed: report every slide flagged rather than silently passing.\n return {\n available: true,\n issues: slides.map((_, i) => ({ slideIndex: i + 1, deadBand: threshold, atY: 0 })),\n error: 'probe produced no result (chrome render failed?)',\n };\n }\n const parsed = JSON.parse(marker[1]);\n const issues: LayoutQaIssue[] = [];\n for (let i = 0; i < parsed.length; i++) {\n const row = parsed[i];\n if (!row) continue;\n if (row.deadBand >= threshold) {\n issues.push({ slideIndex: i + 1, deadBand: row.deadBand, atY: row.atY });\n }\n }\n return { available: true, issues };\n } catch (err: any) {\n return {\n available: true,\n issues: slides.map((_, i) => ({ slideIndex: i + 1, deadBand: threshold, atY: 0 })),\n error: err?.message || String(err),\n };\n } finally {\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n } catch {\n /* temp dir best-effort cleanup */\n }\n }\n}\n","// ═══════════════════════════════════════════════════════════════════════════════\n// @learnwell/presentation-kit/node — Self-contained deck assembly (Node only)\n//\n// Kept OUT of the main entry on purpose: the browser entry (`compileHtmlDeck`,\n// `renderSlide`) must never pull `node:fs`. Server-side callers (curriculum-kit's\n// slide workflow, the app's artifact pipeline) use this module to produce a deck\n// that renders with typography intact and WITHOUT a single outbound request.\n//\n// What \"self-contained\" has to cover (measured on the old artifacts):\n// 1. fonts — were `<link>` to fonts.googleapis.com (gone offline → system fonts)\n// 2. Tailwind — was `<script src=\"https://cdn.tailwindcss.com\">` (gone offline →\n// every slot-layout slide rendered unstyled)\n// 3. anything the AUTHORED slides bring in (handled by the sanitizer)\n// ═══════════════════════════════════════════════════════════════════════════════\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { compileHtmlDeck } from '../html-engine/compiler.js';\nimport type { HtmlCompileOptions, HtmlDeckData } from '../html-engine/types.js';\n\ninterface FontSource {\n family: string;\n file: string;\n weights: string;\n license: string;\n source: string;\n}\n\nlet fontManifest: FontSource[] | null = null;\nlet fontFaceCache = new Map<string, string>();\n\n/** Candidate asset dirs: published `dist/assets/fonts`, then the repo's `assets/fonts`. */\nfunction fontDirCandidates(): string[] {\n const here = path.dirname(fileURLToPath(import.meta.url));\n return [\n process.env.PRESENTATION_KIT_FONT_DIR || '',\n // dist/node/index.js → dist/assets/fonts\n path.resolve(here, '../assets/fonts'),\n path.resolve(here, '../..', 'assets/fonts'),\n // src/node/index.ts (tsx/vitest) → <pkg>/assets/fonts\n path.resolve(here, '../../assets/fonts'),\n ].filter(Boolean) as string[];\n}\n\nexport function resolveFontDir(): string | null {\n for (const dir of fontDirCandidates()) {\n if (fs.existsSync(path.join(dir, 'SOURCES.json'))) return dir;\n }\n return null;\n}\n\nfunction loadManifest(): FontSource[] {\n if (fontManifest) return fontManifest;\n const dir = resolveFontDir();\n if (!dir) {\n console.warn(\n '[presentation-kit/node] vendored fonts not found (assets/fonts/SOURCES.json) — ' +\n 'run `node scripts/vendor-fonts.mjs`. Decks will fall back to external font links.'\n );\n return (fontManifest = []);\n }\n const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'SOURCES.json'), 'utf-8'));\n return (fontManifest = parsed.fonts as FontSource[]);\n}\n\nfunction base64(file: string, dir: string): string {\n return fs.readFileSync(path.join(dir, file)).toString('base64');\n}\n\n/**\n * `@font-face` CSS with base64 woff2 for the requested families (all vendored\n * families when omitted). Returns '' when nothing is vendored, so the shell\n * keeps its (external) fallback instead of silently rendering with no fonts.\n */\nexport function loadEmbeddedFontFaceCss(families?: string[]): string {\n const dir = resolveFontDir();\n const manifest = loadManifest();\n if (!dir || manifest.length === 0) return '';\n\n const wanted = families && families.length > 0 ? families : manifest.map((f) => f.family);\n const cacheKey = [...wanted].sort().join('|');\n const cached = fontFaceCache.get(cacheKey);\n if (cached !== undefined) return cached;\n\n const blocks: string[] = [];\n for (const font of manifest) {\n if (!wanted.includes(font.family)) continue;\n const data = base64(font.file, dir);\n blocks.push(\n `@font-face{font-family:'${font.family}';font-style:normal;font-weight:${font.weights};` +\n `font-display:swap;src:url(data:font/woff2;base64,${data}) format('woff2');}`\n );\n }\n const css = blocks.join('\\n');\n fontFaceCache.set(cacheKey, css);\n return css;\n}\n\n/**\n * Which vendored families a compiled deck actually needs: the active theme's\n * display/body/code pairing, plus any vendored family named in the markup\n * (authored slides may reference one directly).\n */\nexport function collectDeckFontFamilies(html: string, theme: { typography?: Record<string, string> }): string[] {\n const manifest = loadManifest();\n const vendored = new Set(manifest.map((f) => f.family));\n const found = new Set<string>();\n\n // Only the PRIMARY family of each stack needs embedding: fallbacks exist for\n // when the primary is unavailable, and an embedded primary never is. (The old\n // code also pulled every fallback, e.g. Fira Code behind JetBrains Mono → +35KB\n // per deck for a font that can never be reached.)\n for (const value of Object.values(theme.typography || {})) {\n const primary = String(value).split(',')[0].replace(/[\"']/g, '').trim();\n if (vendored.has(primary)) found.add(primary);\n }\n // Authored slides may name a family directly (`font-family: 'Fira Code'`).\n // The theme's own variable declarations are excluded: they legitimately list\n // fallbacks, which must not drag a whole extra font into every deck.\n const authoredOnly = html.replace(/--font-(?:display|body|code)\\s*:[^;]+;/g, '');\n for (const family of vendored) {\n if (new RegExp(`font-family[^;}]*${family.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`, 'i').test(authoredOnly)) {\n found.add(family);\n }\n }\n return Array.from(found);\n}\n\nexport interface StandaloneCompileOptions extends HtmlCompileOptions {\n /** Skip embedding fonts (returns the deck with external font links). */\n embedFonts?: boolean;\n}\n\n/**\n * Compiles a deck that needs no network at all: fonts embedded, utilities\n * inlined (the shell does that unconditionally now), markup sanitized.\n */\nexport function compileStandaloneHtmlDeck(\n deckData: HtmlDeckData,\n options: StandaloneCompileOptions = {}\n): { html: string; slideCount: number; theme: unknown; title: string; embeddedFonts: string[] } {\n const base = compileHtmlDeck(deckData, options);\n if (options.embedFonts === false) return { ...base, embeddedFonts: [] };\n\n const families = collectDeckFontFamilies(base.html, base.theme as any);\n const fontFaceCss = loadEmbeddedFontFaceCss(families);\n if (!fontFaceCss) return { ...base, embeddedFonts: [] };\n\n // The shell marks the block so this is an exact, single replacement — no\n // regex surgery on the rest of the document.\n const html = base.html.replace(\n /<!-- DECK_FONT_BLOCK_START -->[\\s\\S]*?<!-- DECK_FONT_BLOCK_END -->/,\n `<!-- DECK_FONT_BLOCK_START -->\\n <style>\\n${fontFaceCss}\\n </style>\\n <!-- DECK_FONT_BLOCK_END -->`\n );\n\n return { ...base, html, embeddedFonts: families };\n}\n\n/** Off-box references a compiled deck still carries — must be empty to ship. */\nexport function findExternalReferences(html: string): string[] {\n const refs: string[] = [];\n for (const match of html.matchAll(/(?:https?:)?\\/\\/[a-z0-9.-]+\\.[a-z]{2,}[^\\s\"'<>)]*/gi)) {\n const url = match[0];\n // Namespaced identifiers (xmlns, schema URLs) are values, not fetches.\n if (/^\\/\\/(www\\.)?w3\\.org/i.test(url)) continue;\n if (/xmlns/i.test(html.slice(Math.max(0, (match.index ?? 0) - 24), match.index ?? 0))) continue;\n refs.push(url);\n }\n return Array.from(new Set(refs));\n}\n\nexport { runSlideLayoutQa } from './layoutQa.js';\nexport type { LayoutQaIssue, LayoutQaResult, LayoutQaOptions, LayoutQaSlidePage } from './layoutQa.js';\n"]}
@@ -1,5 +1,35 @@
1
1
  import { H as HtmlCompileOptions, a as HtmlDeckData } from '../types-k_eae8UV.cjs';
2
2
 
3
+ interface LayoutQaIssue {
4
+ slideIndex: number;
5
+ /** Largest fully-empty horizontal band, in stage px. */
6
+ deadBand: number;
7
+ /** Top edge of the dead band, in stage px. */
8
+ atY: number;
9
+ }
10
+ interface LayoutQaResult {
11
+ /** false = harness unavailable (no Chrome) — QA skipped, NOT failed. */
12
+ available: boolean;
13
+ issues: LayoutQaIssue[];
14
+ error?: string;
15
+ }
16
+ interface LayoutQaOptions {
17
+ /** Band height (stage px) that flags a slide. Default 400. */
18
+ threshold?: number;
19
+ /** Wall-clock budget for the whole headless run. Default 120s. */
20
+ timeoutMs?: number;
21
+ }
22
+ interface LayoutQaSlidePage {
23
+ /** Authored HTML for one slide (the `.bespoke-slide` inner markup). */
24
+ html: string;
25
+ }
26
+ /**
27
+ * Scanline QA over authored slide HTML: wraps each slide in a minimal deck
28
+ * shell (same class names the compiler emits), renders in headless Chromium,
29
+ * and flags slides whose largest content-free horizontal band ≥ threshold.
30
+ */
31
+ declare function runSlideLayoutQa(slides: LayoutQaSlidePage[], options?: LayoutQaOptions): Promise<LayoutQaResult>;
32
+
3
33
  declare function resolveFontDir(): string | null;
4
34
  /**
5
35
  * `@font-face` CSS with base64 woff2 for the requested families (all vendored
@@ -33,4 +63,4 @@ declare function compileStandaloneHtmlDeck(deckData: HtmlDeckData, options?: Sta
33
63
  /** Off-box references a compiled deck still carries — must be empty to ship. */
34
64
  declare function findExternalReferences(html: string): string[];
35
65
 
36
- export { type StandaloneCompileOptions, collectDeckFontFamilies, compileStandaloneHtmlDeck, findExternalReferences, loadEmbeddedFontFaceCss, resolveFontDir };
66
+ export { type LayoutQaIssue, type LayoutQaOptions, type LayoutQaResult, type LayoutQaSlidePage, type StandaloneCompileOptions, collectDeckFontFamilies, compileStandaloneHtmlDeck, findExternalReferences, loadEmbeddedFontFaceCss, resolveFontDir, runSlideLayoutQa };