@markdstage/markdstage 2.6.0 → 3.1.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 +6 -6
- package/package.json +1 -1
- package/shared/README.md +54 -20
- package/shared/markdstage-guide.mjs +8 -2
- package/shared/renderer/renderer.js +713 -64
- package/shared/renderer/slides.css +33 -0
- package/shared/runtime/browser.mjs +127 -111
- package/shared/runtime/output.mjs +237 -55
- package/shared/runtime/pptx-package.mjs +404 -67
- package/src/cli.mjs +13 -13
- package/src/commands/present.mjs +4 -2
- package/src/skills.mjs +6 -6
|
@@ -276,6 +276,12 @@ body.mermaid-loading .mermaid{visibility:hidden;}
|
|
|
276
276
|
.architecture-routing-warning span{font-family:"Cascadia Code","Cascadia Mono",Consolas,monospace;
|
|
277
277
|
font-size:.85em;overflow-wrap:anywhere;}
|
|
278
278
|
hr{border:0;border-top:1px solid var(--border);margin:.7em 0;}
|
|
279
|
+
/* Flex item margins do not collapse, so regular slide bodies use their gap as
|
|
280
|
+
the single source of vertical spacing. Nested component spacing is unchanged. */
|
|
281
|
+
.deck:not(.title-slide):not(.section-slide):not(.backcover-slide)>.body>:where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table):has(+:where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table)),
|
|
282
|
+
.deck:not(.title-slide):not(.section-slide):not(.backcover-slide)>.body>:where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table)+:where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table){
|
|
283
|
+
margin-block:0;
|
|
284
|
+
}
|
|
279
285
|
footer{flex:0 0 auto;display:flex;justify-content:space-between;align-items:center;
|
|
280
286
|
color:var(--muted);font-size:clamp(12px,1.7vh,16px);
|
|
281
287
|
margin-top:clamp(12px,2.2vh,26px);border-top:1px solid var(--border);padding-top:12px;}
|
|
@@ -547,8 +553,22 @@ body.presenter-view-mode .layout-warning{display:none;}
|
|
|
547
553
|
align-items:center;justify-content:center;border-radius:6px;
|
|
548
554
|
font-size:.82em;font-weight:700;font-variant-numeric:tabular-nums;
|
|
549
555
|
color:var(--muted);background:var(--code);border:1px solid var(--border);}
|
|
556
|
+
.overview-kind{flex:0 0 auto;padding:.18em .48em;border-radius:999px;
|
|
557
|
+
color:var(--accent-strong);background:var(--surface);border:1px solid var(--accent);
|
|
558
|
+
font-size:.66em;font-weight:750;letter-spacing:.06em;text-transform:uppercase;}
|
|
550
559
|
.overview-label{flex:1 1 auto;font-size:.96em;line-height:1.35;
|
|
551
560
|
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
561
|
+
.overview-item-title{margin-bottom:.45em;}
|
|
562
|
+
.overview-item-title .overview-link{background:color-mix(in srgb,var(--accent-soft) 74%,transparent);
|
|
563
|
+
border:1px solid color-mix(in srgb,var(--accent) 48%,var(--border));}
|
|
564
|
+
.overview-item-title .overview-label{color:var(--fg);font-weight:700;}
|
|
565
|
+
.overview-item-section{margin-top:.65em;padding-top:.55em;border-top:1px solid var(--border);}
|
|
566
|
+
.overview-item-section .overview-link{padding-block:.65em;background:var(--accent-soft);
|
|
567
|
+
border-left:4px solid var(--accent);}
|
|
568
|
+
.overview-item-section .overview-num{color:var(--fg);border-color:var(--accent);}
|
|
569
|
+
.overview-item-section .overview-label{color:var(--fg);font-weight:700;}
|
|
570
|
+
.overview-item-section-child .overview-link{width:calc(100% - 1.2em);margin-left:1.2em;
|
|
571
|
+
border-left:1px solid color-mix(in srgb,var(--accent) 42%,var(--border));border-radius:0 8px 8px 0;}
|
|
552
572
|
.overview-item.current .overview-link{background:var(--accent-soft);}
|
|
553
573
|
.overview-item.current .overview-num{color:var(--fg);background:var(--accent);
|
|
554
574
|
border-color:var(--accent);}
|
|
@@ -642,6 +662,7 @@ body.pptx-artwork-mode [data-pptx-native="table"] *{
|
|
|
642
662
|
}
|
|
643
663
|
body.pptx-artwork-mode li[data-pptx-native="text"]::marker{color:transparent!important;}
|
|
644
664
|
body.pptx-artwork-mode [data-pptx-native="table"]{visibility:hidden!important;}
|
|
665
|
+
body.pptx-artwork-mode pre[data-pptx-native="code"]{visibility:hidden!important;}
|
|
645
666
|
body.pptx-artwork-mode img[data-pptx-native="image"]{
|
|
646
667
|
object-fit:none!important;
|
|
647
668
|
object-position:99999px 99999px!important;
|
|
@@ -660,6 +681,18 @@ body.pptx-artwork-mode svg [data-pptx-native="table"]{
|
|
|
660
681
|
}
|
|
661
682
|
body.pptx-artwork-mode svg path[data-pptx-native="connector"]{marker-end:none!important;}
|
|
662
683
|
body.pptx-artwork-mode svg [data-pptx-native="image"]{opacity:0!important;}
|
|
684
|
+
body.pptx-artwork-mode [data-pptx-shadow-fallback]{box-shadow:none!important;}
|
|
685
|
+
body.pptx-artwork-mode .pptx-fallback-hidden{visibility:hidden!important;}
|
|
686
|
+
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template){
|
|
687
|
+
background:transparent!important;
|
|
688
|
+
}
|
|
689
|
+
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)::before{
|
|
690
|
+
display:none!important;
|
|
691
|
+
}
|
|
692
|
+
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)>.theme-cover-background,
|
|
693
|
+
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)>.theme-cover-logo{
|
|
694
|
+
visibility:hidden!important;
|
|
695
|
+
}
|
|
663
696
|
|
|
664
697
|
/* ===== Headless PDF export ===== */
|
|
665
698
|
@page{size:13.333333in 7.5in;margin:0;}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// because the Extension is distributed as a folder ZIP.
|
|
7
7
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
-
import { readFile, open, rm, stat } from "node:fs/promises";
|
|
9
|
+
import { readFile, open, rm, stat, writeFile } from "node:fs/promises";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { execFileSync, spawn } from "node:child_process";
|
|
12
12
|
|
|
@@ -173,84 +173,6 @@ export function delay(milliseconds) {
|
|
|
173
173
|
return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
/**
|
|
177
|
-
* Run a headless browser once with `--print-to-pdf`.
|
|
178
|
-
*
|
|
179
|
-
* ⚠️ **`pageUrl` must include `?print=1&token=...` (#12).**
|
|
180
|
-
*
|
|
181
|
-
* `--print-to-pdf` completes only when the page becomes idle. In renderer `init()`,
|
|
182
|
-
* only print mode returns early. Normal and presenter views keep an unclosed SSE
|
|
183
|
-
* (`new EventSource("./events")`) and a two-second `setInterval` running.
|
|
184
|
-
* Passing a URL without `?print=1` therefore means **the browser never exits**.
|
|
185
|
-
*
|
|
186
|
-
* Observed results, using Chrome arguments byte-for-byte identical to this function:
|
|
187
|
-
*
|
|
188
|
-
* | URL | Result |
|
|
189
|
-
* | -------------------------- | --------------------------------------- |
|
|
190
|
-
* | `/?print=1&token=<valid>` | exit 0 @ 2.4s (valid PDF) |
|
|
191
|
-
* | `/?print=1&token=` (empty) | exit 0 @ 1.9s (blank; renderer reports failure) |
|
|
192
|
-
* | `/` (normal view) | **HANG** (still running after 120 seconds) |
|
|
193
|
-
* | `/?present=1` | **HANG** |
|
|
194
|
-
* | `/nope-404` (no renderer) | exit 0 @ 3.0s |
|
|
195
|
-
*
|
|
196
|
-
* ⚠️ **`--virtual-time-budget` is effectively ignored by `--headless=new`.**
|
|
197
|
-
* The `--virtual-time-budget=12000` argument below does not stop this hang.
|
|
198
|
-
* Adding `--timeout=8000` is also ineffective, as verified empirically.
|
|
199
|
-
* The argument is harmless and remains in place, but **do not treat it as a
|
|
200
|
-
* wall-clock timeout**. Only Node's `PDF_RENDER_TIMEOUT_MS` and
|
|
201
|
-
* `terminateProcessTree` enforce a limit, and failure may take up to 60 seconds.
|
|
202
|
-
*/
|
|
203
|
-
export async function runHeadlessBrowser(browser, args, failureLabel) {
|
|
204
|
-
await new Promise((resolvePromise, rejectPromise) => {
|
|
205
|
-
const child = spawn(browser, args, {
|
|
206
|
-
detached: process.platform !== "win32",
|
|
207
|
-
windowsHide: true,
|
|
208
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
209
|
-
});
|
|
210
|
-
let diagnostics = "";
|
|
211
|
-
let settled = false;
|
|
212
|
-
let timedOut = false;
|
|
213
|
-
const appendDiagnostics = (chunk) => {
|
|
214
|
-
diagnostics = `${diagnostics}${chunk.toString()}`.slice(-12_000);
|
|
215
|
-
};
|
|
216
|
-
child.stdout.on("data", appendDiagnostics);
|
|
217
|
-
child.stderr.on("data", appendDiagnostics);
|
|
218
|
-
|
|
219
|
-
const settle = (error) => {
|
|
220
|
-
if (settled) return;
|
|
221
|
-
settled = true;
|
|
222
|
-
clearTimeout(timer);
|
|
223
|
-
if (error) rejectPromise(error);
|
|
224
|
-
else resolvePromise();
|
|
225
|
-
};
|
|
226
|
-
const timer = setTimeout(async () => {
|
|
227
|
-
if (settled) return;
|
|
228
|
-
timedOut = true;
|
|
229
|
-
await terminateProcessTree(child);
|
|
230
|
-
settle(new Error(`${failureLabel} timed out after ${PDF_RENDER_TIMEOUT_MS / 1000}s.`));
|
|
231
|
-
}, PDF_RENDER_TIMEOUT_MS);
|
|
232
|
-
|
|
233
|
-
child.once("error", (error) => {
|
|
234
|
-
if (!timedOut) settle(error);
|
|
235
|
-
});
|
|
236
|
-
child.once("exit", (code, signal) => {
|
|
237
|
-
if (timedOut) return;
|
|
238
|
-
if (code === 0) {
|
|
239
|
-
settle();
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const detail = diagnostics.trim();
|
|
243
|
-
settle(
|
|
244
|
-
new Error(
|
|
245
|
-
`${failureLabel} failed (${signal ? `signal ${signal}` : `exit ${code}`})${
|
|
246
|
-
detail ? `: ${detail}` : "."
|
|
247
|
-
}`,
|
|
248
|
-
),
|
|
249
|
-
);
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
176
|
function withSandboxFallback(args) {
|
|
255
177
|
if (
|
|
256
178
|
process.platform !== "win32" &&
|
|
@@ -262,35 +184,38 @@ function withSandboxFallback(args) {
|
|
|
262
184
|
return args;
|
|
263
185
|
}
|
|
264
186
|
|
|
265
|
-
export async function runPdfBrowser(browser, pageUrl, outputPath, profileDir) {
|
|
266
|
-
// Enforce the contract at runtime. Otherwise it silently waits 60 seconds
|
|
267
|
-
// before timing out, obscuring the cause; fail immediately with an explanation.
|
|
187
|
+
export async function runPdfBrowser(browser, pageUrl, outputPath, profileDir, job) {
|
|
268
188
|
if (new URL(pageUrl).searchParams.get("print") !== "1") {
|
|
269
189
|
throw new Error(
|
|
270
|
-
`Refusing to
|
|
190
|
+
`Refusing to render PDF from a non-print URL (${pageUrl}).`,
|
|
271
191
|
);
|
|
272
192
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
"
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
193
|
+
if (!job || typeof job !== "object") {
|
|
194
|
+
throw new Error("PDF rendering requires an output job.");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { cdp, child } = await openCdpOutputPage(browser, pageUrl, profileDir, job);
|
|
198
|
+
try {
|
|
199
|
+
// The renderer reports ready only after Mermaid, images, fonts, and layout
|
|
200
|
+
// have settled. Give Chromium two compositor frames before printing.
|
|
201
|
+
await cdp.send("Runtime.evaluate", {
|
|
202
|
+
expression:
|
|
203
|
+
"new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))",
|
|
204
|
+
awaitPromise: true,
|
|
205
|
+
});
|
|
206
|
+
const pdf = await cdp.send("Page.printToPDF", {
|
|
207
|
+
displayHeaderFooter: false,
|
|
208
|
+
printBackground: true,
|
|
209
|
+
preferCSSPageSize: true,
|
|
210
|
+
transferMode: "ReturnAsBase64",
|
|
211
|
+
});
|
|
212
|
+
if (typeof pdf.data !== "string" || pdf.data.length === 0) {
|
|
213
|
+
throw new Error("Chromium DevTools did not return PDF data.");
|
|
214
|
+
}
|
|
215
|
+
await writeFile(outputPath, Buffer.from(pdf.data, "base64"));
|
|
216
|
+
} finally {
|
|
217
|
+
await closeCdpOutputPage(cdp, child);
|
|
218
|
+
}
|
|
294
219
|
}
|
|
295
220
|
|
|
296
221
|
async function waitForDevToolsPort(profileDir, child, diagnostics) {
|
|
@@ -528,7 +453,13 @@ export async function runPptxOutputBrowser(browser, pageUrl, profileDir, job, to
|
|
|
528
453
|
);
|
|
529
454
|
}
|
|
530
455
|
const model = evaluated.result?.value;
|
|
531
|
-
if (
|
|
456
|
+
if (
|
|
457
|
+
!model ||
|
|
458
|
+
!Array.isArray(model.masters) ||
|
|
459
|
+
!Array.isArray(model.layouts) ||
|
|
460
|
+
!Array.isArray(model.slides) ||
|
|
461
|
+
model.slides.length !== total
|
|
462
|
+
) {
|
|
532
463
|
throw new Error("The renderer returned an invalid PowerPoint export model.");
|
|
533
464
|
}
|
|
534
465
|
|
|
@@ -538,26 +469,111 @@ export async function runPptxOutputBrowser(browser, pageUrl, profileDir, job, to
|
|
|
538
469
|
awaitPromise: true,
|
|
539
470
|
});
|
|
540
471
|
|
|
541
|
-
const
|
|
542
|
-
for (
|
|
472
|
+
const layoutArtworks = [];
|
|
473
|
+
for (const [index, layout] of model.layouts.entries()) {
|
|
474
|
+
const captureIndex = Number.isInteger(layout.captureIndex)
|
|
475
|
+
? layout.captureIndex
|
|
476
|
+
: total + index;
|
|
543
477
|
const screenshot = await cdp.send("Page.captureScreenshot", {
|
|
544
478
|
format: "png",
|
|
545
479
|
fromSurface: true,
|
|
546
480
|
captureBeyondViewport: true,
|
|
547
481
|
clip: {
|
|
548
482
|
x: 0,
|
|
549
|
-
y:
|
|
483
|
+
y: captureIndex * 720,
|
|
550
484
|
width: 1280,
|
|
551
485
|
height: 720,
|
|
552
486
|
scale: 1,
|
|
553
487
|
},
|
|
554
488
|
});
|
|
555
489
|
if (typeof screenshot.data !== "string" || screenshot.data.length === 0) {
|
|
556
|
-
throw new Error(`Chromium did not return
|
|
490
|
+
throw new Error(`Chromium did not return artwork for PowerPoint layout ${layout.id}.`);
|
|
491
|
+
}
|
|
492
|
+
layoutArtworks.push(Buffer.from(screenshot.data, "base64"));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
await cdp.send("Runtime.evaluate", {
|
|
496
|
+
expression:
|
|
497
|
+
"document.body.classList.remove('pptx-layout-artwork-mode');" +
|
|
498
|
+
"document.body.classList.add('pptx-slide-artwork-mode');" +
|
|
499
|
+
"new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))",
|
|
500
|
+
awaitPromise: true,
|
|
501
|
+
});
|
|
502
|
+
await cdp.send("Emulation.setDefaultBackgroundColorOverride", {
|
|
503
|
+
color: { r: 0, g: 0, b: 0, a: 0 },
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const slideFallbackImages = [];
|
|
507
|
+
for (const [slideIndex, slide] of model.slides.entries()) {
|
|
508
|
+
const images = [];
|
|
509
|
+
const fallbacks = Array.isArray(slide.fallbacks) ? slide.fallbacks : [];
|
|
510
|
+
for (const [fallbackIndex, fallback] of fallbacks.entries()) {
|
|
511
|
+
if (fallback?.artwork === false) continue;
|
|
512
|
+
if (typeof fallback?.captureId !== "string" || !fallback.captureId) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`PowerPoint fallback ${fallbackIndex + 1} on slide ${slideIndex + 1} is missing its capture id.`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
const left = Math.max(0, Number(fallback?.x));
|
|
518
|
+
const top = Math.max(0, Number(fallback?.y));
|
|
519
|
+
const right = Math.min(1280, Number(fallback?.x) + Number(fallback?.width));
|
|
520
|
+
const bottom = Math.min(720, Number(fallback?.y) + Number(fallback?.height));
|
|
521
|
+
if (
|
|
522
|
+
![left, top, right, bottom].every(Number.isFinite) ||
|
|
523
|
+
right <= left ||
|
|
524
|
+
bottom <= top
|
|
525
|
+
) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`PowerPoint fallback ${fallbackIndex + 1} on slide ${slideIndex + 1} has invalid bounds.`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
const bounds = {
|
|
531
|
+
x: left,
|
|
532
|
+
y: top,
|
|
533
|
+
width: right - left,
|
|
534
|
+
height: bottom - top,
|
|
535
|
+
};
|
|
536
|
+
await cdp.send("Runtime.evaluate", {
|
|
537
|
+
expression: `(() => {
|
|
538
|
+
const active = ${JSON.stringify(fallback.captureId)};
|
|
539
|
+
for (const element of document.querySelectorAll("[data-pptx-fallback-ids]")) {
|
|
540
|
+
const ids = (element.getAttribute("data-pptx-fallback-ids") || "").split(/\\s+/);
|
|
541
|
+
element.classList.toggle("pptx-fallback-hidden", !ids.includes(active));
|
|
542
|
+
}
|
|
543
|
+
return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
544
|
+
})()`,
|
|
545
|
+
awaitPromise: true,
|
|
546
|
+
});
|
|
547
|
+
const screenshot = await cdp.send("Page.captureScreenshot", {
|
|
548
|
+
format: "png",
|
|
549
|
+
fromSurface: true,
|
|
550
|
+
captureBeyondViewport: true,
|
|
551
|
+
clip: {
|
|
552
|
+
x: bounds.x,
|
|
553
|
+
y: slideIndex * 720 + bounds.y,
|
|
554
|
+
width: bounds.width,
|
|
555
|
+
height: bounds.height,
|
|
556
|
+
scale: 1,
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
if (typeof screenshot.data !== "string" || screenshot.data.length === 0) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
`Chromium did not return fallback artwork ${fallbackIndex + 1} for slide ${slideIndex + 1}.`,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
images.push({
|
|
565
|
+
fallbackIndex,
|
|
566
|
+
...bounds,
|
|
567
|
+
data: Buffer.from(screenshot.data, "base64"),
|
|
568
|
+
});
|
|
557
569
|
}
|
|
558
|
-
|
|
570
|
+
slideFallbackImages.push(images);
|
|
559
571
|
}
|
|
560
|
-
|
|
572
|
+
await cdp.send("Runtime.evaluate", {
|
|
573
|
+
expression:
|
|
574
|
+
'document.querySelectorAll(".pptx-fallback-hidden").forEach(element => element.classList.remove("pptx-fallback-hidden"));',
|
|
575
|
+
});
|
|
576
|
+
return { model, layoutArtworks, slideFallbackImages };
|
|
561
577
|
} finally {
|
|
562
578
|
await closeCdpOutputPage(cdp, child);
|
|
563
579
|
}
|