@thanh01.pmt/presentation-kit 0.3.1 → 0.3.3

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.
@@ -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 };
@@ -1,5 +1,35 @@
1
1
  import { H as HtmlCompileOptions, a as HtmlDeckData } from '../types-k_eae8UV.js';
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 };
@@ -1,24 +1,184 @@
1
- import { compileHtmlDeck } from '../chunk-5HIDQQHE.js';
1
+ import { compileHtmlDeck } from '../chunk-LVSQN2VI.js';
2
2
  import fs from 'fs';
3
- import path from 'path';
3
+ import path2 from 'path';
4
4
  import { fileURLToPath } from 'url';
5
+ import os from 'os';
6
+ import { execFile } from 'child_process';
5
7
 
8
+ function chromeCandidates() {
9
+ if (process.env.LAYOUT_QA_CHROME) return [process.env.LAYOUT_QA_CHROME];
10
+ const linux = ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome"];
11
+ const mac = [
12
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
13
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
14
+ ];
15
+ return process.platform === "darwin" ? [...mac, ...linux] : [...linux, ...mac];
16
+ }
17
+ function findChrome() {
18
+ for (const c of chromeCandidates()) {
19
+ try {
20
+ if (c && fs.existsSync(c)) return c;
21
+ } catch {
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ var PROBE = `(function(){
27
+ var H = 1080, ROW = 10, ROWS = Math.ceil(H / ROW);
28
+ var pages = Array.prototype.slice.call(document.querySelectorAll('.slide-page'));
29
+ var wrap = document.createElement('div');
30
+ wrap.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;overflow:hidden;';
31
+ var stage = document.createElement('div');
32
+ stage.className = 'slide-canvas';
33
+ stage.style.cssText = 'position:absolute;left:0;top:0;width:1920px;height:1080px;';
34
+ var inner = document.createElement('div');
35
+ inner.style.cssText = 'position:absolute;inset:0;';
36
+ stage.appendChild(inner);
37
+ wrap.appendChild(stage);
38
+ document.body.appendChild(wrap);
39
+ function hasDirectText(el) {
40
+ for (var n = el.firstChild; n; n = n.nextSibling) {
41
+ if (n.nodeType === 3 && n.textContent.trim().length > 0) return true;
42
+ }
43
+ return false;
44
+ }
45
+ var out = [];
46
+ for (var i = 0; i < pages.length; i++) {
47
+ var src = pages[i].querySelector('.bespoke-slide');
48
+ if (!src) { out.push(null); continue; }
49
+ inner.innerHTML = '';
50
+ inner.appendChild(src.cloneNode(true));
51
+ var all = stage.querySelectorAll('*');
52
+ for (var j = 0; j < all.length; j++) {
53
+ all[j].style.animation = 'none';
54
+ all[j].style.transition = 'none';
55
+ all[j].style.opacity = '1';
56
+ all[j].style.visibility = 'visible';
57
+ if (!/^(SVG|CANVAS|IMG|VIDEO)$/.test(all[j].tagName)) all[j].style.transform = 'none';
58
+ }
59
+ var stageTop = stage.getBoundingClientRect().top;
60
+ var rows = new Array(ROWS);
61
+ for (var k = 0; k < ROWS; k++) rows[k] = false;
62
+ var els = inner.querySelectorAll('*');
63
+ for (var e = 0; e < els.length; e++) {
64
+ var el = els[e];
65
+ var isVisual = /^(IMG|SVG|CANVAS|VIDEO)$/.test(el.tagName);
66
+ if (!isVisual && !hasDirectText(el)) continue;
67
+ var cs = getComputedStyle(el);
68
+ if (cs.display === 'none' || cs.visibility === 'hidden') continue;
69
+ var r = el.getBoundingClientRect();
70
+ if (r.width < 4 || r.height < 4) continue;
71
+ var t = r.top - stageTop;
72
+ var b = r.bottom - stageTop;
73
+ if (b <= 0 || t >= H) continue;
74
+ var k0 = Math.max(0, Math.floor(t / ROW));
75
+ var k1 = Math.min(ROWS, Math.ceil(b / ROW));
76
+ for (var k = k0; k < k1; k++) rows[k] = true;
77
+ }
78
+ var best = 0, bestStart = 0, cur = 0, curStart = 0;
79
+ for (var k = 2; k < ROWS - 2; k++) {
80
+ if (!rows[k]) {
81
+ if (cur === 0) curStart = k;
82
+ cur++;
83
+ if (cur > best) { best = cur; bestStart = curStart; }
84
+ } else cur = 0;
85
+ }
86
+ out.push({ deadBand: best * ROW, atY: bestStart * ROW });
87
+ }
88
+ wrap.remove();
89
+ document.title = 'QA_RESULT:' + JSON.stringify(out);
90
+ })();`;
91
+ async function runSlideLayoutQa(slides, options = {}) {
92
+ const threshold = options.threshold ?? 400;
93
+ const timeoutMs = options.timeoutMs ?? 12e4;
94
+ const chrome = findChrome();
95
+ if (!chrome) return { available: false, issues: [], error: "no chrome binary found" };
96
+ 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;}";
97
+ const pagesHtml = slides.map((s) => `<div class="slide-page"><div class="bespoke-slide">${s.html}</div></div>`).join("\n");
98
+ const probeSafe = PROBE.replace(/<\/script/gi, "<\\/script");
99
+ const html = `<!doctype html><html><head><meta charset="utf-8"><style>
100
+ ${themeCss}
101
+ .slide-canvas{position:relative;width:1920px;height:1080px;overflow:hidden;background:var(--bg);}
102
+ .slide-page{position:absolute;inset:0;width:100%;height:100%;display:none;opacity:0;}
103
+ .slide-page.active{display:block;opacity:1;}
104
+ </style></head><body><div class="slide-canvas">
105
+ ${pagesHtml}
106
+ </div><script>${probeSafe}</script></body></html>`;
107
+ const dir = fs.mkdtempSync(path2.join(os.tmpdir(), "layout-qa-"));
108
+ const file = path2.join(dir, "deck.html");
109
+ try {
110
+ fs.writeFileSync(file, html, "utf-8");
111
+ const args = [
112
+ "--headless=new",
113
+ "--no-sandbox",
114
+ "--disable-gpu",
115
+ "--disable-dev-shm-usage",
116
+ "--hide-scrollbars",
117
+ "--virtual-time-budget=5000",
118
+ "--window-size=1920,1200",
119
+ "--dump-dom",
120
+ `file://${file}`
121
+ ];
122
+ const stdout = await new Promise((resolve, reject) => {
123
+ execFile(
124
+ chrome,
125
+ args,
126
+ { timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 },
127
+ (err, stdout2) => {
128
+ if (err && !stdout2) return reject(err);
129
+ resolve(String(stdout2 || ""));
130
+ }
131
+ );
132
+ });
133
+ const marker = stdout.match(/QA_RESULT:(\[[\s\S]*?\])<\/title>/);
134
+ if (!marker) {
135
+ return {
136
+ available: true,
137
+ issues: slides.map((_, i) => ({ slideIndex: i + 1, deadBand: threshold, atY: 0 })),
138
+ error: "probe produced no result (chrome render failed?)"
139
+ };
140
+ }
141
+ const parsed = JSON.parse(marker[1]);
142
+ const issues = [];
143
+ for (let i = 0; i < parsed.length; i++) {
144
+ const row = parsed[i];
145
+ if (!row) continue;
146
+ if (row.deadBand >= threshold) {
147
+ issues.push({ slideIndex: i + 1, deadBand: row.deadBand, atY: row.atY });
148
+ }
149
+ }
150
+ return { available: true, issues };
151
+ } catch (err) {
152
+ return {
153
+ available: true,
154
+ issues: slides.map((_, i) => ({ slideIndex: i + 1, deadBand: threshold, atY: 0 })),
155
+ error: err?.message || String(err)
156
+ };
157
+ } finally {
158
+ try {
159
+ fs.rmSync(dir, { recursive: true, force: true });
160
+ } catch {
161
+ }
162
+ }
163
+ }
164
+
165
+ // src/node/index.ts
6
166
  var fontManifest = null;
7
167
  var fontFaceCache = /* @__PURE__ */ new Map();
8
168
  function fontDirCandidates() {
9
- const here = path.dirname(fileURLToPath(import.meta.url));
169
+ const here = path2.dirname(fileURLToPath(import.meta.url));
10
170
  return [
11
171
  process.env.PRESENTATION_KIT_FONT_DIR || "",
12
172
  // dist/node/index.js → dist/assets/fonts
13
- path.resolve(here, "../assets/fonts"),
14
- path.resolve(here, "../..", "assets/fonts"),
173
+ path2.resolve(here, "../assets/fonts"),
174
+ path2.resolve(here, "../..", "assets/fonts"),
15
175
  // src/node/index.ts (tsx/vitest) → <pkg>/assets/fonts
16
- path.resolve(here, "../../assets/fonts")
176
+ path2.resolve(here, "../../assets/fonts")
17
177
  ].filter(Boolean);
18
178
  }
19
179
  function resolveFontDir() {
20
180
  for (const dir of fontDirCandidates()) {
21
- if (fs.existsSync(path.join(dir, "SOURCES.json"))) return dir;
181
+ if (fs.existsSync(path2.join(dir, "SOURCES.json"))) return dir;
22
182
  }
23
183
  return null;
24
184
  }
@@ -31,11 +191,11 @@ function loadManifest() {
31
191
  );
32
192
  return fontManifest = [];
33
193
  }
34
- const parsed = JSON.parse(fs.readFileSync(path.join(dir, "SOURCES.json"), "utf-8"));
194
+ const parsed = JSON.parse(fs.readFileSync(path2.join(dir, "SOURCES.json"), "utf-8"));
35
195
  return fontManifest = parsed.fonts;
36
196
  }
