@liustack/pptfast 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -25
- package/README.zh-CN.md +30 -24
- package/dist/chunk-4W2YZPJJ.js +564 -0
- package/dist/chunk-4W2YZPJJ.js.map +1 -0
- package/dist/{chunk-ZXWCOWJI.js → chunk-7N4HGSMW.js} +2820 -1426
- package/dist/chunk-7N4HGSMW.js.map +1 -0
- package/dist/chunk-HFRNKYZ6.js +54 -0
- package/dist/chunk-HFRNKYZ6.js.map +1 -0
- package/dist/chunk-L524YK63.js +19 -0
- package/dist/chunk-L524YK63.js.map +1 -0
- package/dist/cli.js +330 -69
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +947 -200
- package/dist/index.js +42 -34
- package/dist/node.d.ts +3 -2
- package/dist/node.js +2 -2
- package/dist/pixel-audit-4DXKLFBV.js +176 -0
- package/dist/pixel-audit-4DXKLFBV.js.map +1 -0
- package/dist/registry-V079Jkry.d.ts +40 -0
- package/package.json +5 -3
- package/dist/chunk-G76EVNVT.js +0 -14
- package/dist/chunk-G76EVNVT.js.map +0 -1
- package/dist/chunk-SROEW6BH.js +0 -34
- package/dist/chunk-SROEW6BH.js.map +0 -1
- package/dist/chunk-ZXWCOWJI.js.map +0 -1
- package/dist/registry-CYKxZ0-U.d.ts +0 -11
package/dist/cli.js
CHANGED
|
@@ -1,35 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
PptxIRV3Schema,
|
|
4
|
+
VERSION,
|
|
5
|
+
assembleDeck,
|
|
6
|
+
disassembleDeck,
|
|
7
|
+
formatInvalidSpecError,
|
|
8
|
+
migrateDeckPlanToSpec,
|
|
9
|
+
migrateIrV3ToV4,
|
|
10
|
+
resolveSpecThemeId,
|
|
11
|
+
specJsonSchema,
|
|
12
|
+
validateSpec
|
|
13
|
+
} from "./chunk-4W2YZPJJ.js";
|
|
14
|
+
import {
|
|
15
|
+
installNodePlatform
|
|
16
|
+
} from "./chunk-HFRNKYZ6.js";
|
|
2
17
|
import {
|
|
3
18
|
AUDIENCE_VALUES,
|
|
4
|
-
|
|
5
|
-
|
|
19
|
+
NARRATIVE_PRESETS,
|
|
20
|
+
PACING_BUDGETS,
|
|
6
21
|
PptfastError,
|
|
7
|
-
|
|
22
|
+
STRATEGY_DEFINITIONS,
|
|
8
23
|
StyleOverrideSchema,
|
|
9
|
-
VERSION,
|
|
10
|
-
assembleDeck,
|
|
11
24
|
auditDeck,
|
|
12
|
-
disassembleDeck,
|
|
13
|
-
formatInvalidPlanError,
|
|
14
25
|
formatIssues,
|
|
15
26
|
generatePptx,
|
|
16
27
|
getInstalledThemeIds,
|
|
17
28
|
irJsonSchema,
|
|
18
29
|
listThemes,
|
|
19
|
-
planJsonSchema,
|
|
20
30
|
renderSlideSvg,
|
|
21
|
-
|
|
22
|
-
resolveScenario,
|
|
31
|
+
resolveNarrative,
|
|
23
32
|
styleJsonSchema,
|
|
24
|
-
validateIr
|
|
25
|
-
|
|
26
|
-
} from "./chunk-ZXWCOWJI.js";
|
|
27
|
-
import {
|
|
28
|
-
installNodePlatform
|
|
29
|
-
} from "./chunk-SROEW6BH.js";
|
|
33
|
+
validateIr
|
|
34
|
+
} from "./chunk-7N4HGSMW.js";
|
|
30
35
|
import {
|
|
31
36
|
getPlatform
|
|
32
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-L524YK63.js";
|
|
33
38
|
|
|
34
39
|
// src/cli.ts
|
|
35
40
|
import { Command } from "commander";
|
|
@@ -163,6 +168,7 @@ async function resolveLocalAssets(ir, baseDir) {
|
|
|
163
168
|
|
|
164
169
|
// src/cli/deck-dir.ts
|
|
165
170
|
var PLAN_FILENAME = "deck.plan.json";
|
|
171
|
+
var SPEC_FILENAME = "deck.spec.json";
|
|
166
172
|
var PAGES_DIRNAME = "pages";
|
|
167
173
|
var ASSETS_DIRNAME = "assets";
|
|
168
174
|
function assertSafeFileSegment(id, context) {
|
|
@@ -202,31 +208,43 @@ async function resolveDeckTarget(arg, config, cwd = process.cwd()) {
|
|
|
202
208
|
}
|
|
203
209
|
function expectedLayoutHint() {
|
|
204
210
|
const rows = [
|
|
205
|
-
[
|
|
211
|
+
[SPEC_FILENAME, "the locked spec (see `pptfast spec validate`)"],
|
|
206
212
|
[`${PAGES_DIRNAME}/<page-id>.json`, "one file per filled page (missing pages become placeholders)"],
|
|
207
213
|
[`${ASSETS_DIRNAME}/`, "optional local images"]
|
|
208
214
|
];
|
|
209
215
|
const width = Math.max(...rows.map(([name]) => name.length)) + 2;
|
|
210
216
|
return rows.map(([name, desc]) => ` ${name.padEnd(width)}${desc}`).join("\n");
|
|
211
217
|
}
|
|
212
|
-
async function
|
|
218
|
+
async function readSpecFile(dir) {
|
|
219
|
+
const specPath = join3(dir, SPEC_FILENAME);
|
|
213
220
|
const planPath = join3(dir, PLAN_FILENAME);
|
|
221
|
+
const [specExists, planExists] = await Promise.all([pathExists(specPath), pathExists(planPath)]);
|
|
222
|
+
if (specExists && planExists) {
|
|
223
|
+
throw new PptfastError(
|
|
224
|
+
`both ${SPEC_FILENAME} and ${PLAN_FILENAME} exist in ${dir} \u2014 ambiguous, refusing to guess which one wins. Delete ${PLAN_FILENAME} once you have confirmed ${SPEC_FILENAME} is correct (\`pptfast migrate\` never deletes the source file it read)`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (!specExists && planExists) {
|
|
228
|
+
throw new PptfastError(
|
|
229
|
+
`${dir} has ${PLAN_FILENAME} but no ${SPEC_FILENAME} \u2014 deck project directories now use ${SPEC_FILENAME}. Run \`pptfast migrate ${dir} -o ${dir}\` to convert it`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
214
232
|
let text;
|
|
215
233
|
try {
|
|
216
|
-
text = await readFile3(
|
|
234
|
+
text = await readFile3(specPath, "utf8");
|
|
217
235
|
} catch (e) {
|
|
218
236
|
if (e.code === "ENOENT") {
|
|
219
237
|
throw new PptfastError(
|
|
220
|
-
`no ${
|
|
238
|
+
`no ${SPEC_FILENAME} in ${dir} \u2014 expected a deck project directory:
|
|
221
239
|
${expectedLayoutHint()}`
|
|
222
240
|
);
|
|
223
241
|
}
|
|
224
|
-
throw new PptfastError(`cannot read
|
|
242
|
+
throw new PptfastError(`cannot read spec file: ${specPath}`);
|
|
225
243
|
}
|
|
226
244
|
try {
|
|
227
245
|
return JSON.parse(text);
|
|
228
246
|
} catch (e) {
|
|
229
|
-
throw new PptfastError(`
|
|
247
|
+
throw new PptfastError(`spec file ${specPath} is not valid JSON: ${e.message}`);
|
|
230
248
|
}
|
|
231
249
|
}
|
|
232
250
|
async function readPages(dir) {
|
|
@@ -273,9 +291,9 @@ async function scanAssets(dir) {
|
|
|
273
291
|
}
|
|
274
292
|
async function readDeckDir(dir) {
|
|
275
293
|
const deckDir = resolve4(dir);
|
|
276
|
-
const
|
|
294
|
+
const spec2 = await readSpecFile(deckDir);
|
|
277
295
|
const pages = await readPages(deckDir);
|
|
278
|
-
const { ir, generatedSeed, materializedLayoutCount } = assembleDeck(
|
|
296
|
+
const { ir, generatedSeed, materializedLayoutCount } = assembleDeck(spec2, pages);
|
|
279
297
|
const images = await scanAssets(deckDir);
|
|
280
298
|
const merged = { ...ir, assets: { images: { ...ir.assets.images, ...images } } };
|
|
281
299
|
return { ir: merged, generatedSeed, materializedLayoutCount, deckDir };
|
|
@@ -324,10 +342,18 @@ async function writeOneAsset(id, src, assetsDir, sourceBaseDir) {
|
|
|
324
342
|
function escapeHtml(s) {
|
|
325
343
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
326
344
|
}
|
|
327
|
-
function
|
|
345
|
+
function embedJson(value) {
|
|
346
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
347
|
+
}
|
|
348
|
+
function findingBadge(count, className) {
|
|
349
|
+
if (count === 0) return "";
|
|
350
|
+
return `<div class="${className}" aria-hidden="true">${count}</div>`;
|
|
351
|
+
}
|
|
352
|
+
function slideNode(slide, findingCount) {
|
|
328
353
|
const idAttr = slide.id !== void 0 ? ` data-id="${escapeHtml(slide.id)}"` : "";
|
|
329
354
|
const badge = slide.placeholder ? `<div class="pf-badge" aria-hidden="true">unfilled</div>` : "";
|
|
330
|
-
|
|
355
|
+
const fBadge = findingBadge(findingCount, "pf-finding-badge");
|
|
356
|
+
return `<div class="pf-slide" id="pf-slide-${slide.index}" data-index="${slide.index}"${idAttr}>${badge}${fBadge}${slide.svg}</div>`;
|
|
331
357
|
}
|
|
332
358
|
function thumbDescription(slide) {
|
|
333
359
|
const parts = [`slide ${slide.index + 1}`, slide.type];
|
|
@@ -343,10 +369,15 @@ function counterText(slide, total) {
|
|
|
343
369
|
const idPart = slide.id !== void 0 ? ` \xB7 ${slide.id}` : "";
|
|
344
370
|
return escapeHtml(`${slide.index + 1} / ${total}${idPart}`);
|
|
345
371
|
}
|
|
346
|
-
function thumbButton(slide, isActive, slotContent) {
|
|
372
|
+
function thumbButton(slide, isActive, slotContent, findingCount) {
|
|
347
373
|
const description = thumbDescription(slide);
|
|
348
374
|
const badge = slide.placeholder ? `<span class="pf-thumb-badge" aria-hidden="true">unfilled</span>` : "";
|
|
349
|
-
|
|
375
|
+
const fBadge = findingBadge(findingCount, "pf-thumb-finding-badge");
|
|
376
|
+
return `<button type="button" class="pf-thumb${isActive ? " pf-thumb-active" : ""}" id="pf-thumb-${slide.index}" data-index="${slide.index}" title="${description}" aria-label="${description}"><span class="pf-thumb-slot" id="pf-slot-${slide.index}">${slotContent}</span><span class="pf-thumb-label">${positionLabel(slide)}</span>${badge}${fBadge}</button>`;
|
|
377
|
+
}
|
|
378
|
+
function findingPanelEntry(f) {
|
|
379
|
+
const idPart = f.slideId !== void 0 ? ` \xB7 ${escapeHtml(f.slideId)}` : "";
|
|
380
|
+
return `<button type="button" class="pf-finding" data-page-index="${f.page - 1}"><span class="pf-finding-loc">page ${f.page}${idPart}</span><span class="pf-finding-code">[${escapeHtml(f.code)}]</span> <span class="pf-finding-msg">${escapeHtml(f.message)}</span></button>`;
|
|
350
381
|
}
|
|
351
382
|
var CSS = `
|
|
352
383
|
:root{color-scheme:light}
|
|
@@ -356,11 +387,31 @@ body{display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystem
|
|
|
356
387
|
header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:#fff;border-bottom:1px solid #ddd;font-size:14px;flex:0 0 auto}
|
|
357
388
|
#pf-title{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
358
389
|
#pf-counter{font-variant-numeric:tabular-nums;color:#555;white-space:nowrap}
|
|
359
|
-
#pf-
|
|
390
|
+
#pf-audit-note{color:#b45309;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
391
|
+
#pf-export-btn{font:inherit;font-size:13px;padding:6px 10px;border:1px solid #2563eb;background:#2563eb;color:#fff;border-radius:4px;cursor:pointer;white-space:nowrap;flex:0 0 auto}
|
|
392
|
+
#pf-export-btn:hover{background:#1d4ed8}
|
|
393
|
+
#pf-stage-wrap{flex:1 1 auto;min-height:0;display:flex;align-items:center;justify-content:center;gap:16px;padding:16px}
|
|
360
394
|
#pf-stage{position:relative;background:#000;box-shadow:0 2px 16px rgba(0,0,0,.15);aspect-ratio:16/9;width:min(100%,calc((100vh - 190px) * 16 / 9));max-height:100%}
|
|
361
395
|
#pf-stage,.pf-thumb-slot{position:relative}
|
|
362
396
|
.pf-slide{position:absolute;inset:0}
|
|
363
397
|
.pf-slide svg{display:block;width:100%;height:100%}
|
|
398
|
+
#pf-side{flex:0 0 260px;align-self:stretch;overflow-y:auto;background:#fff;border:1px solid #ddd;border-radius:6px;padding:12px;font-size:13px}
|
|
399
|
+
#pf-side h2{margin:0 0 8px;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:#666}
|
|
400
|
+
#pf-side section+section{margin-top:16px;padding-top:16px;border-top:1px solid #eee}
|
|
401
|
+
#pf-audit-checks{margin:0 0 12px;font-size:12px;color:#555}
|
|
402
|
+
.pf-finding{display:block;width:100%;text-align:left;background:#fff;border:1px solid #eee;border-radius:4px;padding:6px 8px;margin-bottom:6px;cursor:pointer;font:inherit}
|
|
403
|
+
.pf-finding:hover{border-color:#93c5fd}
|
|
404
|
+
.pf-finding-loc{display:block;font-size:11px;color:#888}
|
|
405
|
+
.pf-finding-code{display:inline-block;font-size:11px;font-weight:700;color:#b91c1c}
|
|
406
|
+
.pf-finding-msg{font-size:12px;color:#333}
|
|
407
|
+
#pf-annotate-current-label{font-size:11px;color:#888;margin-bottom:6px}
|
|
408
|
+
#pf-annotate-list{list-style:none;margin:0 0 8px;padding:0}
|
|
409
|
+
.pf-annotate-item{display:flex;justify-content:space-between;gap:6px;align-items:flex-start;padding:4px 0;border-bottom:1px solid #f0f0f0;font-size:12px}
|
|
410
|
+
.pf-annotate-remove{border:none;background:none;color:#999;cursor:pointer;font-size:14px;line-height:1;padding:0 2px}
|
|
411
|
+
.pf-annotate-remove:hover{color:#dc2626}
|
|
412
|
+
#pf-annotate-input{width:100%;box-sizing:border-box;font:inherit;font-size:12px;padding:6px;border:1px solid #ddd;border-radius:4px;resize:vertical}
|
|
413
|
+
#pf-annotate-add{font:inherit;font-size:12px;margin-top:6px;width:100%;padding:6px 10px;border:1px solid #2563eb;background:#2563eb;color:#fff;border-radius:4px;cursor:pointer}
|
|
414
|
+
#pf-annotate-add:hover{background:#1d4ed8}
|
|
364
415
|
#pf-filmstrip{display:flex;gap:8px;padding:10px 16px;overflow-x:auto;background:#fff;border-top:1px solid #ddd;flex:0 0 auto}
|
|
365
416
|
.pf-thumb{flex:0 0 auto;width:160px;padding:0;margin:0;border:2px solid transparent;background:#eee;cursor:pointer;border-radius:6px;overflow:hidden;position:relative;font:inherit;text-align:left}
|
|
366
417
|
.pf-thumb:hover{border-color:#93c5fd}
|
|
@@ -368,6 +419,7 @@ header{display:flex;align-items:center;justify-content:space-between;gap:12px;pa
|
|
|
368
419
|
.pf-thumb-slot{display:block;width:100%;aspect-ratio:16/9;background:#ddd}
|
|
369
420
|
.pf-thumb-label{display:block;font-size:11px;line-height:1.4;padding:3px 6px;color:#444;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
370
421
|
.pf-badge,.pf-thumb-badge{position:absolute;top:4px;right:4px;background:#d97706;color:#fff;font-size:10px;font-weight:700;letter-spacing:.03em;padding:2px 6px;border-radius:3px;text-transform:uppercase;z-index:2;pointer-events:none}
|
|
422
|
+
.pf-finding-badge,.pf-thumb-finding-badge{position:absolute;top:4px;left:4px;background:#dc2626;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:3px;z-index:2;pointer-events:none}
|
|
371
423
|
`.trim();
|
|
372
424
|
var JS = `
|
|
373
425
|
(function () {
|
|
@@ -411,6 +463,7 @@ var JS = `
|
|
|
411
463
|
nextThumb.classList.add('pf-thumb-active')
|
|
412
464
|
current = i
|
|
413
465
|
updateCounter(i)
|
|
466
|
+
renderAnnotations()
|
|
414
467
|
}
|
|
415
468
|
|
|
416
469
|
thumbs.forEach(function (t) {
|
|
@@ -427,15 +480,129 @@ var JS = `
|
|
|
427
480
|
activate(parseInt(thumbs[pos - 1].getAttribute('data-index'), 10))
|
|
428
481
|
}
|
|
429
482
|
})
|
|
483
|
+
|
|
484
|
+
// ---- audit findings panel: click a finding, jump to its page (same
|
|
485
|
+
// activate() every thumbnail click already uses) ----
|
|
486
|
+
Array.prototype.slice.call(document.querySelectorAll('.pf-finding')).forEach(function (b) {
|
|
487
|
+
b.addEventListener('click', function () {
|
|
488
|
+
activate(parseInt(b.getAttribute('data-page-index'), 10))
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
// ---- annotations: in-memory only, keyed by the slide's 0-based array
|
|
493
|
+
// index (current) \u2014 never by pageId, since an object key coerces every
|
|
494
|
+
// key to a string and would silently collide a numeric page-fallback
|
|
495
|
+
// pageId with a same-looking slide id (or lose the numeric/string type
|
|
496
|
+
// distinction the exported JSON needs); pageIdFor() below resolves the
|
|
497
|
+
// real pageId fresh from the DOM only at render/export time. ----
|
|
498
|
+
var annotations = {}
|
|
499
|
+
var annotateList = document.getElementById('pf-annotate-list')
|
|
500
|
+
var annotateInput = document.getElementById('pf-annotate-input')
|
|
501
|
+
var annotateLabel = document.getElementById('pf-annotate-current-label')
|
|
502
|
+
var annotateAdd = document.getElementById('pf-annotate-add')
|
|
503
|
+
var exportBtn = document.getElementById('pf-export-btn')
|
|
504
|
+
|
|
505
|
+
// Slide id when the active slide has one, else its 1-based page number \u2014
|
|
506
|
+
// mirrors AuditFinding.page's own "1-based page number when there is no
|
|
507
|
+
// slide id" convention (../svg/audit/deck-audit.ts) rather than this
|
|
508
|
+
// file's internal 0-based data-index, so a revision-request.json's
|
|
509
|
+
// pageId lines up with what pptfast audit/validate already print.
|
|
510
|
+
function pageIdFor(i) {
|
|
511
|
+
var el = slideEl(i)
|
|
512
|
+
var id = el ? el.getAttribute('data-id') : null
|
|
513
|
+
return id !== null ? id : thumbPos(i) + 1
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function renderAnnotations() {
|
|
517
|
+
var pid = pageIdFor(current)
|
|
518
|
+
annotateLabel.textContent = 'page ' + (thumbPos(current) + 1) + (typeof pid === 'string' ? ' \xB7 ' + pid : '')
|
|
519
|
+
var list = annotations[current] || []
|
|
520
|
+
annotateList.innerHTML = ''
|
|
521
|
+
list.forEach(function (text, idx) {
|
|
522
|
+
var li = document.createElement('li')
|
|
523
|
+
li.className = 'pf-annotate-item'
|
|
524
|
+
var span = document.createElement('span')
|
|
525
|
+
span.textContent = text
|
|
526
|
+
var rm = document.createElement('button')
|
|
527
|
+
rm.type = 'button'
|
|
528
|
+
rm.className = 'pf-annotate-remove'
|
|
529
|
+
rm.setAttribute('aria-label', 'remove annotation')
|
|
530
|
+
rm.textContent = '\\u00d7'
|
|
531
|
+
rm.addEventListener('click', function () {
|
|
532
|
+
list.splice(idx, 1)
|
|
533
|
+
renderAnnotations()
|
|
534
|
+
})
|
|
535
|
+
li.appendChild(span)
|
|
536
|
+
li.appendChild(rm)
|
|
537
|
+
annotateList.appendChild(li)
|
|
538
|
+
})
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
annotateAdd.addEventListener('click', function () {
|
|
542
|
+
var text = annotateInput.value.trim()
|
|
543
|
+
if (!text) return
|
|
544
|
+
if (!annotations[current]) annotations[current] = []
|
|
545
|
+
annotations[current].push(text)
|
|
546
|
+
annotateInput.value = ''
|
|
547
|
+
renderAnnotations()
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
// "Export revision requests": preview.html stays read-only end to end \u2014
|
|
551
|
+
// this never writes back into the deck itself, only produces a JSON file
|
|
552
|
+
// of requests for an agent/human to route through pages/*.json (see
|
|
553
|
+
// skills/pptfast/SKILL.md's phase-6 revision-request handling). Zero
|
|
554
|
+
// network/storage \u2014 an in-memory Blob + a synthetic <a download> click,
|
|
555
|
+
// the standard browser-only download pattern.
|
|
556
|
+
exportBtn.addEventListener('click', function () {
|
|
557
|
+
var requests = []
|
|
558
|
+
Object.keys(annotations).forEach(function (key) {
|
|
559
|
+
var i = parseInt(key, 10)
|
|
560
|
+
var pid = pageIdFor(i)
|
|
561
|
+
annotations[i].forEach(function (text) {
|
|
562
|
+
requests.push({ pageId: pid, annotation: text, createdAt: new Date().toISOString() })
|
|
563
|
+
})
|
|
564
|
+
})
|
|
565
|
+
var deckTitle = document.getElementById('pf-title').textContent
|
|
566
|
+
var payload = { version: '1', deck: deckTitle, requests: requests }
|
|
567
|
+
var blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
|
|
568
|
+
var url = URL.createObjectURL(blob)
|
|
569
|
+
var a = document.createElement('a')
|
|
570
|
+
a.href = url
|
|
571
|
+
a.download = 'revision-request.json'
|
|
572
|
+
document.body.appendChild(a)
|
|
573
|
+
a.click()
|
|
574
|
+
document.body.removeChild(a)
|
|
575
|
+
URL.revokeObjectURL(url)
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
renderAnnotations()
|
|
430
579
|
})()
|
|
431
580
|
`.trim();
|
|
581
|
+
var ANNOTATE_PANEL = `<section id="pf-annotate-panel">
|
|
582
|
+
<h2>Annotations</h2>
|
|
583
|
+
<div id="pf-annotate-current-label"></div>
|
|
584
|
+
<ul id="pf-annotate-list"></ul>
|
|
585
|
+
<textarea id="pf-annotate-input" rows="3" placeholder="Add a note for this page\u2026"></textarea>
|
|
586
|
+
<button type="button" id="pf-annotate-add">Add annotation</button>
|
|
587
|
+
</section>`;
|
|
432
588
|
function buildPreviewHtml(input) {
|
|
433
|
-
const { title, slides } = input;
|
|
589
|
+
const { title, slides, findings = [], auditNote, checks } = input;
|
|
434
590
|
const total = slides.length;
|
|
435
591
|
const escapedTitle = escapeHtml(title);
|
|
436
|
-
const
|
|
437
|
-
const
|
|
592
|
+
const findingsByPage = /* @__PURE__ */ new Map();
|
|
593
|
+
for (const f of findings) {
|
|
594
|
+
const list = findingsByPage.get(f.page);
|
|
595
|
+
if (list) list.push(f);
|
|
596
|
+
else findingsByPage.set(f.page, [f]);
|
|
597
|
+
}
|
|
598
|
+
const countFor = (slide) => findingsByPage.get(slide.index + 1)?.length ?? 0;
|
|
599
|
+
const stageSlide = total > 0 ? slideNode(slides[0], countFor(slides[0])) : "";
|
|
600
|
+
const thumbs = slides.map((s, i) => thumbButton(s, i === 0, i === 0 ? "" : slideNode(s, countFor(s)), countFor(s))).join("");
|
|
438
601
|
const initialCounter = total > 0 ? counterText(slides[0], total) : "0 / 0";
|
|
602
|
+
const auditPanel = findings.length > 0 ? `<section id="pf-audit-panel"><h2>Audit findings (${findings.length})</h2><div id="pf-audit-list">${findings.map(findingPanelEntry).join("")}</div></section>` : "";
|
|
603
|
+
const findingsDataScript = findings.length > 0 ? `<script type="application/json" id="pf-audit-findings">${embedJson(findings)}</script>` : "";
|
|
604
|
+
const auditNoteHtml = auditNote !== void 0 ? `<span id="pf-audit-note">${escapeHtml(auditNote)}</span>` : "";
|
|
605
|
+
const checksLine = checks !== void 0 ? `<div id="pf-audit-checks">audit checks: svg ${checks.svg} \xB7 pixels ${checks.pixels}</div>` : "";
|
|
439
606
|
return `<!doctype html>
|
|
440
607
|
<html lang="en">
|
|
441
608
|
<head>
|
|
@@ -448,9 +615,12 @@ function buildPreviewHtml(input) {
|
|
|
448
615
|
<header>
|
|
449
616
|
<span id="pf-title">${escapedTitle}</span>
|
|
450
617
|
<span id="pf-counter">${initialCounter}</span>
|
|
618
|
+
${auditNoteHtml}
|
|
619
|
+
<button type="button" id="pf-export-btn">Export revision requests</button>
|
|
451
620
|
</header>
|
|
452
|
-
<div id="pf-stage-wrap"><div id="pf-stage">${stageSlide}</div></div>
|
|
621
|
+
<div id="pf-stage-wrap"><div id="pf-stage">${stageSlide}</div><aside id="pf-side">${checksLine}${auditPanel}${ANNOTATE_PANEL}</aside></div>
|
|
453
622
|
<nav id="pf-filmstrip" aria-label="slides">${thumbs}</nav>
|
|
623
|
+
${findingsDataScript}
|
|
454
624
|
<script>${JS}</script>
|
|
455
625
|
</body>
|
|
456
626
|
</html>
|
|
@@ -569,6 +739,9 @@ function formatAuditReport(report, ir) {
|
|
|
569
739
|
lines.push(
|
|
570
740
|
`audited ${report.pagesAudited} page${report.pagesAudited === 1 ? "" : "s"}, ${report.pagesSkipped} skipped, ${report.findings.length} finding${report.findings.length === 1 ? "" : "s"}`
|
|
571
741
|
);
|
|
742
|
+
if (report.checks.pixels === "completed") {
|
|
743
|
+
lines.push("pixel-contrast check: completed");
|
|
744
|
+
}
|
|
572
745
|
const note = placeholderNote(ir);
|
|
573
746
|
if (note) lines.push(note);
|
|
574
747
|
return lines.join("\n");
|
|
@@ -586,23 +759,23 @@ ${formatIssues(v.errors)}`
|
|
|
586
759
|
);
|
|
587
760
|
}
|
|
588
761
|
await resolveLocalAssets(v.ir, baseDir);
|
|
589
|
-
const report = auditDeck(v.ir);
|
|
762
|
+
const report = opts.pixels ? await auditDeck(v.ir, { pixels: true }) : auditDeck(v.ir);
|
|
590
763
|
const hasFindings = report.findings.length > 0;
|
|
591
764
|
const output = opts.json ? JSON.stringify(report, null, 2) : formatAuditReport(report, v.ir);
|
|
592
765
|
return { output, hasFindings };
|
|
593
766
|
}
|
|
594
|
-
async function
|
|
595
|
-
const raw = await loadIrFile(
|
|
596
|
-
const v =
|
|
767
|
+
async function runSpecValidate(specPath) {
|
|
768
|
+
const raw = await loadIrFile(specPath, "spec");
|
|
769
|
+
const v = validateSpec(raw);
|
|
597
770
|
if (!v.ok) {
|
|
598
|
-
throw new PptfastError(
|
|
771
|
+
throw new PptfastError(formatInvalidSpecError(v.errors));
|
|
599
772
|
}
|
|
600
|
-
const
|
|
601
|
-
const axes =
|
|
602
|
-
return `OK \u2014 ${
|
|
773
|
+
const spec2 = v.spec;
|
|
774
|
+
const axes = resolveNarrative(spec2.narrative);
|
|
775
|
+
return `OK \u2014 ${spec2.pages.length} pages, narrative ${axes.strategy}/${axes.pacing}/${axes.audience}, theme "${resolveSpecThemeId(spec2)}"`;
|
|
603
776
|
}
|
|
604
777
|
function runSchema(mode) {
|
|
605
|
-
const schema = mode === "style" ? styleJsonSchema() : mode === "
|
|
778
|
+
const schema = mode === "style" ? styleJsonSchema() : mode === "spec" ? specJsonSchema() : irJsonSchema();
|
|
606
779
|
return JSON.stringify(schema, null, 2);
|
|
607
780
|
}
|
|
608
781
|
function runThemes(asJson) {
|
|
@@ -610,22 +783,22 @@ function runThemes(asJson) {
|
|
|
610
783
|
if (asJson) return JSON.stringify(themes, null, 2);
|
|
611
784
|
return themes.map((t) => `${t.id.padEnd(12)} ${t.label}`).join("\n");
|
|
612
785
|
}
|
|
613
|
-
function
|
|
786
|
+
function runNarratives(asJson) {
|
|
614
787
|
if (asJson) {
|
|
615
788
|
return JSON.stringify(
|
|
616
789
|
{
|
|
617
|
-
presets:
|
|
618
|
-
|
|
619
|
-
|
|
790
|
+
presets: NARRATIVE_PRESETS,
|
|
791
|
+
strategies: STRATEGY_DEFINITIONS,
|
|
792
|
+
pacings: PACING_BUDGETS,
|
|
620
793
|
audiences: AUDIENCE_VALUES
|
|
621
794
|
},
|
|
622
795
|
null,
|
|
623
796
|
2
|
|
624
797
|
);
|
|
625
798
|
}
|
|
626
|
-
const rows = Object.values(
|
|
799
|
+
const rows = Object.values(NARRATIVE_PRESETS).map((p) => ({
|
|
627
800
|
id: p.id,
|
|
628
|
-
axes: `${p.axes.
|
|
801
|
+
axes: `${p.axes.strategy}/${p.axes.pacing}/${p.axes.audience}`,
|
|
629
802
|
themes: p.themeRecommendations.join(", ")
|
|
630
803
|
}));
|
|
631
804
|
const idWidth = Math.max(...rows.map((r) => r.id.length));
|
|
@@ -674,6 +847,9 @@ ${formatIssues(v.errors)}`);
|
|
|
674
847
|
if (aliasNote) notes.push(aliasNote);
|
|
675
848
|
if (opts.htmlOut) {
|
|
676
849
|
const htmlPath = join4(outDir, "preview.html");
|
|
850
|
+
const hasPlaceholder = ir.slides.some((slide) => slide.placeholder);
|
|
851
|
+
const auditReport = hasPlaceholder ? void 0 : auditDeck(ir);
|
|
852
|
+
const auditFindings = auditReport?.findings ?? [];
|
|
677
853
|
const html = buildPreviewHtml({
|
|
678
854
|
title: ir.filename,
|
|
679
855
|
slides: ir.slides.map((slide, i) => ({
|
|
@@ -682,10 +858,16 @@ ${formatIssues(v.errors)}`);
|
|
|
682
858
|
type: slide.type,
|
|
683
859
|
svg: svgs[i],
|
|
684
860
|
placeholder: slide.placeholder
|
|
685
|
-
}))
|
|
861
|
+
})),
|
|
862
|
+
findings: auditFindings.map((f) => ({ page: f.page, slideId: f.slideId, code: f.code, message: f.message })),
|
|
863
|
+
auditNote: hasPlaceholder ? "audit overlay skipped \u2014 deck has unfilled placeholder pages; fill every page and re-run `pptfast preview --html` to see audit findings" : void 0,
|
|
864
|
+
checks: auditReport?.checks
|
|
686
865
|
});
|
|
687
866
|
await writeFile2(htmlPath, html);
|
|
688
867
|
notes.push(`note: wrote self-contained preview to ${htmlPath}`);
|
|
868
|
+
if (auditFindings.length > 0) {
|
|
869
|
+
notes.push(`note: audit found ${auditFindings.length} finding${auditFindings.length === 1 ? "" : "s"} \u2014 see preview.html`);
|
|
870
|
+
}
|
|
689
871
|
}
|
|
690
872
|
return notes.length > 0 ? `${ok}
|
|
691
873
|
${notes.join("\n")}` : ok;
|
|
@@ -716,7 +898,7 @@ async function runAssemble(target, opts = {}) {
|
|
|
716
898
|
const summary = `wrote ${outPath} (${outIr.slides.length} slides, ${placeholderCount} placeholder${placeholderCount === 1 ? "" : "s"})`;
|
|
717
899
|
const notes = [];
|
|
718
900
|
if (generatedSeed !== void 0) {
|
|
719
|
-
notes.push(`note: generated seed ${generatedSeed} \u2014 add "seed": ${generatedSeed} to deck.
|
|
901
|
+
notes.push(`note: generated seed ${generatedSeed} \u2014 add "seed": ${generatedSeed} to deck.spec.json for revision stability`);
|
|
720
902
|
}
|
|
721
903
|
if (materializedLayoutCount !== void 0) {
|
|
722
904
|
notes.push(
|
|
@@ -730,16 +912,16 @@ async function runDisassemble(irPath, outDir) {
|
|
|
730
912
|
const v = validateIr(raw);
|
|
731
913
|
if (!v.ok) throw new PptfastError(`invalid IR:
|
|
732
914
|
${formatIssues(v.errors)}`);
|
|
733
|
-
const {
|
|
915
|
+
const { spec: spec2, pages } = disassembleDeck(v.ir);
|
|
734
916
|
const ids = Object.keys(pages);
|
|
735
917
|
for (const id of ids) assertSafeFileSegment(id, "slide id");
|
|
736
|
-
const
|
|
918
|
+
const specPath = join4(outDir, "deck.spec.json");
|
|
737
919
|
await mkdir2(outDir, { recursive: true });
|
|
738
920
|
try {
|
|
739
|
-
await writeFile2(
|
|
921
|
+
await writeFile2(specPath, JSON.stringify(spec2, null, 2) + "\n", { flag: "wx" });
|
|
740
922
|
} catch (e) {
|
|
741
923
|
if (e.code === "EEXIST") {
|
|
742
|
-
throw new PptfastError(`${
|
|
924
|
+
throw new PptfastError(`${specPath} already exists \u2014 refusing to overwrite an existing deck project`);
|
|
743
925
|
}
|
|
744
926
|
throw e;
|
|
745
927
|
}
|
|
@@ -761,13 +943,75 @@ ${formatIssues(v.errors)}`);
|
|
|
761
943
|
);
|
|
762
944
|
const pagesNote = ids.length > 0 ? `${ids.length} page file${ids.length === 1 ? "" : "s"} to ${pagesDir}` : "no pages (every slide was a placeholder)";
|
|
763
945
|
const assetsNote = assetCount > 0 ? `, and ${assetCount} asset file${assetCount === 1 ? "" : "s"} to ${assetsDir}` : "";
|
|
764
|
-
return `wrote ${
|
|
946
|
+
return `wrote ${specPath}, ${pagesNote}${assetsNote}`;
|
|
765
947
|
} catch (e) {
|
|
766
|
-
await rm(
|
|
948
|
+
await rm(specPath, { force: true }).catch(() => {
|
|
767
949
|
});
|
|
768
950
|
throw e;
|
|
769
951
|
}
|
|
770
952
|
}
|
|
953
|
+
async function runMigrate(input, output, cwd = process.cwd()) {
|
|
954
|
+
const resolvedInput = resolve5(cwd, input);
|
|
955
|
+
if (await isDeckDirectory(resolvedInput)) {
|
|
956
|
+
return runMigrateDeckDir(resolvedInput, output, cwd);
|
|
957
|
+
}
|
|
958
|
+
return runMigrateIrFile(resolvedInput, output, cwd);
|
|
959
|
+
}
|
|
960
|
+
async function runMigrateDeckDir(dir, output, cwd) {
|
|
961
|
+
const planPath = join4(dir, PLAN_FILENAME);
|
|
962
|
+
const sourceSpecPath = join4(dir, SPEC_FILENAME);
|
|
963
|
+
if (!await pathExists(planPath) && await pathExists(sourceSpecPath)) {
|
|
964
|
+
throw new PptfastError(
|
|
965
|
+
`${dir} has ${SPEC_FILENAME} but no ${PLAN_FILENAME} \u2014 this deck project is already migrated, nothing to do`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
const raw = await loadIrFile(planPath, "plan");
|
|
969
|
+
const migrated = migrateDeckPlanToSpec(raw);
|
|
970
|
+
const outDir = resolve5(cwd, output);
|
|
971
|
+
const specPath = join4(outDir, SPEC_FILENAME);
|
|
972
|
+
await mkdir2(outDir, { recursive: true });
|
|
973
|
+
try {
|
|
974
|
+
await writeFile2(specPath, JSON.stringify(migrated, null, 2) + "\n", { flag: "wx" });
|
|
975
|
+
} catch (e) {
|
|
976
|
+
if (e.code === "EEXIST") {
|
|
977
|
+
throw new PptfastError(`${specPath} already exists \u2014 refusing to overwrite, delete it first or choose a different -o`);
|
|
978
|
+
}
|
|
979
|
+
throw e;
|
|
980
|
+
}
|
|
981
|
+
return `wrote ${specPath} \u2014 run \`pptfast spec validate ${specPath}\` to confirm it, then delete ${planPath} (a directory with both files present is rejected)`;
|
|
982
|
+
}
|
|
983
|
+
async function runMigrateIrFile(filePath, output, cwd) {
|
|
984
|
+
const raw = await loadIrFile(filePath);
|
|
985
|
+
const version = typeof raw === "object" && raw !== null ? raw.version : void 0;
|
|
986
|
+
if (version === "2") {
|
|
987
|
+
throw new PptfastError(
|
|
988
|
+
"pptfast migrate does not support IR v2 (spec \xA715.3: v2 has no real users) \u2014 run `pptfast validate` on the v2 file to see the full v2\u2192v4 combined field mapping and rewrite it by hand"
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
if (version !== "3") {
|
|
992
|
+
throw new PptfastError(
|
|
993
|
+
`pptfast migrate only converts an IR v3 file (version: "3") or a deck project directory containing ${PLAN_FILENAME} \u2014 got version ${JSON.stringify(version)} in ${filePath}`
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
const parsed = PptxIRV3Schema.safeParse(raw);
|
|
997
|
+
if (!parsed.success) {
|
|
998
|
+
const detail = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
|
|
999
|
+
throw new PptfastError(`invalid IR v3 file ${filePath}:
|
|
1000
|
+
${detail}`);
|
|
1001
|
+
}
|
|
1002
|
+
const migrated = migrateIrV3ToV4(parsed.data);
|
|
1003
|
+
const outPath = resolve5(cwd, output);
|
|
1004
|
+
await mkdir2(dirname2(outPath), { recursive: true });
|
|
1005
|
+
try {
|
|
1006
|
+
await writeFile2(outPath, JSON.stringify(migrated, null, 2) + "\n", { flag: "wx" });
|
|
1007
|
+
} catch (e) {
|
|
1008
|
+
if (e.code === "EEXIST") {
|
|
1009
|
+
throw new PptfastError(`${outPath} already exists \u2014 refusing to overwrite, delete it first or choose a different -o`);
|
|
1010
|
+
}
|
|
1011
|
+
throw e;
|
|
1012
|
+
}
|
|
1013
|
+
return `wrote ${outPath} (migrated IR v3 \u2192 v4)`;
|
|
1014
|
+
}
|
|
771
1015
|
|
|
772
1016
|
// src/cli/update.ts
|
|
773
1017
|
import { execFile } from "child_process";
|
|
@@ -872,43 +1116,60 @@ program.command("validate").description("Validate an IR JSON file, deck project
|
|
|
872
1116
|
}
|
|
873
1117
|
});
|
|
874
1118
|
program.command("audit").description(
|
|
875
|
-
"Deterministic geometry audit (overflow, out-of-bounds, low-contrast, overlap) \u2014 exits 1 when it finds anything"
|
|
876
|
-
).argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptfast/decks").option("--json", "machine-readable output (the full AuditReport)").action(async (target, opts) => {
|
|
1119
|
+
"Deterministic geometry audit (overflow, out-of-bounds, low-contrast, overlap, content-truncated, content-dropped), plus an optional --pixels contrast pass \u2014 exits 1 when it finds anything"
|
|
1120
|
+
).argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptfast/decks").option("--json", "machine-readable output (the full AuditReport)").option("--pixels", "also run the optional pixel-contrast pass over image-backed text (requires sharp)").action(async (target, opts) => {
|
|
877
1121
|
try {
|
|
878
|
-
const { output, hasFindings } = await runAudit(target, { json: opts.json });
|
|
1122
|
+
const { output, hasFindings } = await runAudit(target, { json: opts.json, pixels: opts.pixels });
|
|
879
1123
|
console.log(output);
|
|
880
1124
|
if (hasFindings) process.exit(1);
|
|
881
1125
|
} catch (e) {
|
|
882
1126
|
fail(e);
|
|
883
1127
|
}
|
|
884
1128
|
});
|
|
885
|
-
program.command("schema").description("Print the IR JSON Schema (feed this to a model before it writes IR)").option("--style", "print the style-override schema instead").option("--
|
|
886
|
-
|
|
887
|
-
);
|
|
888
|
-
|
|
889
|
-
|
|
1129
|
+
program.command("schema").description("Print the IR JSON Schema (feed this to a model before it writes IR)").option("--style", "print the style-override schema instead").option("--spec", "print the deck spec schema instead").option("--plan", "removed \u2014 use --spec instead").action((opts) => {
|
|
1130
|
+
if (opts.plan) {
|
|
1131
|
+
fail(new Error("`pptfast schema --plan` has been renamed to `pptfast schema --spec` \u2014 run `pptfast schema --spec` instead"));
|
|
1132
|
+
}
|
|
1133
|
+
console.log(runSchema(opts.spec ? "spec" : opts.style ? "style" : void 0));
|
|
1134
|
+
});
|
|
1135
|
+
var plan = program.command("plan").description("Removed \u2014 use `pptfast spec` instead");
|
|
1136
|
+
plan.command("validate").description("Removed \u2014 use `pptfast spec validate` instead").argument("<file>").action(() => {
|
|
1137
|
+
fail(new Error("`pptfast plan validate` has been renamed to `pptfast spec validate` \u2014 run `pptfast spec validate <file>` instead"));
|
|
1138
|
+
});
|
|
1139
|
+
var spec = program.command("spec").description("Deck spec commands (spec \xA76)");
|
|
1140
|
+
spec.command("validate").description("Validate a deck spec JSON file against the schema and strategy-aware hard gates").argument("<spec.json>").action(async (specPath) => {
|
|
890
1141
|
try {
|
|
891
|
-
console.log(await
|
|
1142
|
+
console.log(await runSpecValidate(specPath));
|
|
892
1143
|
} catch (e) {
|
|
893
1144
|
fail(e);
|
|
894
1145
|
}
|
|
895
1146
|
});
|
|
896
|
-
program.command("assemble").description("Assemble a deck project directory (deck.
|
|
1147
|
+
program.command("assemble").description("Assemble a deck project directory (deck.spec.json + pages/ + assets/) into an IR JSON file").argument("<dir|name>", "deck project directory, or bare name under ~/.pptfast/decks").option("-o, --output <file>", "output IR JSON path (default: <dir>/deck.json)").action(async (target, opts) => {
|
|
897
1148
|
try {
|
|
898
1149
|
console.log(await runAssemble(target, { output: opts.output }));
|
|
899
1150
|
} catch (e) {
|
|
900
1151
|
fail(e);
|
|
901
1152
|
}
|
|
902
1153
|
});
|
|
903
|
-
program.command("disassemble").description("Split an IR JSON file into a deck project directory (deck.
|
|
1154
|
+
program.command("disassemble").description("Split an IR JSON file into a deck project directory (deck.spec.json + pages/)").argument("<ir.json>", "path to the IR file").requiredOption("-o, --output <dir>", "output deck project directory").action(async (irPath, opts) => {
|
|
904
1155
|
try {
|
|
905
1156
|
console.log(await runDisassemble(irPath, opts.output));
|
|
906
1157
|
} catch (e) {
|
|
907
1158
|
fail(e);
|
|
908
1159
|
}
|
|
909
1160
|
});
|
|
1161
|
+
program.command("migrate").description("Convert a v3 IR file to v4, or a deck.plan.json project directory to deck.spec.json \u2014 deterministic, no model").argument("<input>", "IR v3 JSON file, or a deck project directory containing deck.plan.json").requiredOption("-o, --output <output>", "output path \u2014 an IR JSON file for a v3 file input, a directory for a deck-project-directory input").action(async (input, opts) => {
|
|
1162
|
+
try {
|
|
1163
|
+
console.log(await runMigrate(input, opts.output));
|
|
1164
|
+
} catch (e) {
|
|
1165
|
+
fail(e);
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
910
1168
|
program.command("themes").description("List built-in themes").option("--json", "machine-readable output").action((opts) => console.log(runThemes(Boolean(opts.json))));
|
|
911
|
-
program.command("
|
|
1169
|
+
program.command("narratives").description("List named narrative presets (strategy/pacing/audience axes + theme recommendations)").option("--json", "machine-readable output").action((opts) => console.log(runNarratives(Boolean(opts.json))));
|
|
1170
|
+
program.command("scenarios").description("Removed \u2014 use `pptfast narratives` instead").action(() => {
|
|
1171
|
+
fail(new Error("`pptfast scenarios` has been renamed to `pptfast narratives` \u2014 run `pptfast narratives` instead"));
|
|
1172
|
+
});
|
|
912
1173
|
program.command("init").description("Scaffold a pptfast.config.json in the current directory").action(async () => {
|
|
913
1174
|
try {
|
|
914
1175
|
console.log(await runInit());
|