mirror of
https://github.com/jacobinwwey/obsidian-NotEMD.git
synced 2026-07-22 12:40:25 +00:00
1649 lines
63 KiB
JavaScript
1649 lines
63 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Maintainer verification for the real NoteMD Slidev export workflow.
|
|
*
|
|
* This script exercises the production source-preparation and Slidev export
|
|
* modules without starting Obsidian. It intentionally writes the same vault
|
|
* artifacts the UI writes so maintainers can inspect the generated deck and
|
|
* standalone HTML after the command completes.
|
|
*/
|
|
|
|
const childProcess = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const esbuild = require('esbuild');
|
|
const { buildPptxVisualDiff, extractPptxBackgroundImages } = require('./lib/pptx-visual-diff');
|
|
|
|
const DEFAULT_PPTX_VISUAL_THRESHOLDS = {
|
|
maxRmse: 0.12,
|
|
meanRmse: 0.08,
|
|
};
|
|
|
|
const VISIBLE_NATIVE_RENDERED_HTML_PPTX_VISUAL_THRESHOLDS = {
|
|
maxRmse: 0.25,
|
|
meanRmse: 0.145,
|
|
};
|
|
|
|
const LAYOUT_AUDIT_CONFIG = {
|
|
overflowTolerancePx: 6,
|
|
minReadableScale: 0.28,
|
|
maxAutoPatchPasses: 6,
|
|
minEffectiveFontPx: 10,
|
|
minSvgTextFontPx: 9,
|
|
minTableBodyFontPx: 10,
|
|
minCodeFontPx: 10,
|
|
minQualityMarginPx: 18,
|
|
minContentAreaRatio: 0.18,
|
|
lowContentUtilizationScaleThreshold: 0.55,
|
|
mermaidLowZoomReviewScale: 0.72,
|
|
};
|
|
|
|
function parseArgs(argv) {
|
|
const args = {
|
|
vault: 'docs',
|
|
source: 'architecture.zh-CN.md',
|
|
format: 'html',
|
|
htmlMode: 'standalone',
|
|
outputSubfolder: 'export',
|
|
theme: 'default',
|
|
timeoutMs: 180000,
|
|
requireNativeStandalone: false,
|
|
playwright: true,
|
|
screenshots: true,
|
|
sampleSlides: null,
|
|
pptxVisualDiff: false,
|
|
requirePptxVisualMatch: false,
|
|
pptxVisualMaxRmse: DEFAULT_PPTX_VISUAL_THRESHOLDS.maxRmse,
|
|
pptxVisualMeanRmse: DEFAULT_PPTX_VISUAL_THRESHOLDS.meanRmse,
|
|
pptxVisualMaxRmseExplicit: false,
|
|
pptxVisualMeanRmseExplicit: false,
|
|
pptxVisualDpi: 150,
|
|
pptxVisualRenderer: 'auto',
|
|
pptxVisualDiffDir: null,
|
|
pptxVisualReferenceDir: null,
|
|
pptxRenderedHtmlReferenceDiff: false,
|
|
requirePptxRenderedHtmlReferenceMatch: false,
|
|
pptxVisibleNativeExperiment: false,
|
|
requirePptxVisibleNativeMatch: false,
|
|
json: false,
|
|
};
|
|
|
|
for (let index = 0; index < argv.length; index++) {
|
|
const arg = argv[index];
|
|
if (arg === '--vault' && argv[index + 1]) {
|
|
args.vault = argv[++index];
|
|
} else if (arg === '--source' && argv[index + 1]) {
|
|
args.source = argv[++index];
|
|
} else if (arg === '--format' && argv[index + 1]) {
|
|
args.format = argv[++index];
|
|
} else if (arg === '--html-mode' && argv[index + 1]) {
|
|
args.htmlMode = argv[++index];
|
|
} else if (arg === '--output-subfolder' && argv[index + 1]) {
|
|
args.outputSubfolder = argv[++index];
|
|
} else if (arg === '--theme' && argv[index + 1]) {
|
|
args.theme = argv[++index];
|
|
} else if (arg === '--timeout-ms' && argv[index + 1]) {
|
|
args.timeoutMs = Number(argv[++index]);
|
|
} else if (arg === '--require-native-standalone') {
|
|
args.requireNativeStandalone = true;
|
|
} else if (arg === '--sample-slides' && argv[index + 1]) {
|
|
const rawValue = argv[++index].trim().toLowerCase();
|
|
args.sampleSlides = rawValue === 'all'
|
|
? []
|
|
: rawValue.split(',').map(value => Number(value.trim())).filter(Number.isFinite);
|
|
} else if (arg === '--no-playwright') {
|
|
args.playwright = false;
|
|
} else if (arg === '--no-screenshots') {
|
|
args.screenshots = false;
|
|
} else if (arg === '--pptx-visual-diff') {
|
|
args.pptxVisualDiff = true;
|
|
} else if (arg === '--require-pptx-visual-match') {
|
|
args.requirePptxVisualMatch = true;
|
|
args.pptxVisualDiff = true;
|
|
} else if (arg === '--pptx-visual-max-rmse' && argv[index + 1]) {
|
|
args.pptxVisualMaxRmse = Number(argv[++index]);
|
|
args.pptxVisualMaxRmseExplicit = true;
|
|
} else if (arg === '--pptx-visual-mean-rmse' && argv[index + 1]) {
|
|
args.pptxVisualMeanRmse = Number(argv[++index]);
|
|
args.pptxVisualMeanRmseExplicit = true;
|
|
} else if (arg === '--pptx-visual-dpi' && argv[index + 1]) {
|
|
args.pptxVisualDpi = Number(argv[++index]);
|
|
} else if (arg === '--pptx-visual-renderer' && argv[index + 1]) {
|
|
args.pptxVisualRenderer = String(argv[++index]).toLowerCase();
|
|
} else if (arg === '--pptx-visual-diff-dir' && argv[index + 1]) {
|
|
args.pptxVisualDiffDir = argv[++index];
|
|
} else if (arg === '--pptx-visual-reference-dir' && argv[index + 1]) {
|
|
args.pptxVisualReferenceDir = argv[++index];
|
|
args.pptxVisualDiff = true;
|
|
} else if (arg === '--pptx-rendered-html-reference-diff') {
|
|
args.pptxRenderedHtmlReferenceDiff = true;
|
|
} else if (arg === '--require-pptx-rendered-html-reference-match') {
|
|
args.requirePptxRenderedHtmlReferenceMatch = true;
|
|
args.pptxRenderedHtmlReferenceDiff = true;
|
|
} else if (arg === '--pptx-visible-native-experiment') {
|
|
args.pptxVisibleNativeExperiment = true;
|
|
} else if (arg === '--require-pptx-visible-native-match') {
|
|
args.requirePptxVisibleNativeMatch = true;
|
|
args.pptxVisibleNativeExperiment = true;
|
|
} else if (arg === '--json') {
|
|
args.json = true;
|
|
} else if (arg === '--help' || arg === '-h') {
|
|
printHelp();
|
|
process.exit(0);
|
|
} else {
|
|
throw new Error(`Unknown argument: ${arg}`);
|
|
}
|
|
}
|
|
|
|
if (!['html', 'pdf', 'png', 'pptx', 'mp4'].includes(args.format)) {
|
|
throw new Error(`Unsupported --format ${args.format}`);
|
|
}
|
|
if (!['standalone', 'server-script'].includes(args.htmlMode)) {
|
|
throw new Error(`Unsupported --html-mode ${args.htmlMode}`);
|
|
}
|
|
if (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0) {
|
|
throw new Error('--timeout-ms must be a positive number');
|
|
}
|
|
if (args.requireNativeStandalone && (args.format !== 'html' || args.htmlMode !== 'standalone')) {
|
|
throw new Error('--require-native-standalone requires --format html --html-mode standalone');
|
|
}
|
|
if (args.pptxVisualDiff && args.format !== 'pptx') {
|
|
throw new Error('--pptx-visual-diff requires --format pptx');
|
|
}
|
|
if (args.pptxVisualReferenceDir && args.format !== 'pptx') {
|
|
throw new Error('--pptx-visual-reference-dir requires --format pptx');
|
|
}
|
|
if (args.pptxRenderedHtmlReferenceDiff && args.format !== 'pptx') {
|
|
throw new Error('--pptx-rendered-html-reference-diff requires --format pptx');
|
|
}
|
|
if (args.pptxVisibleNativeExperiment && args.format !== 'pptx') {
|
|
throw new Error('--pptx-visible-native-experiment requires --format pptx');
|
|
}
|
|
if (!Number.isFinite(args.pptxVisualMaxRmse) || args.pptxVisualMaxRmse < 0) {
|
|
throw new Error('--pptx-visual-max-rmse must be a non-negative number');
|
|
}
|
|
if (!Number.isFinite(args.pptxVisualMeanRmse) || args.pptxVisualMeanRmse < 0) {
|
|
throw new Error('--pptx-visual-mean-rmse must be a non-negative number');
|
|
}
|
|
if (!Number.isFinite(args.pptxVisualDpi) || args.pptxVisualDpi <= 0) {
|
|
throw new Error('--pptx-visual-dpi must be a positive number');
|
|
}
|
|
if (!['auto', 'libreoffice', 'powerpoint'].includes(args.pptxVisualRenderer)) {
|
|
throw new Error('--pptx-visual-renderer must be one of: auto, libreoffice, powerpoint');
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
function printHelp() {
|
|
console.log([
|
|
'Usage: node scripts/verify-slidev-export-workflow.cjs [options]',
|
|
'',
|
|
'Options:',
|
|
' --vault <path> Vault root, default: docs',
|
|
' --source <path> Vault-relative source Markdown, default: architecture.zh-CN.md',
|
|
' --format <html|pdf|png|pptx|mp4> Export format, default: html',
|
|
' --html-mode <standalone|server-script> HTML mode, default: standalone',
|
|
' --output-subfolder <path> Vault-relative output folder, default: export',
|
|
' --theme <name> Slidev theme, default: default',
|
|
' --require-native-standalone Fail if HTML export falls back from native standalone',
|
|
' --sample-slides <list|all> Comma-separated slide numbers for Playwright, default: all slides',
|
|
' --no-playwright Skip browser rendering checks',
|
|
' --no-screenshots Do not write Playwright screenshots',
|
|
' --pptx-visual-diff Render PPTX back to PNG and compare each page with the PPTX frozen background reference',
|
|
' For visible-native PPTX defaults, this is a diagnostic reference, not the hard visual gate',
|
|
' --require-pptx-visual-match Fail when PPTX/PNG visual diff exceeds thresholds; visible-native defaults auto-gate against rendered HTML',
|
|
' --pptx-visual-max-rmse <n> Max per-slide normalized RMSE, default: 0.12 raster / 0.25 visible-native rendered HTML',
|
|
' --pptx-visual-mean-rmse <n> Mean normalized RMSE, default: 0.08 raster / 0.145 visible-native rendered HTML',
|
|
' --pptx-visual-dpi <n> PPTX render-back DPI for the LibreOffice/PDF path, default: 150',
|
|
' --pptx-visual-renderer <auto|libreoffice|powerpoint> Render-back engine, default: auto',
|
|
' --pptx-visual-diff-dir <path> Output directory for visual diff artifacts',
|
|
' --pptx-visual-reference-dir <path> External PNG sequence directory for advisory cross-export comparison',
|
|
' --pptx-rendered-html-reference-diff Capture PNG references from the same rendered HTML used by PPTX and compare against PPTX render-back',
|
|
' --require-pptx-rendered-html-reference-match Fail when rendered-HTML reference diff exceeds PPTX visual thresholds',
|
|
' --pptx-visible-native-experiment Also export experimental visible-native PPTX and compare it against the default frozen-background reference',
|
|
' --require-pptx-visible-native-match Fail when the visible-native experiment exceeds PPTX visual thresholds',
|
|
' --json Print only the final JSON report',
|
|
].join('\n'));
|
|
}
|
|
|
|
async function bundleSlideExportModules() {
|
|
const result = await esbuild.build({
|
|
stdin: {
|
|
contents: [
|
|
"export { probeEnvironment } from './src/slideExport/environmentProber';",
|
|
"export { prepareSlidevExportSource } from './src/slideExport/slidevSourcePreparer';",
|
|
"export { exportSlidevHtml, exportSlidevHtmlWithOutcome, exportSlidevPdf, exportSlidevPng } from './src/slideExport/slidevExporter';",
|
|
"export { exportSlidevPptxFromHtml, exportSlidevPptxRenderedHtmlReferencePngSequence, exportSlidevVisibleNativePptxExperimentFromHtml } from './src/slideExport/pptxExporter';",
|
|
"export { convergeSlidevDeckLayout } from './src/slideExport/slidevLayoutWorkflow';",
|
|
"export { exportVideoMp4 } from './src/slideExport/videoExporter';",
|
|
"export { analyzeRenderedSlideMeasurement, summarizeLayoutAudits, patchDeckWithLayoutAudit, countSlideDeckSlides } from './src/slideExport/slidevLayoutAudit';",
|
|
"export { resolveWorkspaceHomeCandidates } from './src/slideExport/platformUtils';",
|
|
"export { startLocalServer, stopLocalServer } from './src/slideExport/localServer';",
|
|
].join('\n'),
|
|
resolveDir: process.cwd(),
|
|
sourcefile: 'notemd-slidev-workflow-entry.ts',
|
|
loader: 'ts',
|
|
},
|
|
bundle: true,
|
|
platform: 'node',
|
|
format: 'cjs',
|
|
write: false,
|
|
logLevel: 'silent',
|
|
plugins: [obsidianShimPlugin()],
|
|
});
|
|
|
|
const code = result.outputFiles[0].text;
|
|
const bundledModule = { exports: {} };
|
|
const evaluate = new Function('require', 'module', 'exports', code);
|
|
evaluate(require, bundledModule, bundledModule.exports);
|
|
return bundledModule.exports;
|
|
}
|
|
|
|
function obsidianShimPlugin() {
|
|
return {
|
|
name: 'notemd-obsidian-shim',
|
|
setup(build) {
|
|
build.onResolve({ filter: /^obsidian$/ }, () => ({
|
|
path: 'obsidian-shim',
|
|
namespace: 'notemd-obsidian-shim',
|
|
}));
|
|
build.onLoad({ filter: /.*/, namespace: 'notemd-obsidian-shim' }, () => ({
|
|
loader: 'js',
|
|
contents: [
|
|
'export const Platform = { isDesktopApp: true };',
|
|
'export class Notice { constructor(message) { this.message = message; } }',
|
|
'export class Modal { constructor(app) { this.app = app; this.contentEl = { empty() {}, createEl() { return this; }, createDiv() { return this; }, appendChild() {}, addClass() {}, setText() {} }; } open() {} close() {} }',
|
|
'export class Setting { constructor() {} setName() { return this; } setDesc() { return this; } addButton() { return this; } addText() { return this; } addDropdown() { return this; } addToggle() { return this; } }',
|
|
'export class Plugin {}',
|
|
'export class PluginSettingTab {}',
|
|
'export class ItemView {}',
|
|
'export class TFile {}',
|
|
'export class TFolder {}',
|
|
'export class MarkdownView {}',
|
|
'export class WorkspaceLeaf {}',
|
|
'export class ButtonComponent {}',
|
|
'export class TextAreaComponent {}',
|
|
'export const requestUrl = async () => { throw new Error("requestUrl unavailable in Slidev export workflow verification"); };',
|
|
'export const getLanguage = () => "en";',
|
|
'export const loadPdfJs = async () => null;',
|
|
].join('\n'),
|
|
}));
|
|
},
|
|
};
|
|
}
|
|
|
|
function createApp(vaultRoot) {
|
|
const adapter = {
|
|
basePath: vaultRoot,
|
|
getBasePath: () => vaultRoot,
|
|
exists: async vaultPath => fs.existsSync(path.join(vaultRoot, vaultPath)),
|
|
mkdir: async vaultPath => fs.mkdirSync(path.join(vaultRoot, vaultPath), { recursive: true }),
|
|
read: async vaultPath => fs.readFileSync(path.join(vaultRoot, vaultPath), 'utf8'),
|
|
list: async vaultPath => {
|
|
const absolutePath = path.join(vaultRoot, vaultPath);
|
|
const entries = fs.existsSync(absolutePath) ? fs.readdirSync(absolutePath, { withFileTypes: true }) : [];
|
|
return {
|
|
files: entries.filter(entry => entry.isFile()).map(entry => path.join(vaultPath, entry.name).replace(/\\/g, '/')),
|
|
folders: entries.filter(entry => entry.isDirectory()).map(entry => path.join(vaultPath, entry.name).replace(/\\/g, '/')),
|
|
};
|
|
},
|
|
stat: async vaultPath => {
|
|
const absolutePath = path.join(vaultRoot, vaultPath);
|
|
if (!fs.existsSync(absolutePath)) {
|
|
return null;
|
|
}
|
|
return fs.statSync(absolutePath);
|
|
},
|
|
write: async (vaultPath, content) => {
|
|
const absolutePath = path.join(vaultRoot, vaultPath);
|
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
fs.writeFileSync(absolutePath, content, 'utf8');
|
|
},
|
|
};
|
|
|
|
return {
|
|
vault: {
|
|
adapter,
|
|
read: async file => fs.readFileSync(path.join(vaultRoot, file.path), 'utf8'),
|
|
},
|
|
};
|
|
}
|
|
|
|
function createSourceFile(vaultRoot, sourcePath) {
|
|
const normalizedSource = sourcePath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
const absolutePath = path.join(vaultRoot, normalizedSource);
|
|
if (!fs.existsSync(absolutePath)) {
|
|
throw new Error(`Source file does not exist: ${absolutePath}`);
|
|
}
|
|
|
|
const extension = path.extname(normalizedSource);
|
|
return {
|
|
path: normalizedSource,
|
|
name: path.basename(normalizedSource),
|
|
basename: path.basename(normalizedSource, extension),
|
|
extension: extension.replace(/^\./, ''),
|
|
};
|
|
}
|
|
|
|
function createConfig(args) {
|
|
return {
|
|
format: args.format,
|
|
withClicks: false,
|
|
outputSubfolder: args.outputSubfolder,
|
|
ffmpegFps: 1,
|
|
ffmpegCrf: 23,
|
|
slidevTheme: args.theme,
|
|
timeoutMs: args.timeoutMs,
|
|
htmlMode: args.htmlMode,
|
|
imageScale: 3,
|
|
};
|
|
}
|
|
|
|
function collectDeckSummary(deckPath) {
|
|
if (!deckPath || !fs.existsSync(deckPath)) {
|
|
return null;
|
|
}
|
|
const deck = fs.readFileSync(deckPath, 'utf8');
|
|
const mermaidFences = extractMermaidFenceBlocks(deck);
|
|
return {
|
|
path: deckPath,
|
|
bytes: fs.statSync(deckPath).size,
|
|
theme: (deck.match(/^theme:\s*(.+)$/m) || [])[1] || null,
|
|
mermaidBlocks: mermaidFences.length,
|
|
zoomLines: [...deck.matchAll(/^zoom:\s*(.+)$/gm)].map(match => match[1]),
|
|
containsKnownStaleText: deck.includes('快速定位'),
|
|
containsMissingTheme: deck.includes('seriph'),
|
|
};
|
|
}
|
|
|
|
function collectMermaidSourcePreservation(sourcePath, deckPath) {
|
|
if (!sourcePath || !deckPath || !fs.existsSync(sourcePath) || !fs.existsSync(deckPath)) {
|
|
return null;
|
|
}
|
|
|
|
const sourceFences = extractMermaidFenceBlocks(fs.readFileSync(sourcePath, 'utf8'));
|
|
const deckFences = extractMermaidFenceBlocks(fs.readFileSync(deckPath, 'utf8'));
|
|
const changedFenceIndexes = [];
|
|
const comparedCount = Math.max(sourceFences.length, deckFences.length);
|
|
for (let index = 0; index < comparedCount; index++) {
|
|
if (sourceFences[index] !== deckFences[index]) {
|
|
changedFenceIndexes.push(index + 1);
|
|
}
|
|
}
|
|
|
|
return {
|
|
required: sourceFences.length > 0,
|
|
passed: changedFenceIndexes.length === 0,
|
|
sourceFenceCount: sourceFences.length,
|
|
deckFenceCount: deckFences.length,
|
|
changedFenceIndexes,
|
|
};
|
|
}
|
|
|
|
function buildTableBodyLayoutGate(layoutAudits) {
|
|
const tableBodyAudits = layoutAudits.filter(audit => hasTableOrBodyTextSurface(audit));
|
|
const failures = [];
|
|
|
|
for (const audit of tableBodyAudits) {
|
|
for (const finding of audit.findings || []) {
|
|
if (!isTableBodyLayoutFinding(audit, finding)) {
|
|
continue;
|
|
}
|
|
failures.push({
|
|
slide: audit.slide,
|
|
kind: finding.kind,
|
|
target: finding.target,
|
|
message: finding.message,
|
|
recommendedPatch: finding.recommendedPatch || null,
|
|
overflowAxis: finding.overflowAxis || null,
|
|
effectiveFontPx: typeof finding.effectiveFontPx === 'number' ? finding.effectiveFontPx : null,
|
|
fontThresholdPx: typeof finding.fontThresholdPx === 'number' ? finding.fontThresholdPx : null,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
passed: failures.length === 0,
|
|
auditedSlideCount: tableBodyAudits.length,
|
|
tableSlideCount: tableBodyAudits.filter(audit => (audit.elementKinds || []).includes('table')).length,
|
|
bodyTextSlideCount: tableBodyAudits.filter(audit => (audit.elementKinds || []).includes('text')).length,
|
|
failureCount: failures.length,
|
|
failureSlides: Array.from(new Set(failures.map(failure => failure.slide))).sort((left, right) => left - right),
|
|
failures,
|
|
};
|
|
}
|
|
|
|
function buildRenderedLayoutGate(options) {
|
|
const required = options?.required === true;
|
|
const auditedSlides = Array.isArray(options?.auditedSlides) ? options.auditedSlides : [];
|
|
const layoutAudits = Array.isArray(options?.layoutAudits) ? options.layoutAudits : [];
|
|
const playwrightChecks = Array.isArray(options?.playwrightChecks) ? options.playwrightChecks : [];
|
|
const auditSkippedReason = options?.auditSkippedReason || null;
|
|
const failures = [];
|
|
|
|
if (required && auditSkippedReason) {
|
|
failures.push({
|
|
kind: 'audit-skipped',
|
|
message: auditSkippedReason,
|
|
});
|
|
}
|
|
|
|
if (required && !auditSkippedReason && (auditedSlides.length === 0 || layoutAudits.length === 0)) {
|
|
failures.push({
|
|
kind: 'audit-empty',
|
|
message: 'Strict standalone verification requires rendered Playwright layout audits.',
|
|
});
|
|
}
|
|
|
|
for (const check of playwrightChecks) {
|
|
if (!check?.failed) {
|
|
continue;
|
|
}
|
|
failures.push({
|
|
kind: 'playwright-check-failed',
|
|
slide: check.slide,
|
|
message: (check.errors || []).join('; ') || 'Rendered slide check failed.',
|
|
});
|
|
}
|
|
|
|
for (const audit of layoutAudits) {
|
|
for (const finding of audit.findings || []) {
|
|
failures.push({
|
|
kind: finding.kind || 'layout-finding',
|
|
slide: audit.slide,
|
|
target: finding.target || null,
|
|
message: finding.message || 'Rendered layout audit reported a finding.',
|
|
recommendedPatch: finding.recommendedPatch || null,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
required,
|
|
passed: failures.length === 0,
|
|
auditedSlideCount: layoutAudits.length,
|
|
failureCount: failures.length,
|
|
failures,
|
|
};
|
|
}
|
|
|
|
function hasTableOrBodyTextSurface(audit) {
|
|
const elementKinds = audit?.elementKinds || [];
|
|
return elementKinds.includes('table') || elementKinds.includes('text');
|
|
}
|
|
|
|
function isTableBodyLayoutFinding(audit, finding) {
|
|
if (!finding) {
|
|
return false;
|
|
}
|
|
if (finding.target === 'table' || finding.target === 'text') {
|
|
return true;
|
|
}
|
|
if (finding.target === 'content' || finding.target === 'slide-root') {
|
|
return hasTableOrBodyTextSurface(audit);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function extractMermaidFenceBlocks(markdown) {
|
|
const lines = markdown.split(/\r?\n/);
|
|
const fences = [];
|
|
let activeFence = null;
|
|
|
|
for (const line of lines) {
|
|
if (!activeFence) {
|
|
const openingMatch = line.trim().match(/^(```+|~~~+)\s*mermaid(?:\s+\{[^}]+\})?\s*$/i);
|
|
if (openingMatch) {
|
|
activeFence = {
|
|
marker: openingMatch[1],
|
|
lines: [line],
|
|
};
|
|
}
|
|
continue;
|
|
}
|
|
|
|
activeFence.lines.push(line);
|
|
if (isClosingFenceLine(line, activeFence.marker)) {
|
|
fences.push(activeFence.lines.join('\n'));
|
|
activeFence = null;
|
|
}
|
|
}
|
|
|
|
return fences;
|
|
}
|
|
|
|
function isClosingFenceLine(line, openingMarker) {
|
|
const markerCharacter = openingMarker[0];
|
|
const markerCount = openingMarker.length;
|
|
const escapedMarker = markerCharacter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
return new RegExp(`^${escapedMarker}{${markerCount},}\\s*$`).test(line.trim());
|
|
}
|
|
|
|
async function collectRenderedSlideMeasurement(page, slide) {
|
|
return page.evaluate(slotZoneAttr => {
|
|
const toRect = rect => ({
|
|
left: rect.left,
|
|
top: rect.top,
|
|
right: rect.right,
|
|
bottom: rect.bottom,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
});
|
|
const toTextPreview = value => value
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
.slice(0, 160);
|
|
const overlaps = (a, b) => !(a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom);
|
|
const unionRects = rects => rects.reduce((acc, rect) => {
|
|
if (!acc) {
|
|
return { ...rect };
|
|
}
|
|
return {
|
|
left: Math.min(acc.left, rect.left),
|
|
top: Math.min(acc.top, rect.top),
|
|
right: Math.max(acc.right, rect.right),
|
|
bottom: Math.max(acc.bottom, rect.bottom),
|
|
width: Math.max(acc.right, rect.right) - Math.min(acc.left, rect.left),
|
|
height: Math.max(acc.bottom, rect.bottom) - Math.min(acc.top, rect.top),
|
|
};
|
|
}, null);
|
|
const collectTextContentRect = element => {
|
|
const textRects = [];
|
|
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
let node = walker.nextNode();
|
|
while (node) {
|
|
if ((node.textContent || '').trim().length > 0) {
|
|
const range = document.createRange();
|
|
range.selectNodeContents(node);
|
|
for (const rect of Array.from(range.getClientRects())) {
|
|
if (rect.width > 1 && rect.height > 1) {
|
|
textRects.push(toRect(rect));
|
|
}
|
|
}
|
|
range.detach();
|
|
}
|
|
node = walker.nextNode();
|
|
}
|
|
return unionRects(textRects);
|
|
};
|
|
const pickVisibleLargest = selector => Array.from(document.querySelectorAll(selector))
|
|
.filter(element => element instanceof Element)
|
|
.map(element => ({ element, rect: element.getBoundingClientRect(), style: window.getComputedStyle(element) }))
|
|
.filter(entry => entry.style.display !== 'none' && entry.style.visibility !== 'hidden' && Number(entry.style.opacity || '1') > 0 && entry.rect.width > 2 && entry.rect.height > 2)
|
|
.sort((left, right) => (right.rect.width * right.rect.height) - (left.rect.width * left.rect.height))[0]?.element ?? null;
|
|
|
|
const slideRoot = pickVisibleLargest('.slidev-page')
|
|
|| pickVisibleLargest('.slidev-layout')
|
|
|| pickVisibleLargest('.slidev-slide-content')
|
|
|| document.querySelector('#app');
|
|
if (!slideRoot) {
|
|
return {
|
|
slide: null,
|
|
slideRoot: null,
|
|
safeRect: null,
|
|
contentBounds: null,
|
|
pageScale: null,
|
|
elements: [],
|
|
slotZones: [],
|
|
errors: ['Slide root not found'],
|
|
};
|
|
}
|
|
|
|
const slideRootRect = toRect(slideRoot.getBoundingClientRect());
|
|
const safeInsetX = slideRootRect.width * 0.04;
|
|
const safeInsetY = slideRootRect.height * 0.06;
|
|
const safeRect = {
|
|
left: slideRootRect.left + safeInsetX,
|
|
top: slideRootRect.top + safeInsetY,
|
|
right: slideRootRect.right - safeInsetX,
|
|
bottom: slideRootRect.bottom - safeInsetY,
|
|
width: slideRootRect.width - safeInsetX * 2,
|
|
height: slideRootRect.height - safeInsetY * 2,
|
|
};
|
|
|
|
const selectors = [
|
|
['mermaid', '.mermaid, [class*="mermaid"]'],
|
|
['table', 'table'],
|
|
['code', 'pre, .shiki'],
|
|
['image', 'img, svg'],
|
|
['text', 'h1, h2, h3, h4, p, li, blockquote'],
|
|
['other', 'div, section, article, aside, span'],
|
|
];
|
|
const seen = new Set();
|
|
const measuredElements = [];
|
|
|
|
for (const [kind, selector] of selectors) {
|
|
for (const element of slideRoot.querySelectorAll(selector)) {
|
|
if (!(element instanceof Element) || seen.has(element)) {
|
|
continue;
|
|
}
|
|
seen.add(element);
|
|
|
|
const style = window.getComputedStyle(element);
|
|
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) {
|
|
continue;
|
|
}
|
|
|
|
const elementRect = toRect(element.getBoundingClientRect());
|
|
const rect = kind === 'text'
|
|
? collectTextContentRect(element) || elementRect
|
|
: elementRect;
|
|
const ownerElement = element.closest(`[${slotZoneAttr}]`);
|
|
if (rect.width < 2 || rect.height < 2 || (!ownerElement && !overlaps(rect, slideRootRect))) {
|
|
continue;
|
|
}
|
|
|
|
const textLength = (element.textContent || '').trim().length;
|
|
if (kind === 'other') {
|
|
const directTextLength = Array.from(element.childNodes)
|
|
.filter(node => node.nodeType === Node.TEXT_NODE)
|
|
.map(node => node.textContent || '')
|
|
.join(' ')
|
|
.trim()
|
|
.length;
|
|
if (directTextLength === 0 || textLength === 0) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
measuredElements.push({
|
|
ownerElement,
|
|
measured: {
|
|
kind,
|
|
selector,
|
|
slotZone: ownerElement?.getAttribute(slotZoneAttr) || undefined,
|
|
slotOwner: element.hasAttribute(slotZoneAttr),
|
|
textLength,
|
|
textPreview: textLength > 0 ? toTextPreview(element.textContent || '') : undefined,
|
|
scrollWidth: element.scrollWidth || rect.width,
|
|
scrollHeight: element.scrollHeight || rect.height,
|
|
clientWidth: element.clientWidth || rect.width,
|
|
clientHeight: element.clientHeight || rect.height,
|
|
rect,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
const elements = measuredElements.map(entry => entry.measured);
|
|
const slotZones = Array.from(slideRoot.querySelectorAll(`[${slotZoneAttr}]`))
|
|
.filter(element => element instanceof Element)
|
|
.map(ownerElement => {
|
|
const ownerRect = toRect(ownerElement.getBoundingClientRect());
|
|
if (ownerRect.width < 2 || ownerRect.height < 2 || !overlaps(ownerRect, slideRootRect)) {
|
|
return null;
|
|
}
|
|
|
|
const style = window.getComputedStyle(ownerElement);
|
|
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) {
|
|
return null;
|
|
}
|
|
|
|
const zoneName = ownerElement.getAttribute(slotZoneAttr);
|
|
if (!zoneName) {
|
|
return null;
|
|
}
|
|
|
|
const zoneElementRects = measuredElements
|
|
.filter(entry => entry.ownerElement === ownerElement && !entry.measured.slotOwner)
|
|
.map(entry => entry.measured.rect);
|
|
const contentBounds = unionRects(zoneElementRects.length > 0 ? zoneElementRects : [ownerRect]);
|
|
if (contentBounds) {
|
|
contentBounds.width = contentBounds.right - contentBounds.left;
|
|
contentBounds.height = contentBounds.bottom - contentBounds.top;
|
|
}
|
|
|
|
const textValue = (ownerElement.textContent || '').trim();
|
|
return {
|
|
name: zoneName,
|
|
textLength: textValue.length,
|
|
textPreview: textValue.length > 0 ? toTextPreview(textValue) : undefined,
|
|
ownerRect,
|
|
contentBounds,
|
|
scrollWidth: ownerElement.scrollWidth || ownerRect.width,
|
|
scrollHeight: ownerElement.scrollHeight || ownerRect.height,
|
|
clientWidth: ownerElement.clientWidth || ownerRect.width,
|
|
clientHeight: ownerElement.clientHeight || ownerRect.height,
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
|
|
const contentBounds = unionRects(elements.map(element => element.rect));
|
|
if (contentBounds) {
|
|
contentBounds.width = contentBounds.right - contentBounds.left;
|
|
contentBounds.height = contentBounds.bottom - contentBounds.top;
|
|
}
|
|
|
|
const zoomRaw = window.getComputedStyle(slideRoot).getPropertyValue('--slidev-slide-zoom-scale').trim()
|
|
|| window.getComputedStyle(slideRoot).scale
|
|
|| '1';
|
|
const pageScale = Number.parseFloat(zoomRaw);
|
|
|
|
return {
|
|
slideRoot: slideRootRect,
|
|
safeRect,
|
|
contentBounds,
|
|
pageScale: Number.isFinite(pageScale) ? pageScale : null,
|
|
elements,
|
|
slotZones,
|
|
errors: [],
|
|
};
|
|
}, 'data-notemd-slot-zone');
|
|
}
|
|
|
|
function configurePlaywrightBrowserPath(slideExport) {
|
|
if (process.env.PLAYWRIGHT_BROWSERS_PATH) {
|
|
return;
|
|
}
|
|
|
|
for (const home of slideExport.resolveWorkspaceHomeCandidates()) {
|
|
const candidate = path.join(home, '.cache', 'ms-playwright');
|
|
try {
|
|
if (fs.existsSync(candidate)) {
|
|
process.env.PLAYWRIGHT_BROWSERS_PATH = candidate;
|
|
return;
|
|
}
|
|
} catch {
|
|
// Try the next candidate.
|
|
}
|
|
}
|
|
}
|
|
|
|
async function runPlaywrightChecks(htmlPath, sampleSlides, writeScreenshots, slideExport) {
|
|
configurePlaywrightBrowserPath(slideExport);
|
|
const { chromium } = require('playwright');
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
|
const checks = [];
|
|
const layoutAudits = [];
|
|
let serverDirectory = null;
|
|
let baseUrl = null;
|
|
|
|
try {
|
|
if (isSlidevServerHtmlEntryPath(htmlPath)) {
|
|
serverDirectory = path.dirname(htmlPath);
|
|
const port = await slideExport.startLocalServer(serverDirectory);
|
|
baseUrl = `http://localhost:${port}/index.html`;
|
|
}
|
|
|
|
for (const slide of sampleSlides) {
|
|
const errors = [];
|
|
page.removeAllListeners('pageerror');
|
|
page.removeAllListeners('console');
|
|
const keepError = text => !/Wake Lock permission request denied/i.test(text);
|
|
page.on('pageerror', error => {
|
|
if (keepError(error.message)) errors.push(error.message);
|
|
});
|
|
page.on('console', message => {
|
|
if (message.type() === 'error' && keepError(message.text())) errors.push(message.text());
|
|
});
|
|
|
|
const targetUrl = baseUrl ? `${baseUrl}#/${slide}` : `file://${htmlPath}#/${slide}`;
|
|
await openSlideForAudit(page, targetUrl);
|
|
await page.waitForTimeout(1000);
|
|
const text = await page.locator('body').innerText({ timeout: 5000 }).catch(() => '');
|
|
const measurement = await collectRenderedSlideMeasurement(page, slide);
|
|
measurement.slide = slide;
|
|
const layoutAudit = slideExport.analyzeRenderedSlideMeasurement(measurement, LAYOUT_AUDIT_CONFIG);
|
|
let screenshotPath = null;
|
|
if (writeScreenshots) {
|
|
screenshotPath = path.join(path.dirname(htmlPath), `slide-${String(slide).padStart(2, '0')}-workflow.png`);
|
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
}
|
|
checks.push({
|
|
slide,
|
|
failed: errors.length > 0 || !text.includes(`${slide} /`),
|
|
errors,
|
|
screenshotPath,
|
|
preview: text.split('\n').filter(Boolean).slice(0, 4).join(' | '),
|
|
layoutFindingCount: layoutAudit.findings.length,
|
|
});
|
|
layoutAudits.push(layoutAudit);
|
|
}
|
|
} finally {
|
|
await browser.close();
|
|
if (serverDirectory) {
|
|
slideExport.stopLocalServer(serverDirectory);
|
|
}
|
|
}
|
|
|
|
return { checks, layoutAudits };
|
|
}
|
|
|
|
async function openSlideForAudit(page, targetUrl) {
|
|
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
if (typeof page.waitForLoadState === 'function') {
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => undefined);
|
|
}
|
|
if (typeof page.waitForFunction === 'function') {
|
|
await page.waitForFunction(
|
|
() => {
|
|
const root = document.querySelector('.slidev-page, .slidev-layout, .slidev-slide-content, #app');
|
|
return Boolean(root && (root.textContent || '').trim().length > 0);
|
|
},
|
|
null,
|
|
{ timeout: 15000 }
|
|
).catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
function resolveSlidesToAudit(sampleSlides, deckMarkdown, slideExport) {
|
|
const deckSlideCount = deckMarkdown ? slideExport.countSlideDeckSlides(deckMarkdown) : 0;
|
|
const allSlides = Array.from({ length: Math.max(deckSlideCount, 1) }, (_, index) => index + 1);
|
|
if (!Array.isArray(sampleSlides) || sampleSlides.length === 0) {
|
|
return allSlides;
|
|
}
|
|
|
|
const filteredSlides = sampleSlides.filter(slide => Number.isFinite(slide) && slide >= 1 && slide <= deckSlideCount);
|
|
return filteredSlides.length > 0 ? filteredSlides : allSlides;
|
|
}
|
|
|
|
function checkGitIgnoreStatus(pathsToCheck) {
|
|
const existingPaths = pathsToCheck.filter(Boolean).filter(filePath => fs.existsSync(filePath));
|
|
if (existingPaths.length === 0) {
|
|
return {
|
|
ignoredOutputs: [],
|
|
unignoredOutputs: [],
|
|
error: null,
|
|
};
|
|
}
|
|
|
|
const relativePaths = existingPaths.map(filePath => normalizeGitRelativePath(path.relative(process.cwd(), filePath)));
|
|
const result = childProcess.spawnSync('git', ['check-ignore', '-v', '--stdin'], {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
input: `${relativePaths.join('\n')}\n`,
|
|
});
|
|
|
|
const ignoredOutputs = result.stdout
|
|
.split(/\r?\n/)
|
|
.map(line => line.trim())
|
|
.filter(Boolean);
|
|
const ignoredRelativePaths = new Set(ignoredOutputs.map(line => {
|
|
const parts = line.split('\t');
|
|
return normalizeGitCheckIgnoreOutputPath((parts[parts.length - 1] || line).trim());
|
|
}));
|
|
|
|
return {
|
|
ignoredOutputs,
|
|
unignoredOutputs: relativePaths.filter(filePath => !ignoredRelativePaths.has(filePath)),
|
|
error: result.status && result.status > 1 ? (result.stderr || result.stdout || `git check-ignore exited ${result.status}`) : null,
|
|
};
|
|
}
|
|
|
|
function normalizeGitRelativePath(relativePath) {
|
|
return String(relativePath || '').replace(/\\/g, '/').replace(/\r$/, '');
|
|
}
|
|
|
|
function normalizeGitCheckIgnoreOutputPath(rawPath) {
|
|
const trimmed = String(rawPath || '').trim();
|
|
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
try {
|
|
return normalizeGitRelativePath(JSON.parse(trimmed));
|
|
} catch (_error) {
|
|
return normalizeGitRelativePath(trimmed.slice(1, -1));
|
|
}
|
|
}
|
|
return normalizeGitRelativePath(trimmed);
|
|
}
|
|
|
|
function injectMermaidPostFit(htmlPath, onProgress) {
|
|
const html = fs.readFileSync(htmlPath, 'utf8');
|
|
if (html.includes('data-notemd-mermaid-post-fit')) {
|
|
return;
|
|
}
|
|
const POST_FIT_SCRIPT = '<script data-notemd-mermaid-post-fit="1">' + fs.readFileSync(path.join(__dirname, '..', 'src', 'slideExport', 'mermaidPostFitScript.txt'), 'utf8').trim() + '</script>';
|
|
const lastBodyIdx = html.lastIndexOf("</body>");
|
|
const patched = lastBodyIdx >= 0
|
|
? html.slice(0, lastBodyIdx) + POST_FIT_SCRIPT + "\n" + html.slice(lastBodyIdx)
|
|
: html + "\n" + POST_FIT_SCRIPT + "\n";
|
|
fs.writeFileSync(htmlPath, patched, 'utf8');
|
|
onProgress?.('mermaid-fit', 'Injected post-fit script into standalone HTML');
|
|
}
|
|
|
|
function isSlidevServerHtmlEntryPath(htmlPath) {
|
|
return String(htmlPath || '').replace(/\\/g, '/').endsWith('/index.html');
|
|
}
|
|
|
|
function inspectPptx(pptxPath) {
|
|
if (!pptxPath || !fs.existsSync(pptxPath)) {
|
|
return null;
|
|
}
|
|
try {
|
|
const { strFromU8, unzipSync } = require('fflate');
|
|
const entries = unzipSync(new Uint8Array(fs.readFileSync(pptxPath)));
|
|
const names = Object.keys(entries);
|
|
const slideXmlPaths = names
|
|
.filter(name => /^ppt\/slides\/slide\d+\.xml$/.test(name))
|
|
.sort((left, right) => {
|
|
const leftNumber = Number(left.match(/slide(\d+)\.xml$/)?.[1] || 0);
|
|
const rightNumber = Number(right.match(/slide(\d+)\.xml$/)?.[1] || 0);
|
|
return leftNumber - rightNumber;
|
|
});
|
|
const slideTextRuns = slideXmlPaths.map(name => {
|
|
const xml = strFromU8(entries[name]);
|
|
const textRunXmlBlocks = xml.match(/<a:r\b[\s\S]*?<\/a:r>/g) || [];
|
|
const textRunAlphaValues = textRunXmlBlocks
|
|
.filter(runXml => /<a:t(?:\s[^>]*)?>/.test(runXml))
|
|
.flatMap(runXml => Array.from(runXml.matchAll(/<a:alpha\s+val="(\d+)"\s*\/>/g), match => Number(match[1])))
|
|
.filter(value => Number.isFinite(value));
|
|
const nonOpaqueTextRunCount = textRunXmlBlocks.filter(runXml =>
|
|
/<a:t(?:\s[^>]*)?>/.test(runXml)
|
|
&& Array.from(runXml.matchAll(/<a:alpha\s+val="(\d+)"\s*\/>/g), match => Number(match[1]))
|
|
.some(value => Number.isFinite(value) && value < 100000),
|
|
).length;
|
|
const transparentTextRunCount = textRunXmlBlocks.filter(runXml =>
|
|
/<a:t(?:\s[^>]*)?>/.test(runXml)
|
|
&& Array.from(runXml.matchAll(/<a:alpha\s+val="(\d+)"\s*\/>/g), match => Number(match[1]))
|
|
.some(value => Number.isFinite(value) && value <= 10000),
|
|
).length;
|
|
const textRunCount = (xml.match(/<a:t(?:\s[^>]*)?>/g) || []).length;
|
|
const pictureCount = (xml.match(/<p:pic>/g) || []).length;
|
|
const tableCount = (xml.match(/<a:tbl>/g) || []).length;
|
|
const shapeNames = Array.from(
|
|
xml.matchAll(/<p:cNvPr\b[^>]*\bname="([^"]*)"/g),
|
|
match => match[1],
|
|
);
|
|
const visibleNativeTextShapeCounts = shapeNames.reduce((counts, shapeName) => {
|
|
if (/^Visible Native Table Cell Overlay Text\b/.test(shapeName)) {
|
|
counts.tableCellOverlay += 1;
|
|
} else if (/^Visible Native Code Text\b/.test(shapeName)) {
|
|
counts.code += 1;
|
|
} else if (/^Visible Native SVG Text\b/.test(shapeName)) {
|
|
counts.svg += 1;
|
|
} else if (/^Visible Native Mermaid Text\b/.test(shapeName)) {
|
|
counts.mermaid += 1;
|
|
} else if (/^Visible Native Text\b/.test(shapeName)) {
|
|
counts.body += 1;
|
|
} else if (/^Visible Native Table\b/.test(shapeName)) {
|
|
counts.table += 1;
|
|
}
|
|
return counts;
|
|
}, {
|
|
body: 0,
|
|
code: 0,
|
|
svg: 0,
|
|
mermaid: 0,
|
|
tableCellOverlay: 0,
|
|
table: 0,
|
|
});
|
|
const nativeTableOverlayLeakCount = tableCount === 0 ? visibleNativeTextShapeCounts.tableCellOverlay : 0;
|
|
return {
|
|
path: name,
|
|
textRunCount,
|
|
pictureCount,
|
|
tableCount,
|
|
visibleNativeTextShapeCounts,
|
|
visibleNativeTextShapeCount:
|
|
visibleNativeTextShapeCounts.body +
|
|
visibleNativeTextShapeCounts.code +
|
|
visibleNativeTextShapeCounts.svg +
|
|
visibleNativeTextShapeCounts.mermaid +
|
|
visibleNativeTextShapeCounts.tableCellOverlay,
|
|
visibleNativeTableShapeCount: visibleNativeTextShapeCounts.table,
|
|
tableCellOverlayTextShapeCount: visibleNativeTextShapeCounts.tableCellOverlay,
|
|
nativeTableOverlayLeakCount,
|
|
nonOpaqueTextRunCount,
|
|
transparentTextRunCount,
|
|
lowOpacityTextRunCount: Math.max(0, nonOpaqueTextRunCount - transparentTextRunCount),
|
|
minimumTextRunAlpha: textRunAlphaValues.length > 0 ? Math.min(...textRunAlphaValues) : 100000,
|
|
hasEditableText: textRunCount > 0,
|
|
};
|
|
});
|
|
const textRunCount = slideTextRuns.reduce((total, slide) => total + slide.textRunCount, 0);
|
|
const nonOpaqueTextRunCount = slideTextRuns.reduce((total, slide) => total + slide.nonOpaqueTextRunCount, 0);
|
|
const transparentTextRunCount = slideTextRuns.reduce((total, slide) => total + slide.transparentTextRunCount, 0);
|
|
const nativeTableOverlayLeakCount = slideTextRuns.reduce((total, slide) => total + slide.nativeTableOverlayLeakCount, 0);
|
|
const slidesWithoutEditableText = slideTextRuns.filter(slide => !slide.hasEditableText).map(slide => slide.path);
|
|
const slidesWithNonOpaqueText = slideTextRuns.filter(slide => slide.nonOpaqueTextRunCount > 0).map(slide => slide.path);
|
|
const slidesWithNativeTableOverlayLeak = slideTextRuns
|
|
.filter(slide => slide.nativeTableOverlayLeakCount > 0)
|
|
.map(slide => slide.path);
|
|
return {
|
|
isZip: true,
|
|
entryCount: names.length,
|
|
slideCount: slideXmlPaths.length,
|
|
mediaCount: names.filter(name => /^ppt\/media\/image\d+\.(png|jpg|jpeg)$/.test(name)).length,
|
|
textRunCount,
|
|
pictureCount: slideTextRuns.reduce((total, slide) => total + slide.pictureCount, 0),
|
|
tableCount: slideTextRuns.reduce((total, slide) => total + slide.tableCount, 0),
|
|
visibleNativeTextShapeCount: slideTextRuns.reduce((total, slide) => total + slide.visibleNativeTextShapeCount, 0),
|
|
visibleNativeTableShapeCount: slideTextRuns.reduce((total, slide) => total + slide.visibleNativeTableShapeCount, 0),
|
|
tableCellOverlayTextShapeCount: slideTextRuns.reduce((total, slide) => total + slide.tableCellOverlayTextShapeCount, 0),
|
|
nonOpaqueTextRunCount,
|
|
transparentTextRunCount,
|
|
lowOpacityTextRunCount: slideTextRuns.reduce((total, slide) => total + slide.lowOpacityTextRunCount, 0),
|
|
nativeTableOverlayLeakCount,
|
|
minimumTextRunAlpha: slideTextRuns.length > 0
|
|
? Math.min(...slideTextRuns.map(slide => slide.minimumTextRunAlpha))
|
|
: 100000,
|
|
slidesWithoutEditableText,
|
|
slidesWithNonOpaqueText,
|
|
slidesWithNativeTableOverlayLeak,
|
|
editableTextAudit: {
|
|
passed: textRunCount > 0
|
|
&& slidesWithoutEditableText.length === 0
|
|
&& nonOpaqueTextRunCount === 0
|
|
&& nativeTableOverlayLeakCount === 0,
|
|
failures: [
|
|
...(textRunCount > 0 ? [] : ['pptx contains no editable text runs']),
|
|
...(slidesWithoutEditableText.length === 0
|
|
? []
|
|
: [`slides without editable text: ${slidesWithoutEditableText.join(', ')}`]),
|
|
...(nonOpaqueTextRunCount === 0
|
|
? []
|
|
: [`non-opaque text runs: ${nonOpaqueTextRunCount}`]),
|
|
...(nativeTableOverlayLeakCount === 0
|
|
? []
|
|
: [`native table overlay text leaks: ${nativeTableOverlayLeakCount}`]),
|
|
],
|
|
},
|
|
slideTextRuns,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
isZip: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
}
|
|
|
|
function resolvePptxVisualReferenceDirectory(vaultRoot, referenceDirectory) {
|
|
if (!referenceDirectory) {
|
|
return null;
|
|
}
|
|
if (path.isAbsolute(referenceDirectory)) {
|
|
return referenceDirectory;
|
|
}
|
|
const workspaceRelative = path.resolve(process.cwd(), referenceDirectory);
|
|
if (fs.existsSync(workspaceRelative)) {
|
|
return workspaceRelative;
|
|
}
|
|
return path.join(vaultRoot, referenceDirectory);
|
|
}
|
|
|
|
function resolvePptxVisualDiffDirectory(vaultRoot, configuredDirectory, pptxPath) {
|
|
if (configuredDirectory) {
|
|
return path.isAbsolute(configuredDirectory)
|
|
? configuredDirectory
|
|
: path.join(vaultRoot, configuredDirectory);
|
|
}
|
|
return path.join(path.dirname(pptxPath), `${path.basename(pptxPath, path.extname(pptxPath))}-pptx-visual-diff`);
|
|
}
|
|
|
|
function buildUnavailablePptxVisualDiff(outputDirectory, error, thresholds) {
|
|
return {
|
|
available: false,
|
|
outputDirectory,
|
|
error,
|
|
gate: {
|
|
passed: false,
|
|
failures: [error],
|
|
thresholds,
|
|
},
|
|
};
|
|
}
|
|
|
|
function usesVisibleNativeDefaultPptxTextLayer(pptxReport) {
|
|
return Boolean(
|
|
pptxReport
|
|
&& pptxReport.visibleTextLayer === 'native-text-and-background-image'
|
|
&& pptxReport.editableLayerRenderMode === 'visible-native-text',
|
|
);
|
|
}
|
|
|
|
function selectPptxHardGateReferenceSource(args, pptxReport) {
|
|
if (!args.requirePptxVisualMatch) {
|
|
return 'none';
|
|
}
|
|
if (args.pptxVisualReferenceDir) {
|
|
return 'configured-png-sequence';
|
|
}
|
|
if (usesVisibleNativeDefaultPptxTextLayer(pptxReport)) {
|
|
return 'pptx-rendered-html-reference';
|
|
}
|
|
return 'pptx-background-images';
|
|
}
|
|
|
|
function shouldRunRenderedHtmlPptxReferenceDiff(args, pptxReport) {
|
|
return Boolean(
|
|
args.pptxRenderedHtmlReferenceDiff
|
|
|| selectPptxHardGateReferenceSource(args, pptxReport) === 'pptx-rendered-html-reference',
|
|
);
|
|
}
|
|
|
|
function shouldRunBackgroundPptxVisualDiff(args, pptxReport) {
|
|
const hardGateReferenceSource = selectPptxHardGateReferenceSource(args, pptxReport);
|
|
if (args.pptxVisibleNativeExperiment || args.pptxVisualReferenceDir) {
|
|
return true;
|
|
}
|
|
return Boolean(args.pptxVisualDiff && hardGateReferenceSource !== 'pptx-rendered-html-reference');
|
|
}
|
|
|
|
function selectPptxVisualThresholdProfile(args, pptxReport, hardGateReferenceSource) {
|
|
const visibleNativeRenderedHtmlGate = hardGateReferenceSource === 'pptx-rendered-html-reference'
|
|
&& usesVisibleNativeDefaultPptxTextLayer(pptxReport);
|
|
const profileDefaults = visibleNativeRenderedHtmlGate
|
|
? VISIBLE_NATIVE_RENDERED_HTML_PPTX_VISUAL_THRESHOLDS
|
|
: DEFAULT_PPTX_VISUAL_THRESHOLDS;
|
|
return {
|
|
name: visibleNativeRenderedHtmlGate
|
|
? 'visible-native-rendered-html'
|
|
: 'default-raster',
|
|
explicit: {
|
|
maxRmse: args.pptxVisualMaxRmseExplicit,
|
|
meanRmse: args.pptxVisualMeanRmseExplicit,
|
|
},
|
|
thresholds: {
|
|
maxRmse: args.pptxVisualMaxRmseExplicit ? args.pptxVisualMaxRmse : profileDefaults.maxRmse,
|
|
meanRmse: args.pptxVisualMeanRmseExplicit ? args.pptxVisualMeanRmse : profileDefaults.meanRmse,
|
|
},
|
|
};
|
|
}
|
|
|
|
function runPptxVisualDiff({ pptxPath, outputDirectory, referenceDirectory, referenceSource, renderer, dpi, timeoutMs, thresholds }) {
|
|
try {
|
|
return buildPptxVisualDiff({
|
|
pptxPath,
|
|
outputDirectory,
|
|
referenceDirectory,
|
|
referenceSource,
|
|
renderer,
|
|
dpi,
|
|
timeoutMs,
|
|
thresholds,
|
|
});
|
|
} catch (error) {
|
|
return buildUnavailablePptxVisualDiff(
|
|
outputDirectory,
|
|
error instanceof Error ? error.message : String(error),
|
|
thresholds,
|
|
);
|
|
}
|
|
}
|
|
|
|
function extractDefaultPptxBackgroundReference(pptxPath, outputDirectory) {
|
|
try {
|
|
return extractPptxBackgroundImages({
|
|
pptxPath,
|
|
outputDirectory,
|
|
referenceDirectory: path.join(outputDirectory, 'pptx-background-reference'),
|
|
});
|
|
} catch (error) {
|
|
return {
|
|
source: 'pptx-background-images',
|
|
referenceDirectory: path.join(outputDirectory, 'pptx-background-reference'),
|
|
referenceImages: [],
|
|
error: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
}
|
|
|
|
function printProgress(enabled, phase, detail) {
|
|
if (!enabled) return;
|
|
console.log(detail ? `${phase}: ${detail}` : phase);
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const vaultRoot = path.resolve(args.vault);
|
|
const app = createApp(vaultRoot);
|
|
const sourceFile = createSourceFile(vaultRoot, args.source);
|
|
const config = createConfig(args);
|
|
const slideExport = await bundleSlideExportModules();
|
|
const progress = [];
|
|
const onProgress = (phase, detail) => {
|
|
progress.push({ phase, detail: detail || '' });
|
|
printProgress(!args.json, phase, detail);
|
|
};
|
|
|
|
printProgress(!args.json, 'slidev-workflow', `Vault: ${vaultRoot}`);
|
|
printProgress(!args.json, 'slidev-workflow', `Source: ${sourceFile.path}`);
|
|
|
|
const environment = await slideExport.probeEnvironment([vaultRoot]);
|
|
const slideSource = await slideExport.prepareSlidevExportSource(app, sourceFile, config, {}, onProgress);
|
|
let layoutConvergence = null;
|
|
if (args.playwright) {
|
|
layoutConvergence = await slideExport.convergeSlidevDeckLayout(app, slideSource, config, onProgress, {
|
|
sampleSlides: args.sampleSlides,
|
|
writeScreenshots: args.screenshots,
|
|
navigationTimeoutMs: args.timeoutMs,
|
|
auditConfig: LAYOUT_AUDIT_CONFIG,
|
|
});
|
|
}
|
|
|
|
let exportPath = layoutConvergence?.exportPath;
|
|
let htmlExport = layoutConvergence?.htmlExport ?? null;
|
|
let htmlExportHistory = layoutConvergence?.htmlExportHistory ?? [];
|
|
let pptxReportPath = null;
|
|
let pptxReport = null;
|
|
let htmlExportPathForPptx = null;
|
|
let visibleNativeExperimentResult = null;
|
|
if (args.format === 'pdf') {
|
|
exportPath = await slideExport.exportSlidevPdf(app, slideSource, config, onProgress);
|
|
} else if (args.format === 'png') {
|
|
exportPath = await slideExport.exportSlidevPng(app, slideSource, config, onProgress);
|
|
} else if (args.format === 'pptx') {
|
|
if (!exportPath) {
|
|
htmlExport = await slideExport.exportSlidevHtmlWithOutcome(app, slideSource, config, onProgress);
|
|
htmlExportHistory = [htmlExport];
|
|
exportPath = htmlExport.path;
|
|
}
|
|
htmlExportPathForPptx = exportPath;
|
|
const pptxResult = await slideExport.exportSlidevPptxFromHtml(app, slideSource, config, htmlExportPathForPptx, onProgress);
|
|
if (args.pptxVisibleNativeExperiment) {
|
|
visibleNativeExperimentResult = await slideExport.exportSlidevVisibleNativePptxExperimentFromHtml(
|
|
app,
|
|
slideSource,
|
|
config,
|
|
htmlExportPathForPptx,
|
|
onProgress,
|
|
);
|
|
}
|
|
exportPath = pptxResult.path;
|
|
htmlExport = layoutConvergence?.htmlExport ?? htmlExport;
|
|
htmlExportHistory = layoutConvergence?.htmlExportHistory ?? htmlExportHistory;
|
|
pptxReportPath = path.join(vaultRoot, pptxResult.reportPath);
|
|
pptxReport = pptxResult.report;
|
|
} else if (args.format === 'mp4') {
|
|
const pngDirectory = await slideExport.exportSlidevPng(app, slideSource, config, onProgress);
|
|
exportPath = await slideExport.exportVideoMp4(app, pngDirectory, slideSource.outputBasename, config, onProgress);
|
|
} else if (!exportPath) {
|
|
htmlExport = await slideExport.exportSlidevHtmlWithOutcome(app, slideSource, config, onProgress);
|
|
htmlExportHistory = [htmlExport];
|
|
exportPath = htmlExport.path;
|
|
}
|
|
|
|
// Inject mermaid post-fit script into standalone HTML after full layout settle.
|
|
if (args.format === 'html' && args.htmlMode === 'standalone') {
|
|
const htmlPath = path.join(vaultRoot, exportPath);
|
|
injectMermaidPostFit(htmlPath, onProgress);
|
|
}
|
|
|
|
|
|
let absoluteExportPath = path.join(vaultRoot, exportPath);
|
|
const absoluteDeckPath = slideSource.preparedDeckPath ? path.join(vaultRoot, slideSource.preparedDeckPath) : null;
|
|
let deckSummary = collectDeckSummary(absoluteDeckPath);
|
|
const mermaidSourcePreservation = collectMermaidSourcePreservation(path.join(vaultRoot, sourceFile.path), absoluteDeckPath);
|
|
let playwrightChecks = layoutConvergence?.checks ?? [];
|
|
let layoutAudits = layoutConvergence?.layoutAudits ?? [];
|
|
let layoutAuditSummary = layoutConvergence?.layoutAuditSummary ?? slideExport.summarizeLayoutAudits([], 0);
|
|
let layoutPatchAttempts = layoutConvergence?.layoutPatchAttempts ?? [];
|
|
let auditedSlides = layoutConvergence?.auditedSlides ?? [];
|
|
const renderedLayoutGate = buildRenderedLayoutGate({
|
|
required: args.requireNativeStandalone,
|
|
auditedSlides,
|
|
layoutAudits,
|
|
playwrightChecks,
|
|
auditSkippedReason: args.playwright
|
|
? (layoutConvergence?.auditSkippedReason || null)
|
|
: 'Playwright disabled by --no-playwright.',
|
|
});
|
|
const pptxInspection = args.format === 'pptx' ? inspectPptx(absoluteExportPath) : null;
|
|
const visibleNativeExperimentPptxPath = visibleNativeExperimentResult
|
|
? path.join(vaultRoot, visibleNativeExperimentResult.path)
|
|
: null;
|
|
const visibleNativeExperimentReportPath = visibleNativeExperimentResult
|
|
? path.join(vaultRoot, visibleNativeExperimentResult.reportPath)
|
|
: null;
|
|
const visibleNativeExperimentInspection = visibleNativeExperimentPptxPath
|
|
? inspectPptx(visibleNativeExperimentPptxPath)
|
|
: null;
|
|
let pptxReferencePngDirectory = null;
|
|
let pptxVisualDiff = null;
|
|
let pptxRenderedHtmlReference = null;
|
|
let pptxRenderedHtmlReferenceVisualDiff = null;
|
|
let visibleNativeExperimentVisualDiff = null;
|
|
let visibleNativeExperimentVisualReference = null;
|
|
const pptxHardGateReferenceSource = args.format === 'pptx'
|
|
? selectPptxHardGateReferenceSource(args, pptxReport)
|
|
: 'none';
|
|
const pptxVisualThresholdProfile = selectPptxVisualThresholdProfile(args, pptxReport, pptxHardGateReferenceSource);
|
|
const pptxVisualThresholds = pptxVisualThresholdProfile.thresholds;
|
|
const needsDefaultPptxVisualDiff = args.format === 'pptx' && shouldRunBackgroundPptxVisualDiff(args, pptxReport);
|
|
const needsRenderedHtmlPptxReferenceDiff = args.format === 'pptx'
|
|
&& shouldRunRenderedHtmlPptxReferenceDiff(args, pptxReport);
|
|
if (pptxHardGateReferenceSource === 'pptx-rendered-html-reference') {
|
|
onProgress(
|
|
'pptx-visual-diff',
|
|
'Visible-native PPTX default detected; using same rendered HTML as the hard visual-match reference.',
|
|
);
|
|
}
|
|
if (needsDefaultPptxVisualDiff) {
|
|
const visualDiffDirectory = resolvePptxVisualDiffDirectory(vaultRoot, args.pptxVisualDiffDir, absoluteExportPath);
|
|
const visualReferenceDirectory = resolvePptxVisualReferenceDirectory(vaultRoot, args.pptxVisualReferenceDir);
|
|
onProgress(
|
|
'pptx-visual-diff',
|
|
visualReferenceDirectory
|
|
? `Rendering PPTX back to PNG and comparing pages with external PNG reference: ${visualReferenceDirectory}`
|
|
: 'Rendering PPTX back to PNG and comparing pages with frozen background references...',
|
|
);
|
|
pptxVisualDiff = runPptxVisualDiff({
|
|
pptxPath: absoluteExportPath,
|
|
outputDirectory: visualDiffDirectory,
|
|
referenceDirectory: visualReferenceDirectory,
|
|
renderer: args.pptxVisualRenderer,
|
|
dpi: args.pptxVisualDpi,
|
|
timeoutMs: args.timeoutMs,
|
|
thresholds: pptxVisualThresholds,
|
|
});
|
|
if (pptxVisualDiff.error) {
|
|
onProgress('pptx-visual-diff', `PPTX visual diff failed: ${pptxVisualDiff.error}`);
|
|
} else {
|
|
onProgress(
|
|
'pptx-visual-diff',
|
|
pptxVisualDiff.gate?.passed
|
|
? 'PPTX visual diff is within thresholds.'
|
|
: `PPTX visual diff exceeded thresholds: ${(pptxVisualDiff.gate?.failures || []).join('; ')}`,
|
|
);
|
|
}
|
|
pptxReferencePngDirectory = pptxVisualDiff.reference?.referenceDirectory || null;
|
|
}
|
|
|
|
if (needsRenderedHtmlPptxReferenceDiff) {
|
|
if (!htmlExportPathForPptx) {
|
|
throw new Error('Rendered-HTML PPTX reference diff requires the HTML export path used for PPTX.');
|
|
}
|
|
onProgress(
|
|
'pptx-rendered-html-reference',
|
|
'Capturing PNG reference from the same rendered HTML used by PPTX export...',
|
|
);
|
|
pptxRenderedHtmlReference = await slideExport.exportSlidevPptxRenderedHtmlReferencePngSequence(
|
|
app,
|
|
slideSource,
|
|
config,
|
|
htmlExportPathForPptx,
|
|
onProgress,
|
|
);
|
|
const renderedHtmlReferenceDiffDirectory = path.join(
|
|
path.dirname(absoluteExportPath),
|
|
`${path.basename(absoluteExportPath, path.extname(absoluteExportPath))}-rendered-html-reference-diff`,
|
|
);
|
|
onProgress(
|
|
'pptx-rendered-html-reference',
|
|
`Rendering PPTX back to PNG and comparing against same rendered HTML reference: ${pptxRenderedHtmlReference.absolutePath}`,
|
|
);
|
|
pptxRenderedHtmlReferenceVisualDiff = runPptxVisualDiff({
|
|
pptxPath: absoluteExportPath,
|
|
outputDirectory: renderedHtmlReferenceDiffDirectory,
|
|
referenceDirectory: pptxRenderedHtmlReference.absolutePath,
|
|
referenceSource: 'pptx-rendered-html-reference',
|
|
renderer: args.pptxVisualRenderer,
|
|
dpi: args.pptxVisualDpi,
|
|
timeoutMs: args.timeoutMs,
|
|
thresholds: pptxVisualThresholds,
|
|
});
|
|
if (pptxRenderedHtmlReferenceVisualDiff.error) {
|
|
onProgress('pptx-rendered-html-reference', `Rendered-HTML reference diff failed: ${pptxRenderedHtmlReferenceVisualDiff.error}`);
|
|
} else {
|
|
onProgress(
|
|
'pptx-rendered-html-reference',
|
|
pptxRenderedHtmlReferenceVisualDiff.gate?.passed
|
|
? 'Rendered-HTML reference diff is within thresholds.'
|
|
: `Rendered-HTML reference diff exceeded thresholds: ${(pptxRenderedHtmlReferenceVisualDiff.gate?.failures || []).join('; ')}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (visibleNativeExperimentPptxPath) {
|
|
const defaultReferenceSource = pptxVisualDiff?.reference?.source === 'pptx-background-images'
|
|
? pptxVisualDiff.reference
|
|
: extractDefaultPptxBackgroundReference(
|
|
absoluteExportPath,
|
|
path.join(path.dirname(absoluteExportPath), `${path.basename(absoluteExportPath, path.extname(absoluteExportPath))}-default-frozen-reference`),
|
|
);
|
|
visibleNativeExperimentVisualReference = {
|
|
source: 'default-frozen-background',
|
|
referenceDirectory: defaultReferenceSource.referenceDirectory,
|
|
referenceImageCount: defaultReferenceSource.referenceImages?.length || 0,
|
|
error: defaultReferenceSource.error || null,
|
|
};
|
|
const visualDiffDirectory = path.join(
|
|
path.dirname(visibleNativeExperimentPptxPath),
|
|
`${path.basename(visibleNativeExperimentPptxPath, path.extname(visibleNativeExperimentPptxPath))}-pptx-visual-diff`,
|
|
);
|
|
if (visibleNativeExperimentVisualReference.error || !visibleNativeExperimentVisualReference.referenceImageCount) {
|
|
visibleNativeExperimentVisualDiff = buildUnavailablePptxVisualDiff(
|
|
visualDiffDirectory,
|
|
visibleNativeExperimentVisualReference.error || 'Default PPTX frozen-background reference contains no images.',
|
|
pptxVisualThresholds,
|
|
);
|
|
onProgress('pptx-visible-native-experiment', `Visible-native visual diff failed: ${visibleNativeExperimentVisualDiff.error}`);
|
|
} else {
|
|
onProgress(
|
|
'pptx-visible-native-experiment',
|
|
`Rendering visible-native PPTX back to PNG and comparing against default frozen background: ${visibleNativeExperimentVisualReference.referenceDirectory}`,
|
|
);
|
|
visibleNativeExperimentVisualDiff = runPptxVisualDiff({
|
|
pptxPath: visibleNativeExperimentPptxPath,
|
|
outputDirectory: visualDiffDirectory,
|
|
referenceDirectory: visibleNativeExperimentVisualReference.referenceDirectory,
|
|
renderer: args.pptxVisualRenderer,
|
|
dpi: args.pptxVisualDpi,
|
|
timeoutMs: args.timeoutMs,
|
|
thresholds: pptxVisualThresholds,
|
|
});
|
|
if (visibleNativeExperimentVisualDiff.error) {
|
|
onProgress('pptx-visible-native-experiment', `Visible-native visual diff failed: ${visibleNativeExperimentVisualDiff.error}`);
|
|
} else {
|
|
onProgress(
|
|
'pptx-visible-native-experiment',
|
|
visibleNativeExperimentVisualDiff.gate?.passed
|
|
? 'Visible-native visual diff is within thresholds.'
|
|
: `Visible-native visual diff exceeded thresholds: ${(visibleNativeExperimentVisualDiff.gate?.failures || []).join('; ')}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const gitIgnoreStatus = checkGitIgnoreStatus([
|
|
absoluteDeckPath,
|
|
absoluteExportPath,
|
|
pptxReportPath,
|
|
visibleNativeExperimentPptxPath,
|
|
visibleNativeExperimentReportPath,
|
|
pptxReferencePngDirectory,
|
|
pptxVisualDiff?.outputDirectory,
|
|
pptxRenderedHtmlReference?.absolutePath,
|
|
pptxRenderedHtmlReferenceVisualDiff?.outputDirectory,
|
|
visibleNativeExperimentVisualReference?.referenceDirectory,
|
|
visibleNativeExperimentVisualDiff?.outputDirectory,
|
|
htmlExport?.standaloneAttempt?.preservedFailurePath
|
|
? path.join(vaultRoot, htmlExport.standaloneAttempt.preservedFailurePath)
|
|
: null,
|
|
htmlExport?.standaloneAttempt?.outputPath
|
|
? path.join(vaultRoot, htmlExport.standaloneAttempt.outputPath)
|
|
: null,
|
|
htmlExport?.fallbackPath
|
|
? path.join(vaultRoot, htmlExport.fallbackPath)
|
|
: null,
|
|
...playwrightChecks.map(check => check.screenshotPath),
|
|
]);
|
|
const ignoredOutputs = gitIgnoreStatus.ignoredOutputs;
|
|
const unignoredOutputs = gitIgnoreStatus.unignoredOutputs;
|
|
const hasLayoutFailures = layoutAudits.some(audit => audit.findings.length > 0);
|
|
const tableBodyLayoutGate = buildTableBodyLayoutGate(layoutAudits);
|
|
const nativeStandalonePassed = htmlExport?.actualMode === 'standalone'
|
|
&& htmlExport?.standaloneAttempt?.accepted === true
|
|
&& fs.existsSync(path.join(vaultRoot, htmlExport.path));
|
|
const standaloneGate = {
|
|
required: args.requireNativeStandalone,
|
|
passed: !args.requireNativeStandalone || nativeStandalonePassed,
|
|
reason: args.requireNativeStandalone && !nativeStandalonePassed
|
|
? `Expected native standalone HTML but actual mode was ${htmlExport?.actualMode || 'unavailable'}`
|
|
: null,
|
|
};
|
|
const pptxVisualDiffExecutionPassed = !needsDefaultPptxVisualDiff || Boolean(pptxVisualDiff?.available && !pptxVisualDiff?.error);
|
|
const pptxHardGateVisualDiff = pptxHardGateReferenceSource === 'pptx-rendered-html-reference'
|
|
? pptxRenderedHtmlReferenceVisualDiff
|
|
: pptxVisualDiff;
|
|
const pptxVisualGate = {
|
|
required: args.requirePptxVisualMatch,
|
|
referenceSource: pptxHardGateReferenceSource === 'none'
|
|
? (pptxVisualDiff?.reference?.source || null)
|
|
: (pptxHardGateReferenceSource === 'pptx-rendered-html-reference'
|
|
? 'pptx-rendered-html-reference'
|
|
: (pptxHardGateVisualDiff?.reference?.source || pptxHardGateReferenceSource)),
|
|
observedPassed: Boolean(pptxHardGateVisualDiff?.gate?.passed),
|
|
passed: !args.requirePptxVisualMatch || Boolean(pptxHardGateVisualDiff?.gate?.passed),
|
|
failures: pptxHardGateVisualDiff?.gate?.failures || [],
|
|
thresholds: pptxVisualThresholds,
|
|
thresholdProfile: pptxVisualThresholdProfile.name,
|
|
thresholdOverrides: pptxVisualThresholdProfile.explicit,
|
|
};
|
|
const pptxRenderedHtmlReferenceDiffExecutionPassed = !needsRenderedHtmlPptxReferenceDiff
|
|
|| Boolean(pptxRenderedHtmlReferenceVisualDiff?.available && !pptxRenderedHtmlReferenceVisualDiff?.error);
|
|
const pptxRenderedHtmlReferenceGate = {
|
|
required: args.requirePptxRenderedHtmlReferenceMatch,
|
|
observedPassed: Boolean(pptxRenderedHtmlReferenceVisualDiff?.gate?.passed),
|
|
passed: !args.requirePptxRenderedHtmlReferenceMatch || Boolean(pptxRenderedHtmlReferenceVisualDiff?.gate?.passed),
|
|
failures: pptxRenderedHtmlReferenceVisualDiff?.gate?.failures || [],
|
|
thresholds: pptxVisualThresholds,
|
|
thresholdProfile: pptxVisualThresholdProfile.name,
|
|
thresholdOverrides: pptxVisualThresholdProfile.explicit,
|
|
};
|
|
const visibleNativeExperimentVisualDiffExecutionPassed = !args.pptxVisibleNativeExperiment
|
|
|| Boolean(visibleNativeExperimentVisualDiff?.available && !visibleNativeExperimentVisualDiff?.error);
|
|
const visibleNativeExperimentGate = {
|
|
required: args.requirePptxVisibleNativeMatch,
|
|
observedPassed: Boolean(visibleNativeExperimentVisualDiff?.gate?.passed),
|
|
passed: !args.requirePptxVisibleNativeMatch || Boolean(visibleNativeExperimentVisualDiff?.gate?.passed),
|
|
failures: visibleNativeExperimentVisualDiff?.gate?.failures || [],
|
|
thresholds: pptxVisualThresholds,
|
|
thresholdProfile: pptxVisualThresholdProfile.name,
|
|
thresholdOverrides: pptxVisualThresholdProfile.explicit,
|
|
};
|
|
const pptxVisibleNativeExperiment = visibleNativeExperimentResult
|
|
? {
|
|
path: visibleNativeExperimentPptxPath,
|
|
reportPath: visibleNativeExperimentReportPath,
|
|
bytes: visibleNativeExperimentPptxPath && fs.existsSync(visibleNativeExperimentPptxPath) && fs.statSync(visibleNativeExperimentPptxPath).isFile()
|
|
? fs.statSync(visibleNativeExperimentPptxPath).size
|
|
: null,
|
|
inspection: visibleNativeExperimentInspection,
|
|
sidecarReport: visibleNativeExperimentResult.report,
|
|
visualReference: visibleNativeExperimentVisualReference,
|
|
visualDiff: visibleNativeExperimentVisualDiff,
|
|
gate: visibleNativeExperimentGate,
|
|
}
|
|
: null;
|
|
const visibleNativeExperimentPackagePassed = !args.pptxVisibleNativeExperiment
|
|
|| Boolean(visibleNativeExperimentInspection?.isZip && visibleNativeExperimentInspection.textRunCount > 0);
|
|
const visibleNativeExperimentOutputPassed = !args.pptxVisibleNativeExperiment
|
|
|| Boolean(visibleNativeExperimentPptxPath && fs.existsSync(visibleNativeExperimentPptxPath));
|
|
|
|
const pptxVisualDiffGatePassed = pptxVisualGate.passed;
|
|
const pptxRenderedHtmlReferenceGatePassed = pptxRenderedHtmlReferenceGate.passed;
|
|
const visibleNativeExperimentGatePassed = visibleNativeExperimentGate.passed;
|
|
|
|
const report = {
|
|
ok: playwrightChecks.every(check => !check.failed)
|
|
&& environment.capabilities[args.format] === true
|
|
&& fs.existsSync(absoluteExportPath)
|
|
&& !gitIgnoreStatus.error
|
|
&& unignoredOutputs.length === 0
|
|
&& (args.format !== 'pptx' || Boolean(pptxInspection?.isZip && pptxInspection.editableTextAudit?.passed))
|
|
&& visibleNativeExperimentOutputPassed
|
|
&& visibleNativeExperimentPackagePassed
|
|
&& pptxVisualDiffExecutionPassed
|
|
&& pptxRenderedHtmlReferenceDiffExecutionPassed
|
|
&& visibleNativeExperimentVisualDiffExecutionPassed
|
|
&& pptxVisualDiffGatePassed
|
|
&& pptxRenderedHtmlReferenceGatePassed
|
|
&& visibleNativeExperimentGatePassed
|
|
&& renderedLayoutGate.passed
|
|
&& tableBodyLayoutGate.passed
|
|
&& !hasLayoutFailures
|
|
&& (!deckSummary || (!deckSummary.containsKnownStaleText && !deckSummary.containsMissingTheme))
|
|
&& (!mermaidSourcePreservation || mermaidSourcePreservation.passed)
|
|
&& standaloneGate.passed,
|
|
source: {
|
|
vaultRoot,
|
|
sourcePath: sourceFile.path,
|
|
},
|
|
environment: {
|
|
node: environment.node,
|
|
slidev: environment.slidev,
|
|
playwright: environment.playwright,
|
|
ffmpeg: environment.ffmpeg,
|
|
capabilities: environment.capabilities,
|
|
},
|
|
slideSource: {
|
|
inputFilePath: slideSource.inputFilePath,
|
|
outputBasename: slideSource.outputBasename,
|
|
preparedDeckPath: slideSource.preparedDeckPath || null,
|
|
skillRootPath: slideSource.skillRootPath || null,
|
|
skillReferenceCount: slideSource.skillReferencePaths?.length || 0,
|
|
},
|
|
output: {
|
|
format: args.format,
|
|
path: absoluteExportPath,
|
|
bytes: fs.existsSync(absoluteExportPath) && fs.statSync(absoluteExportPath).isFile()
|
|
? fs.statSync(absoluteExportPath).size
|
|
: null,
|
|
pptxReportPath,
|
|
pptxReport,
|
|
},
|
|
pptxInspection,
|
|
pptxVisualDiff,
|
|
pptxVisualGate,
|
|
pptxRenderedHtmlReference,
|
|
pptxRenderedHtmlReferenceVisualDiff,
|
|
pptxRenderedHtmlReferenceGate,
|
|
pptxVisibleNativeExperiment,
|
|
htmlExport,
|
|
htmlExportHistory,
|
|
standaloneGate,
|
|
renderedLayoutGate,
|
|
deck: deckSummary,
|
|
mermaidSourcePreservation,
|
|
tableBodyLayoutGate,
|
|
playwright: playwrightChecks,
|
|
playwrightSlides: auditedSlides,
|
|
layoutAudit: layoutAudits,
|
|
layoutAuditSummary,
|
|
layoutPatchAttempts,
|
|
ignoredOutputs,
|
|
unignoredOutputs,
|
|
gitIgnoreCheckError: gitIgnoreStatus.error,
|
|
progress,
|
|
};
|
|
|
|
if (args.json) {
|
|
console.log(JSON.stringify(report, null, 2));
|
|
} else {
|
|
console.log('\n=== Slidev workflow verification report ===');
|
|
console.log(JSON.stringify(report, null, 2));
|
|
}
|
|
|
|
if (!report.ok) {
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
if (typeof esbuild.stop === 'function') {
|
|
await esbuild.stop();
|
|
}
|
|
|
|
process.exit(process.exitCode || 0);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main().catch(error => {
|
|
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
|
process.exit(1);
|
|
});
|
|
} else {
|
|
module.exports = {
|
|
buildRenderedLayoutGate,
|
|
buildTableBodyLayoutGate,
|
|
inspectPptx,
|
|
selectPptxVisualThresholdProfile,
|
|
selectPptxHardGateReferenceSource,
|
|
shouldRunBackgroundPptxVisualDiff,
|
|
shouldRunRenderedHtmlPptxReferenceDiff,
|
|
usesVisibleNativeDefaultPptxTextLayer,
|
|
normalizeGitCheckIgnoreOutputPath,
|
|
normalizeGitRelativePath,
|
|
};
|
|
}
|