@markdstage/markdstage 0.1.3 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -6
- package/package.json +3 -1
- package/shared/README.md +55 -31
- package/shared/architecture-editor/editor.css +155 -0
- package/shared/architecture-editor/editor.js +1775 -0
- package/shared/architecture-editor/index.html +99 -0
- package/shared/markdstage-guide.mjs +2 -2
- package/shared/renderer/architecture.mjs +233 -0
- package/shared/renderer/index.html +53 -12
- package/shared/renderer/renderer.js +1263 -25
- package/shared/renderer/slides.css +81 -6
- package/shared/runtime/architecture-editor-server.mjs +651 -0
- package/shared/runtime/architecture-source.mjs +195 -0
- package/shared/runtime/browser.mjs +71 -4
- package/shared/runtime/deck-session.mjs +3 -1
- package/shared/runtime/output-paths.mjs +18 -1
- package/shared/runtime/output.mjs +267 -4
- package/shared/runtime/pptx-package.mjs +1088 -0
- package/shared/runtime/presentation-server.mjs +268 -12
- package/src/cli.mjs +42 -4
- package/src/commands/export.mjs +31 -11
- package/src/commands/present.mjs +138 -49
- package/src/deck.mjs +2 -0
- package/src/runtime.mjs +8 -2
- package/src/skills.mjs +42 -16
|
@@ -90,16 +90,22 @@ let lastMermaidTheme = null;
|
|
|
90
90
|
// Editing mode is available only in normal view, not presenter or print mode.
|
|
91
91
|
// Print mode returns early in init, so presenterMode is the effective branch here.
|
|
92
92
|
let architectureEditMode = false;
|
|
93
|
+
let architectureEditAvailable = false;
|
|
93
94
|
let architectureDetailedEdit = false;
|
|
95
|
+
let architectureDetailedEditTarget = "";
|
|
94
96
|
let presenterMode = false;
|
|
95
97
|
let previewMode = false;
|
|
96
98
|
let previewOffset = 0;
|
|
97
99
|
let navigationEnabled = true;
|
|
98
100
|
let fixedPreviewMode = false;
|
|
101
|
+
let moreControlsOpen = false;
|
|
99
102
|
// Markdown for the most recently rendered slide, retained for editing-mode rerenders.
|
|
100
103
|
let lastMarkdown = "";
|
|
101
104
|
// Editing UI attached to the rendered slide; destroyed on every rerender.
|
|
102
105
|
let architectureEditors = [];
|
|
106
|
+
// Serialize saves from every Architecture block so each request uses the deck
|
|
107
|
+
// version returned by the previous save.
|
|
108
|
+
let architectureSaveQueue = Promise.resolve();
|
|
103
109
|
// `layoutTarget` is the slide currently on screen (cover and back cover
|
|
104
110
|
// included); `autoSize` says whether it also takes part in the font auto-fit.
|
|
105
111
|
let layoutTarget = null;
|
|
@@ -422,12 +428,14 @@ function updateFixedPreviewWarning() {
|
|
|
422
428
|
warning.textContent = "";
|
|
423
429
|
}
|
|
424
430
|
if (button) button.dataset.state = fixedPreviewMode ? "active" : "";
|
|
431
|
+
syncMoreControls();
|
|
425
432
|
return;
|
|
426
433
|
}
|
|
427
434
|
|
|
428
435
|
const diagnostic = collectSlideLayout(layoutTarget, navIndex);
|
|
429
436
|
document.body.classList.toggle("fixed-preview-overflow", diagnostic.pdfClipped);
|
|
430
437
|
if (button) button.dataset.state = diagnostic.pdfClipped ? "error" : "active";
|
|
438
|
+
syncMoreControls();
|
|
431
439
|
if (!warning) return;
|
|
432
440
|
if (!diagnostic.pdfClipped) {
|
|
433
441
|
warning.hidden = true;
|
|
@@ -701,13 +709,15 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
|
|
|
701
709
|
host.className = "architecture-edit-host";
|
|
702
710
|
host.setAttribute("data-architecture-block", String(blockIndex));
|
|
703
711
|
target.replaceWith(host);
|
|
712
|
+
const editorRenderToken = renderToken;
|
|
704
713
|
const editor = attachArchitectureEditor(host, {
|
|
705
714
|
source,
|
|
706
715
|
documentRef: document,
|
|
707
716
|
canOpenDetail: architectureDetailedEdit,
|
|
708
717
|
onOpenDetail: () => openDetailedArchitectureEditor(slideIndex, blockIndex),
|
|
709
718
|
// Return the save result to the editor; omitting it makes failures look successful.
|
|
710
|
-
onCommit: (next) =>
|
|
719
|
+
onCommit: (next) =>
|
|
720
|
+
saveArchitectureBlock(slideIndex, blockIndex, next, editorRenderToken),
|
|
711
721
|
});
|
|
712
722
|
if (!editor) {
|
|
713
723
|
// Do not edit invalid DSL; fall back to the standard error display.
|
|
@@ -759,6 +769,7 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
|
|
|
759
769
|
sizeMode,
|
|
760
770
|
titleSlide,
|
|
761
771
|
sectionSlide,
|
|
772
|
+
centerSlide,
|
|
762
773
|
backcoverSlide,
|
|
763
774
|
title: meta.title || meta.deck || "Slide",
|
|
764
775
|
};
|
|
@@ -771,11 +782,11 @@ function renderSlide(markdown) {
|
|
|
771
782
|
architectureEditors.forEach((editor) => editor.destroy());
|
|
772
783
|
architectureEditors = [];
|
|
773
784
|
applyCustomThemeCss(customThemeCss);
|
|
785
|
+
const token = ++renderToken;
|
|
774
786
|
const slide = createSlide(markdown, deckTheme);
|
|
775
787
|
document.title = slide.title;
|
|
776
788
|
document.documentElement.setAttribute("data-theme", slide.theme);
|
|
777
789
|
|
|
778
|
-
const token = ++renderToken;
|
|
779
790
|
document.body.classList.add("mermaid-loading");
|
|
780
791
|
document.getElementById("stage").replaceChildren(slide.deck);
|
|
781
792
|
if (layoutFrame) {
|
|
@@ -857,6 +868,990 @@ async function reportOutputStatus(token, status, error = "", layout = null) {
|
|
|
857
868
|
if (!response.ok) throw new Error(`Could not report output status (${response.status}).`);
|
|
858
869
|
}
|
|
859
870
|
|
|
871
|
+
const PPTX_RASTER_IMAGE = /\.(?:png|jpe?g|gif)(?:$|[?#])/i;
|
|
872
|
+
|
|
873
|
+
function normalizeCssColor(value) {
|
|
874
|
+
const text = String(value || "").trim();
|
|
875
|
+
if (!text || text === "none" || text === "transparent") return null;
|
|
876
|
+
const shortHex = text.match(/^#([\da-f])([\da-f])([\da-f])$/i);
|
|
877
|
+
if (shortHex) {
|
|
878
|
+
return `#${shortHex.slice(1).map((part) => part.repeat(2)).join("")}`.toUpperCase();
|
|
879
|
+
}
|
|
880
|
+
const hex = text.match(/^#([\da-f]{6})(?:[\da-f]{2})?$/i);
|
|
881
|
+
if (hex) return `#${text.slice(1).toUpperCase()}`;
|
|
882
|
+
const rgb = text.match(
|
|
883
|
+
/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i,
|
|
884
|
+
);
|
|
885
|
+
if (!rgb) return text;
|
|
886
|
+
if (rgb[4] === "0" || rgb[4] === "0%") return null;
|
|
887
|
+
const channels = rgb
|
|
888
|
+
.slice(1, 4)
|
|
889
|
+
.map((part) => Math.max(0, Math.min(255, Math.round(Number(part)))));
|
|
890
|
+
if (rgb[4] !== undefined) {
|
|
891
|
+
const alpha = rgb[4].endsWith("%") ? Number.parseFloat(rgb[4]) / 100 : Number(rgb[4]);
|
|
892
|
+
if (Number.isFinite(alpha) && alpha < 1) {
|
|
893
|
+
return `rgba(${channels.join(", ")}, ${Math.max(0, alpha)})`;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return `#${channels
|
|
897
|
+
.map((part) => part.toString(16).padStart(2, "0"))
|
|
898
|
+
.join("")
|
|
899
|
+
.toUpperCase()}`;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function resolveModelColor(value, context) {
|
|
903
|
+
if (!value || value === "none" || value === "transparent") return null;
|
|
904
|
+
const probe = document.createElement("span");
|
|
905
|
+
probe.style.color = String(value);
|
|
906
|
+
probe.style.position = "absolute";
|
|
907
|
+
probe.style.visibility = "hidden";
|
|
908
|
+
context.appendChild(probe);
|
|
909
|
+
const resolved = normalizeCssColor(getComputedStyle(probe).color);
|
|
910
|
+
probe.remove();
|
|
911
|
+
return resolved;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function relativeBounds(element, deck) {
|
|
915
|
+
const rect = element.getBoundingClientRect();
|
|
916
|
+
const slide = deck.getBoundingClientRect();
|
|
917
|
+
return {
|
|
918
|
+
x: roundedMetric(rect.left - slide.left),
|
|
919
|
+
y: roundedMetric(rect.top - slide.top),
|
|
920
|
+
width: roundedMetric(rect.width),
|
|
921
|
+
height: roundedMetric(rect.height),
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function textContentBounds(element, deck) {
|
|
926
|
+
const range = document.createRange();
|
|
927
|
+
range.selectNodeContents(element);
|
|
928
|
+
const rect = range.getBoundingClientRect();
|
|
929
|
+
if (rect.width <= 0 || rect.height <= 0) return relativeBounds(element, deck);
|
|
930
|
+
const slide = deck.getBoundingClientRect();
|
|
931
|
+
return {
|
|
932
|
+
x: roundedMetric(rect.left - slide.left),
|
|
933
|
+
y: roundedMetric(rect.top - slide.top),
|
|
934
|
+
width: roundedMetric(rect.width),
|
|
935
|
+
height: roundedMetric(rect.height),
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function rasterImageSupported(image) {
|
|
940
|
+
const source = image.currentSrc || image.getAttribute("src") || "";
|
|
941
|
+
if (["cover", "none"].includes(getComputedStyle(image).objectFit)) return false;
|
|
942
|
+
if (/^data:image\/(?:png|jpeg|gif)[;,]/i.test(source)) return true;
|
|
943
|
+
try {
|
|
944
|
+
const url = new URL(source, window.location.href);
|
|
945
|
+
return url.origin === window.location.origin && PPTX_RASTER_IMAGE.test(url.href);
|
|
946
|
+
} catch (_) {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function runStyle(element) {
|
|
952
|
+
const style = getComputedStyle(element);
|
|
953
|
+
const numericWeight = Number.parseInt(style.fontWeight, 10);
|
|
954
|
+
return {
|
|
955
|
+
fontFace: style.fontFamily.split(",")[0].trim().replace(/^["']|["']$/g, ""),
|
|
956
|
+
fontSize: roundedMetric(parseFloat(style.fontSize)),
|
|
957
|
+
bold: Number.isFinite(numericWeight) ? numericWeight >= 600 : style.fontWeight === "bold",
|
|
958
|
+
italic: style.fontStyle !== "normal",
|
|
959
|
+
underline: style.textDecorationLine.includes("underline"),
|
|
960
|
+
color: normalizeCssColor(style.color),
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function collectTextRuns(root, { omitNestedLists = false } = {}) {
|
|
965
|
+
const runs = [];
|
|
966
|
+
const append = (text, element) => {
|
|
967
|
+
if (!text) return;
|
|
968
|
+
const anchor = element.closest("a[href]");
|
|
969
|
+
const run = {
|
|
970
|
+
text,
|
|
971
|
+
...runStyle(element),
|
|
972
|
+
...(anchor ? { href: anchor.getAttribute("href") || anchor.href } : {}),
|
|
973
|
+
};
|
|
974
|
+
const previous = runs.at(-1);
|
|
975
|
+
const previousStyle = previous && { ...previous, text: undefined };
|
|
976
|
+
const nextStyle = { ...run, text: undefined };
|
|
977
|
+
if (previous && JSON.stringify(previousStyle) === JSON.stringify(nextStyle)) {
|
|
978
|
+
previous.text += text;
|
|
979
|
+
} else {
|
|
980
|
+
runs.push(run);
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
const visit = (node) => {
|
|
984
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
985
|
+
append(node.nodeValue || "", node.parentElement || root);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
989
|
+
if (node.tagName === "BR") {
|
|
990
|
+
append("\n", node.parentElement || root);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (
|
|
994
|
+
node !== root &&
|
|
995
|
+
(node.matches("pre, table, .architecture-diagram, .architecture-error, .mermaid") ||
|
|
996
|
+
(omitNestedLists && node.matches("ul, ol")))
|
|
997
|
+
) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
node.childNodes.forEach(visit);
|
|
1001
|
+
};
|
|
1002
|
+
root.childNodes.forEach(visit);
|
|
1003
|
+
return runs;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function pptxAlignment(value) {
|
|
1007
|
+
if (value === "center" || value === "right" || value === "justify") return value;
|
|
1008
|
+
if (value === "end") return "right";
|
|
1009
|
+
return "left";
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function paragraphFor(element, options = {}) {
|
|
1013
|
+
const style = getComputedStyle(element);
|
|
1014
|
+
const runs = collectTextRuns(element, options);
|
|
1015
|
+
return {
|
|
1016
|
+
alignment: pptxAlignment(style.textAlign),
|
|
1017
|
+
lineHeight: roundedMetric(parseFloat(style.lineHeight)),
|
|
1018
|
+
runs,
|
|
1019
|
+
...(options.level !== undefined ? { level: options.level } : {}),
|
|
1020
|
+
...(options.bullet ? { bullet: options.bullet } : {}),
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function renderedTextLineCount(element) {
|
|
1025
|
+
const range = document.createRange();
|
|
1026
|
+
range.selectNodeContents(element);
|
|
1027
|
+
const tops = [];
|
|
1028
|
+
for (const rect of range.getClientRects()) {
|
|
1029
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
1030
|
+
if (!tops.some((top) => Math.abs(top - rect.top) < 1)) tops.push(rect.top);
|
|
1031
|
+
}
|
|
1032
|
+
return Math.max(1, tops.length);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function unsupportedEffects(element) {
|
|
1036
|
+
const style = getComputedStyle(element);
|
|
1037
|
+
const effects = [];
|
|
1038
|
+
if (style.textShadow && style.textShadow !== "none") effects.push("text-shadow");
|
|
1039
|
+
if (style.boxShadow && style.boxShadow !== "none") effects.push("box-shadow");
|
|
1040
|
+
if (style.filter && style.filter !== "none") effects.push("filter");
|
|
1041
|
+
if (style.backdropFilter && style.backdropFilter !== "none") effects.push("backdrop-filter");
|
|
1042
|
+
if (style.mixBlendMode && style.mixBlendMode !== "normal") effects.push("mix-blend-mode");
|
|
1043
|
+
if (style.transform && style.transform !== "none") effects.push("transform");
|
|
1044
|
+
if (Number(style.opacity) < 1) effects.push("opacity");
|
|
1045
|
+
return effects;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function effectFallbackRoot(element) {
|
|
1049
|
+
return element.closest(
|
|
1050
|
+
"p, li, blockquote, table, img, h1, h2, h3, h4, h5, h6, .kicker, .slide-title, .theme-backcover-logo-text, .theme-backcover-copyright, .body, header, footer",
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function pptxFallback(type, element, deck, reason) {
|
|
1055
|
+
return {
|
|
1056
|
+
type,
|
|
1057
|
+
path: elementPath(element, deck),
|
|
1058
|
+
reason,
|
|
1059
|
+
...relativeBounds(element, deck),
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function preserveBoxShadow(element, deck) {
|
|
1064
|
+
const style = getComputedStyle(element);
|
|
1065
|
+
if (!style.boxShadow || style.boxShadow === "none") return;
|
|
1066
|
+
const bounds = relativeBounds(element, deck);
|
|
1067
|
+
const decoration = document.createElement("div");
|
|
1068
|
+
decoration.className = "pptx-effect-fallback";
|
|
1069
|
+
Object.assign(decoration.style, {
|
|
1070
|
+
position: "absolute",
|
|
1071
|
+
left: `${bounds.x}px`,
|
|
1072
|
+
top: `${bounds.y}px`,
|
|
1073
|
+
width: `${bounds.width}px`,
|
|
1074
|
+
height: `${bounds.height}px`,
|
|
1075
|
+
borderRadius: style.borderRadius,
|
|
1076
|
+
boxShadow: style.boxShadow,
|
|
1077
|
+
pointerEvents: "none",
|
|
1078
|
+
});
|
|
1079
|
+
decoration.setAttribute("aria-hidden", "true");
|
|
1080
|
+
deck.appendChild(decoration);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function blobDataUrl(blob) {
|
|
1084
|
+
return new Promise((resolve, reject) => {
|
|
1085
|
+
const reader = new FileReader();
|
|
1086
|
+
reader.addEventListener("load", () => resolve(String(reader.result || "")), {
|
|
1087
|
+
once: true,
|
|
1088
|
+
});
|
|
1089
|
+
reader.addEventListener(
|
|
1090
|
+
"error",
|
|
1091
|
+
() => reject(reader.error || new Error("Could not encode Architecture artwork.")),
|
|
1092
|
+
{ once: true },
|
|
1093
|
+
);
|
|
1094
|
+
reader.readAsDataURL(blob);
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function freezeSvgPaint(source, clone) {
|
|
1099
|
+
const sourceNodes = [source, ...source.querySelectorAll("*")];
|
|
1100
|
+
const cloneNodes = [clone, ...clone.querySelectorAll("*")];
|
|
1101
|
+
sourceNodes.forEach((node, index) => {
|
|
1102
|
+
const target = cloneNodes[index];
|
|
1103
|
+
if (!target) return;
|
|
1104
|
+
const style = getComputedStyle(node);
|
|
1105
|
+
for (const property of ["fill", "stroke", "color"]) {
|
|
1106
|
+
if (style[property]) target.setAttribute(property, style[property]);
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function inlineSvgImageSources(root) {
|
|
1112
|
+
for (const image of root.querySelectorAll("image")) {
|
|
1113
|
+
const href = image.getAttribute("href") || image.getAttribute("xlink:href");
|
|
1114
|
+
if (!href || href.startsWith("data:")) continue;
|
|
1115
|
+
const response = await fetch(new URL(href, window.location.href), { cache: "no-store" });
|
|
1116
|
+
if (!response.ok) {
|
|
1117
|
+
throw new Error(`Could not load Architecture image artwork (${response.status}).`);
|
|
1118
|
+
}
|
|
1119
|
+
const dataUrl = await blobDataUrl(await response.blob());
|
|
1120
|
+
image.setAttribute("href", dataUrl);
|
|
1121
|
+
image.removeAttribute("xlink:href");
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
async function architectureForegroundPng(svg, sources, width, height, crop) {
|
|
1126
|
+
if (!sources.length) return "";
|
|
1127
|
+
const clone = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1128
|
+
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
1129
|
+
const viewBox = svg.viewBox?.baseVal;
|
|
1130
|
+
clone.setAttribute("viewBox", svg.getAttribute("viewBox") || "0 0 1 1");
|
|
1131
|
+
clone.setAttribute("width", String(viewBox?.width || 1));
|
|
1132
|
+
clone.setAttribute("height", String(viewBox?.height || 1));
|
|
1133
|
+
clone.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
1134
|
+
const defs = svg.querySelector(":scope > defs");
|
|
1135
|
+
if (defs) clone.appendChild(defs.cloneNode(true));
|
|
1136
|
+
for (const source of sources) {
|
|
1137
|
+
const copy = source.cloneNode(true);
|
|
1138
|
+
freezeSvgPaint(source, copy);
|
|
1139
|
+
const ownOpacityValue = Number(getComputedStyle(source).opacity);
|
|
1140
|
+
const ownOpacity = Number.isFinite(ownOpacityValue) ? ownOpacityValue : 1;
|
|
1141
|
+
const node = source.closest('[data-architecture-type="node"]');
|
|
1142
|
+
const parentOpacityAttribute =
|
|
1143
|
+
node && node !== source ? node.getAttribute("opacity") : null;
|
|
1144
|
+
const parentOpacityValue =
|
|
1145
|
+
parentOpacityAttribute === null ? 1 : Number(parentOpacityAttribute);
|
|
1146
|
+
const parentOpacity = Number.isFinite(parentOpacityValue) ? parentOpacityValue : 1;
|
|
1147
|
+
copy.setAttribute("opacity", String(ownOpacity * parentOpacity));
|
|
1148
|
+
clone.appendChild(copy);
|
|
1149
|
+
}
|
|
1150
|
+
await inlineSvgImageSources(clone);
|
|
1151
|
+
const markup = new XMLSerializer().serializeToString(clone);
|
|
1152
|
+
const url = URL.createObjectURL(new Blob([markup], { type: "image/svg+xml" }));
|
|
1153
|
+
try {
|
|
1154
|
+
const image = new Image();
|
|
1155
|
+
image.src = url;
|
|
1156
|
+
await image.decode();
|
|
1157
|
+
const canvas = document.createElement("canvas");
|
|
1158
|
+
canvas.width = Math.max(1, Math.ceil(width));
|
|
1159
|
+
canvas.height = Math.max(1, Math.ceil(height));
|
|
1160
|
+
const context = canvas.getContext("2d");
|
|
1161
|
+
if (!context) throw new Error("Could not create Architecture artwork canvas.");
|
|
1162
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
1163
|
+
if (!crop) return canvas.toDataURL("image/png");
|
|
1164
|
+
const cropped = document.createElement("canvas");
|
|
1165
|
+
cropped.width = Math.max(1, Math.ceil(crop.width));
|
|
1166
|
+
cropped.height = Math.max(1, Math.ceil(crop.height));
|
|
1167
|
+
const croppedContext = cropped.getContext("2d");
|
|
1168
|
+
if (!croppedContext) throw new Error("Could not crop Architecture artwork.");
|
|
1169
|
+
croppedContext.drawImage(
|
|
1170
|
+
canvas,
|
|
1171
|
+
crop.x,
|
|
1172
|
+
crop.y,
|
|
1173
|
+
crop.width,
|
|
1174
|
+
crop.height,
|
|
1175
|
+
0,
|
|
1176
|
+
0,
|
|
1177
|
+
cropped.width,
|
|
1178
|
+
cropped.height,
|
|
1179
|
+
);
|
|
1180
|
+
return cropped.toDataURL("image/png");
|
|
1181
|
+
} finally {
|
|
1182
|
+
URL.revokeObjectURL(url);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
1187
|
+
const snapshot = wrapper.__presentationPptxSnapshot;
|
|
1188
|
+
const svg = wrapper.querySelector("svg.architecture-svg");
|
|
1189
|
+
if (!snapshot || !svg) {
|
|
1190
|
+
return {
|
|
1191
|
+
elements: [],
|
|
1192
|
+
fallbacks: [pptxFallback("architecture", wrapper, deck, "architecture-model-unavailable")],
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
const svgRect = svg.getBoundingClientRect();
|
|
1196
|
+
const deckRect = deck.getBoundingClientRect();
|
|
1197
|
+
const scale = Math.min(
|
|
1198
|
+
svgRect.width / snapshot.canvas.width,
|
|
1199
|
+
svgRect.height / snapshot.canvas.height,
|
|
1200
|
+
);
|
|
1201
|
+
const usedWidth = snapshot.canvas.width * scale;
|
|
1202
|
+
const usedHeight = snapshot.canvas.height * scale;
|
|
1203
|
+
const originX = svgRect.left - deckRect.left + (svgRect.width - usedWidth) / 2;
|
|
1204
|
+
const originY = svgRect.top - deckRect.top + (svgRect.height - usedHeight) / 2;
|
|
1205
|
+
const mapBounds = (object) => ({
|
|
1206
|
+
x: roundedMetric(originX + object.x * scale),
|
|
1207
|
+
y: roundedMetric(originY + object.y * scale),
|
|
1208
|
+
width: roundedMetric(object.width * scale),
|
|
1209
|
+
height: roundedMetric(object.height * scale),
|
|
1210
|
+
});
|
|
1211
|
+
const mapColorFields = (object) => {
|
|
1212
|
+
const mapped = { ...object };
|
|
1213
|
+
const mapParagraphs = (paragraphs) =>
|
|
1214
|
+
paragraphs.map((paragraph) => ({
|
|
1215
|
+
...paragraph,
|
|
1216
|
+
runs: paragraph.runs.map((run) => ({
|
|
1217
|
+
...run,
|
|
1218
|
+
color: resolveModelColor(run.color, deck),
|
|
1219
|
+
fontFace: getComputedStyle(svg).fontFamily
|
|
1220
|
+
.split(",")[0]
|
|
1221
|
+
.trim()
|
|
1222
|
+
.replace(/^["']|["']$/g, ""),
|
|
1223
|
+
fontSize: roundedMetric(run.fontSize * scale),
|
|
1224
|
+
bold: Number(run.fontWeight) >= 600,
|
|
1225
|
+
})),
|
|
1226
|
+
}));
|
|
1227
|
+
if (mapped.dash !== undefined) mapped.dash = mapped.dash ? "dash" : "solid";
|
|
1228
|
+
for (const key of ["fill", "stroke", "color"]) {
|
|
1229
|
+
if (key in mapped) mapped[key] = resolveModelColor(mapped[key], deck);
|
|
1230
|
+
}
|
|
1231
|
+
if (Array.isArray(mapped.paragraphs)) {
|
|
1232
|
+
mapped.paragraphs = mapParagraphs(mapped.paragraphs);
|
|
1233
|
+
}
|
|
1234
|
+
if (mapped.text?.paragraphs) {
|
|
1235
|
+
mapped.text = {
|
|
1236
|
+
...mapped.text,
|
|
1237
|
+
paragraphs: mapParagraphs(mapped.text.paragraphs),
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
if (mapped.textInsets) {
|
|
1241
|
+
mapped.textInsets = Object.fromEntries(
|
|
1242
|
+
Object.entries(mapped.textInsets).map(([key, value]) => [
|
|
1243
|
+
key,
|
|
1244
|
+
roundedMetric(value * scale),
|
|
1245
|
+
]),
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
return mapped;
|
|
1249
|
+
};
|
|
1250
|
+
const fallbacks = snapshot.fallbacks.map((fallback) => ({
|
|
1251
|
+
...fallback,
|
|
1252
|
+
path: `architecture[${blockIndex}].${fallback.path}`,
|
|
1253
|
+
...mapBounds(fallback),
|
|
1254
|
+
}));
|
|
1255
|
+
const elements = [];
|
|
1256
|
+
const architectureGroups = [...wrapper.querySelectorAll("[data-architecture-type]")];
|
|
1257
|
+
const findById = (id) =>
|
|
1258
|
+
architectureGroups.find((element) => element.getAttribute("data-architecture-id") === id);
|
|
1259
|
+
const foregroundCandidates = [];
|
|
1260
|
+
for (const icon of snapshot.icons || []) {
|
|
1261
|
+
const group = findById(icon.id);
|
|
1262
|
+
const source = group?.querySelector("[data-architecture-icon]");
|
|
1263
|
+
if (source) {
|
|
1264
|
+
const rawBounds = mapBounds(icon);
|
|
1265
|
+
const padding = Math.max(
|
|
1266
|
+
0,
|
|
1267
|
+
Math.min(2, rawBounds.x - originX, rawBounds.y - originY),
|
|
1268
|
+
);
|
|
1269
|
+
const bounds = {
|
|
1270
|
+
x: rawBounds.x - padding,
|
|
1271
|
+
y: rawBounds.y - padding,
|
|
1272
|
+
width: rawBounds.width + padding * 2,
|
|
1273
|
+
height: rawBounds.height + padding * 2,
|
|
1274
|
+
};
|
|
1275
|
+
foregroundCandidates.push({
|
|
1276
|
+
key: `icon:${icon.id}`,
|
|
1277
|
+
source,
|
|
1278
|
+
alt: `${icon.icon} icon`,
|
|
1279
|
+
bounds,
|
|
1280
|
+
crop: {
|
|
1281
|
+
x: bounds.x - originX,
|
|
1282
|
+
y: bounds.y - originY,
|
|
1283
|
+
width: bounds.width,
|
|
1284
|
+
height: bounds.height,
|
|
1285
|
+
},
|
|
1286
|
+
architecture: {
|
|
1287
|
+
kind: "icon-picture",
|
|
1288
|
+
id: icon.id,
|
|
1289
|
+
sourcePath: icon.sourcePath,
|
|
1290
|
+
order: icon.order,
|
|
1291
|
+
z: icon.z,
|
|
1292
|
+
},
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
for (const object of snapshot.objects.filter((entry) => entry.type === "image")) {
|
|
1297
|
+
const source = findById(object.architecture.id);
|
|
1298
|
+
if (source) {
|
|
1299
|
+
const rawBounds = mapBounds(object);
|
|
1300
|
+
const padding = Math.max(
|
|
1301
|
+
0,
|
|
1302
|
+
Math.min(2, rawBounds.x - originX, rawBounds.y - originY),
|
|
1303
|
+
);
|
|
1304
|
+
const bounds = {
|
|
1305
|
+
x: rawBounds.x - padding,
|
|
1306
|
+
y: rawBounds.y - padding,
|
|
1307
|
+
width: rawBounds.width + padding * 2,
|
|
1308
|
+
height: rawBounds.height + padding * 2,
|
|
1309
|
+
};
|
|
1310
|
+
foregroundCandidates.push({
|
|
1311
|
+
key: `image:${object.architecture.id}`,
|
|
1312
|
+
source,
|
|
1313
|
+
alt: `${object.architecture.id} image`,
|
|
1314
|
+
bounds,
|
|
1315
|
+
crop: {
|
|
1316
|
+
x: bounds.x - originX,
|
|
1317
|
+
y: bounds.y - originY,
|
|
1318
|
+
width: bounds.width,
|
|
1319
|
+
height: bounds.height,
|
|
1320
|
+
},
|
|
1321
|
+
architecture: {
|
|
1322
|
+
...object.architecture,
|
|
1323
|
+
kind: "image-picture",
|
|
1324
|
+
},
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
const foregroundLayers = new Map();
|
|
1329
|
+
const expectedForegrounds =
|
|
1330
|
+
(snapshot.icons?.length || 0) +
|
|
1331
|
+
snapshot.objects.filter((object) => object.type === "image").length;
|
|
1332
|
+
let foregroundReady = foregroundCandidates.length === expectedForegrounds;
|
|
1333
|
+
try {
|
|
1334
|
+
if (foregroundReady) {
|
|
1335
|
+
const generated = await Promise.all(
|
|
1336
|
+
foregroundCandidates.map(async (candidate) => ({
|
|
1337
|
+
...candidate,
|
|
1338
|
+
src: await architectureForegroundPng(
|
|
1339
|
+
svg,
|
|
1340
|
+
[candidate.source],
|
|
1341
|
+
usedWidth,
|
|
1342
|
+
usedHeight,
|
|
1343
|
+
candidate.crop,
|
|
1344
|
+
),
|
|
1345
|
+
})),
|
|
1346
|
+
);
|
|
1347
|
+
generated.forEach((layer) => foregroundLayers.set(layer.key, layer));
|
|
1348
|
+
} else {
|
|
1349
|
+
throw new Error("Architecture foreground source was not found.");
|
|
1350
|
+
}
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
foregroundReady = false;
|
|
1353
|
+
foregroundLayers.clear();
|
|
1354
|
+
fallbacks.push(
|
|
1355
|
+
pptxFallback(
|
|
1356
|
+
"architecture-foreground",
|
|
1357
|
+
wrapper,
|
|
1358
|
+
deck,
|
|
1359
|
+
`foreground-picture-failed: ${error?.message || "unknown error"}`,
|
|
1360
|
+
),
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
const foregroundElement = (layer) => ({
|
|
1364
|
+
type: "image",
|
|
1365
|
+
src: layer.src,
|
|
1366
|
+
alt: layer.alt,
|
|
1367
|
+
fit: "fill",
|
|
1368
|
+
opacity: 1,
|
|
1369
|
+
...layer.bounds,
|
|
1370
|
+
architecture: layer.architecture,
|
|
1371
|
+
});
|
|
1372
|
+
if (!foregroundReady) {
|
|
1373
|
+
fallbacks.push(
|
|
1374
|
+
pptxFallback(
|
|
1375
|
+
"architecture",
|
|
1376
|
+
wrapper,
|
|
1377
|
+
deck,
|
|
1378
|
+
"architecture-rendered-as-artwork-after-foreground-failure",
|
|
1379
|
+
),
|
|
1380
|
+
);
|
|
1381
|
+
return { elements: [], fallbacks };
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
for (const sourceObject of snapshot.objects) {
|
|
1385
|
+
const object = mapColorFields({
|
|
1386
|
+
...sourceObject,
|
|
1387
|
+
...mapBounds(sourceObject),
|
|
1388
|
+
...(sourceObject.points
|
|
1389
|
+
? {
|
|
1390
|
+
points: sourceObject.points.map((point) => ({
|
|
1391
|
+
x: roundedMetric(originX + point.x * scale),
|
|
1392
|
+
y: roundedMetric(originY + point.y * scale),
|
|
1393
|
+
})),
|
|
1394
|
+
}
|
|
1395
|
+
: {}),
|
|
1396
|
+
...(sourceObject.strokeWidth !== undefined
|
|
1397
|
+
? { strokeWidth: roundedMetric(sourceObject.strokeWidth * scale) }
|
|
1398
|
+
: {}),
|
|
1399
|
+
...(sourceObject.cornerRadius !== undefined
|
|
1400
|
+
? { cornerRadius: roundedMetric(sourceObject.cornerRadius * scale) }
|
|
1401
|
+
: {}),
|
|
1402
|
+
});
|
|
1403
|
+
if (object.type === "shape") {
|
|
1404
|
+
const opacity = Number.isFinite(object.opacity) ? object.opacity : 1;
|
|
1405
|
+
if (object.text?.paragraphs) {
|
|
1406
|
+
object.text = {
|
|
1407
|
+
...object.text,
|
|
1408
|
+
paragraphs: object.text.paragraphs.map((paragraph) => ({
|
|
1409
|
+
...paragraph,
|
|
1410
|
+
runs: paragraph.runs.map((run) => ({
|
|
1411
|
+
...run,
|
|
1412
|
+
opacity: (Number.isFinite(run.opacity) ? run.opacity : 1) * opacity,
|
|
1413
|
+
})),
|
|
1414
|
+
})),
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
if (object.type === "image") {
|
|
1419
|
+
const layer = foregroundLayers.get(`image:${object.architecture.id}`);
|
|
1420
|
+
fallbacks.push({
|
|
1421
|
+
type: "architecture-image",
|
|
1422
|
+
path: `architecture[${blockIndex}].${object.architecture.sourcePath}`,
|
|
1423
|
+
reason: foregroundReady && layer
|
|
1424
|
+
? "architecture-image-rendered-as-foreground-picture"
|
|
1425
|
+
: "architecture-image-rendered-as-artwork",
|
|
1426
|
+
...mapBounds(sourceObject),
|
|
1427
|
+
});
|
|
1428
|
+
if (foregroundReady && layer) elements.push(foregroundElement(layer));
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
elements.push(object);
|
|
1432
|
+
if (object.type === "shape" && object.architecture?.kind === "node" && sourceObject.icon) {
|
|
1433
|
+
const layer = foregroundLayers.get(`icon:${object.architecture.id}`);
|
|
1434
|
+
if (foregroundReady && layer) elements.push(foregroundElement(layer));
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
for (const icon of snapshot.icons || []) {
|
|
1438
|
+
const layer = foregroundLayers.get(`icon:${icon.id}`);
|
|
1439
|
+
fallbacks.push({
|
|
1440
|
+
type: "architecture-icon",
|
|
1441
|
+
path: `architecture[${blockIndex}].${icon.sourcePath}`,
|
|
1442
|
+
reason: foregroundReady && layer
|
|
1443
|
+
? "icon-rendered-as-foreground-picture"
|
|
1444
|
+
: "icon-rendered-as-artwork",
|
|
1445
|
+
icon: icon.icon,
|
|
1446
|
+
...mapBounds(icon),
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
for (const sourceObject of snapshot.objects) {
|
|
1450
|
+
const architecture = sourceObject.architecture;
|
|
1451
|
+
if (!architecture) continue;
|
|
1452
|
+
if (architecture.kind === "group" || architecture.kind === "node") {
|
|
1453
|
+
const group = findById(architecture.id);
|
|
1454
|
+
if (!group) continue;
|
|
1455
|
+
[...group.children]
|
|
1456
|
+
.filter((child) =>
|
|
1457
|
+
foregroundReady
|
|
1458
|
+
? child.matches("rect, ellipse, text")
|
|
1459
|
+
: child.matches("text"),
|
|
1460
|
+
)
|
|
1461
|
+
.forEach((child) => child.setAttribute("data-pptx-native", sourceObject.type));
|
|
1462
|
+
} else if (architecture.kind === "connector") {
|
|
1463
|
+
architectureGroups
|
|
1464
|
+
.filter(
|
|
1465
|
+
(element) =>
|
|
1466
|
+
element.getAttribute("data-architecture-type") === "connector" &&
|
|
1467
|
+
element.getAttribute("data-architecture-order") === String(architecture.order),
|
|
1468
|
+
)
|
|
1469
|
+
.forEach((group) =>
|
|
1470
|
+
[...group.children]
|
|
1471
|
+
.filter((child) => child.matches("path"))
|
|
1472
|
+
.forEach((child) => child.setAttribute("data-pptx-native", "connector")),
|
|
1473
|
+
);
|
|
1474
|
+
} else if (architecture.kind.startsWith("connector-label")) {
|
|
1475
|
+
wrapper
|
|
1476
|
+
.querySelectorAll(
|
|
1477
|
+
`[data-architecture-connector-label][data-architecture-label-layer]`,
|
|
1478
|
+
)
|
|
1479
|
+
.forEach((label) => label.setAttribute("data-pptx-native", sourceObject.type));
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (foregroundReady) {
|
|
1483
|
+
foregroundCandidates.forEach((candidate) => {
|
|
1484
|
+
if (foregroundLayers.has(candidate.key)) {
|
|
1485
|
+
candidate.source.setAttribute("data-pptx-native", "image");
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
if (snapshot.routing.degraded) {
|
|
1490
|
+
fallbacks.push(
|
|
1491
|
+
pptxFallback(
|
|
1492
|
+
"architecture-routing",
|
|
1493
|
+
wrapper,
|
|
1494
|
+
deck,
|
|
1495
|
+
"routing-warning-rendered-as-artwork",
|
|
1496
|
+
),
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
return { elements, fallbacks };
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
async function collectPptxSlide(slide, index) {
|
|
1503
|
+
const { deck } = slide;
|
|
1504
|
+
const elements = [];
|
|
1505
|
+
const fallbacks = [];
|
|
1506
|
+
const fallbackRoots = new Set();
|
|
1507
|
+
const addFallback = (type, element, reason) => {
|
|
1508
|
+
if (fallbackRoots.has(element)) return;
|
|
1509
|
+
fallbackRoots.add(element);
|
|
1510
|
+
fallbacks.push(pptxFallback(type, element, deck, reason));
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
for (const element of deck.querySelectorAll("header *, .body, .body *, footer *")) {
|
|
1514
|
+
if (element.closest("pre, .architecture-diagram, .architecture-error")) continue;
|
|
1515
|
+
const effects = unsupportedEffects(element).filter((effect) => effect !== "box-shadow");
|
|
1516
|
+
if (!effects.length) continue;
|
|
1517
|
+
const root = effectFallbackRoot(element);
|
|
1518
|
+
if (root) {
|
|
1519
|
+
addFallback(
|
|
1520
|
+
"effect",
|
|
1521
|
+
root,
|
|
1522
|
+
`element-rendered-as-artwork: ${effects.join(", ")}`,
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
deck.querySelectorAll("pre.mermaid").forEach((element) =>
|
|
1528
|
+
addFallback("mermaid", element, "mermaid-rendered-as-artwork"),
|
|
1529
|
+
);
|
|
1530
|
+
deck.querySelectorAll("pre:not(.mermaid)").forEach((element) =>
|
|
1531
|
+
addFallback("code", element, "code-block-rendered-as-artwork"),
|
|
1532
|
+
);
|
|
1533
|
+
deck.querySelectorAll(".architecture-error").forEach((element) =>
|
|
1534
|
+
addFallback("architecture", element, "architecture-error-rendered-as-artwork"),
|
|
1535
|
+
);
|
|
1536
|
+
deck
|
|
1537
|
+
.querySelectorAll(
|
|
1538
|
+
".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",
|
|
1539
|
+
)
|
|
1540
|
+
.forEach((element) => {
|
|
1541
|
+
if (!element.closest(".architecture-diagram")) {
|
|
1542
|
+
addFallback("html", element, "arbitrary-html-rendered-as-artwork");
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
const insideFallback = (element) =>
|
|
1547
|
+
[...fallbackRoots].some((root) => root === element || root.contains(element));
|
|
1548
|
+
const textCandidates = [
|
|
1549
|
+
...deck.querySelectorAll(
|
|
1550
|
+
".kicker, .slide-title, .body h1, .body h2, .body h3, .body h4, .body h5, .body h6, .body p, .body blockquote, .body li, footer > span, .theme-backcover-logo-text, .theme-backcover-copyright",
|
|
1551
|
+
),
|
|
1552
|
+
].filter(
|
|
1553
|
+
(element) =>
|
|
1554
|
+
!insideFallback(element) &&
|
|
1555
|
+
!element.closest(".architecture-diagram") &&
|
|
1556
|
+
!element.closest("table") &&
|
|
1557
|
+
!(element.matches("p") && element.closest("blockquote, li")),
|
|
1558
|
+
);
|
|
1559
|
+
for (const element of textCandidates) {
|
|
1560
|
+
const list = element.matches("li")
|
|
1561
|
+
? [...element.querySelectorAll(":scope > ul, :scope > ol")]
|
|
1562
|
+
: [];
|
|
1563
|
+
const parentList = element.matches("li") ? element.parentElement : null;
|
|
1564
|
+
const actualLevel = element.matches("li")
|
|
1565
|
+
? Math.max(0, [...element.closest(".body").querySelectorAll("ul, ol")].filter((candidate) =>
|
|
1566
|
+
candidate.contains(element),
|
|
1567
|
+
).length - 1)
|
|
1568
|
+
: undefined;
|
|
1569
|
+
const paragraph = paragraphFor(element, {
|
|
1570
|
+
omitNestedLists: list.length > 0,
|
|
1571
|
+
level: actualLevel,
|
|
1572
|
+
bullet: parentList
|
|
1573
|
+
? {
|
|
1574
|
+
type: parentList.tagName === "OL" ? "number" : "bullet",
|
|
1575
|
+
character:
|
|
1576
|
+
parentList.tagName === "OL"
|
|
1577
|
+
? `${Number(parentList.getAttribute("start") || 1) + [...parentList.children].indexOf(element)}.`
|
|
1578
|
+
: "•",
|
|
1579
|
+
...(parentList.tagName === "OL"
|
|
1580
|
+
? { start: Number(parentList.getAttribute("start") || 1) + [...parentList.children].indexOf(element) }
|
|
1581
|
+
: {}),
|
|
1582
|
+
}
|
|
1583
|
+
: undefined,
|
|
1584
|
+
});
|
|
1585
|
+
if (!paragraph.runs.some((run) => run.text.trim())) continue;
|
|
1586
|
+
elements.push({
|
|
1587
|
+
type: "text",
|
|
1588
|
+
path: elementPath(element, deck),
|
|
1589
|
+
...(element.classList.contains("kicker")
|
|
1590
|
+
? textContentBounds(element, deck)
|
|
1591
|
+
: relativeBounds(element, deck)),
|
|
1592
|
+
paragraphs: [paragraph],
|
|
1593
|
+
opacity: Number(getComputedStyle(element).opacity) || 1,
|
|
1594
|
+
...(element.matches("h1, h2, .slide-title") && renderedTextLineCount(element) === 1
|
|
1595
|
+
? { textWrap: "none" }
|
|
1596
|
+
: {}),
|
|
1597
|
+
});
|
|
1598
|
+
element.setAttribute("data-pptx-native", "text");
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
for (const table of deck.querySelectorAll(".body table")) {
|
|
1602
|
+
if (insideFallback(table)) continue;
|
|
1603
|
+
const descendantEffects = new Set();
|
|
1604
|
+
for (const descendant of table.querySelectorAll("*")) {
|
|
1605
|
+
for (const effect of unsupportedEffects(descendant)) descendantEffects.add(effect);
|
|
1606
|
+
}
|
|
1607
|
+
if (descendantEffects.size) {
|
|
1608
|
+
addFallback(
|
|
1609
|
+
"effect",
|
|
1610
|
+
table,
|
|
1611
|
+
`element-rendered-as-artwork: ${[...descendantEffects].join(", ")}`,
|
|
1612
|
+
);
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
const effects = unsupportedEffects(table);
|
|
1616
|
+
if (parseFloat(getComputedStyle(table).borderRadius) > 0) effects.push("border-radius");
|
|
1617
|
+
const artworkEffects = effects.filter(
|
|
1618
|
+
(effect) => effect !== "box-shadow" && effect !== "border-radius",
|
|
1619
|
+
);
|
|
1620
|
+
if (artworkEffects.length) {
|
|
1621
|
+
addFallback(
|
|
1622
|
+
"effect",
|
|
1623
|
+
table,
|
|
1624
|
+
`element-rendered-as-artwork: ${effects.join(", ")}`,
|
|
1625
|
+
);
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
const domRows = [...table.rows];
|
|
1629
|
+
const columnCount = domRows[0]?.cells.length || 0;
|
|
1630
|
+
if (
|
|
1631
|
+
!columnCount ||
|
|
1632
|
+
domRows.some(
|
|
1633
|
+
(row) =>
|
|
1634
|
+
row.cells.length !== columnCount ||
|
|
1635
|
+
[...row.cells].some((cell) => cell.colSpan !== 1 || cell.rowSpan !== 1),
|
|
1636
|
+
)
|
|
1637
|
+
) {
|
|
1638
|
+
addFallback("table", table, "merged-table-rendered-as-artwork");
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
const rows = domRows.map((row) => ({
|
|
1642
|
+
height: roundedMetric(row.getBoundingClientRect().height),
|
|
1643
|
+
cells: [...row.cells].map((cell) => {
|
|
1644
|
+
const style = getComputedStyle(cell);
|
|
1645
|
+
return {
|
|
1646
|
+
...relativeBounds(cell, deck),
|
|
1647
|
+
header: cell.tagName === "TH",
|
|
1648
|
+
colspan: cell.colSpan,
|
|
1649
|
+
rowspan: cell.rowSpan,
|
|
1650
|
+
fill: normalizeCssColor(style.backgroundColor),
|
|
1651
|
+
color: normalizeCssColor(style.color),
|
|
1652
|
+
stroke: normalizeCssColor(style.borderColor),
|
|
1653
|
+
strokeWidth: roundedMetric(parseFloat(style.borderWidth)) || 1,
|
|
1654
|
+
alignment: pptxAlignment(style.textAlign),
|
|
1655
|
+
paragraphs: [paragraphFor(cell)],
|
|
1656
|
+
};
|
|
1657
|
+
}),
|
|
1658
|
+
}));
|
|
1659
|
+
elements.push({
|
|
1660
|
+
type: "table",
|
|
1661
|
+
path: elementPath(table, deck),
|
|
1662
|
+
...relativeBounds(table, deck),
|
|
1663
|
+
rows,
|
|
1664
|
+
});
|
|
1665
|
+
if (effects.length) {
|
|
1666
|
+
fallbacks.push(
|
|
1667
|
+
pptxFallback(
|
|
1668
|
+
"effect",
|
|
1669
|
+
table,
|
|
1670
|
+
deck,
|
|
1671
|
+
`native-table-approximates: ${effects.join(", ")}`,
|
|
1672
|
+
),
|
|
1673
|
+
);
|
|
1674
|
+
if (effects.includes("box-shadow")) preserveBoxShadow(table, deck);
|
|
1675
|
+
}
|
|
1676
|
+
table.setAttribute("data-pptx-native", "table");
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
for (const image of deck.querySelectorAll("img")) {
|
|
1680
|
+
if (
|
|
1681
|
+
image.closest(".architecture-diagram") ||
|
|
1682
|
+
image.classList.contains("theme-cover-background") ||
|
|
1683
|
+
insideFallback(image)
|
|
1684
|
+
) {
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
if (!rasterImageSupported(image)) {
|
|
1688
|
+
const fit = getComputedStyle(image).objectFit;
|
|
1689
|
+
addFallback(
|
|
1690
|
+
"image",
|
|
1691
|
+
image,
|
|
1692
|
+
["cover", "none"].includes(fit) ? "unsupported-image-fit" : "unsupported-image-format",
|
|
1693
|
+
);
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
const style = getComputedStyle(image);
|
|
1697
|
+
const effects = unsupportedEffects(image);
|
|
1698
|
+
if (parseFloat(style.borderRadius) > 0) effects.push("border-radius");
|
|
1699
|
+
const artworkEffects = effects.filter(
|
|
1700
|
+
(effect) => effect !== "box-shadow" && effect !== "border-radius",
|
|
1701
|
+
);
|
|
1702
|
+
if (artworkEffects.length) {
|
|
1703
|
+
addFallback(
|
|
1704
|
+
"effect",
|
|
1705
|
+
image,
|
|
1706
|
+
`element-rendered-as-artwork: ${effects.join(", ")}`,
|
|
1707
|
+
);
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
elements.push({
|
|
1711
|
+
type: "image",
|
|
1712
|
+
path: elementPath(image, deck),
|
|
1713
|
+
...relativeBounds(image, deck),
|
|
1714
|
+
src: image.currentSrc || image.src,
|
|
1715
|
+
alt: image.alt || "",
|
|
1716
|
+
fit: style.objectFit || "contain",
|
|
1717
|
+
opacity: Number(style.opacity) || 1,
|
|
1718
|
+
naturalWidth: image.naturalWidth,
|
|
1719
|
+
naturalHeight: image.naturalHeight,
|
|
1720
|
+
});
|
|
1721
|
+
image.setAttribute("data-pptx-native", "image");
|
|
1722
|
+
if (effects.length) {
|
|
1723
|
+
fallbacks.push(
|
|
1724
|
+
pptxFallback(
|
|
1725
|
+
"effect",
|
|
1726
|
+
image,
|
|
1727
|
+
deck,
|
|
1728
|
+
`native-image-approximates: ${effects.join(", ")}`,
|
|
1729
|
+
),
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const architectureWrappers = [...deck.querySelectorAll(".architecture-diagram")];
|
|
1735
|
+
for (const [blockIndex, wrapper] of architectureWrappers.entries()) {
|
|
1736
|
+
if (insideFallback(wrapper)) continue;
|
|
1737
|
+
const architecture = await collectArchitectureObjects(wrapper, deck, blockIndex);
|
|
1738
|
+
elements.push(...architecture.elements);
|
|
1739
|
+
fallbacks.push(...architecture.fallbacks);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
const layout = slide.titleSlide
|
|
1743
|
+
? "title"
|
|
1744
|
+
: slide.sectionSlide
|
|
1745
|
+
? "section"
|
|
1746
|
+
: slide.centerSlide
|
|
1747
|
+
? "center"
|
|
1748
|
+
: slide.backcoverSlide
|
|
1749
|
+
? "backcover"
|
|
1750
|
+
: "standard";
|
|
1751
|
+
const visibleTitle = deck.querySelector("h1, h2")?.textContent?.trim();
|
|
1752
|
+
return {
|
|
1753
|
+
index,
|
|
1754
|
+
layout,
|
|
1755
|
+
theme: slide.theme,
|
|
1756
|
+
title: visibleTitle || slide.title,
|
|
1757
|
+
width: OUTPUT_WIDTH,
|
|
1758
|
+
height: OUTPUT_HEIGHT,
|
|
1759
|
+
elements,
|
|
1760
|
+
fallbacks,
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
async function renderPptxDeck(
|
|
1765
|
+
slides,
|
|
1766
|
+
theme,
|
|
1767
|
+
customCss = "",
|
|
1768
|
+
themeMetadata = null,
|
|
1769
|
+
themeLocked = false,
|
|
1770
|
+
) {
|
|
1771
|
+
deckTheme = normalizeTheme(theme);
|
|
1772
|
+
deckThemeLocked = Boolean(themeLocked);
|
|
1773
|
+
customThemeMeta = themeMetadata && typeof themeMetadata === "object" ? themeMetadata : null;
|
|
1774
|
+
applyCustomThemeCss(customCss);
|
|
1775
|
+
document.documentElement.setAttribute("data-theme", deckTheme);
|
|
1776
|
+
document.body.classList.add("pptx-mode", "fixed-output-mode", "mermaid-loading");
|
|
1777
|
+
const rendered = slides.map((markdown) => createSlide(markdown, deckTheme));
|
|
1778
|
+
const stage = document.getElementById("stage");
|
|
1779
|
+
stage.replaceChildren(...rendered.map((slide) => slide.deck));
|
|
1780
|
+
document.title = rendered[0]?.title || "MarkdStage";
|
|
1781
|
+
|
|
1782
|
+
if (document.fonts?.ready) await document.fonts.ready;
|
|
1783
|
+
await afterLayout();
|
|
1784
|
+
for (const slide of rendered) {
|
|
1785
|
+
if (
|
|
1786
|
+
slide.sizeMode === "auto" &&
|
|
1787
|
+
!slide.titleSlide &&
|
|
1788
|
+
!slide.sectionSlide &&
|
|
1789
|
+
!slide.backcoverSlide
|
|
1790
|
+
) {
|
|
1791
|
+
applyAutoSize(slide.deck, slide.bodyEl);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
const token = ++renderToken;
|
|
1795
|
+
for (const slide of rendered) {
|
|
1796
|
+
await runMermaid(slide.bodyEl, slide.theme, token, false);
|
|
1797
|
+
}
|
|
1798
|
+
await waitForImages(stage);
|
|
1799
|
+
await afterLayout();
|
|
1800
|
+
|
|
1801
|
+
const pptxSlides = [];
|
|
1802
|
+
for (const [index, slide] of rendered.entries()) {
|
|
1803
|
+
pptxSlides.push(await collectPptxSlide(slide, index));
|
|
1804
|
+
}
|
|
1805
|
+
const model = {
|
|
1806
|
+
version: 1,
|
|
1807
|
+
width: OUTPUT_WIDTH,
|
|
1808
|
+
height: OUTPUT_HEIGHT,
|
|
1809
|
+
slides: pptxSlides,
|
|
1810
|
+
};
|
|
1811
|
+
window.__presentationPptxModel = JSON.parse(JSON.stringify(model));
|
|
1812
|
+
document.body.classList.add("pptx-artwork-mode");
|
|
1813
|
+
document.body.setAttribute("data-pptx-artwork", "ready");
|
|
1814
|
+
document.body.classList.remove("mermaid-loading");
|
|
1815
|
+
document.documentElement.setAttribute("data-pptx-ready", "true");
|
|
1816
|
+
return {
|
|
1817
|
+
model: window.__presentationPptxModel,
|
|
1818
|
+
layout: collectDeckLayout(rendered),
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
async function initPptx(params) {
|
|
1823
|
+
const token = params.get("token") || "";
|
|
1824
|
+
if (!token) throw new Error("Missing PowerPoint export token.");
|
|
1825
|
+
try {
|
|
1826
|
+
const response = await fetch(`./export-data?token=${encodeURIComponent(token)}`, {
|
|
1827
|
+
cache: "no-store",
|
|
1828
|
+
});
|
|
1829
|
+
if (!response.ok) throw new Error(`Could not load PowerPoint export data (${response.status}).`);
|
|
1830
|
+
const data = await response.json();
|
|
1831
|
+
if (
|
|
1832
|
+
!Array.isArray(data.slides) ||
|
|
1833
|
+
data.slides.length === 0 ||
|
|
1834
|
+
!data.slides.every((slide) => typeof slide === "string")
|
|
1835
|
+
) {
|
|
1836
|
+
throw new Error("PowerPoint export data does not contain a valid deck.");
|
|
1837
|
+
}
|
|
1838
|
+
const output = await renderPptxDeck(
|
|
1839
|
+
data.slides,
|
|
1840
|
+
data.theme,
|
|
1841
|
+
data.customThemeCss,
|
|
1842
|
+
data.customThemeMeta,
|
|
1843
|
+
data.themeLocked,
|
|
1844
|
+
);
|
|
1845
|
+
await reportOutputStatus(token, "ready", "", output.layout);
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
const message = error?.message || "PowerPoint rendering failed.";
|
|
1848
|
+
console.error(message);
|
|
1849
|
+
document.body.classList.remove("mermaid-loading");
|
|
1850
|
+
document.documentElement.setAttribute("data-pptx-error", "true");
|
|
1851
|
+
await reportOutputStatus(token, "error", message).catch(() => {});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
860
1855
|
async function renderPrintDeck(
|
|
861
1856
|
slides,
|
|
862
1857
|
theme,
|
|
@@ -1048,6 +2043,13 @@ function reportCaptureBootstrapFailure(error) {
|
|
|
1048
2043
|
document.documentElement.setAttribute("data-capture-error", "true");
|
|
1049
2044
|
}
|
|
1050
2045
|
|
|
2046
|
+
function reportPptxBootstrapFailure(error) {
|
|
2047
|
+
const message = error?.message || "PowerPoint rendering failed.";
|
|
2048
|
+
console.error(message);
|
|
2049
|
+
document.body.classList.remove("mermaid-loading");
|
|
2050
|
+
document.documentElement.setAttribute("data-pptx-error", "true");
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1051
2053
|
// --- live update -----------------------------------------------------------
|
|
1052
2054
|
// /state is the single source of truth for *what to show* (latest slide markdown
|
|
1053
2055
|
// + a monotonic version + the deck position). SSE is just a low-latency "version
|
|
@@ -1066,13 +2068,21 @@ let importOpen = false;
|
|
|
1066
2068
|
let importPending = false;
|
|
1067
2069
|
let importFiles = [];
|
|
1068
2070
|
let sourceBacked = false;
|
|
2071
|
+
let sourceModeAvailable = false;
|
|
1069
2072
|
let sourceMode = "snapshot";
|
|
1070
2073
|
let sourceWatchStatus = "inactive";
|
|
1071
2074
|
let sourceWatchError = "";
|
|
1072
2075
|
let presenterRequestPending = false;
|
|
1073
2076
|
let presenterRunning = false;
|
|
2077
|
+
let presenterWindowAvailable = false;
|
|
2078
|
+
let presenterViewAvailable = false;
|
|
2079
|
+
let pdfExportAvailable = false;
|
|
2080
|
+
let pptxExportAvailable = false;
|
|
2081
|
+
let markdownImportAvailable = false;
|
|
1074
2082
|
let presenterViewOpen = false;
|
|
2083
|
+
let presenterViewRequested = false;
|
|
1075
2084
|
let pdfExportPending = false;
|
|
2085
|
+
let pptxExportPending = false;
|
|
1076
2086
|
|
|
1077
2087
|
// Derive a short overview title from a slide fragment: first heading, else first
|
|
1078
2088
|
// non-empty body line, trimmed. Mirrors the skill's title rule.
|
|
@@ -1119,7 +2129,7 @@ async function fetchDeck() {
|
|
|
1119
2129
|
* early in init and never reaches this code. Return true only when the state changes.
|
|
1120
2130
|
*/
|
|
1121
2131
|
function setArchitectureEditMode(enabled) {
|
|
1122
|
-
const next = Boolean(enabled) && !presenterMode;
|
|
2132
|
+
const next = Boolean(enabled) && architectureEditAvailable && !presenterMode;
|
|
1123
2133
|
if (next === architectureEditMode) return false;
|
|
1124
2134
|
architectureEditMode = next;
|
|
1125
2135
|
document.body.classList.toggle("architecture-edit-mode", next);
|
|
@@ -1130,10 +2140,11 @@ function setArchitectureEditMode(enabled) {
|
|
|
1130
2140
|
function updateArchitectureEditButton(enabled = architectureEditMode) {
|
|
1131
2141
|
const button = document.getElementById("navEdit");
|
|
1132
2142
|
if (!button) return;
|
|
1133
|
-
button.hidden = presenterMode;
|
|
2143
|
+
button.hidden = presenterMode || !architectureEditAvailable;
|
|
1134
2144
|
button.dataset.state = enabled && !presenterMode ? "active" : "";
|
|
1135
2145
|
button.title = enabled ? "Exit shape editing mode" : "Shape editing mode";
|
|
1136
2146
|
button.setAttribute("aria-label", button.title);
|
|
2147
|
+
syncMoreControls();
|
|
1137
2148
|
}
|
|
1138
2149
|
|
|
1139
2150
|
function sourceWatchErrorMessage(code) {
|
|
@@ -1149,10 +2160,11 @@ function updateSourceModeButton() {
|
|
|
1149
2160
|
const button = document.getElementById("navSourceMode");
|
|
1150
2161
|
const status = document.getElementById("sourceStatus");
|
|
1151
2162
|
if (!button) return;
|
|
1152
|
-
button.hidden = presenterMode || !sourceBacked;
|
|
2163
|
+
button.hidden = presenterMode || !sourceBacked || !sourceModeAvailable;
|
|
1153
2164
|
if (!sourceBacked) {
|
|
1154
2165
|
button.dataset.state = "";
|
|
1155
2166
|
if (status) status.textContent = "";
|
|
2167
|
+
syncMoreControls();
|
|
1156
2168
|
return;
|
|
1157
2169
|
}
|
|
1158
2170
|
if (sourceMode === "live" && sourceWatchStatus === "error") {
|
|
@@ -1161,6 +2173,7 @@ function updateSourceModeButton() {
|
|
|
1161
2173
|
button.title = `${message}. Click to pin the display to the loaded snapshot`;
|
|
1162
2174
|
button.setAttribute("aria-label", button.title);
|
|
1163
2175
|
if (status) status.textContent = message;
|
|
2176
|
+
syncMoreControls();
|
|
1164
2177
|
return;
|
|
1165
2178
|
}
|
|
1166
2179
|
const live = sourceMode === "live";
|
|
@@ -1174,6 +2187,7 @@ function updateSourceModeButton() {
|
|
|
1174
2187
|
? "Slides refresh automatically when Markdown is saved"
|
|
1175
2188
|
: "Markdown retains the display from the loaded snapshot";
|
|
1176
2189
|
}
|
|
2190
|
+
syncMoreControls();
|
|
1177
2191
|
}
|
|
1178
2192
|
|
|
1179
2193
|
async function requestSourceMode(mode) {
|
|
@@ -1197,7 +2211,7 @@ async function requestSourceMode(mode) {
|
|
|
1197
2211
|
}
|
|
1198
2212
|
|
|
1199
2213
|
async function toggleSourceMode() {
|
|
1200
|
-
if (presenterMode || !sourceBacked) return;
|
|
2214
|
+
if (presenterMode || !sourceBacked || !sourceModeAvailable) return;
|
|
1201
2215
|
await requestSourceMode(sourceMode === "live" ? "snapshot" : "live");
|
|
1202
2216
|
}
|
|
1203
2217
|
|
|
@@ -1218,7 +2232,7 @@ async function requestArchitectureEditMode(enabled) {
|
|
|
1218
2232
|
}
|
|
1219
2233
|
|
|
1220
2234
|
async function toggleArchitectureEditMode() {
|
|
1221
|
-
if (presenterMode) return;
|
|
2235
|
+
if (presenterMode || !architectureEditAvailable) return;
|
|
1222
2236
|
await requestArchitectureEditMode(!architectureEditMode);
|
|
1223
2237
|
await fetchState();
|
|
1224
2238
|
}
|
|
@@ -1230,7 +2244,23 @@ async function toggleArchitectureEditMode() {
|
|
|
1230
2244
|
* Always return save success or failure to the caller. Swallowing it would make
|
|
1231
2245
|
* an unsaved edit look successful, recreating the silent-ignore behavior fixed in Phase 5.
|
|
1232
2246
|
*/
|
|
1233
|
-
|
|
2247
|
+
function saveArchitectureBlock(index, block, source, editorRenderToken) {
|
|
2248
|
+
const pending = architectureSaveQueue
|
|
2249
|
+
.catch(() => {})
|
|
2250
|
+
.then(() => {
|
|
2251
|
+
if (editorRenderToken !== renderToken) {
|
|
2252
|
+
return {
|
|
2253
|
+
ok: false,
|
|
2254
|
+
message: "The displayed deck was replaced. Select the diagram again",
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
return saveArchitectureBlockNow(index, block, source);
|
|
2258
|
+
});
|
|
2259
|
+
architectureSaveQueue = pending;
|
|
2260
|
+
return pending;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
async function saveArchitectureBlockNow(index, block, source) {
|
|
1234
2264
|
let res;
|
|
1235
2265
|
try {
|
|
1236
2266
|
res = await fetch("./edit", {
|
|
@@ -1283,6 +2313,9 @@ async function saveArchitectureBlock(index, block, source) {
|
|
|
1283
2313
|
}
|
|
1284
2314
|
|
|
1285
2315
|
async function openDetailedArchitectureEditor(index, block) {
|
|
2316
|
+
const pendingWindow =
|
|
2317
|
+
architectureDetailedEditTarget === "window" ? window.open("", "_blank") : null;
|
|
2318
|
+
if (pendingWindow) pendingWindow.opener = null;
|
|
1286
2319
|
let response;
|
|
1287
2320
|
try {
|
|
1288
2321
|
response = await fetch("./architecture-editor/open", {
|
|
@@ -1291,10 +2324,22 @@ async function openDetailedArchitectureEditor(index, block) {
|
|
|
1291
2324
|
body: JSON.stringify({ index, block }),
|
|
1292
2325
|
});
|
|
1293
2326
|
} catch (_) {
|
|
2327
|
+
pendingWindow?.close();
|
|
1294
2328
|
return { ok: false, message: "Could not connect to the server." };
|
|
1295
2329
|
}
|
|
1296
2330
|
const result = await response.json().catch(() => ({}));
|
|
1297
|
-
if (response.ok && result.ok === true)
|
|
2331
|
+
if (response.ok && result.ok === true) {
|
|
2332
|
+
if (typeof result.url === "string" && result.url) {
|
|
2333
|
+
if (pendingWindow) pendingWindow.location.replace(result.url);
|
|
2334
|
+
else if (!window.open(result.url, "_blank", "noopener")) {
|
|
2335
|
+
return { ok: false, message: "Allow pop-ups to open the Architecture Editor." };
|
|
2336
|
+
}
|
|
2337
|
+
} else {
|
|
2338
|
+
pendingWindow?.close();
|
|
2339
|
+
}
|
|
2340
|
+
return result;
|
|
2341
|
+
}
|
|
2342
|
+
pendingWindow?.close();
|
|
1298
2343
|
if (result.error === "source_not_available") {
|
|
1299
2344
|
return {
|
|
1300
2345
|
ok: false,
|
|
@@ -1320,10 +2365,29 @@ async function fetchState() {
|
|
|
1320
2365
|
data.customThemeMeta && typeof data.customThemeMeta === "object"
|
|
1321
2366
|
? data.customThemeMeta
|
|
1322
2367
|
: null;
|
|
2368
|
+
if (typeof data.presenterWindowAvailable === "boolean") {
|
|
2369
|
+
presenterWindowAvailable = data.presenterWindowAvailable;
|
|
2370
|
+
}
|
|
2371
|
+
if (typeof data.presenterViewAvailable === "boolean") {
|
|
2372
|
+
presenterViewAvailable = data.presenterViewAvailable;
|
|
2373
|
+
}
|
|
2374
|
+
if (typeof data.pdfExportAvailable === "boolean") {
|
|
2375
|
+
pdfExportAvailable = data.pdfExportAvailable;
|
|
2376
|
+
}
|
|
2377
|
+
if (typeof data.pptxExportAvailable === "boolean") {
|
|
2378
|
+
pptxExportAvailable = data.pptxExportAvailable;
|
|
2379
|
+
}
|
|
2380
|
+
if (typeof data.markdownImportAvailable === "boolean") {
|
|
2381
|
+
markdownImportAvailable = data.markdownImportAvailable;
|
|
2382
|
+
}
|
|
1323
2383
|
if (typeof data.presenterRunning === "boolean") {
|
|
1324
2384
|
updatePresenterButton(data.presenterRunning);
|
|
1325
2385
|
}
|
|
2386
|
+
updateHostActionButtons();
|
|
1326
2387
|
if (typeof data.sourceBacked === "boolean") sourceBacked = data.sourceBacked;
|
|
2388
|
+
if (typeof data.sourceModeAvailable === "boolean") {
|
|
2389
|
+
sourceModeAvailable = data.sourceModeAvailable;
|
|
2390
|
+
}
|
|
1327
2391
|
sourceMode = data.sourceMode === "live" ? "live" : "snapshot";
|
|
1328
2392
|
sourceWatchStatus =
|
|
1329
2393
|
data.sourceWatchStatus === "watching" || data.sourceWatchStatus === "error"
|
|
@@ -1331,6 +2395,14 @@ async function fetchState() {
|
|
|
1331
2395
|
: "inactive";
|
|
1332
2396
|
sourceWatchError = typeof data.sourceWatchError === "string" ? data.sourceWatchError : "";
|
|
1333
2397
|
updateSourceModeButton();
|
|
2398
|
+
const editAvailabilityChanged =
|
|
2399
|
+
typeof data.architectureEditAvailable === "boolean" &&
|
|
2400
|
+
data.architectureEditAvailable !== architectureEditAvailable;
|
|
2401
|
+
if (typeof data.architectureEditAvailable === "boolean") {
|
|
2402
|
+
architectureEditAvailable = data.architectureEditAvailable;
|
|
2403
|
+
}
|
|
2404
|
+
architectureDetailedEditTarget =
|
|
2405
|
+
data.architectureDetailedEditTarget === "window" ? "window" : "canvas";
|
|
1334
2406
|
const detailedEditChanged =
|
|
1335
2407
|
typeof data.architectureDetailedEdit === "boolean" &&
|
|
1336
2408
|
data.architectureDetailedEdit !== architectureDetailedEdit;
|
|
@@ -1338,7 +2410,15 @@ async function fetchState() {
|
|
|
1338
2410
|
architectureDetailedEdit = data.architectureDetailedEdit;
|
|
1339
2411
|
}
|
|
1340
2412
|
// Editing-mode changes do not increment the version, so process them before the version guard.
|
|
2413
|
+
let availabilityDisabledEditMode = false;
|
|
2414
|
+
if (editAvailabilityChanged) {
|
|
2415
|
+
if (!architectureEditAvailable) {
|
|
2416
|
+
availabilityDisabledEditMode = setArchitectureEditMode(false);
|
|
2417
|
+
}
|
|
2418
|
+
updateArchitectureEditButton();
|
|
2419
|
+
}
|
|
1341
2420
|
if (
|
|
2421
|
+
availabilityDisabledEditMode ||
|
|
1342
2422
|
(typeof data.architectureEdit === "boolean" &&
|
|
1343
2423
|
setArchitectureEditMode(data.architectureEdit)) ||
|
|
1344
2424
|
(architectureEditMode && detailedEditChanged)
|
|
@@ -1360,6 +2440,10 @@ async function fetchState() {
|
|
|
1360
2440
|
navMode = data.mode === "adhoc" ? "adhoc" : "deck";
|
|
1361
2441
|
renderSlide(typeof data.markdown === "string" ? data.markdown : "");
|
|
1362
2442
|
updateNav();
|
|
2443
|
+
if (presenterViewRequested) {
|
|
2444
|
+
presenterViewRequested = false;
|
|
2445
|
+
openPresenterView();
|
|
2446
|
+
}
|
|
1363
2447
|
}
|
|
1364
2448
|
|
|
1365
2449
|
// --- navigation ------------------------------------------------------------
|
|
@@ -1391,7 +2475,7 @@ function goToIndex(i) {
|
|
|
1391
2475
|
}
|
|
1392
2476
|
|
|
1393
2477
|
async function setPresenterRunning(running) {
|
|
1394
|
-
if (presenterRequestPending) return;
|
|
2478
|
+
if (!presenterWindowAvailable || presenterRequestPending) return;
|
|
1395
2479
|
presenterRequestPending = true;
|
|
1396
2480
|
const button = document.getElementById("navPresent");
|
|
1397
2481
|
const status = document.getElementById("presentStatus");
|
|
@@ -1431,6 +2515,7 @@ async function setPresenterRunning(running) {
|
|
|
1431
2515
|
button.dataset.state = "error";
|
|
1432
2516
|
button.title = message;
|
|
1433
2517
|
}
|
|
2518
|
+
syncMoreControls();
|
|
1434
2519
|
} finally {
|
|
1435
2520
|
presenterRequestPending = false;
|
|
1436
2521
|
if (button) button.disabled = false;
|
|
@@ -1445,6 +2530,82 @@ function togglePresenterWindow() {
|
|
|
1445
2530
|
return setPresenterRunning(!presenterRunning);
|
|
1446
2531
|
}
|
|
1447
2532
|
|
|
2533
|
+
function visibleMoreControlButtons() {
|
|
2534
|
+
const panel = document.getElementById("navMorePanel");
|
|
2535
|
+
if (!panel) return [];
|
|
2536
|
+
return [...panel.querySelectorAll(".nav-more-item")].filter((button) => !button.hidden);
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
function setMoreControlsOpen(enabled, { focusFirst = false, restoreFocus = false } = {}) {
|
|
2540
|
+
const nav = document.getElementById("nav");
|
|
2541
|
+
const trigger = document.getElementById("navMore");
|
|
2542
|
+
const panel = document.getElementById("navMorePanel");
|
|
2543
|
+
if (!nav || !trigger || !panel) return;
|
|
2544
|
+
|
|
2545
|
+
const next = Boolean(enabled) && !nav.classList.contains("nav-empty") && !trigger.hidden;
|
|
2546
|
+
moreControlsOpen = next;
|
|
2547
|
+
trigger.setAttribute("aria-expanded", next ? "true" : "false");
|
|
2548
|
+
panel.hidden = nav.classList.contains("nav-empty") ? !markdownImportAvailable : !next;
|
|
2549
|
+
|
|
2550
|
+
if (next && focusFirst) {
|
|
2551
|
+
requestAnimationFrame(() => visibleMoreControlButtons()[0]?.focus());
|
|
2552
|
+
} else if (!next && restoreFocus) {
|
|
2553
|
+
trigger.focus();
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
function toggleMoreControls() {
|
|
2558
|
+
setMoreControlsOpen(!moreControlsOpen, { focusFirst: !moreControlsOpen });
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
function syncMoreControls() {
|
|
2562
|
+
const nav = document.getElementById("nav");
|
|
2563
|
+
const trigger = document.getElementById("navMore");
|
|
2564
|
+
const panel = document.getElementById("navMorePanel");
|
|
2565
|
+
if (!nav || !trigger || !panel) return;
|
|
2566
|
+
|
|
2567
|
+
panel.querySelectorAll(".nav-more-group").forEach((group) => {
|
|
2568
|
+
group.hidden = ![...group.querySelectorAll(".nav-more-item")].some(
|
|
2569
|
+
(button) => !button.hidden,
|
|
2570
|
+
);
|
|
2571
|
+
});
|
|
2572
|
+
|
|
2573
|
+
const buttons = visibleMoreControlButtons();
|
|
2574
|
+
const hasError = buttons.some((button) => button.dataset.state === "error");
|
|
2575
|
+
const hasActive = buttons.some((button) => button.dataset.state === "active");
|
|
2576
|
+
trigger.dataset.state = hasError ? "error" : hasActive ? "active" : "";
|
|
2577
|
+
trigger.title = hasError
|
|
2578
|
+
? "More controls (attention required)"
|
|
2579
|
+
: hasActive
|
|
2580
|
+
? "More controls (an option is active)"
|
|
2581
|
+
: "More controls";
|
|
2582
|
+
trigger.setAttribute("aria-label", trigger.title);
|
|
2583
|
+
|
|
2584
|
+
const empty = nav.classList.contains("nav-empty");
|
|
2585
|
+
trigger.hidden = empty || buttons.length === 0;
|
|
2586
|
+
if (trigger.hidden) moreControlsOpen = false;
|
|
2587
|
+
trigger.setAttribute("aria-expanded", moreControlsOpen ? "true" : "false");
|
|
2588
|
+
panel.hidden = empty ? !markdownImportAvailable : !moreControlsOpen;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
function updateHostActionButtons() {
|
|
2592
|
+
const present = document.getElementById("navPresent");
|
|
2593
|
+
if (present) present.hidden = presenterMode || !presenterWindowAvailable;
|
|
2594
|
+
const presenterView = document.getElementById("navPresenterView");
|
|
2595
|
+
if (presenterView) presenterView.hidden = presenterMode || !presenterViewAvailable;
|
|
2596
|
+
const presenterToggle = document.getElementById("presenterToggleButton");
|
|
2597
|
+
if (presenterToggle) presenterToggle.hidden = !presenterWindowAvailable;
|
|
2598
|
+
const exportButton = document.getElementById("navExport");
|
|
2599
|
+
if (exportButton) exportButton.hidden = presenterMode || !pdfExportAvailable;
|
|
2600
|
+
const pptxButton = document.getElementById("navExportPptx");
|
|
2601
|
+
if (pptxButton) pptxButton.hidden = presenterMode || !pptxExportAvailable;
|
|
2602
|
+
const importButton = document.getElementById("navImport");
|
|
2603
|
+
if (importButton) importButton.hidden = presenterMode || !markdownImportAvailable;
|
|
2604
|
+
if (!presenterViewAvailable && presenterViewOpen) closePresenterView();
|
|
2605
|
+
if (!markdownImportAvailable && importOpen) closeImportPicker();
|
|
2606
|
+
syncMoreControls();
|
|
2607
|
+
}
|
|
2608
|
+
|
|
1448
2609
|
function updatePresenterButton(running, message = "") {
|
|
1449
2610
|
presenterRunning = running;
|
|
1450
2611
|
const button = document.getElementById("navPresent");
|
|
@@ -1459,14 +2620,17 @@ function updatePresenterButton(running, message = "") {
|
|
|
1459
2620
|
toggle.textContent = running ? "End presentation" : "Start presentation";
|
|
1460
2621
|
toggle.dataset.state = running ? "active" : "";
|
|
1461
2622
|
}
|
|
2623
|
+
syncMoreControls();
|
|
1462
2624
|
}
|
|
1463
2625
|
|
|
1464
2626
|
async function exportPdfFromCanvas() {
|
|
1465
|
-
if (pdfExportPending) return;
|
|
2627
|
+
if (!pdfExportAvailable || pdfExportPending || pptxExportPending) return;
|
|
1466
2628
|
pdfExportPending = true;
|
|
1467
2629
|
const button = document.getElementById("navExport");
|
|
2630
|
+
const pptxButton = document.getElementById("navExportPptx");
|
|
1468
2631
|
const status = document.getElementById("exportStatus");
|
|
1469
2632
|
if (button) button.disabled = true;
|
|
2633
|
+
if (pptxButton) pptxButton.disabled = true;
|
|
1470
2634
|
if (status) status.textContent = "Saving PDF.";
|
|
1471
2635
|
|
|
1472
2636
|
try {
|
|
@@ -1486,6 +2650,7 @@ async function exportPdfFromCanvas() {
|
|
|
1486
2650
|
button.dataset.state = "active";
|
|
1487
2651
|
button.title = message;
|
|
1488
2652
|
}
|
|
2653
|
+
syncMoreControls();
|
|
1489
2654
|
} catch (error) {
|
|
1490
2655
|
const message = error?.message || "Could not save the PDF.";
|
|
1491
2656
|
console.error("PDF export failed", error);
|
|
@@ -1494,9 +2659,57 @@ async function exportPdfFromCanvas() {
|
|
|
1494
2659
|
button.dataset.state = "error";
|
|
1495
2660
|
button.title = message;
|
|
1496
2661
|
}
|
|
2662
|
+
syncMoreControls();
|
|
1497
2663
|
} finally {
|
|
1498
2664
|
pdfExportPending = false;
|
|
1499
2665
|
if (button) button.disabled = false;
|
|
2666
|
+
if (pptxButton) pptxButton.disabled = false;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
async function exportPptxFromCanvas() {
|
|
2671
|
+
if (!pptxExportAvailable || pdfExportPending || pptxExportPending) return;
|
|
2672
|
+
pptxExportPending = true;
|
|
2673
|
+
const button = document.getElementById("navExportPptx");
|
|
2674
|
+
const pdfButton = document.getElementById("navExport");
|
|
2675
|
+
const status = document.getElementById("exportStatus");
|
|
2676
|
+
if (button) button.disabled = true;
|
|
2677
|
+
if (pdfButton) pdfButton.disabled = true;
|
|
2678
|
+
if (status) status.textContent = "Saving editable PowerPoint.";
|
|
2679
|
+
|
|
2680
|
+
try {
|
|
2681
|
+
const response = await fetch("./export-pptx", {
|
|
2682
|
+
method: "POST",
|
|
2683
|
+
headers: { Accept: "application/json" },
|
|
2684
|
+
cache: "no-store",
|
|
2685
|
+
});
|
|
2686
|
+
const data = await response.json().catch(() => ({}));
|
|
2687
|
+
if (!response.ok) {
|
|
2688
|
+
throw new Error(data.message || `PowerPoint export failed (${response.status}).`);
|
|
2689
|
+
}
|
|
2690
|
+
const filename = data.path ? data.path.split(/[\\/]/).pop() : "PowerPoint";
|
|
2691
|
+
const fallback =
|
|
2692
|
+
data.fallbackCount > 0 ? ` ${data.fallbackCount} fallback item(s) preserved.` : "";
|
|
2693
|
+
const message = `Saved ${filename}.${fallback}`;
|
|
2694
|
+
if (status) status.textContent = message;
|
|
2695
|
+
if (button) {
|
|
2696
|
+
button.dataset.state = "active";
|
|
2697
|
+
button.title = message;
|
|
2698
|
+
}
|
|
2699
|
+
syncMoreControls();
|
|
2700
|
+
} catch (error) {
|
|
2701
|
+
const message = error?.message || "Could not save the PowerPoint presentation.";
|
|
2702
|
+
console.error("PowerPoint export failed", error);
|
|
2703
|
+
if (status) status.textContent = message;
|
|
2704
|
+
if (button) {
|
|
2705
|
+
button.dataset.state = "error";
|
|
2706
|
+
button.title = message;
|
|
2707
|
+
}
|
|
2708
|
+
syncMoreControls();
|
|
2709
|
+
} finally {
|
|
2710
|
+
pptxExportPending = false;
|
|
2711
|
+
if (button) button.disabled = false;
|
|
2712
|
+
if (pdfButton) pdfButton.disabled = false;
|
|
1500
2713
|
}
|
|
1501
2714
|
}
|
|
1502
2715
|
|
|
@@ -1512,6 +2725,7 @@ function setFixedPreviewMode(enabled) {
|
|
|
1512
2725
|
? "Return to responsive canvas layout"
|
|
1513
2726
|
: "Preview PDF layout at 16:9";
|
|
1514
2727
|
}
|
|
2728
|
+
syncMoreControls();
|
|
1515
2729
|
if (fixedPreviewMode) {
|
|
1516
2730
|
updateFixedPreviewScale();
|
|
1517
2731
|
} else {
|
|
@@ -1529,10 +2743,10 @@ function toggleFixedPreviewMode() {
|
|
|
1529
2743
|
function updateNav() {
|
|
1530
2744
|
const nav = document.getElementById("nav");
|
|
1531
2745
|
if (!nav) return;
|
|
1532
|
-
// Outside presenter view, show only the load button
|
|
1533
|
-
//
|
|
2746
|
+
// Outside presenter view, show only the load button when the host supports
|
|
2747
|
+
// Markdown import before any slide has been loaded.
|
|
1534
2748
|
const empty = navTotal <= 0;
|
|
1535
|
-
nav.hidden = previewMode || (empty && presenterMode);
|
|
2749
|
+
nav.hidden = previewMode || (empty && (presenterMode || !markdownImportAvailable));
|
|
1536
2750
|
nav.classList.toggle("nav-empty", empty);
|
|
1537
2751
|
const counter = document.getElementById("navCounter");
|
|
1538
2752
|
if (counter) {
|
|
@@ -1546,10 +2760,11 @@ function updateNav() {
|
|
|
1546
2760
|
if (next) next.disabled = navMode === "deck" && navIndex >= navTotal - 1;
|
|
1547
2761
|
highlightOverview();
|
|
1548
2762
|
updatePresenterView();
|
|
2763
|
+
syncMoreControls();
|
|
1549
2764
|
}
|
|
1550
2765
|
|
|
1551
2766
|
function openPresenterView() {
|
|
1552
|
-
if (presenterMode || navTotal <= 0) return;
|
|
2767
|
+
if (presenterMode || !presenterViewAvailable || navTotal <= 0) return;
|
|
1553
2768
|
presenterViewOpen = true;
|
|
1554
2769
|
document.body.classList.add("presenter-view-mode");
|
|
1555
2770
|
const view = document.getElementById("presenterView");
|
|
@@ -1570,7 +2785,7 @@ function closePresenterView() {
|
|
|
1570
2785
|
document.body.classList.remove("presenter-view-mode");
|
|
1571
2786
|
const view = document.getElementById("presenterView");
|
|
1572
2787
|
if (view) view.hidden = true;
|
|
1573
|
-
document.getElementById("
|
|
2788
|
+
document.getElementById("navMore")?.focus();
|
|
1574
2789
|
}
|
|
1575
2790
|
|
|
1576
2791
|
function updatePresenterView() {
|
|
@@ -1770,7 +2985,7 @@ async function importMarkdown(path) {
|
|
|
1770
2985
|
}
|
|
1771
2986
|
|
|
1772
2987
|
function openImportPicker() {
|
|
1773
|
-
if (presenterMode) return;
|
|
2988
|
+
if (presenterMode || !markdownImportAvailable) return;
|
|
1774
2989
|
importOpen = true;
|
|
1775
2990
|
const el = document.getElementById("importPicker");
|
|
1776
2991
|
if (el) el.hidden = false;
|
|
@@ -1894,10 +3109,11 @@ function wirePreviewKeyboardNavigation() {
|
|
|
1894
3109
|
}
|
|
1895
3110
|
|
|
1896
3111
|
function wireControls() {
|
|
1897
|
-
const bind = (id, fn) => {
|
|
3112
|
+
const bind = (id, fn, { closeMore = false } = {}) => {
|
|
1898
3113
|
const el = document.getElementById(id);
|
|
1899
3114
|
if (!el) return;
|
|
1900
3115
|
el.addEventListener("click", () => {
|
|
3116
|
+
if (closeMore) setMoreControlsOpen(false);
|
|
1901
3117
|
fn();
|
|
1902
3118
|
// Drop focus so a follow-up Space/Enter doesn't re-trigger the button on
|
|
1903
3119
|
// top of the global keyboard handler.
|
|
@@ -1906,14 +3122,16 @@ function wireControls() {
|
|
|
1906
3122
|
};
|
|
1907
3123
|
bind("navPrev", goPrev);
|
|
1908
3124
|
bind("navNext", goNext);
|
|
1909
|
-
bind("navEdit", toggleArchitectureEditMode);
|
|
1910
|
-
bind("navPresent", openPresenterWindow);
|
|
1911
|
-
bind("navPresenterView", openPresenterView);
|
|
1912
|
-
bind("navFixedPreview", toggleFixedPreviewMode);
|
|
1913
|
-
bind("navExport", exportPdfFromCanvas);
|
|
1914
|
-
bind("navImport", toggleImportPicker);
|
|
1915
|
-
bind("navSourceMode", toggleSourceMode);
|
|
1916
3125
|
bind("navList", toggleOverview);
|
|
3126
|
+
bind("navMore", toggleMoreControls);
|
|
3127
|
+
bind("navEdit", toggleArchitectureEditMode, { closeMore: true });
|
|
3128
|
+
bind("navPresent", openPresenterWindow, { closeMore: true });
|
|
3129
|
+
bind("navPresenterView", openPresenterView, { closeMore: true });
|
|
3130
|
+
bind("navFixedPreview", toggleFixedPreviewMode, { closeMore: true });
|
|
3131
|
+
bind("navExport", exportPdfFromCanvas, { closeMore: true });
|
|
3132
|
+
bind("navExportPptx", exportPptxFromCanvas, { closeMore: true });
|
|
3133
|
+
bind("navImport", toggleImportPicker, { closeMore: true });
|
|
3134
|
+
bind("navSourceMode", toggleSourceMode, { closeMore: true });
|
|
1917
3135
|
bind("overviewClose", closeOverview);
|
|
1918
3136
|
bind("importClose", closeImportPicker);
|
|
1919
3137
|
bind("presenterPrevButton", goPrev);
|
|
@@ -1941,6 +3159,14 @@ function wireControls() {
|
|
|
1941
3159
|
});
|
|
1942
3160
|
}
|
|
1943
3161
|
|
|
3162
|
+
document.addEventListener("pointerdown", (e) => {
|
|
3163
|
+
if (!moreControlsOpen) return;
|
|
3164
|
+
const tools = document.getElementById("navTools");
|
|
3165
|
+
const panel = document.getElementById("navMorePanel");
|
|
3166
|
+
if (tools?.contains(e.target) || panel?.contains(e.target)) return;
|
|
3167
|
+
setMoreControlsOpen(false);
|
|
3168
|
+
});
|
|
3169
|
+
|
|
1944
3170
|
wirePointerNavigation();
|
|
1945
3171
|
|
|
1946
3172
|
// The iframe must be focused to receive key events; grab focus up front and
|
|
@@ -1963,16 +3189,23 @@ function wireControls() {
|
|
|
1963
3189
|
e.preventDefault();
|
|
1964
3190
|
return;
|
|
1965
3191
|
}
|
|
3192
|
+
if (e.key === "Escape" && moreControlsOpen) {
|
|
3193
|
+
setMoreControlsOpen(false, { restoreFocus: true });
|
|
3194
|
+
e.preventDefault();
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
1966
3197
|
if (handleSlideNavigationKey(e)) return;
|
|
1967
3198
|
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
|
|
1968
3199
|
switch (e.key) {
|
|
1969
3200
|
case "o":
|
|
1970
3201
|
case "O":
|
|
3202
|
+
setMoreControlsOpen(false);
|
|
1971
3203
|
toggleOverview();
|
|
1972
3204
|
e.preventDefault();
|
|
1973
3205
|
break;
|
|
1974
3206
|
case "i":
|
|
1975
3207
|
case "I":
|
|
3208
|
+
setMoreControlsOpen(false);
|
|
1976
3209
|
toggleImportPicker();
|
|
1977
3210
|
e.preventDefault();
|
|
1978
3211
|
break;
|
|
@@ -2014,6 +3247,11 @@ function init() {
|
|
|
2014
3247
|
} catch (_) {}
|
|
2015
3248
|
|
|
2016
3249
|
const params = new URLSearchParams(window.location.search);
|
|
3250
|
+
presenterViewRequested = params.get("presenter") === "1";
|
|
3251
|
+
if (params.get("pptx") === "1") {
|
|
3252
|
+
initPptx(params).catch(reportPptxBootstrapFailure);
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
2017
3255
|
if (params.get("capture") === "1") {
|
|
2018
3256
|
initCapture(params).catch(reportCaptureBootstrapFailure);
|
|
2019
3257
|
return;
|