37
197
  function base64(file, dir) {
38
- return fs.readFileSync(path.join(dir, file)).toString("base64");
198
+ return fs.readFileSync(path2.join(dir, file)).toString("base64");
39
199
  }
40
200
  function loadEmbeddedFontFaceCss(families) {
41
201
  const dir = resolveFontDir();
@@ -100,6 +260,6 @@ function findExternalReferences(html) {
100
260
  return Array.from(new Set(refs));
101
261
  }
102
262
 
103
- export { collectDeckFontFamilies, compileStandaloneHtmlDeck, findExternalReferences, loadEmbeddedFontFaceCss, resolveFontDir };
263
+ export { collectDeckFontFamilies, compileStandaloneHtmlDeck, findExternalReferences, loadEmbeddedFontFaceCss, resolveFontDir, runSlideLayoutQa };
104
264
  //# sourceMappingURL=index.js.map
105
265
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/node/index.ts"],"names":[],"mappings":"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","file":"index.js","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":["path","stdout","fs"],"mappings":"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","file":"index.js","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,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- var chunkCVEC72CT_cjs = require('../chunk-CVEC72CT.cjs');
3
+ var chunk2FJGU32M_cjs = require('../chunk-2FJGU32M.cjs');
4
4
  var chunk3Q4RMZVQ_cjs = require('../chunk-3Q4RMZVQ.cjs');
5
- var chunk5WEZOGQA_cjs = require('../chunk-5WEZOGQA.cjs');
5
+ var chunkYO6TN5TA_cjs = require('../chunk-YO6TN5TA.cjs');
6
6
  var React2 = require('react');
7
7
  var jsxRuntime = require('react/jsx-runtime');
8
8
 
@@ -26,7 +26,7 @@ function useSlideState(options) {
26
26
  const result = React2.useMemo(() => {
27
27
  if (!markdown || !markdown.trim()) return null;
28
28
  renderCountRef.current++;
29
- return chunkCVEC72CT_cjs.renderSlide(markdown, {
29
+ return chunk2FJGU32M_cjs.renderSlide(markdown, {
30
30
  theme,
31
31
  enableMermaid: true,
32
32
  enableClicks,
@@ -1338,7 +1338,7 @@ function SlideViewer({
1338
1338
  containers.forEach((container) => {
1339
1339
  if (container instanceof HTMLElement) {
1340
1340
  const themeName2 = typeof theme === "string" ? theme : "dark";
1341
- chunkCVEC72CT_cjs.renderMermaidDiagrams(container, themeName2).catch(() => {
1341
+ chunk2FJGU32M_cjs.renderMermaidDiagrams(container, themeName2).catch(() => {
1342
1342
  });
1343
1343
  }
1344
1344
  });
@@ -1622,7 +1622,7 @@ var HtmlSlideViewer = ({
1622
1622
  const fullHtml = React2.useMemo(() => {
1623
1623
  if (initialHtml) return initialHtml;
1624
1624
  if (deckData) {
1625
- return chunk5WEZOGQA_cjs.compileHtmlDeck(deckData).html;
1625
+ return chunkYO6TN5TA_cjs.compileHtmlDeck(deckData).html;
1626
1626
  }
1627
1627
  return "";
1628
1628
  }, [initialHtml, deckData]);
@@ -1778,35 +1778,35 @@ var HtmlSlideViewer = ({
1778
1778
 
1779
1779
  Object.defineProperty(exports, "defineTheme", {
1780
1780
  enumerable: true,
1781
- get: function () { return chunkCVEC72CT_cjs.defineTheme; }
1781
+ get: function () { return chunk2FJGU32M_cjs.defineTheme; }
1782
1782
  });
1783
1783
  Object.defineProperty(exports, "extractMaxClicks", {
1784
1784
  enumerable: true,
1785
- get: function () { return chunkCVEC72CT_cjs.extractMaxClicks; }
1785
+ get: function () { return chunk2FJGU32M_cjs.extractMaxClicks; }
1786
1786
  });
1787
1787
  Object.defineProperty(exports, "generateClickCss", {
1788
1788
  enumerable: true,
1789
- get: function () { return chunkCVEC72CT_cjs.generateClickCss; }
1789
+ get: function () { return chunk2FJGU32M_cjs.generateClickCss; }
1790
1790
  });
1791
1791
  Object.defineProperty(exports, "postprocessClicks", {
1792
1792
  enumerable: true,
1793
- get: function () { return chunkCVEC72CT_cjs.postprocessClicks; }
1793
+ get: function () { return chunk2FJGU32M_cjs.postprocessClicks; }
1794
1794
  });
1795
1795
  Object.defineProperty(exports, "preprocessClicks", {
1796
1796
  enumerable: true,
1797
- get: function () { return chunkCVEC72CT_cjs.preprocessClicks; }
1797
+ get: function () { return chunk2FJGU32M_cjs.preprocessClicks; }
1798
1798
  });
1799
1799
  Object.defineProperty(exports, "renderSlide", {
1800
1800
  enumerable: true,
1801
- get: function () { return chunkCVEC72CT_cjs.renderSlide; }
1801
+ get: function () { return chunk2FJGU32M_cjs.renderSlide; }
1802
1802
  });
1803
1803
  Object.defineProperty(exports, "renderSlideAsync", {
1804
1804
  enumerable: true,
1805
- get: function () { return chunkCVEC72CT_cjs.renderSlideAsync; }
1805
+ get: function () { return chunk2FJGU32M_cjs.renderSlideAsync; }
1806
1806
  });
1807
1807
  Object.defineProperty(exports, "resolveTheme", {
1808
1808
  enumerable: true,
1809
- get: function () { return chunkCVEC72CT_cjs.resolveTheme; }
1809
+ get: function () { return chunk2FJGU32M_cjs.resolveTheme; }
1810
1810
  });
1811
1811
  exports.DeckView = DeckView;
1812
1812
  exports.HtmlSlideViewer = HtmlSlideViewer;
@@ -1,7 +1,7 @@
1
- import { renderSlide, renderMermaidDiagrams } from '../chunk-3DXMAPAY.js';
2
- export { defineTheme, extractMaxClicks, generateClickCss, postprocessClicks, preprocessClicks, renderSlide, renderSlideAsync, resolveTheme } from '../chunk-3DXMAPAY.js';
1
+ import { renderSlide, renderMermaidDiagrams } from '../chunk-FIBXWRWQ.js';
2
+ export { defineTheme, extractMaxClicks, generateClickCss, postprocessClicks, preprocessClicks, renderSlide, renderSlideAsync, resolveTheme } from '../chunk-FIBXWRWQ.js';
3
3
  import { SLIDE_LAYOUT_PRESETS, updateSlideLayoutInMarkdown } from '../chunk-V6X7D44H.js';
4
- import { compileHtmlDeck } from '../chunk-5HIDQQHE.js';
4
+ import { compileHtmlDeck } from '../chunk-LVSQN2VI.js';
5
5
  import React2, { useState, useRef, useMemo, useEffect, useCallback } from 'react';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7