@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
|
@@ -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
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
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
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
-
|
|
1615
|
-
|
|
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
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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:
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
: "
|
|
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");
|
|
@@ -2148,6 +2767,7 @@ let currentVersion = -1;
|
|
|
2148
2767
|
let knownDeckVersion = -1;
|
|
2149
2768
|
let deckSlides = [];
|
|
2150
2769
|
let deckTitles = [];
|
|
2770
|
+
let deckLayouts = [];
|
|
2151
2771
|
let navIndex = 0;
|
|
2152
2772
|
let navTotal = 0;
|
|
2153
2773
|
let navMode = "deck";
|
|
@@ -2189,6 +2809,11 @@ function deriveTitle(md) {
|
|
|
2189
2809
|
return fallback ? trimTitle(fallback) : "(Untitled)";
|
|
2190
2810
|
}
|
|
2191
2811
|
|
|
2812
|
+
function deriveLayout(md) {
|
|
2813
|
+
const { meta } = splitFrontMatter(typeof md === "string" ? md : "");
|
|
2814
|
+
return typeof meta.layout === "string" ? meta.layout.trim().toLowerCase() : "";
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2192
2817
|
function trimTitle(text) {
|
|
2193
2818
|
const stripped = text
|
|
2194
2819
|
.replace(/[*_`>#~]/g, "")
|
|
@@ -2205,6 +2830,7 @@ async function fetchDeck() {
|
|
|
2205
2830
|
if (Array.isArray(data.slides)) {
|
|
2206
2831
|
deckSlides = data.slides;
|
|
2207
2832
|
deckTitles = deckSlides.map(deriveTitle);
|
|
2833
|
+
deckLayouts = deckSlides.map(deriveLayout);
|
|
2208
2834
|
}
|
|
2209
2835
|
if (typeof data.deckVersion === "number") knownDeckVersion = data.deckVersion;
|
|
2210
2836
|
buildOverview();
|
|
@@ -3032,24 +3658,51 @@ function buildOverview() {
|
|
|
3032
3658
|
const list = document.getElementById("overviewList");
|
|
3033
3659
|
if (!list) return;
|
|
3034
3660
|
list.replaceChildren();
|
|
3661
|
+
let insideSection = false;
|
|
3035
3662
|
deckTitles.forEach((title, i) => {
|
|
3663
|
+
const layout = deckLayouts[i] || "";
|
|
3664
|
+
if (layout === "title" || layout === "backcover") insideSection = false;
|
|
3665
|
+
const sectionChild =
|
|
3666
|
+
insideSection && layout !== "title" && layout !== "section" && layout !== "backcover";
|
|
3667
|
+
|
|
3036
3668
|
const li = document.createElement("li");
|
|
3037
3669
|
li.className = "overview-item";
|
|
3670
|
+
if (layout === "title" || layout === "section") {
|
|
3671
|
+
li.classList.add(`overview-item-${layout}`);
|
|
3672
|
+
}
|
|
3673
|
+
if (sectionChild) li.classList.add("overview-item-section-child");
|
|
3038
3674
|
li.dataset.index = String(i);
|
|
3675
|
+
if (layout) li.dataset.layout = layout;
|
|
3676
|
+
|
|
3039
3677
|
const btn = document.createElement("button");
|
|
3040
3678
|
btn.type = "button";
|
|
3041
3679
|
btn.className = "overview-link";
|
|
3680
|
+
btn.setAttribute(
|
|
3681
|
+
"aria-label",
|
|
3682
|
+
`${i + 1} ${title}${layout === "title" || layout === "section" ? `, ${layout} slide` : ""}`,
|
|
3683
|
+
);
|
|
3684
|
+
|
|
3042
3685
|
const num = document.createElement("span");
|
|
3043
3686
|
num.className = "overview-num";
|
|
3044
3687
|
num.textContent = String(i + 1);
|
|
3045
3688
|
const label = document.createElement("span");
|
|
3046
3689
|
label.className = "overview-label";
|
|
3047
3690
|
label.textContent = title;
|
|
3691
|
+
|
|
3048
3692
|
btn.appendChild(num);
|
|
3693
|
+
if (layout === "title" || layout === "section") {
|
|
3694
|
+
const kind = document.createElement("span");
|
|
3695
|
+
kind.className = "overview-kind";
|
|
3696
|
+
kind.textContent = layout === "title" ? "Title" : "Section";
|
|
3697
|
+
kind.setAttribute("aria-hidden", "true");
|
|
3698
|
+
btn.appendChild(kind);
|
|
3699
|
+
}
|
|
3049
3700
|
btn.appendChild(label);
|
|
3050
3701
|
btn.addEventListener("click", () => goToIndex(i));
|
|
3051
3702
|
li.appendChild(btn);
|
|
3052
3703
|
list.appendChild(li);
|
|
3704
|
+
|
|
3705
|
+
if (layout === "section") insideSection = true;
|
|
3053
3706
|
});
|
|
3054
3707
|
highlightOverview();
|
|
3055
3708
|
}
|
|
@@ -3478,13 +4131,9 @@ function init() {
|
|
|
3478
4131
|
return;
|
|
3479
4132
|
}
|
|
3480
4133
|
if (params.get("print") === "1") {
|
|
3481
|
-
// Print mode
|
|
3482
|
-
//
|
|
3483
|
-
//
|
|
3484
|
-
// This early return is also **the primary fix for the #12 hang**. Only print
|
|
3485
|
-
// mode avoids connectEvents() (an unclosed SSE) and the two-second setInterval,
|
|
3486
|
-
// allowing the page to become idle and --print-to-pdf to complete.
|
|
3487
|
-
// Removing the return makes printing hang forever.
|
|
4134
|
+
// Print mode renders one immutable output snapshot and never reaches editing,
|
|
4135
|
+
// SSE, or polling branches. The output runtime waits for its ready report
|
|
4136
|
+
// before asking Chromium to create the PDF.
|
|
3488
4137
|
initPrint(params).catch(reportPrintBootstrapFailure);
|
|
3489
4138
|
return;
|
|
3490
4139
|
}
|