@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.
@@ -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 run --print-to-pdf against a non-print URL (${pageUrl}): only ?print=1 stops the renderer's SSE and polling loops, so any other page hangs the browser forever.`,
190
+ `Refusing to render PDF from a non-print URL (${pageUrl}).`,
271
191
  );
272
192
  }
273
- const args = withSandboxFallback([
274
- "--headless=new",
275
- "--disable-gpu",
276
- "--disable-background-networking",
277
- "--disable-component-update",
278
- "--disable-default-apps",
279
- "--disable-extensions",
280
- "--force-color-profile=srgb",
281
- "--hide-scrollbars",
282
- "--no-first-run",
283
- "--no-pdf-header-footer",
284
- "--print-to-pdf-no-header",
285
- "--run-all-compositor-stages-before-draw",
286
- // Ineffective (#12): --headless=new ignores it. See the JSDoc above.
287
- // The actual safeguards are PDF_RENDER_TIMEOUT_MS + terminateProcessTree.
288
- "--virtual-time-budget=12000",
289
- `--user-data-dir=${profileDir}`,
290
- `--print-to-pdf=${outputPath}`,
291
- pageUrl,
292
- ]);
293
- await runHeadlessBrowser(browser, args, "Browser PDF rendering");
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 (!model || !Array.isArray(model.slides) || model.slides.length !== total) {
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 backgrounds = [];
542
- for (let index = 0; index < total; index += 1) {
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: index * 720,
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 fallback artwork for slide ${index + 1}.`);
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
- backgrounds.push(Buffer.from(screenshot.data, "base64"));
570
+ slideFallbackImages.push(images);
559
571
  }
560
- return { model, backgrounds };
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
  }