harden presenter runtime lifecycle

This commit is contained in:
郭浩 committed 2026-08-07 11:08:33 +08:00
1 parent 472b7f06fc
commit 3c46a4ea14
9 files changed
+154 -32

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env node
import {readFileSync} from 'node:fs';
import {dirname,resolve} from 'node:path';
import {fileURLToPath} from 'node:url';
const root=resolve(dirname(fileURLToPath(import.meta.url)),'..');
const files=['assets/template.html','assets/template-swiss.html'];
const blocks=[
['presenter CSS','/* ============ 演讲者模式 ============ */','/* ============ /演讲者模式 ============ */'],
['presenter JavaScript','/* =============== 演讲者模式 / 观众屏同步 =============== */','/* =============== /演讲者模式 / 观众屏同步 =============== */'],
];
function extract(source,label,startMarker,endMarker,file){
const start=source.indexOf(startMarker),end=source.indexOf(endMarker,start+startMarker.length);
if(start<0||end<0)throw new Error(`${file}: missing ${label} boundary marker.`);
return source.slice(start,end+endMarker.length);
}
function firstDifferentLine(a,b){
const left=a.split('\n'),right=b.split('\n'),count=Math.max(left.length,right.length);
for(let i=0;i<count;i++)if(left[i]!==right[i])return i+1;
return 0;
}
const sources=files.map(file=>[file,readFileSync(resolve(root,file),'utf8')]);
let failed=false;
for(const [label,start,end] of blocks){
const extracted=sources.map(([file,source])=>[file,extract(source,label,start,end,file)]);
const [baseFile,base]=extracted[0];
let blockFailed=false;
for(const [file,value] of extracted.slice(1)){
if(value===base)continue;
failed=true;
blockFailed=true;
console.error(`ERROR ${label} drift: ${baseFile} and ${file} first differ at block line ${firstDifferentLine(base,value)}.`);
}
if(!blockFailed)console.log(`PASS ${label} is byte-identical across ${files.length} templates.`);
}
process.exit(failed?1:0);
+7 -3
View File
@@ -39,6 +39,7 @@ function extractArray(name){
const slideTags=[...source.matchAll(/<section\b(?=[^>]*\bclass="[^"]*\bslide\b[^"]*")[^>]*>/g)].map(m=>m[0]);
const slideIds=slideTags.map((tag,i)=>tag.match(/\bdata-slide-id="([^"]+)"/)?.[1]||'');
if(!slideIds.length)warnings.push('No slides found; only the reusable presenter runtime can be validated.');
slideIds.forEach((id,i)=>{
if(!id)errors.push(`Slide ${i+1}: missing data-slide-id.`);
@@ -86,16 +87,15 @@ else{
const runtimeChecks=[
['right-bottom presenter entry','id="ppt-presenter-btn"'],
['presenter shell','id="ppt-presenter"'],
['vertical preview stack','grid-template-rows:minmax(0,1fr) clamp(132px,22vh,210px)'],
['16:9 preview viewport','class="ppt-frame-viewport"'],
['proportional preview fitter','fitPresenterFrames'],
['exact 16:9 fitting','availableHeight*(16/9)'],
['overview control','id="ppt-grid"'],
['embedded presenter overview','id="ppt-presenter-overview"'],
['first-page control','id="ppt-first"'],
['last/restart control','id="ppt-last"'],
['audience status','id="ppt-sync"'],
['audience recovery','id="ppt-reopen"'],
['audience shutdown handling','showAudienceEnded'],
['direct window sync','postMessage('],
['BroadcastChannel fallback','BroadcastChannel'],
['storage fallback','__guizangPptSync'],
@@ -110,12 +110,16 @@ const runtimeChecks=[
['layout presets','data-layout-choice'],
['capsule auto-advance switch','class="ppt-switch"'],
['styled interval stepper','class="ppt-stepper"'],
['reload-free preview navigation','preview-goto'],
['shortcut help','openShortcuts'],
];
for(const [label,needle]of runtimeChecks)if(!html.includes(needle))errors.push(`Presenter runtime missing ${label}.`);
if(!/\.ppt-preview-stack\s*\{[^}]*grid-template-rows\s*:/s.test(html))errors.push('Presenter preview stack must define vertical grid rows.');
if(!/\.ppt-frame\s*\{[^}]*aspect-ratio\s*:\s*16\s*\/\s*9/s.test(html))errors.push('Presenter preview frames must declare a 16:9 aspect ratio.');
if(!/fitPresenterFrames[\s\S]*?16\s*\/\s*9/.test(html))errors.push('Presenter preview fitter must preserve the 16:9 ratio.');
if(!/id="ppt-timer-toggle"[^>]*>开始计时<\/button>/.test(html))errors.push('Presenter timer must use the explicit label "开始计时".');
if(!/id="ppt-timer-reset"[^>]*>重置计时<\/button>/.test(html))errors.push('Presenter timer reset must use the explicit label "重置计时".');
if(!html.includes("totalUsed?'继续计时':'开始计时'"))errors.push('Presenter timer resume state must use the explicit label "继续计时".');
if(!html.includes('继续计时'))errors.push('Presenter timer resume state must use the explicit label "继续计时".');
if(/\.ppt-preview-stack\{[^}]*grid-template-columns/.test(html))errors.push('Presenter previews must stack vertically; remove grid-template-columns from .ppt-preview-stack.');
const timedNotes=speakerNotes.filter(n=>Number.isFinite(Number(n?.minutes))&&Number(n.minutes)>0);