@markdstage/markdstage 3.0.0 → 3.2.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.
@@ -89,6 +89,8 @@ let customThemeMeta = null;
89
89
  // reveal a newer, still-rendering one.
90
90
  let renderToken = 0;
91
91
  let lastMermaidTheme = null;
92
+ let pptxFallbackSequence = 0;
93
+ const pptxFallbackCaptureElements = new Map();
92
94
  // Editing mode is available only in normal view, not presenter or print mode.
93
95
  // Print mode returns early in init, so presenterMode is the effective branch here.
94
96
  let architectureEditMode = false;
@@ -119,6 +121,7 @@ let layoutFrame = 0;
119
121
  const SCROLL_EPSILON = 2;
120
122
  const OUTPUT_WIDTH = 1280;
121
123
  const OUTPUT_HEIGHT = 720;
124
+ const PPTX_LAYOUT_NAMES = ["title", "default", "center", "section", "backcover"];
122
125
  const LAYOUT_HINT_LIMIT = 5;
123
126
 
124
127
  function applyCustomThemeCss(css) {
@@ -877,7 +880,7 @@ async function reportOutputStatus(token, status, error = "", layout = null) {
877
880
  if (!response.ok) throw new Error(`Could not report output status (${response.status}).`);
878
881
  }
879
882
 
880
- const PPTX_RASTER_IMAGE = /\.(?:png|jpe?g|gif)(?:$|[?#])/i;
883
+ const PPTX_DIRECT_IMAGE = /\.(?:png|jpe?g|gif|svg)(?:$|[?#])/i;
881
884
 
882
885
  function normalizeCssColor(value) {
883
886
  const text = String(value || "").trim();
@@ -945,6 +948,39 @@ function textContentBounds(element, deck) {
945
948
  };
946
949
  }
947
950
 
951
+ function listItemTextBounds(element, deck) {
952
+ const rects = [];
953
+ const visit = (node) => {
954
+ if (node.nodeType === Node.TEXT_NODE) {
955
+ if (!node.nodeValue) return;
956
+ const range = document.createRange();
957
+ range.selectNodeContents(node);
958
+ rects.push(
959
+ ...[...range.getClientRects()].filter(
960
+ (rect) => rect.width > 0 && rect.height > 0,
961
+ ),
962
+ );
963
+ return;
964
+ }
965
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
966
+ if (node !== element && node.matches("ul, ol")) return;
967
+ node.childNodes.forEach(visit);
968
+ };
969
+ element.childNodes.forEach(visit);
970
+ if (rects.length === 0) return relativeBounds(element, deck);
971
+ const slide = deck.getBoundingClientRect();
972
+ const left = Math.min(...rects.map((rect) => rect.left));
973
+ const top = Math.min(...rects.map((rect) => rect.top));
974
+ const right = Math.max(...rects.map((rect) => rect.right));
975
+ const bottom = Math.max(...rects.map((rect) => rect.bottom));
976
+ return {
977
+ x: roundedMetric(left - slide.left),
978
+ y: roundedMetric(top - slide.top),
979
+ width: roundedMetric(right - left),
980
+ height: roundedMetric(bottom - top),
981
+ };
982
+ }
983
+
948
984
  function textInsetsFor(element) {
949
985
  const style = getComputedStyle(element);
950
986
  const metric = (padding, border) => {
@@ -999,13 +1035,13 @@ function singleLineTextLayout(element, deck, bounds, textInsets, alignment) {
999
1035
  };
1000
1036
  }
1001
1037
 
1002
- function rasterImageSupported(image) {
1038
+ function directImageSupported(image) {
1003
1039
  const source = image.currentSrc || image.getAttribute("src") || "";
1004
1040
  if (["cover", "none"].includes(getComputedStyle(image).objectFit)) return false;
1005
- if (/^data:image\/(?:png|jpeg|gif)[;,]/i.test(source)) return true;
1041
+ if (/^data:image\/(?:png|jpeg|gif|svg\+xml)[;,]/i.test(source)) return true;
1006
1042
  try {
1007
1043
  const url = new URL(source, window.location.href);
1008
- return url.origin === window.location.origin && PPTX_RASTER_IMAGE.test(url.href);
1044
+ return url.origin === window.location.origin && PPTX_DIRECT_IMAGE.test(url.href);
1009
1045
  } catch (_) {
1010
1046
  return false;
1011
1047
  }
@@ -1084,6 +1120,141 @@ function paragraphFor(element, options = {}) {
1084
1120
  };
1085
1121
  }
1086
1122
 
1123
+ function listBulletFor(element) {
1124
+ const parentList = element.parentElement;
1125
+ const ordered = parentList?.tagName === "OL";
1126
+ const start = Number(parentList?.getAttribute("start") || 1);
1127
+ const itemIndex = parentList
1128
+ ? [...parentList.children].filter((child) => child.tagName === "LI").indexOf(element)
1129
+ : 0;
1130
+ const number = start + Math.max(0, itemIndex);
1131
+ return {
1132
+ type: ordered ? "number" : "bullet",
1133
+ character: ordered ? `${number}.` : "•",
1134
+ color: normalizeCssColor(getComputedStyle(element, "::marker").color),
1135
+ ...(ordered ? { start: number } : {}),
1136
+ };
1137
+ }
1138
+
1139
+ function outermostListFor(element) {
1140
+ let list = element.parentElement;
1141
+ while (list?.matches("ul, ol")) {
1142
+ const parentItem = list.parentElement?.closest("li");
1143
+ const parentList = parentItem?.parentElement;
1144
+ if (!parentItem || !parentList?.matches("ul, ol")) return list;
1145
+ list = parentList;
1146
+ }
1147
+ return null;
1148
+ }
1149
+
1150
+ function trimListItemRuns(runs) {
1151
+ const trimmed = runs.map((run) => ({ ...run }));
1152
+ while (trimmed.length > 0 && /^[ \t\r\n]*$/.test(trimmed[0].text)) {
1153
+ trimmed.shift();
1154
+ }
1155
+ while (trimmed.length > 0 && /^[ \t\r\n]*$/.test(trimmed.at(-1).text)) {
1156
+ trimmed.pop();
1157
+ }
1158
+ if (trimmed.length > 0) {
1159
+ trimmed[0].text = trimmed[0].text.replace(/^[ \t\r\n]+/, "");
1160
+ trimmed.at(-1).text = trimmed.at(-1).text.replace(/[ \t\r\n]+$/, "");
1161
+ }
1162
+ return trimmed.filter((run) => run.text);
1163
+ }
1164
+
1165
+ function nativeListTextElement(list, deck, eligibleItems) {
1166
+ const items = [...list.querySelectorAll("li")].filter((item) =>
1167
+ eligibleItems.has(item),
1168
+ );
1169
+ if (items.length === 0) return null;
1170
+ const entries = items
1171
+ .map((item) => {
1172
+ const paragraph = paragraphFor(item, {
1173
+ omitNestedLists: true,
1174
+ level: Math.max(
1175
+ 0,
1176
+ [...item.closest(".body").querySelectorAll("ul, ol")].filter((candidate) =>
1177
+ candidate.contains(item),
1178
+ ).length - 1,
1179
+ ),
1180
+ bullet: listBulletFor(item),
1181
+ });
1182
+ paragraph.runs = trimListItemRuns(paragraph.runs);
1183
+ return {
1184
+ item,
1185
+ paragraph,
1186
+ bounds: listItemTextBounds(item, deck),
1187
+ availableBounds: relativeBounds(item, deck),
1188
+ };
1189
+ })
1190
+ .filter(({ paragraph }) => paragraph.runs.length > 0);
1191
+ if (entries.length === 0) return null;
1192
+ const x = Math.min(...entries.map(({ bounds }) => bounds.x));
1193
+ const y = Math.min(...entries.map(({ bounds }) => bounds.y));
1194
+ const right = Math.max(
1195
+ ...entries.map(({ availableBounds }) => availableBounds.x + availableBounds.width),
1196
+ );
1197
+ const bottom = Math.max(
1198
+ ...entries.map(({ bounds }) => bounds.y + bounds.height),
1199
+ );
1200
+ const paragraphs = entries.map(({ item, paragraph, bounds }, index) => {
1201
+ const style = getComputedStyle(item);
1202
+ const lineSpacing = roundedMetric(Number.parseFloat(style.lineHeight));
1203
+ const nextBounds = entries[index + 1]?.bounds;
1204
+ const leftMargin = roundedMetric(Math.max(0, bounds.x - x));
1205
+ const spaceAfter = roundedMetric(
1206
+ Math.max(0, nextBounds ? nextBounds.y - (bounds.y + bounds.height) : 0),
1207
+ );
1208
+ return {
1209
+ ...paragraph,
1210
+ ...(lineSpacing > 0 ? { lineSpacing } : {}),
1211
+ ...(leftMargin > 0 ? { leftMargin } : {}),
1212
+ spaceBefore: 0,
1213
+ spaceAfter,
1214
+ };
1215
+ });
1216
+ for (const { item } of entries) item.setAttribute("data-pptx-native", "text");
1217
+ list.setAttribute("data-pptx-native", "text");
1218
+ return {
1219
+ type: "text",
1220
+ path: elementPath(list, deck),
1221
+ x,
1222
+ y,
1223
+ width: roundedMetric(right - x),
1224
+ height: roundedMetric(bottom - y),
1225
+ zOrder: Math.max(
1226
+ Number(list.dataset.pptxZOrder) || 0,
1227
+ ...items.map((item) => Number(item.dataset.pptxZOrder) || 0),
1228
+ ),
1229
+ paragraphs,
1230
+ opacity: Number(getComputedStyle(list).opacity) || 1,
1231
+ };
1232
+ }
1233
+
1234
+ function codeParagraphsFor(element) {
1235
+ const style = getComputedStyle(element);
1236
+ const baseRun = { text: "", ...runStyle(element) };
1237
+ const lines = [[]];
1238
+ for (const run of collectTextRuns(element)) {
1239
+ const segments = run.text.replace(/\r\n?/g, "\n").split("\n");
1240
+ segments.forEach((text, index) => {
1241
+ if (text) lines.at(-1).push({ ...run, text });
1242
+ if (index < segments.length - 1) lines.push([]);
1243
+ });
1244
+ }
1245
+ // marked terminates fenced code with one newline. Remove that synthetic line
1246
+ // while retaining deliberate blank lines before the closing fence.
1247
+ if (lines.length > 1 && lines.at(-1).length === 0) lines.pop();
1248
+ const lineSpacing = roundedMetric(Number.parseFloat(style.lineHeight));
1249
+ return lines.map((runs) => ({
1250
+ alignment: "left",
1251
+ runs: runs.length ? runs : [{ ...baseRun }],
1252
+ ...(lineSpacing > 0 ? { lineSpacing } : {}),
1253
+ spaceBefore: 0,
1254
+ spaceAfter: 0,
1255
+ }));
1256
+ }
1257
+
1087
1258
  function renderedTextLineCount(element) {
1088
1259
  const range = document.createRange();
1089
1260
  range.selectNodeContents(element);
@@ -1118,24 +1289,237 @@ function unsupportedEffects(element) {
1118
1289
  return effects;
1119
1290
  }
1120
1291
 
1292
+ function effectPaintPadding(element, effects) {
1293
+ if (
1294
+ !effects.some((effect) =>
1295
+ ["text-shadow", "box-shadow", "filter", "backdrop-filter"].includes(effect),
1296
+ )
1297
+ ) {
1298
+ return 0;
1299
+ }
1300
+ const style = getComputedStyle(element);
1301
+ const values = [
1302
+ effects.includes("text-shadow") ? style.textShadow : "",
1303
+ effects.includes("box-shadow") ? style.boxShadow : "",
1304
+ effects.includes("filter") ? style.filter : "",
1305
+ effects.includes("backdrop-filter") ? style.backdropFilter : "",
1306
+ ];
1307
+ const lengths = values.flatMap((value) =>
1308
+ [...String(value).matchAll(/-?\d+(?:\.\d+)?px/g)].map((match) =>
1309
+ Math.abs(Number.parseFloat(match[0])),
1310
+ ),
1311
+ );
1312
+ return Math.min(
1313
+ 256,
1314
+ Math.max(32, Math.ceil(lengths.reduce((sum, value) => sum + value, 0) * 2)),
1315
+ );
1316
+ }
1317
+
1318
+ function subtreeEffectPaintPadding(element) {
1319
+ return Math.max(
1320
+ 0,
1321
+ ...[element, ...element.querySelectorAll("*")].map((candidate) => {
1322
+ const effects = unsupportedEffects(candidate);
1323
+ return effectPaintPadding(candidate, effects);
1324
+ }),
1325
+ );
1326
+ }
1327
+
1328
+ function assignPptxPaintOrder(deck) {
1329
+ const elements = [...deck.querySelectorAll("*")];
1330
+ const entries = elements.map((element, domOrder) => {
1331
+ const stacking = [];
1332
+ const ancestors = [];
1333
+ for (let current = element; current && current !== deck; current = current.parentElement) {
1334
+ ancestors.push(current);
1335
+ }
1336
+ for (const current of ancestors.reverse()) {
1337
+ const zIndex = getComputedStyle(current).zIndex;
1338
+ if (zIndex !== "auto" && Number.isFinite(Number(zIndex))) {
1339
+ stacking.push(Number(zIndex));
1340
+ }
1341
+ }
1342
+ return { element, domOrder, stacking };
1343
+ });
1344
+ entries.sort((left, right) => {
1345
+ const length = Math.max(left.stacking.length, right.stacking.length);
1346
+ for (let index = 0; index < length; index += 1) {
1347
+ const difference = (left.stacking[index] || 0) - (right.stacking[index] || 0);
1348
+ if (difference) return difference;
1349
+ }
1350
+ return left.domOrder - right.domOrder;
1351
+ });
1352
+ entries.forEach(({ element }, zOrder) => {
1353
+ element.dataset.pptxZOrder = String(zOrder);
1354
+ });
1355
+ }
1356
+
1121
1357
  function effectFallbackRoot(element) {
1122
1358
  return element.closest(
1123
- "p, li, blockquote, table, img, h1, h2, h3, h4, h5, h6, .kicker, .slide-title, .theme-backcover-logo-text, .theme-backcover-copyright, .body, header, footer",
1359
+ "p, li, blockquote, table, img, h1, h2, h3, h4, h5, h6, div, section, article, aside, details, video, audio, iframe, canvas, object, embed, .kicker, .slide-title, .theme-backcover-logo-text, .theme-backcover-copyright, .body, header, footer",
1124
1360
  );
1125
1361
  }
1126
1362
 
1127
- function pptxFallback(type, element, deck, reason) {
1363
+ function fallbackBounds(element, deck, padding = 0, includeDescendants = false) {
1364
+ const candidates = (
1365
+ includeDescendants ? [element, ...element.querySelectorAll("*")] : [element]
1366
+ ).filter((candidate) => {
1367
+ const style = getComputedStyle(candidate);
1368
+ return style.display !== "none";
1369
+ });
1370
+ const slide = deck.getBoundingClientRect();
1371
+ const relativeRect = (rect) => ({
1372
+ x: rect.left - slide.left,
1373
+ y: rect.top - slide.top,
1374
+ width: rect.width,
1375
+ height: rect.height,
1376
+ });
1377
+ const bounds = candidates.map((candidate) =>
1378
+ relativeRect(candidate.getBoundingClientRect()),
1379
+ );
1380
+ for (const candidate of candidates) {
1381
+ for (const node of candidate.childNodes) {
1382
+ if (node.nodeType !== Node.TEXT_NODE || !node.textContent?.trim()) continue;
1383
+ const range = document.createRange();
1384
+ range.selectNodeContents(node);
1385
+ bounds.push(
1386
+ ...[...range.getClientRects()]
1387
+ .filter((rect) => rect.width > 0 && rect.height > 0)
1388
+ .map(relativeRect),
1389
+ );
1390
+ }
1391
+ }
1392
+ const left = Math.min(
1393
+ OUTPUT_WIDTH,
1394
+ Math.max(0, Math.min(...bounds.map((candidate) => candidate.x)) - padding),
1395
+ );
1396
+ const top = Math.min(
1397
+ OUTPUT_HEIGHT,
1398
+ Math.max(0, Math.min(...bounds.map((candidate) => candidate.y)) - padding),
1399
+ );
1400
+ const right = Math.min(
1401
+ OUTPUT_WIDTH,
1402
+ Math.max(
1403
+ 0,
1404
+ Math.max(...bounds.map((candidate) => candidate.x + candidate.width)) + padding,
1405
+ ),
1406
+ );
1407
+ const bottom = Math.min(
1408
+ OUTPUT_HEIGHT,
1409
+ Math.max(
1410
+ 0,
1411
+ Math.max(...bounds.map((candidate) => candidate.y + candidate.height)) + padding,
1412
+ ),
1413
+ );
1414
+ return {
1415
+ x: roundedMetric(left),
1416
+ y: roundedMetric(top),
1417
+ width: roundedMetric(Math.max(0, right - left)),
1418
+ height: roundedMetric(Math.max(0, bottom - top)),
1419
+ };
1420
+ }
1421
+
1422
+ function pptxFallback(type, element, deck, reason, options = {}) {
1423
+ const bounds = fallbackBounds(
1424
+ element,
1425
+ deck,
1426
+ options.padding,
1427
+ options.includeDescendants,
1428
+ );
1429
+ const artwork = options.artwork !== false && bounds.width > 0 && bounds.height > 0;
1430
+ const captureElement = options.captureElement || element;
1431
+ let captureId;
1432
+ if (artwork) {
1433
+ captureId = `pptx-fallback-${++pptxFallbackSequence}`;
1434
+ const ids = new Set(
1435
+ (captureElement.getAttribute("data-pptx-fallback-ids") || "")
1436
+ .split(/\s+/)
1437
+ .filter(Boolean),
1438
+ );
1439
+ ids.add(captureId);
1440
+ captureElement.setAttribute("data-pptx-fallback-ids", [...ids].join(" "));
1441
+ pptxFallbackCaptureElements.set(captureId, captureElement);
1442
+ }
1443
+ const sourceOrder = Number(element.dataset.pptxZOrder);
1444
+ const zOrder = Number.isFinite(sourceOrder)
1445
+ ? sourceOrder - (options.behindNative ? 0.25 : 0)
1446
+ : undefined;
1128
1447
  return {
1129
1448
  type,
1130
1449
  path: elementPath(element, deck),
1131
1450
  reason,
1132
- ...relativeBounds(element, deck),
1451
+ ...bounds,
1452
+ ...(captureId ? { captureId } : {}),
1453
+ ...(zOrder !== undefined ? { zOrder } : {}),
1454
+ ...(!artwork ? { artwork: false } : {}),
1133
1455
  };
1134
1456
  }
1135
1457
 
1458
+ function styleHasVisualDecoration(style) {
1459
+ const backgroundVisible =
1460
+ style.backgroundImage !== "none" ||
1461
+ !["transparent", "rgba(0, 0, 0, 0)"].includes(style.backgroundColor);
1462
+ const borderVisible = ["Top", "Right", "Bottom", "Left"].some(
1463
+ (side) =>
1464
+ Number.parseFloat(style[`border${side}Width`]) > 0 &&
1465
+ style[`border${side}Style`] !== "none" &&
1466
+ !["transparent", "rgba(0, 0, 0, 0)"].includes(style[`border${side}Color`]),
1467
+ );
1468
+ return backgroundVisible || borderVisible;
1469
+ }
1470
+
1471
+ function hasVisualDecoration(element) {
1472
+ return styleHasVisualDecoration(getComputedStyle(element));
1473
+ }
1474
+
1475
+ function hasVisiblePseudoElement(element) {
1476
+ return ["::before", "::after"].some((pseudo) => {
1477
+ const style = getComputedStyle(element, pseudo);
1478
+ const contentVisible = !["none", "normal", '""', "''"].includes(style.content);
1479
+ const paintedEmptyContent =
1480
+ styleHasVisualDecoration(style) &&
1481
+ Number.parseFloat(style.width) > 0 &&
1482
+ Number.parseFloat(style.height) > 0;
1483
+ return (
1484
+ style.display !== "none" &&
1485
+ style.visibility !== "hidden" &&
1486
+ (contentVisible || paintedEmptyContent)
1487
+ );
1488
+ });
1489
+ }
1490
+
1491
+ function collectPptxLayoutElements(deck) {
1492
+ const elements = [];
1493
+ for (const image of deck.querySelectorAll(":scope > .theme-cover-logo")) {
1494
+ const style = getComputedStyle(image);
1495
+ if (
1496
+ !directImageSupported(image) ||
1497
+ unsupportedEffects(image).length > 0 ||
1498
+ Number.parseFloat(style.borderRadius) > 0
1499
+ ) {
1500
+ continue;
1501
+ }
1502
+ const opacity = Number(style.opacity);
1503
+ elements.push({
1504
+ type: "image",
1505
+ path: elementPath(image, deck),
1506
+ ...relativeBounds(image, deck),
1507
+ src: image.currentSrc || image.src,
1508
+ alt: image.alt || "",
1509
+ fit: style.objectFit || "contain",
1510
+ opacity: Number.isFinite(opacity) ? opacity : 1,
1511
+ naturalWidth: image.naturalWidth,
1512
+ naturalHeight: image.naturalHeight,
1513
+ });
1514
+ image.setAttribute("data-pptx-native", "image");
1515
+ }
1516
+ return elements;
1517
+ }
1518
+
1136
1519
  function preserveBoxShadow(element, deck) {
1137
1520
  const style = getComputedStyle(element);
1138
- if (!style.boxShadow || style.boxShadow === "none") return;
1521
+ if (!style.boxShadow || style.boxShadow === "none") return null;
1522
+ element.setAttribute("data-pptx-shadow-fallback", "true");
1139
1523
  const bounds = relativeBounds(element, deck);
1140
1524
  const decoration = document.createElement("div");
1141
1525
  decoration.className = "pptx-effect-fallback";
@@ -1151,6 +1535,7 @@ function preserveBoxShadow(element, deck) {
1151
1535
  });
1152
1536
  decoration.setAttribute("aria-hidden", "true");
1153
1537
  deck.appendChild(decoration);
1538
+ return decoration;
1154
1539
  }
1155
1540
 
1156
1541
  function blobDataUrl(blob) {
@@ -1430,6 +1815,7 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
1430
1815
  wrapper,
1431
1816
  deck,
1432
1817
  `foreground-picture-failed: ${error?.message || "unknown error"}`,
1818
+ { artwork: false },
1433
1819
  ),
1434
1820
  );
1435
1821
  }
@@ -1497,6 +1883,7 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
1497
1883
  ? "architecture-image-rendered-as-foreground-picture"
1498
1884
  : "architecture-image-rendered-as-artwork",
1499
1885
  ...mapBounds(sourceObject),
1886
+ ...(foregroundReady && layer ? { artwork: false } : {}),
1500
1887
  });
1501
1888
  if (foregroundReady && layer) elements.push(foregroundElement(layer));
1502
1889
  continue;
@@ -1517,6 +1904,7 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
1517
1904
  : "icon-rendered-as-artwork",
1518
1905
  icon: icon.icon,
1519
1906
  ...mapBounds(icon),
1907
+ ...(foregroundReady && layer ? { artwork: false } : {}),
1520
1908
  });
1521
1909
  }
1522
1910
  for (const sourceObject of snapshot.objects) {
@@ -1574,35 +1962,90 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
1574
1962
 
1575
1963
  async function collectPptxSlide(slide, index) {
1576
1964
  const { deck } = slide;
1965
+ assignPptxPaintOrder(deck);
1577
1966
  const elements = [];
1578
1967
  const fallbacks = [];
1579
1968
  const fallbackRoots = new Set();
1580
- const addFallback = (type, element, reason) => {
1969
+ const fallbackByRoot = new Map();
1970
+ const removeFallback = (root) => {
1971
+ const fallback = fallbackByRoot.get(root);
1972
+ if (!fallback) return;
1973
+ fallbackByRoot.delete(root);
1974
+ fallbackRoots.delete(root);
1975
+ const index = fallbacks.indexOf(fallback);
1976
+ if (index >= 0) fallbacks.splice(index, 1);
1977
+ if (!fallback.captureId) return;
1978
+ const captureElement = pptxFallbackCaptureElements.get(fallback.captureId);
1979
+ pptxFallbackCaptureElements.delete(fallback.captureId);
1980
+ if (!captureElement) return;
1981
+ const ids = (captureElement.getAttribute("data-pptx-fallback-ids") || "")
1982
+ .split(/\s+/)
1983
+ .filter((id) => id && id !== fallback.captureId);
1984
+ if (ids.length) captureElement.setAttribute("data-pptx-fallback-ids", ids.join(" "));
1985
+ else captureElement.removeAttribute("data-pptx-fallback-ids");
1986
+ };
1987
+ const addFallback = (type, element, reason, options) => {
1581
1988
  if (fallbackRoots.has(element)) return;
1989
+ if ([...fallbackRoots].some((root) => root.contains(element))) return;
1990
+ [...fallbackRoots]
1991
+ .filter((root) => element.contains(root))
1992
+ .forEach(removeFallback);
1582
1993
  fallbackRoots.add(element);
1583
- fallbacks.push(pptxFallback(type, element, deck, reason));
1994
+ const fallback = pptxFallback(type, element, deck, reason, options);
1995
+ fallbackByRoot.set(element, fallback);
1996
+ fallbacks.push(fallback);
1584
1997
  };
1585
1998
 
1999
+ const effectFallbacks = new Map();
2000
+ const genericShadowElements = new Map();
1586
2001
  for (const element of deck.querySelectorAll("header *, .body, .body *, footer *")) {
1587
2002
  if (element.closest("pre, .architecture-diagram, .architecture-error")) continue;
1588
- const effects = unsupportedEffects(element).filter((effect) => effect !== "box-shadow");
2003
+ const allEffects = unsupportedEffects(element);
2004
+ if (
2005
+ allEffects.includes("box-shadow") &&
2006
+ !element.closest("table, img")
2007
+ ) {
2008
+ genericShadowElements.set(
2009
+ element,
2010
+ effectPaintPadding(element, ["box-shadow"]),
2011
+ );
2012
+ }
2013
+ const effects = allEffects.filter((effect) => effect !== "box-shadow");
1589
2014
  if (!effects.length) continue;
1590
2015
  const root = effectFallbackRoot(element);
1591
2016
  if (root) {
1592
- addFallback(
1593
- "effect",
1594
- root,
1595
- `element-rendered-as-artwork: ${effects.join(", ")}`,
1596
- );
2017
+ const pending = effectFallbacks.get(root) || { effects: new Set(), padding: 0 };
2018
+ effects.forEach((effect) => pending.effects.add(effect));
2019
+ pending.padding = Math.max(pending.padding, effectPaintPadding(element, allEffects));
2020
+ effectFallbacks.set(root, pending);
1597
2021
  }
1598
2022
  }
2023
+ for (const [root, pending] of effectFallbacks) {
2024
+ addFallback(
2025
+ "effect",
2026
+ root,
2027
+ `element-rendered-as-artwork: ${[...pending.effects].join(", ")}`,
2028
+ { padding: pending.padding, includeDescendants: true },
2029
+ );
2030
+ }
1599
2031
 
1600
- deck.querySelectorAll("pre.mermaid").forEach((element) =>
1601
- addFallback("mermaid", element, "mermaid-rendered-as-artwork"),
1602
- );
1603
- deck.querySelectorAll("pre:not(.mermaid)").forEach((element) =>
1604
- addFallback("code", element, "code-block-rendered-as-artwork"),
1605
- );
2032
+ deck.querySelectorAll("pre:not(.mermaid)").forEach((element) => {
2033
+ const effects = unsupportedEffects(element);
2034
+ const artworkEffects = effects.filter(
2035
+ (effect) => effect !== "box-shadow",
2036
+ );
2037
+ if (artworkEffects.length) {
2038
+ addFallback(
2039
+ "code",
2040
+ element,
2041
+ `code-block-rendered-as-artwork: ${artworkEffects.join(", ")}`,
2042
+ {
2043
+ padding: effectPaintPadding(element, effects),
2044
+ includeDescendants: true,
2045
+ },
2046
+ );
2047
+ }
2048
+ });
1606
2049
  deck.querySelectorAll(".architecture-error").forEach((element) =>
1607
2050
  addFallback("architecture", element, "architecture-error-rendered-as-artwork"),
1608
2051
  );
@@ -1611,10 +2054,20 @@ async function collectPptxSlide(slide, index) {
1611
2054
  ".body div:not(.architecture-diagram):not(.architecture-error):not(.architecture-routing-warning), .body section, .body article, .body aside, .body details, .body video, .body audio, .body iframe, .body canvas, .body object, .body embed",
1612
2055
  )
1613
2056
  .forEach((element) => {
1614
- if (!element.closest(".architecture-diagram")) {
1615
- addFallback("html", element, "arbitrary-html-rendered-as-artwork");
2057
+ const covered = [...fallbackRoots].some(
2058
+ (root) => root === element || root.contains(element),
2059
+ );
2060
+ if (!element.closest("pre, .architecture-diagram") && !covered) {
2061
+ addFallback("html", element, "arbitrary-html-rendered-as-artwork", {
2062
+ includeDescendants: true,
2063
+ padding: subtreeEffectPaintPadding(element),
2064
+ });
1616
2065
  }
1617
2066
  });
2067
+ deck.querySelectorAll("pre.mermaid").forEach((element) => {
2068
+ const covered = [...fallbackRoots].some((root) => root === element || root.contains(element));
2069
+ if (!covered) addFallback("mermaid", element, "mermaid-rendered-as-artwork");
2070
+ });
1618
2071
 
1619
2072
  const insideFallback = (element) =>
1620
2073
  [...fallbackRoots].some((root) => root === element || root.contains(element));
@@ -1629,31 +2082,18 @@ async function collectPptxSlide(slide, index) {
1629
2082
  !element.closest("table") &&
1630
2083
  !(element.matches("p") && element.closest("blockquote, li")),
1631
2084
  );
1632
- for (const element of textCandidates) {
1633
- const list = element.matches("li")
1634
- ? [...element.querySelectorAll(":scope > ul, :scope > ol")]
1635
- : [];
1636
- const parentList = element.matches("li") ? element.parentElement : null;
1637
- const actualLevel = element.matches("li")
1638
- ? Math.max(0, [...element.closest(".body").querySelectorAll("ul, ol")].filter((candidate) =>
1639
- candidate.contains(element),
1640
- ).length - 1)
1641
- : undefined;
2085
+ const listItems = textCandidates.filter((element) => element.matches("li"));
2086
+ const eligibleListItems = new Set(listItems);
2087
+ const listRoots = [
2088
+ ...new Set(listItems.map(outermostListFor).filter(Boolean)),
2089
+ ];
2090
+ for (const list of listRoots) {
2091
+ const textElement = nativeListTextElement(list, deck, eligibleListItems);
2092
+ if (textElement) elements.push(textElement);
2093
+ }
2094
+ for (const element of textCandidates.filter((candidate) => !candidate.matches("li"))) {
1642
2095
  const paragraph = paragraphFor(element, {
1643
- omitNestedLists: list.length > 0,
1644
- level: actualLevel,
1645
- bullet: parentList
1646
- ? {
1647
- type: parentList.tagName === "OL" ? "number" : "bullet",
1648
- character:
1649
- parentList.tagName === "OL"
1650
- ? `${Number(parentList.getAttribute("start") || 1) + [...parentList.children].indexOf(element)}.`
1651
- : "•",
1652
- ...(parentList.tagName === "OL"
1653
- ? { start: Number(parentList.getAttribute("start") || 1) + [...parentList.children].indexOf(element) }
1654
- : {}),
1655
- }
1656
- : undefined,
2096
+ omitNestedLists: false,
1657
2097
  });
1658
2098
  if (!paragraph.runs.some((run) => run.text.trim())) continue;
1659
2099
  const disableTextWrap = renderedTextLineCount(element) === 1;
@@ -1676,6 +2116,7 @@ async function collectPptxSlide(slide, index) {
1676
2116
  type: "text",
1677
2117
  path: elementPath(element, deck),
1678
2118
  ...bounds,
2119
+ zOrder: Number(element.dataset.pptxZOrder),
1679
2120
  paragraphs: [paragraph],
1680
2121
  opacity: Number(getComputedStyle(element).opacity) || 1,
1681
2122
  ...(fittedTextInsets ? { textInsets: fittedTextInsets } : {}),
@@ -1683,6 +2124,117 @@ async function collectPptxSlide(slide, index) {
1683
2124
  });
1684
2125
  element.setAttribute("data-pptx-native", "text");
1685
2126
  }
2127
+ for (const element of textCandidates) {
2128
+ const pseudoElementVisible = hasVisiblePseudoElement(element);
2129
+ if (
2130
+ element.closest("footer") ||
2131
+ element.classList.contains("kicker") ||
2132
+ (!hasVisualDecoration(element) && !pseudoElementVisible)
2133
+ ) {
2134
+ continue;
2135
+ }
2136
+ fallbacks.push(
2137
+ pptxFallback(
2138
+ "decoration",
2139
+ element,
2140
+ deck,
2141
+ "native-text-decoration-rendered-as-artwork",
2142
+ { behindNative: true, padding: pseudoElementVisible ? 32 : 0 },
2143
+ ),
2144
+ );
2145
+ }
2146
+ deck.querySelectorAll(".kicker").forEach((element) => {
2147
+ if (insideFallback(element)) return;
2148
+ fallbacks.push(
2149
+ pptxFallback("decoration", element, deck, "kicker-mark-rendered-as-artwork", {
2150
+ behindNative: true,
2151
+ }),
2152
+ );
2153
+ });
2154
+ deck.querySelectorAll("footer").forEach((element) => {
2155
+ if (insideFallback(element)) return;
2156
+ fallbacks.push(
2157
+ pptxFallback("decoration", element, deck, "footer-decoration-rendered-as-artwork", {
2158
+ behindNative: true,
2159
+ }),
2160
+ );
2161
+ });
2162
+ deck.querySelectorAll(".body hr").forEach((element) => {
2163
+ if (insideFallback(element)) return;
2164
+ fallbacks.push(
2165
+ pptxFallback("decoration", element, deck, "horizontal-rule-rendered-as-artwork"),
2166
+ );
2167
+ });
2168
+ for (const [element, padding] of genericShadowElements) {
2169
+ if (insideFallback(element)) continue;
2170
+ const decoration = preserveBoxShadow(element, deck);
2171
+ fallbacks.push(
2172
+ pptxFallback("effect", element, deck, "native-element-approximates: box-shadow", {
2173
+ padding,
2174
+ behindNative: true,
2175
+ captureElement: decoration || element,
2176
+ }),
2177
+ );
2178
+ }
2179
+
2180
+ for (const pre of deck.querySelectorAll("pre:not(.mermaid)")) {
2181
+ if (insideFallback(pre)) continue;
2182
+ const code = pre.querySelector("code") || pre;
2183
+ const style = getComputedStyle(pre);
2184
+ const bounds = relativeBounds(pre, deck);
2185
+ const borderRadius = roundedMetric(Number.parseFloat(style.borderRadius));
2186
+ const borderWidth = roundedMetric(Number.parseFloat(style.borderTopWidth));
2187
+ const textInsets = textInsetsFor(pre);
2188
+ elements.push({
2189
+ type: "shape",
2190
+ path: elementPath(pre, deck),
2191
+ shape: borderRadius > 0 ? "roundedRect" : "rect",
2192
+ ...bounds,
2193
+ zOrder: Number(pre.dataset.pptxZOrder),
2194
+ fill: normalizeCssColor(style.backgroundColor),
2195
+ stroke: normalizeCssColor(style.borderTopColor),
2196
+ strokeWidth: borderWidth || 1,
2197
+ opacity: Number(style.opacity) || 1,
2198
+ paragraphs: codeParagraphsFor(code),
2199
+ verticalAlignment: "top",
2200
+ ...(textInsets ? { textInsets } : {}),
2201
+ textWrap: "none",
2202
+ });
2203
+ const accentWidth = roundedMetric(Number.parseFloat(style.borderLeftWidth));
2204
+ const accentColor = normalizeCssColor(style.borderLeftColor);
2205
+ if (accentWidth > borderWidth && accentColor) {
2206
+ const accentInset = Math.min(borderRadius, Math.max(0, (bounds.height - accentWidth) / 2));
2207
+ elements.push({
2208
+ type: "shape",
2209
+ path: `${elementPath(pre, deck)}.accent`,
2210
+ shape: borderRadius > 0 ? "roundedRect" : "rect",
2211
+ x: bounds.x,
2212
+ y: bounds.y + accentInset,
2213
+ width: accentWidth,
2214
+ height: bounds.height - accentInset * 2,
2215
+ fill: accentColor,
2216
+ stroke: null,
2217
+ zOrder: Number(pre.dataset.pptxZOrder) + 0.01,
2218
+ });
2219
+ }
2220
+ if (style.boxShadow && style.boxShadow !== "none") {
2221
+ const decoration = preserveBoxShadow(pre, deck);
2222
+ fallbacks.push(
2223
+ pptxFallback(
2224
+ "effect",
2225
+ pre,
2226
+ deck,
2227
+ "native-code-approximates: box-shadow",
2228
+ {
2229
+ padding: effectPaintPadding(pre, ["box-shadow"]),
2230
+ behindNative: true,
2231
+ captureElement: decoration || pre,
2232
+ },
2233
+ ),
2234
+ );
2235
+ }
2236
+ pre.setAttribute("data-pptx-native", "code");
2237
+ }
1686
2238
 
1687
2239
  for (const table of deck.querySelectorAll(".body table")) {
1688
2240
  if (insideFallback(table)) continue;
@@ -1746,18 +2298,24 @@ async function collectPptxSlide(slide, index) {
1746
2298
  type: "table",
1747
2299
  path: elementPath(table, deck),
1748
2300
  ...relativeBounds(table, deck),
2301
+ zOrder: Number(table.dataset.pptxZOrder),
1749
2302
  rows,
1750
2303
  });
1751
2304
  if (effects.length) {
2305
+ const decoration = effects.includes("box-shadow") ? preserveBoxShadow(table, deck) : null;
1752
2306
  fallbacks.push(
1753
2307
  pptxFallback(
1754
2308
  "effect",
1755
2309
  table,
1756
2310
  deck,
1757
2311
  `native-table-approximates: ${effects.join(", ")}`,
2312
+ {
2313
+ padding: effectPaintPadding(table, effects),
2314
+ behindNative: true,
2315
+ captureElement: decoration || table,
2316
+ },
1758
2317
  ),
1759
2318
  );
1760
- if (effects.includes("box-shadow")) preserveBoxShadow(table, deck);
1761
2319
  }
1762
2320
  table.setAttribute("data-pptx-native", "table");
1763
2321
  }
@@ -1766,25 +2324,29 @@ async function collectPptxSlide(slide, index) {
1766
2324
  if (
1767
2325
  image.closest(".architecture-diagram") ||
1768
2326
  image.classList.contains("theme-cover-background") ||
2327
+ image.classList.contains("theme-cover-logo") ||
1769
2328
  insideFallback(image)
1770
2329
  ) {
1771
2330
  continue;
1772
2331
  }
1773
- if (!rasterImageSupported(image)) {
2332
+ if (!directImageSupported(image)) {
1774
2333
  const fit = getComputedStyle(image).objectFit;
2334
+ const effects = unsupportedEffects(image);
1775
2335
  addFallback(
1776
2336
  "image",
1777
2337
  image,
1778
2338
  ["cover", "none"].includes(fit) ? "unsupported-image-fit" : "unsupported-image-format",
2339
+ {
2340
+ padding: effectPaintPadding(image, effects),
2341
+ includeDescendants: true,
2342
+ },
1779
2343
  );
1780
2344
  continue;
1781
2345
  }
1782
2346
  const style = getComputedStyle(image);
1783
2347
  const effects = unsupportedEffects(image);
1784
- if (parseFloat(style.borderRadius) > 0) effects.push("border-radius");
1785
- const artworkEffects = effects.filter(
1786
- (effect) => effect !== "box-shadow" && effect !== "border-radius",
1787
- );
2348
+ const borderRadius = parseFloat(style.borderRadius);
2349
+ const artworkEffects = effects.filter((effect) => effect !== "box-shadow");
1788
2350
  if (artworkEffects.length) {
1789
2351
  addFallback(
1790
2352
  "effect",
@@ -1797,10 +2359,12 @@ async function collectPptxSlide(slide, index) {
1797
2359
  type: "image",
1798
2360
  path: elementPath(image, deck),
1799
2361
  ...relativeBounds(image, deck),
2362
+ zOrder: Number(image.dataset.pptxZOrder),
1800
2363
  src: image.currentSrc || image.src,
1801
2364
  alt: image.alt || "",
1802
2365
  fit: style.objectFit || "contain",
1803
2366
  opacity: Number(style.opacity) || 1,
2367
+ shape: borderRadius > 0 ? "roundedRect" : "rect",
1804
2368
  naturalWidth: image.naturalWidth,
1805
2369
  naturalHeight: image.naturalHeight,
1806
2370
  });
@@ -1812,6 +2376,10 @@ async function collectPptxSlide(slide, index) {
1812
2376
  image,
1813
2377
  deck,
1814
2378
  `native-image-approximates: ${effects.join(", ")}`,
2379
+ {
2380
+ padding: effectPaintPadding(image, effects),
2381
+ behindNative: true,
2382
+ },
1815
2383
  ),
1816
2384
  );
1817
2385
  }
@@ -1821,7 +2389,13 @@ async function collectPptxSlide(slide, index) {
1821
2389
  for (const [blockIndex, wrapper] of architectureWrappers.entries()) {
1822
2390
  if (insideFallback(wrapper)) continue;
1823
2391
  const architecture = await collectArchitectureObjects(wrapper, deck, blockIndex);
1824
- elements.push(...architecture.elements);
2392
+ const zOrder = Number(wrapper.dataset.pptxZOrder);
2393
+ elements.push(
2394
+ ...architecture.elements.map((element, elementIndex) => ({
2395
+ ...element,
2396
+ zOrder: zOrder + elementIndex / 1000,
2397
+ })),
2398
+ );
1825
2399
  fallbacks.push(...architecture.fallbacks);
1826
2400
  }
1827
2401
 
@@ -1833,12 +2407,13 @@ async function collectPptxSlide(slide, index) {
1833
2407
  ? "center"
1834
2408
  : slide.backcoverSlide
1835
2409
  ? "backcover"
1836
- : "standard";
2410
+ : "default";
1837
2411
  const visibleTitle = deck.querySelector("h1, h2")?.textContent?.trim();
1838
2412
  const notes = speakerNotesToPlainText(slide.speakerNotes, window.marked, document);
1839
2413
  return {
1840
2414
  index,
1841
2415
  layout,
2416
+ layoutId: `${slide.theme}:${layout}`,
1842
2417
  theme: slide.theme,
1843
2418
  title: visibleTitle || slide.title,
1844
2419
  width: OUTPUT_WIDTH,
@@ -1849,6 +2424,23 @@ async function collectPptxSlide(slide, index) {
1849
2424
  };
1850
2425
  }
1851
2426
 
2427
+ function createPptxLayoutTemplate(theme, layout) {
2428
+ const markdown = `---
2429
+ layout: ${layout}
2430
+ theme: ${theme}
2431
+ ---
2432
+ `;
2433
+ const slide = createSlide(markdown, theme, true);
2434
+ if (layout === "backcover") {
2435
+ slide.deck
2436
+ .querySelectorAll(".theme-backcover-logo, .theme-backcover-copyright")
2437
+ .forEach((element) => element.remove());
2438
+ }
2439
+ slide.deck.classList.add("pptx-layout-template");
2440
+ slide.deck.dataset.pptxLayoutId = `${theme}:${layout}`;
2441
+ return slide;
2442
+ }
2443
+
1852
2444
  async function renderPptxDeck(
1853
2445
  slides,
1854
2446
  theme,
@@ -1862,6 +2454,8 @@ async function renderPptxDeck(
1862
2454
  applyCustomThemeCss(customCss);
1863
2455
  document.documentElement.setAttribute("data-theme", deckTheme);
1864
2456
  document.body.classList.add("pptx-mode", "fixed-output-mode", "mermaid-loading");
2457
+ pptxFallbackSequence = 0;
2458
+ pptxFallbackCaptureElements.clear();
1865
2459
  const rendered = slides.map((markdown) => createSlide(markdown, deckTheme));
1866
2460
  const stage = document.getElementById("stage");
1867
2461
  stage.replaceChildren(...rendered.map((slide) => slide.deck));
@@ -1890,14 +2484,39 @@ async function renderPptxDeck(
1890
2484
  for (const [index, slide] of rendered.entries()) {
1891
2485
  pptxSlides.push(await collectPptxSlide(slide, index));
1892
2486
  }
2487
+ const themes = [...new Set(rendered.map((slide) => slide.theme))];
2488
+ const layoutTemplates = themes.flatMap((slideTheme) =>
2489
+ PPTX_LAYOUT_NAMES.map((layout) => ({
2490
+ id: `${slideTheme}:${layout}`,
2491
+ name: layout,
2492
+ theme: slideTheme,
2493
+ slide: createPptxLayoutTemplate(slideTheme, layout),
2494
+ })),
2495
+ );
2496
+ stage.append(...layoutTemplates.map((layout) => layout.slide.deck));
2497
+ await waitForImages(stage);
2498
+ await afterLayout();
2499
+ const pptxLayouts = layoutTemplates.map(({ id, name, theme: slideTheme, slide }, index) => ({
2500
+ id,
2501
+ name,
2502
+ theme: slideTheme,
2503
+ captureIndex: rendered.length + index,
2504
+ elements: collectPptxLayoutElements(slide.deck),
2505
+ }));
1893
2506
  const model = {
1894
2507
  version: 1,
1895
2508
  width: OUTPUT_WIDTH,
1896
2509
  height: OUTPUT_HEIGHT,
2510
+ masters: themes.map((slideTheme) => ({
2511
+ id: slideTheme,
2512
+ theme: slideTheme,
2513
+ layoutIds: PPTX_LAYOUT_NAMES.map((layout) => `${slideTheme}:${layout}`),
2514
+ })),
2515
+ layouts: pptxLayouts,
1897
2516
  slides: pptxSlides,
1898
2517
  };
1899
2518
  window.__presentationPptxModel = JSON.parse(JSON.stringify(model));
1900
- document.body.classList.add("pptx-artwork-mode");
2519
+ document.body.classList.add("pptx-artwork-mode", "pptx-layout-artwork-mode");
1901
2520
  document.body.setAttribute("data-pptx-artwork", "ready");
1902
2521
  document.body.classList.remove("mermaid-loading");
1903
2522
  document.documentElement.setAttribute("data-pptx-ready", "true");