@markdstage/markdstage 0.1.3 → 2.3.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 +29 -12
- 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 +1 -1
- package/shared/renderer/architecture.mjs +233 -0
- package/shared/renderer/index.html +7 -6
- package/shared/renderer/renderer.js +1163 -16
- package/shared/renderer/slides.css +42 -2
- 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 +9 -3
|
@@ -90,7 +90,9 @@ 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;
|
|
@@ -100,6 +102,9 @@ let fixedPreviewMode = false;
|
|
|
100
102
|
let lastMarkdown = "";
|
|
101
103
|
// Editing UI attached to the rendered slide; destroyed on every rerender.
|
|
102
104
|
let architectureEditors = [];
|
|
105
|
+
// Serialize saves from every Architecture block so each request uses the deck
|
|
106
|
+
// version returned by the previous save.
|
|
107
|
+
let architectureSaveQueue = Promise.resolve();
|
|
103
108
|
// `layoutTarget` is the slide currently on screen (cover and back cover
|
|
104
109
|
// included); `autoSize` says whether it also takes part in the font auto-fit.
|
|
105
110
|
let layoutTarget = null;
|
|
@@ -701,13 +706,15 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
|
|
|
701
706
|
host.className = "architecture-edit-host";
|
|
702
707
|
host.setAttribute("data-architecture-block", String(blockIndex));
|
|
703
708
|
target.replaceWith(host);
|
|
709
|
+
const editorRenderToken = renderToken;
|
|
704
710
|
const editor = attachArchitectureEditor(host, {
|
|
705
711
|
source,
|
|
706
712
|
documentRef: document,
|
|
707
713
|
canOpenDetail: architectureDetailedEdit,
|
|
708
714
|
onOpenDetail: () => openDetailedArchitectureEditor(slideIndex, blockIndex),
|
|
709
715
|
// Return the save result to the editor; omitting it makes failures look successful.
|
|
710
|
-
onCommit: (next) =>
|
|
716
|
+
onCommit: (next) =>
|
|
717
|
+
saveArchitectureBlock(slideIndex, blockIndex, next, editorRenderToken),
|
|
711
718
|
});
|
|
712
719
|
if (!editor) {
|
|
713
720
|
// Do not edit invalid DSL; fall back to the standard error display.
|
|
@@ -759,6 +766,7 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
|
|
|
759
766
|
sizeMode,
|
|
760
767
|
titleSlide,
|
|
761
768
|
sectionSlide,
|
|
769
|
+
centerSlide,
|
|
762
770
|
backcoverSlide,
|
|
763
771
|
title: meta.title || meta.deck || "Slide",
|
|
764
772
|
};
|
|
@@ -771,11 +779,11 @@ function renderSlide(markdown) {
|
|
|
771
779
|
architectureEditors.forEach((editor) => editor.destroy());
|
|
772
780
|
architectureEditors = [];
|
|
773
781
|
applyCustomThemeCss(customThemeCss);
|
|
782
|
+
const token = ++renderToken;
|
|
774
783
|
const slide = createSlide(markdown, deckTheme);
|
|
775
784
|
document.title = slide.title;
|
|
776
785
|
document.documentElement.setAttribute("data-theme", slide.theme);
|
|
777
786
|
|
|
778
|
-
const token = ++renderToken;
|
|
779
787
|
document.body.classList.add("mermaid-loading");
|
|
780
788
|
document.getElementById("stage").replaceChildren(slide.deck);
|
|
781
789
|
if (layoutFrame) {
|
|
@@ -857,6 +865,990 @@ async function reportOutputStatus(token, status, error = "", layout = null) {
|
|
|
857
865
|
if (!response.ok) throw new Error(`Could not report output status (${response.status}).`);
|
|
858
866
|
}
|
|
859
867
|
|
|
868
|
+
const PPTX_RASTER_IMAGE = /\.(?:png|jpe?g|gif)(?:$|[?#])/i;
|
|
869
|
+
|
|
870
|
+
function normalizeCssColor(value) {
|
|
871
|
+
const text = String(value || "").trim();
|
|
872
|
+
if (!text || text === "none" || text === "transparent") return null;
|
|
873
|
+
const shortHex = text.match(/^#([\da-f])([\da-f])([\da-f])$/i);
|
|
874
|
+
if (shortHex) {
|
|
875
|
+
return `#${shortHex.slice(1).map((part) => part.repeat(2)).join("")}`.toUpperCase();
|
|
876
|
+
}
|
|
877
|
+
const hex = text.match(/^#([\da-f]{6})(?:[\da-f]{2})?$/i);
|
|
878
|
+
if (hex) return `#${text.slice(1).toUpperCase()}`;
|
|
879
|
+
const rgb = text.match(
|
|
880
|
+
/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i,
|
|
881
|
+
);
|
|
882
|
+
if (!rgb) return text;
|
|
883
|
+
if (rgb[4] === "0" || rgb[4] === "0%") return null;
|
|
884
|
+
const channels = rgb
|
|
885
|
+
.slice(1, 4)
|
|
886
|
+
.map((part) => Math.max(0, Math.min(255, Math.round(Number(part)))));
|
|
887
|
+
if (rgb[4] !== undefined) {
|
|
888
|
+
const alpha = rgb[4].endsWith("%") ? Number.parseFloat(rgb[4]) / 100 : Number(rgb[4]);
|
|
889
|
+
if (Number.isFinite(alpha) && alpha < 1) {
|
|
890
|
+
return `rgba(${channels.join(", ")}, ${Math.max(0, alpha)})`;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return `#${channels
|
|
894
|
+
.map((part) => part.toString(16).padStart(2, "0"))
|
|
895
|
+
.join("")
|
|
896
|
+
.toUpperCase()}`;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function resolveModelColor(value, context) {
|
|
900
|
+
if (!value || value === "none" || value === "transparent") return null;
|
|
901
|
+
const probe = document.createElement("span");
|
|
902
|
+
probe.style.color = String(value);
|
|
903
|
+
probe.style.position = "absolute";
|
|
904
|
+
probe.style.visibility = "hidden";
|
|
905
|
+
context.appendChild(probe);
|
|
906
|
+
const resolved = normalizeCssColor(getComputedStyle(probe).color);
|
|
907
|
+
probe.remove();
|
|
908
|
+
return resolved;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function relativeBounds(element, deck) {
|
|
912
|
+
const rect = element.getBoundingClientRect();
|
|
913
|
+
const slide = deck.getBoundingClientRect();
|
|
914
|
+
return {
|
|
915
|
+
x: roundedMetric(rect.left - slide.left),
|
|
916
|
+
y: roundedMetric(rect.top - slide.top),
|
|
917
|
+
width: roundedMetric(rect.width),
|
|
918
|
+
height: roundedMetric(rect.height),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function textContentBounds(element, deck) {
|
|
923
|
+
const range = document.createRange();
|
|
924
|
+
range.selectNodeContents(element);
|
|
925
|
+
const rect = range.getBoundingClientRect();
|
|
926
|
+
if (rect.width <= 0 || rect.height <= 0) return relativeBounds(element, deck);
|
|
927
|
+
const slide = deck.getBoundingClientRect();
|
|
928
|
+
return {
|
|
929
|
+
x: roundedMetric(rect.left - slide.left),
|
|
930
|
+
y: roundedMetric(rect.top - slide.top),
|
|
931
|
+
width: roundedMetric(rect.width),
|
|
932
|
+
height: roundedMetric(rect.height),
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function rasterImageSupported(image) {
|
|
937
|
+
const source = image.currentSrc || image.getAttribute("src") || "";
|
|
938
|
+
if (["cover", "none"].includes(getComputedStyle(image).objectFit)) return false;
|
|
939
|
+
if (/^data:image\/(?:png|jpeg|gif)[;,]/i.test(source)) return true;
|
|
940
|
+
try {
|
|
941
|
+
const url = new URL(source, window.location.href);
|
|
942
|
+
return url.origin === window.location.origin && PPTX_RASTER_IMAGE.test(url.href);
|
|
943
|
+
} catch (_) {
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function runStyle(element) {
|
|
949
|
+
const style = getComputedStyle(element);
|
|
950
|
+
const numericWeight = Number.parseInt(style.fontWeight, 10);
|
|
951
|
+
return {
|
|
952
|
+
fontFace: style.fontFamily.split(",")[0].trim().replace(/^["']|["']$/g, ""),
|
|
953
|
+
fontSize: roundedMetric(parseFloat(style.fontSize)),
|
|
954
|
+
bold: Number.isFinite(numericWeight) ? numericWeight >= 600 : style.fontWeight === "bold",
|
|
955
|
+
italic: style.fontStyle !== "normal",
|
|
956
|
+
underline: style.textDecorationLine.includes("underline"),
|
|
957
|
+
color: normalizeCssColor(style.color),
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function collectTextRuns(root, { omitNestedLists = false } = {}) {
|
|
962
|
+
const runs = [];
|
|
963
|
+
const append = (text, element) => {
|
|
964
|
+
if (!text) return;
|
|
965
|
+
const anchor = element.closest("a[href]");
|
|
966
|
+
const run = {
|
|
967
|
+
text,
|
|
968
|
+
...runStyle(element),
|
|
969
|
+
...(anchor ? { href: anchor.getAttribute("href") || anchor.href } : {}),
|
|
970
|
+
};
|
|
971
|
+
const previous = runs.at(-1);
|
|
972
|
+
const previousStyle = previous && { ...previous, text: undefined };
|
|
973
|
+
const nextStyle = { ...run, text: undefined };
|
|
974
|
+
if (previous && JSON.stringify(previousStyle) === JSON.stringify(nextStyle)) {
|
|
975
|
+
previous.text += text;
|
|
976
|
+
} else {
|
|
977
|
+
runs.push(run);
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
const visit = (node) => {
|
|
981
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
982
|
+
append(node.nodeValue || "", node.parentElement || root);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
986
|
+
if (node.tagName === "BR") {
|
|
987
|
+
append("\n", node.parentElement || root);
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (
|
|
991
|
+
node !== root &&
|
|
992
|
+
(node.matches("pre, table, .architecture-diagram, .architecture-error, .mermaid") ||
|
|
993
|
+
(omitNestedLists && node.matches("ul, ol")))
|
|
994
|
+
) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
node.childNodes.forEach(visit);
|
|
998
|
+
};
|
|
999
|
+
root.childNodes.forEach(visit);
|
|
1000
|
+
return runs;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function pptxAlignment(value) {
|
|
1004
|
+
if (value === "center" || value === "right" || value === "justify") return value;
|
|
1005
|
+
if (value === "end") return "right";
|
|
1006
|
+
return "left";
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function paragraphFor(element, options = {}) {
|
|
1010
|
+
const style = getComputedStyle(element);
|
|
1011
|
+
const runs = collectTextRuns(element, options);
|
|
1012
|
+
return {
|
|
1013
|
+
alignment: pptxAlignment(style.textAlign),
|
|
1014
|
+
lineHeight: roundedMetric(parseFloat(style.lineHeight)),
|
|
1015
|
+
runs,
|
|
1016
|
+
...(options.level !== undefined ? { level: options.level } : {}),
|
|
1017
|
+
...(options.bullet ? { bullet: options.bullet } : {}),
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function renderedTextLineCount(element) {
|
|
1022
|
+
const range = document.createRange();
|
|
1023
|
+
range.selectNodeContents(element);
|
|
1024
|
+
const tops = [];
|
|
1025
|
+
for (const rect of range.getClientRects()) {
|
|
1026
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
1027
|
+
if (!tops.some((top) => Math.abs(top - rect.top) < 1)) tops.push(rect.top);
|
|
1028
|
+
}
|
|
1029
|
+
return Math.max(1, tops.length);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function unsupportedEffects(element) {
|
|
1033
|
+
const style = getComputedStyle(element);
|
|
1034
|
+
const effects = [];
|
|
1035
|
+
if (style.textShadow && style.textShadow !== "none") effects.push("text-shadow");
|
|
1036
|
+
if (style.boxShadow && style.boxShadow !== "none") effects.push("box-shadow");
|
|
1037
|
+
if (style.filter && style.filter !== "none") effects.push("filter");
|
|
1038
|
+
if (style.backdropFilter && style.backdropFilter !== "none") effects.push("backdrop-filter");
|
|
1039
|
+
if (style.mixBlendMode && style.mixBlendMode !== "normal") effects.push("mix-blend-mode");
|
|
1040
|
+
if (style.transform && style.transform !== "none") effects.push("transform");
|
|
1041
|
+
if (Number(style.opacity) < 1) effects.push("opacity");
|
|
1042
|
+
return effects;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function effectFallbackRoot(element) {
|
|
1046
|
+
return element.closest(
|
|
1047
|
+
"p, li, blockquote, table, img, h1, h2, h3, h4, h5, h6, .kicker, .slide-title, .theme-backcover-logo-text, .theme-backcover-copyright, .body, header, footer",
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function pptxFallback(type, element, deck, reason) {
|
|
1052
|
+
return {
|
|
1053
|
+
type,
|
|
1054
|
+
path: elementPath(element, deck),
|
|
1055
|
+
reason,
|
|
1056
|
+
...relativeBounds(element, deck),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function preserveBoxShadow(element, deck) {
|
|
1061
|
+
const style = getComputedStyle(element);
|
|
1062
|
+
if (!style.boxShadow || style.boxShadow === "none") return;
|
|
1063
|
+
const bounds = relativeBounds(element, deck);
|
|
1064
|
+
const decoration = document.createElement("div");
|
|
1065
|
+
decoration.className = "pptx-effect-fallback";
|
|
1066
|
+
Object.assign(decoration.style, {
|
|
1067
|
+
position: "absolute",
|
|
1068
|
+
left: `${bounds.x}px`,
|
|
1069
|
+
top: `${bounds.y}px`,
|
|
1070
|
+
width: `${bounds.width}px`,
|
|
1071
|
+
height: `${bounds.height}px`,
|
|
1072
|
+
borderRadius: style.borderRadius,
|
|
1073
|
+
boxShadow: style.boxShadow,
|
|
1074
|
+
pointerEvents: "none",
|
|
1075
|
+
});
|
|
1076
|
+
decoration.setAttribute("aria-hidden", "true");
|
|
1077
|
+
deck.appendChild(decoration);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function blobDataUrl(blob) {
|
|
1081
|
+
return new Promise((resolve, reject) => {
|
|
1082
|
+
const reader = new FileReader();
|
|
1083
|
+
reader.addEventListener("load", () => resolve(String(reader.result || "")), {
|
|
1084
|
+
once: true,
|
|
1085
|
+
});
|
|
1086
|
+
reader.addEventListener(
|
|
1087
|
+
"error",
|
|
1088
|
+
() => reject(reader.error || new Error("Could not encode Architecture artwork.")),
|
|
1089
|
+
{ once: true },
|
|
1090
|
+
);
|
|
1091
|
+
reader.readAsDataURL(blob);
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function freezeSvgPaint(source, clone) {
|
|
1096
|
+
const sourceNodes = [source, ...source.querySelectorAll("*")];
|
|
1097
|
+
const cloneNodes = [clone, ...clone.querySelectorAll("*")];
|
|
1098
|
+
sourceNodes.forEach((node, index) => {
|
|
1099
|
+
const target = cloneNodes[index];
|
|
1100
|
+
if (!target) return;
|
|
1101
|
+
const style = getComputedStyle(node);
|
|
1102
|
+
for (const property of ["fill", "stroke", "color"]) {
|
|
1103
|
+
if (style[property]) target.setAttribute(property, style[property]);
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
async function inlineSvgImageSources(root) {
|
|
1109
|
+
for (const image of root.querySelectorAll("image")) {
|
|
1110
|
+
const href = image.getAttribute("href") || image.getAttribute("xlink:href");
|
|
1111
|
+
if (!href || href.startsWith("data:")) continue;
|
|
1112
|
+
const response = await fetch(new URL(href, window.location.href), { cache: "no-store" });
|
|
1113
|
+
if (!response.ok) {
|
|
1114
|
+
throw new Error(`Could not load Architecture image artwork (${response.status}).`);
|
|
1115
|
+
}
|
|
1116
|
+
const dataUrl = await blobDataUrl(await response.blob());
|
|
1117
|
+
image.setAttribute("href", dataUrl);
|
|
1118
|
+
image.removeAttribute("xlink:href");
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function architectureForegroundPng(svg, sources, width, height, crop) {
|
|
1123
|
+
if (!sources.length) return "";
|
|
1124
|
+
const clone = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1125
|
+
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
1126
|
+
const viewBox = svg.viewBox?.baseVal;
|
|
1127
|
+
clone.setAttribute("viewBox", svg.getAttribute("viewBox") || "0 0 1 1");
|
|
1128
|
+
clone.setAttribute("width", String(viewBox?.width || 1));
|
|
1129
|
+
clone.setAttribute("height", String(viewBox?.height || 1));
|
|
1130
|
+
clone.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
1131
|
+
const defs = svg.querySelector(":scope > defs");
|
|
1132
|
+
if (defs) clone.appendChild(defs.cloneNode(true));
|
|
1133
|
+
for (const source of sources) {
|
|
1134
|
+
const copy = source.cloneNode(true);
|
|
1135
|
+
freezeSvgPaint(source, copy);
|
|
1136
|
+
const ownOpacityValue = Number(getComputedStyle(source).opacity);
|
|
1137
|
+
const ownOpacity = Number.isFinite(ownOpacityValue) ? ownOpacityValue : 1;
|
|
1138
|
+
const node = source.closest('[data-architecture-type="node"]');
|
|
1139
|
+
const parentOpacityAttribute =
|
|
1140
|
+
node && node !== source ? node.getAttribute("opacity") : null;
|
|
1141
|
+
const parentOpacityValue =
|
|
1142
|
+
parentOpacityAttribute === null ? 1 : Number(parentOpacityAttribute);
|
|
1143
|
+
const parentOpacity = Number.isFinite(parentOpacityValue) ? parentOpacityValue : 1;
|
|
1144
|
+
copy.setAttribute("opacity", String(ownOpacity * parentOpacity));
|
|
1145
|
+
clone.appendChild(copy);
|
|
1146
|
+
}
|
|
1147
|
+
await inlineSvgImageSources(clone);
|
|
1148
|
+
const markup = new XMLSerializer().serializeToString(clone);
|
|
1149
|
+
const url = URL.createObjectURL(new Blob([markup], { type: "image/svg+xml" }));
|
|
1150
|
+
try {
|
|
1151
|
+
const image = new Image();
|
|
1152
|
+
image.src = url;
|
|
1153
|
+
await image.decode();
|
|
1154
|
+
const canvas = document.createElement("canvas");
|
|
1155
|
+
canvas.width = Math.max(1, Math.ceil(width));
|
|
1156
|
+
canvas.height = Math.max(1, Math.ceil(height));
|
|
1157
|
+
const context = canvas.getContext("2d");
|
|
1158
|
+
if (!context) throw new Error("Could not create Architecture artwork canvas.");
|
|
1159
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
1160
|
+
if (!crop) return canvas.toDataURL("image/png");
|
|
1161
|
+
const cropped = document.createElement("canvas");
|
|
1162
|
+
cropped.width = Math.max(1, Math.ceil(crop.width));
|
|
1163
|
+
cropped.height = Math.max(1, Math.ceil(crop.height));
|
|
1164
|
+
const croppedContext = cropped.getContext("2d");
|
|
1165
|
+
if (!croppedContext) throw new Error("Could not crop Architecture artwork.");
|
|
1166
|
+
croppedContext.drawImage(
|
|
1167
|
+
canvas,
|
|
1168
|
+
crop.x,
|
|
1169
|
+
crop.y,
|
|
1170
|
+
crop.width,
|
|
1171
|
+
crop.height,
|
|
1172
|
+
0,
|
|
1173
|
+
0,
|
|
1174
|
+
cropped.width,
|
|
1175
|
+
cropped.height,
|
|
1176
|
+
);
|
|
1177
|
+
return cropped.toDataURL("image/png");
|
|
1178
|
+
} finally {
|
|
1179
|
+
URL.revokeObjectURL(url);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async function collectArchitectureObjects(wrapper, deck, blockIndex) {
|
|
1184
|
+
const snapshot = wrapper.__presentationPptxSnapshot;
|
|
1185
|
+
const svg = wrapper.querySelector("svg.architecture-svg");
|
|
1186
|
+
if (!snapshot || !svg) {
|
|
1187
|
+
return {
|
|
1188
|
+
elements: [],
|
|
1189
|
+
fallbacks: [pptxFallback("architecture", wrapper, deck, "architecture-model-unavailable")],
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
const svgRect = svg.getBoundingClientRect();
|
|
1193
|
+
const deckRect = deck.getBoundingClientRect();
|
|
1194
|
+
const scale = Math.min(
|
|
1195
|
+
svgRect.width / snapshot.canvas.width,
|
|
1196
|
+
svgRect.height / snapshot.canvas.height,
|
|
1197
|
+
);
|
|
1198
|
+
const usedWidth = snapshot.canvas.width * scale;
|
|
1199
|
+
const usedHeight = snapshot.canvas.height * scale;
|
|
1200
|
+
const originX = svgRect.left - deckRect.left + (svgRect.width - usedWidth) / 2;
|
|
1201
|
+
const originY = svgRect.top - deckRect.top + (svgRect.height - usedHeight) / 2;
|
|
1202
|
+
const mapBounds = (object) => ({
|
|
1203
|
+
x: roundedMetric(originX + object.x * scale),
|
|
1204
|
+
y: roundedMetric(originY + object.y * scale),
|
|
1205
|
+
width: roundedMetric(object.width * scale),
|
|
1206
|
+
height: roundedMetric(object.height * scale),
|
|
1207
|
+
});
|
|
1208
|
+
const mapColorFields = (object) => {
|
|
1209
|
+
const mapped = { ...object };
|
|
1210
|
+
const mapParagraphs = (paragraphs) =>
|
|
1211
|
+
paragraphs.map((paragraph) => ({
|
|
1212
|
+
...paragraph,
|
|
1213
|
+
runs: paragraph.runs.map((run) => ({
|
|
1214
|
+
...run,
|
|
1215
|
+
color: resolveModelColor(run.color, deck),
|
|
1216
|
+
fontFace: getComputedStyle(svg).fontFamily
|
|
1217
|
+
.split(",")[0]
|
|
1218
|
+
.trim()
|
|
1219
|
+
.replace(/^["']|["']$/g, ""),
|
|
1220
|
+
fontSize: roundedMetric(run.fontSize * scale),
|
|
1221
|
+
bold: Number(run.fontWeight) >= 600,
|
|
1222
|
+
})),
|
|
1223
|
+
}));
|
|
1224
|
+
if (mapped.dash !== undefined) mapped.dash = mapped.dash ? "dash" : "solid";
|
|
1225
|
+
for (const key of ["fill", "stroke", "color"]) {
|
|
1226
|
+
if (key in mapped) mapped[key] = resolveModelColor(mapped[key], deck);
|
|
1227
|
+
}
|
|
1228
|
+
if (Array.isArray(mapped.paragraphs)) {
|
|
1229
|
+
mapped.paragraphs = mapParagraphs(mapped.paragraphs);
|
|
1230
|
+
}
|
|
1231
|
+
if (mapped.text?.paragraphs) {
|
|
1232
|
+
mapped.text = {
|
|
1233
|
+
...mapped.text,
|
|
1234
|
+
paragraphs: mapParagraphs(mapped.text.paragraphs),
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
if (mapped.textInsets) {
|
|
1238
|
+
mapped.textInsets = Object.fromEntries(
|
|
1239
|
+
Object.entries(mapped.textInsets).map(([key, value]) => [
|
|
1240
|
+
key,
|
|
1241
|
+
roundedMetric(value * scale),
|
|
1242
|
+
]),
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
return mapped;
|
|
1246
|
+
};
|
|
1247
|
+
const fallbacks = snapshot.fallbacks.map((fallback) => ({
|
|
1248
|
+
...fallback,
|
|
1249
|
+
path: `architecture[${blockIndex}].${fallback.path}`,
|
|
1250
|
+
...mapBounds(fallback),
|
|
1251
|
+
}));
|
|
1252
|
+
const elements = [];
|
|
1253
|
+
const architectureGroups = [...wrapper.querySelectorAll("[data-architecture-type]")];
|
|
1254
|
+
const findById = (id) =>
|
|
1255
|
+
architectureGroups.find((element) => element.getAttribute("data-architecture-id") === id);
|
|
1256
|
+
const foregroundCandidates = [];
|
|
1257
|
+
for (const icon of snapshot.icons || []) {
|
|
1258
|
+
const group = findById(icon.id);
|
|
1259
|
+
const source = group?.querySelector("[data-architecture-icon]");
|
|
1260
|
+
if (source) {
|
|
1261
|
+
const rawBounds = mapBounds(icon);
|
|
1262
|
+
const padding = Math.max(
|
|
1263
|
+
0,
|
|
1264
|
+
Math.min(2, rawBounds.x - originX, rawBounds.y - originY),
|
|
1265
|
+
);
|
|
1266
|
+
const bounds = {
|
|
1267
|
+
x: rawBounds.x - padding,
|
|
1268
|
+
y: rawBounds.y - padding,
|
|
1269
|
+
width: rawBounds.width + padding * 2,
|
|
1270
|
+
height: rawBounds.height + padding * 2,
|
|
1271
|
+
};
|
|
1272
|
+
foregroundCandidates.push({
|
|
1273
|
+
key: `icon:${icon.id}`,
|
|
1274
|
+
source,
|
|
1275
|
+
alt: `${icon.icon} icon`,
|
|
1276
|
+
bounds,
|
|
1277
|
+
crop: {
|
|
1278
|
+
x: bounds.x - originX,
|
|
1279
|
+
y: bounds.y - originY,
|
|
1280
|
+
width: bounds.width,
|
|
1281
|
+
height: bounds.height,
|
|
1282
|
+
},
|
|
1283
|
+
architecture: {
|
|
1284
|
+
kind: "icon-picture",
|
|
1285
|
+
id: icon.id,
|
|
1286
|
+
sourcePath: icon.sourcePath,
|
|
1287
|
+
order: icon.order,
|
|
1288
|
+
z: icon.z,
|
|
1289
|
+
},
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
for (const object of snapshot.objects.filter((entry) => entry.type === "image")) {
|
|
1294
|
+
const source = findById(object.architecture.id);
|
|
1295
|
+
if (source) {
|
|
1296
|
+
const rawBounds = mapBounds(object);
|
|
1297
|
+
const padding = Math.max(
|
|
1298
|
+
0,
|
|
1299
|
+
Math.min(2, rawBounds.x - originX, rawBounds.y - originY),
|
|
1300
|
+
);
|
|
1301
|
+
const bounds = {
|
|
1302
|
+
x: rawBounds.x - padding,
|
|
1303
|
+
y: rawBounds.y - padding,
|
|
1304
|
+
width: rawBounds.width + padding * 2,
|
|
1305
|
+
height: rawBounds.height + padding * 2,
|
|
1306
|
+
};
|
|
1307
|
+
foregroundCandidates.push({
|
|
1308
|
+
key: `image:${object.architecture.id}`,
|
|
1309
|
+
source,
|
|
1310
|
+
alt: `${object.architecture.id} image`,
|
|
1311
|
+
bounds,
|
|
1312
|
+
crop: {
|
|
1313
|
+
x: bounds.x - originX,
|
|
1314
|
+
y: bounds.y - originY,
|
|
1315
|
+
width: bounds.width,
|
|
1316
|
+
height: bounds.height,
|
|
1317
|
+
},
|
|
1318
|
+
architecture: {
|
|
1319
|
+
...object.architecture,
|
|
1320
|
+
kind: "image-picture",
|
|
1321
|
+
},
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const foregroundLayers = new Map();
|
|
1326
|
+
const expectedForegrounds =
|
|
1327
|
+
(snapshot.icons?.length || 0) +
|
|
1328
|
+
snapshot.objects.filter((object) => object.type === "image").length;
|
|
1329
|
+
let foregroundReady = foregroundCandidates.length === expectedForegrounds;
|
|
1330
|
+
try {
|
|
1331
|
+
if (foregroundReady) {
|
|
1332
|
+
const generated = await Promise.all(
|
|
1333
|
+
foregroundCandidates.map(async (candidate) => ({
|
|
1334
|
+
...candidate,
|
|
1335
|
+
src: await architectureForegroundPng(
|
|
1336
|
+
svg,
|
|
1337
|
+
[candidate.source],
|
|
1338
|
+
usedWidth,
|
|
1339
|
+
usedHeight,
|
|
1340
|
+
candidate.crop,
|
|
1341
|
+
),
|
|
1342
|
+
})),
|
|
1343
|
+
);
|
|
1344
|
+
generated.forEach((layer) => foregroundLayers.set(layer.key, layer));
|
|
1345
|
+
} else {
|
|
1346
|
+
throw new Error("Architecture foreground source was not found.");
|
|
1347
|
+
}
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
foregroundReady = false;
|
|
1350
|
+
foregroundLayers.clear();
|
|
1351
|
+
fallbacks.push(
|
|
1352
|
+
pptxFallback(
|
|
1353
|
+
"architecture-foreground",
|
|
1354
|
+
wrapper,
|
|
1355
|
+
deck,
|
|
1356
|
+
`foreground-picture-failed: ${error?.message || "unknown error"}`,
|
|
1357
|
+
),
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
const foregroundElement = (layer) => ({
|
|
1361
|
+
type: "image",
|
|
1362
|
+
src: layer.src,
|
|
1363
|
+
alt: layer.alt,
|
|
1364
|
+
fit: "fill",
|
|
1365
|
+
opacity: 1,
|
|
1366
|
+
...layer.bounds,
|
|
1367
|
+
architecture: layer.architecture,
|
|
1368
|
+
});
|
|
1369
|
+
if (!foregroundReady) {
|
|
1370
|
+
fallbacks.push(
|
|
1371
|
+
pptxFallback(
|
|
1372
|
+
"architecture",
|
|
1373
|
+
wrapper,
|
|
1374
|
+
deck,
|
|
1375
|
+
"architecture-rendered-as-artwork-after-foreground-failure",
|
|
1376
|
+
),
|
|
1377
|
+
);
|
|
1378
|
+
return { elements: [], fallbacks };
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
for (const sourceObject of snapshot.objects) {
|
|
1382
|
+
const object = mapColorFields({
|
|
1383
|
+
...sourceObject,
|
|
1384
|
+
...mapBounds(sourceObject),
|
|
1385
|
+
...(sourceObject.points
|
|
1386
|
+
? {
|
|
1387
|
+
points: sourceObject.points.map((point) => ({
|
|
1388
|
+
x: roundedMetric(originX + point.x * scale),
|
|
1389
|
+
y: roundedMetric(originY + point.y * scale),
|
|
1390
|
+
})),
|
|
1391
|
+
}
|
|
1392
|
+
: {}),
|
|
1393
|
+
...(sourceObject.strokeWidth !== undefined
|
|
1394
|
+
? { strokeWidth: roundedMetric(sourceObject.strokeWidth * scale) }
|
|
1395
|
+
: {}),
|
|
1396
|
+
...(sourceObject.cornerRadius !== undefined
|
|
1397
|
+
? { cornerRadius: roundedMetric(sourceObject.cornerRadius * scale) }
|
|
1398
|
+
: {}),
|
|
1399
|
+
});
|
|
1400
|
+
if (object.type === "shape") {
|
|
1401
|
+
const opacity = Number.isFinite(object.opacity) ? object.opacity : 1;
|
|
1402
|
+
if (object.text?.paragraphs) {
|
|
1403
|
+
object.text = {
|
|
1404
|
+
...object.text,
|
|
1405
|
+
paragraphs: object.text.paragraphs.map((paragraph) => ({
|
|
1406
|
+
...paragraph,
|
|
1407
|
+
runs: paragraph.runs.map((run) => ({
|
|
1408
|
+
...run,
|
|
1409
|
+
opacity: (Number.isFinite(run.opacity) ? run.opacity : 1) * opacity,
|
|
1410
|
+
})),
|
|
1411
|
+
})),
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (object.type === "image") {
|
|
1416
|
+
const layer = foregroundLayers.get(`image:${object.architecture.id}`);
|
|
1417
|
+
fallbacks.push({
|
|
1418
|
+
type: "architecture-image",
|
|
1419
|
+
path: `architecture[${blockIndex}].${object.architecture.sourcePath}`,
|
|
1420
|
+
reason: foregroundReady && layer
|
|
1421
|
+
? "architecture-image-rendered-as-foreground-picture"
|
|
1422
|
+
: "architecture-image-rendered-as-artwork",
|
|
1423
|
+
...mapBounds(sourceObject),
|
|
1424
|
+
});
|
|
1425
|
+
if (foregroundReady && layer) elements.push(foregroundElement(layer));
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
elements.push(object);
|
|
1429
|
+
if (object.type === "shape" && object.architecture?.kind === "node" && sourceObject.icon) {
|
|
1430
|
+
const layer = foregroundLayers.get(`icon:${object.architecture.id}`);
|
|
1431
|
+
if (foregroundReady && layer) elements.push(foregroundElement(layer));
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
for (const icon of snapshot.icons || []) {
|
|
1435
|
+
const layer = foregroundLayers.get(`icon:${icon.id}`);
|
|
1436
|
+
fallbacks.push({
|
|
1437
|
+
type: "architecture-icon",
|
|
1438
|
+
path: `architecture[${blockIndex}].${icon.sourcePath}`,
|
|
1439
|
+
reason: foregroundReady && layer
|
|
1440
|
+
? "icon-rendered-as-foreground-picture"
|
|
1441
|
+
: "icon-rendered-as-artwork",
|
|
1442
|
+
icon: icon.icon,
|
|
1443
|
+
...mapBounds(icon),
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
for (const sourceObject of snapshot.objects) {
|
|
1447
|
+
const architecture = sourceObject.architecture;
|
|
1448
|
+
if (!architecture) continue;
|
|
1449
|
+
if (architecture.kind === "group" || architecture.kind === "node") {
|
|
1450
|
+
const group = findById(architecture.id);
|
|
1451
|
+
if (!group) continue;
|
|
1452
|
+
[...group.children]
|
|
1453
|
+
.filter((child) =>
|
|
1454
|
+
foregroundReady
|
|
1455
|
+
? child.matches("rect, ellipse, text")
|
|
1456
|
+
: child.matches("text"),
|
|
1457
|
+
)
|
|
1458
|
+
.forEach((child) => child.setAttribute("data-pptx-native", sourceObject.type));
|
|
1459
|
+
} else if (architecture.kind === "connector") {
|
|
1460
|
+
architectureGroups
|
|
1461
|
+
.filter(
|
|
1462
|
+
(element) =>
|
|
1463
|
+
element.getAttribute("data-architecture-type") === "connector" &&
|
|
1464
|
+
element.getAttribute("data-architecture-order") === String(architecture.order),
|
|
1465
|
+
)
|
|
1466
|
+
.forEach((group) =>
|
|
1467
|
+
[...group.children]
|
|
1468
|
+
.filter((child) => child.matches("path"))
|
|
1469
|
+
.forEach((child) => child.setAttribute("data-pptx-native", "connector")),
|
|
1470
|
+
);
|
|
1471
|
+
} else if (architecture.kind.startsWith("connector-label")) {
|
|
1472
|
+
wrapper
|
|
1473
|
+
.querySelectorAll(
|
|
1474
|
+
`[data-architecture-connector-label][data-architecture-label-layer]`,
|
|
1475
|
+
)
|
|
1476
|
+
.forEach((label) => label.setAttribute("data-pptx-native", sourceObject.type));
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
if (foregroundReady) {
|
|
1480
|
+
foregroundCandidates.forEach((candidate) => {
|
|
1481
|
+
if (foregroundLayers.has(candidate.key)) {
|
|
1482
|
+
candidate.source.setAttribute("data-pptx-native", "image");
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
if (snapshot.routing.degraded) {
|
|
1487
|
+
fallbacks.push(
|
|
1488
|
+
pptxFallback(
|
|
1489
|
+
"architecture-routing",
|
|
1490
|
+
wrapper,
|
|
1491
|
+
deck,
|
|
1492
|
+
"routing-warning-rendered-as-artwork",
|
|
1493
|
+
),
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
return { elements, fallbacks };
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
async function collectPptxSlide(slide, index) {
|
|
1500
|
+
const { deck } = slide;
|
|
1501
|
+
const elements = [];
|
|
1502
|
+
const fallbacks = [];
|
|
1503
|
+
const fallbackRoots = new Set();
|
|
1504
|
+
const addFallback = (type, element, reason) => {
|
|
1505
|
+
if (fallbackRoots.has(element)) return;
|
|
1506
|
+
fallbackRoots.add(element);
|
|
1507
|
+
fallbacks.push(pptxFallback(type, element, deck, reason));
|
|
1508
|
+
};
|
|
1509
|
+
|
|
1510
|
+
for (const element of deck.querySelectorAll("header *, .body, .body *, footer *")) {
|
|
1511
|
+
if (element.closest("pre, .architecture-diagram, .architecture-error")) continue;
|
|
1512
|
+
const effects = unsupportedEffects(element).filter((effect) => effect !== "box-shadow");
|
|
1513
|
+
if (!effects.length) continue;
|
|
1514
|
+
const root = effectFallbackRoot(element);
|
|
1515
|
+
if (root) {
|
|
1516
|
+
addFallback(
|
|
1517
|
+
"effect",
|
|
1518
|
+
root,
|
|
1519
|
+
`element-rendered-as-artwork: ${effects.join(", ")}`,
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
deck.querySelectorAll("pre.mermaid").forEach((element) =>
|
|
1525
|
+
addFallback("mermaid", element, "mermaid-rendered-as-artwork"),
|
|
1526
|
+
);
|
|
1527
|
+
deck.querySelectorAll("pre:not(.mermaid)").forEach((element) =>
|
|
1528
|
+
addFallback("code", element, "code-block-rendered-as-artwork"),
|
|
1529
|
+
);
|
|
1530
|
+
deck.querySelectorAll(".architecture-error").forEach((element) =>
|
|
1531
|
+
addFallback("architecture", element, "architecture-error-rendered-as-artwork"),
|
|
1532
|
+
);
|
|
1533
|
+
deck
|
|
1534
|
+
.querySelectorAll(
|
|
1535
|
+
".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",
|
|
1536
|
+
)
|
|
1537
|
+
.forEach((element) => {
|
|
1538
|
+
if (!element.closest(".architecture-diagram")) {
|
|
1539
|
+
addFallback("html", element, "arbitrary-html-rendered-as-artwork");
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
const insideFallback = (element) =>
|
|
1544
|
+
[...fallbackRoots].some((root) => root === element || root.contains(element));
|
|
1545
|
+
const textCandidates = [
|
|
1546
|
+
...deck.querySelectorAll(
|
|
1547
|
+
".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",
|
|
1548
|
+
),
|
|
1549
|
+
].filter(
|
|
1550
|
+
(element) =>
|
|
1551
|
+
!insideFallback(element) &&
|
|
1552
|
+
!element.closest(".architecture-diagram") &&
|
|
1553
|
+
!element.closest("table") &&
|
|
1554
|
+
!(element.matches("p") && element.closest("blockquote, li")),
|
|
1555
|
+
);
|
|
1556
|
+
for (const element of textCandidates) {
|
|
1557
|
+
const list = element.matches("li")
|
|
1558
|
+
? [...element.querySelectorAll(":scope > ul, :scope > ol")]
|
|
1559
|
+
: [];
|
|
1560
|
+
const parentList = element.matches("li") ? element.parentElement : null;
|
|
1561
|
+
const actualLevel = element.matches("li")
|
|
1562
|
+
? Math.max(0, [...element.closest(".body").querySelectorAll("ul, ol")].filter((candidate) =>
|
|
1563
|
+
candidate.contains(element),
|
|
1564
|
+
).length - 1)
|
|
1565
|
+
: undefined;
|
|
1566
|
+
const paragraph = paragraphFor(element, {
|
|
1567
|
+
omitNestedLists: list.length > 0,
|
|
1568
|
+
level: actualLevel,
|
|
1569
|
+
bullet: parentList
|
|
1570
|
+
? {
|
|
1571
|
+
type: parentList.tagName === "OL" ? "number" : "bullet",
|
|
1572
|
+
character:
|
|
1573
|
+
parentList.tagName === "OL"
|
|
1574
|
+
? `${Number(parentList.getAttribute("start") || 1) + [...parentList.children].indexOf(element)}.`
|
|
1575
|
+
: "•",
|
|
1576
|
+
...(parentList.tagName === "OL"
|
|
1577
|
+
? { start: Number(parentList.getAttribute("start") || 1) + [...parentList.children].indexOf(element) }
|
|
1578
|
+
: {}),
|
|
1579
|
+
}
|
|
1580
|
+
: undefined,
|
|
1581
|
+
});
|
|
1582
|
+
if (!paragraph.runs.some((run) => run.text.trim())) continue;
|
|
1583
|
+
elements.push({
|
|
1584
|
+
type: "text",
|
|
1585
|
+
path: elementPath(element, deck),
|
|
1586
|
+
...(element.classList.contains("kicker")
|
|
1587
|
+
? textContentBounds(element, deck)
|
|
1588
|
+
: relativeBounds(element, deck)),
|
|
1589
|
+
paragraphs: [paragraph],
|
|
1590
|
+
opacity: Number(getComputedStyle(element).opacity) || 1,
|
|
1591
|
+
...(element.matches("h1, h2, .slide-title") && renderedTextLineCount(element) === 1
|
|
1592
|
+
? { textWrap: "none" }
|
|
1593
|
+
: {}),
|
|
1594
|
+
});
|
|
1595
|
+
element.setAttribute("data-pptx-native", "text");
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
for (const table of deck.querySelectorAll(".body table")) {
|
|
1599
|
+
if (insideFallback(table)) continue;
|
|
1600
|
+
const descendantEffects = new Set();
|
|
1601
|
+
for (const descendant of table.querySelectorAll("*")) {
|
|
1602
|
+
for (const effect of unsupportedEffects(descendant)) descendantEffects.add(effect);
|
|
1603
|
+
}
|
|
1604
|
+
if (descendantEffects.size) {
|
|
1605
|
+
addFallback(
|
|
1606
|
+
"effect",
|
|
1607
|
+
table,
|
|
1608
|
+
`element-rendered-as-artwork: ${[...descendantEffects].join(", ")}`,
|
|
1609
|
+
);
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
const effects = unsupportedEffects(table);
|
|
1613
|
+
if (parseFloat(getComputedStyle(table).borderRadius) > 0) effects.push("border-radius");
|
|
1614
|
+
const artworkEffects = effects.filter(
|
|
1615
|
+
(effect) => effect !== "box-shadow" && effect !== "border-radius",
|
|
1616
|
+
);
|
|
1617
|
+
if (artworkEffects.length) {
|
|
1618
|
+
addFallback(
|
|
1619
|
+
"effect",
|
|
1620
|
+
table,
|
|
1621
|
+
`element-rendered-as-artwork: ${effects.join(", ")}`,
|
|
1622
|
+
);
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
const domRows = [...table.rows];
|
|
1626
|
+
const columnCount = domRows[0]?.cells.length || 0;
|
|
1627
|
+
if (
|
|
1628
|
+
!columnCount ||
|
|
1629
|
+
domRows.some(
|
|
1630
|
+
(row) =>
|
|
1631
|
+
row.cells.length !== columnCount ||
|
|
1632
|
+
[...row.cells].some((cell) => cell.colSpan !== 1 || cell.rowSpan !== 1),
|
|
1633
|
+
)
|
|
1634
|
+
) {
|
|
1635
|
+
addFallback("table", table, "merged-table-rendered-as-artwork");
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
const rows = domRows.map((row) => ({
|
|
1639
|
+
height: roundedMetric(row.getBoundingClientRect().height),
|
|
1640
|
+
cells: [...row.cells].map((cell) => {
|
|
1641
|
+
const style = getComputedStyle(cell);
|
|
1642
|
+
return {
|
|
1643
|
+
...relativeBounds(cell, deck),
|
|
1644
|
+
header: cell.tagName === "TH",
|
|
1645
|
+
colspan: cell.colSpan,
|
|
1646
|
+
rowspan: cell.rowSpan,
|
|
1647
|
+
fill: normalizeCssColor(style.backgroundColor),
|
|
1648
|
+
color: normalizeCssColor(style.color),
|
|
1649
|
+
stroke: normalizeCssColor(style.borderColor),
|
|
1650
|
+
strokeWidth: roundedMetric(parseFloat(style.borderWidth)) || 1,
|
|
1651
|
+
alignment: pptxAlignment(style.textAlign),
|
|
1652
|
+
paragraphs: [paragraphFor(cell)],
|
|
1653
|
+
};
|
|
1654
|
+
}),
|
|
1655
|
+
}));
|
|
1656
|
+
elements.push({
|
|
1657
|
+
type: "table",
|
|
1658
|
+
path: elementPath(table, deck),
|
|
1659
|
+
...relativeBounds(table, deck),
|
|
1660
|
+
rows,
|
|
1661
|
+
});
|
|
1662
|
+
if (effects.length) {
|
|
1663
|
+
fallbacks.push(
|
|
1664
|
+
pptxFallback(
|
|
1665
|
+
"effect",
|
|
1666
|
+
table,
|
|
1667
|
+
deck,
|
|
1668
|
+
`native-table-approximates: ${effects.join(", ")}`,
|
|
1669
|
+
),
|
|
1670
|
+
);
|
|
1671
|
+
if (effects.includes("box-shadow")) preserveBoxShadow(table, deck);
|
|
1672
|
+
}
|
|
1673
|
+
table.setAttribute("data-pptx-native", "table");
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
for (const image of deck.querySelectorAll("img")) {
|
|
1677
|
+
if (
|
|
1678
|
+
image.closest(".architecture-diagram") ||
|
|
1679
|
+
image.classList.contains("theme-cover-background") ||
|
|
1680
|
+
insideFallback(image)
|
|
1681
|
+
) {
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
if (!rasterImageSupported(image)) {
|
|
1685
|
+
const fit = getComputedStyle(image).objectFit;
|
|
1686
|
+
addFallback(
|
|
1687
|
+
"image",
|
|
1688
|
+
image,
|
|
1689
|
+
["cover", "none"].includes(fit) ? "unsupported-image-fit" : "unsupported-image-format",
|
|
1690
|
+
);
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
const style = getComputedStyle(image);
|
|
1694
|
+
const effects = unsupportedEffects(image);
|
|
1695
|
+
if (parseFloat(style.borderRadius) > 0) effects.push("border-radius");
|
|
1696
|
+
const artworkEffects = effects.filter(
|
|
1697
|
+
(effect) => effect !== "box-shadow" && effect !== "border-radius",
|
|
1698
|
+
);
|
|
1699
|
+
if (artworkEffects.length) {
|
|
1700
|
+
addFallback(
|
|
1701
|
+
"effect",
|
|
1702
|
+
image,
|
|
1703
|
+
`element-rendered-as-artwork: ${effects.join(", ")}`,
|
|
1704
|
+
);
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
elements.push({
|
|
1708
|
+
type: "image",
|
|
1709
|
+
path: elementPath(image, deck),
|
|
1710
|
+
...relativeBounds(image, deck),
|
|
1711
|
+
src: image.currentSrc || image.src,
|
|
1712
|
+
alt: image.alt || "",
|
|
1713
|
+
fit: style.objectFit || "contain",
|
|
1714
|
+
opacity: Number(style.opacity) || 1,
|
|
1715
|
+
naturalWidth: image.naturalWidth,
|
|
1716
|
+
naturalHeight: image.naturalHeight,
|
|
1717
|
+
});
|
|
1718
|
+
image.setAttribute("data-pptx-native", "image");
|
|
1719
|
+
if (effects.length) {
|
|
1720
|
+
fallbacks.push(
|
|
1721
|
+
pptxFallback(
|
|
1722
|
+
"effect",
|
|
1723
|
+
image,
|
|
1724
|
+
deck,
|
|
1725
|
+
`native-image-approximates: ${effects.join(", ")}`,
|
|
1726
|
+
),
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const architectureWrappers = [...deck.querySelectorAll(".architecture-diagram")];
|
|
1732
|
+
for (const [blockIndex, wrapper] of architectureWrappers.entries()) {
|
|
1733
|
+
if (insideFallback(wrapper)) continue;
|
|
1734
|
+
const architecture = await collectArchitectureObjects(wrapper, deck, blockIndex);
|
|
1735
|
+
elements.push(...architecture.elements);
|
|
1736
|
+
fallbacks.push(...architecture.fallbacks);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
const layout = slide.titleSlide
|
|
1740
|
+
? "title"
|
|
1741
|
+
: slide.sectionSlide
|
|
1742
|
+
? "section"
|
|
1743
|
+
: slide.centerSlide
|
|
1744
|
+
? "center"
|
|
1745
|
+
: slide.backcoverSlide
|
|
1746
|
+
? "backcover"
|
|
1747
|
+
: "standard";
|
|
1748
|
+
const visibleTitle = deck.querySelector("h1, h2")?.textContent?.trim();
|
|
1749
|
+
return {
|
|
1750
|
+
index,
|
|
1751
|
+
layout,
|
|
1752
|
+
theme: slide.theme,
|
|
1753
|
+
title: visibleTitle || slide.title,
|
|
1754
|
+
width: OUTPUT_WIDTH,
|
|
1755
|
+
height: OUTPUT_HEIGHT,
|
|
1756
|
+
elements,
|
|
1757
|
+
fallbacks,
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
async function renderPptxDeck(
|
|
1762
|
+
slides,
|
|
1763
|
+
theme,
|
|
1764
|
+
customCss = "",
|
|
1765
|
+
themeMetadata = null,
|
|
1766
|
+
themeLocked = false,
|
|
1767
|
+
) {
|
|
1768
|
+
deckTheme = normalizeTheme(theme);
|
|
1769
|
+
deckThemeLocked = Boolean(themeLocked);
|
|
1770
|
+
customThemeMeta = themeMetadata && typeof themeMetadata === "object" ? themeMetadata : null;
|
|
1771
|
+
applyCustomThemeCss(customCss);
|
|
1772
|
+
document.documentElement.setAttribute("data-theme", deckTheme);
|
|
1773
|
+
document.body.classList.add("pptx-mode", "fixed-output-mode", "mermaid-loading");
|
|
1774
|
+
const rendered = slides.map((markdown) => createSlide(markdown, deckTheme));
|
|
1775
|
+
const stage = document.getElementById("stage");
|
|
1776
|
+
stage.replaceChildren(...rendered.map((slide) => slide.deck));
|
|
1777
|
+
document.title = rendered[0]?.title || "MarkdStage";
|
|
1778
|
+
|
|
1779
|
+
if (document.fonts?.ready) await document.fonts.ready;
|
|
1780
|
+
await afterLayout();
|
|
1781
|
+
for (const slide of rendered) {
|
|
1782
|
+
if (
|
|
1783
|
+
slide.sizeMode === "auto" &&
|
|
1784
|
+
!slide.titleSlide &&
|
|
1785
|
+
!slide.sectionSlide &&
|
|
1786
|
+
!slide.backcoverSlide
|
|
1787
|
+
) {
|
|
1788
|
+
applyAutoSize(slide.deck, slide.bodyEl);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
const token = ++renderToken;
|
|
1792
|
+
for (const slide of rendered) {
|
|
1793
|
+
await runMermaid(slide.bodyEl, slide.theme, token, false);
|
|
1794
|
+
}
|
|
1795
|
+
await waitForImages(stage);
|
|
1796
|
+
await afterLayout();
|
|
1797
|
+
|
|
1798
|
+
const pptxSlides = [];
|
|
1799
|
+
for (const [index, slide] of rendered.entries()) {
|
|
1800
|
+
pptxSlides.push(await collectPptxSlide(slide, index));
|
|
1801
|
+
}
|
|
1802
|
+
const model = {
|
|
1803
|
+
version: 1,
|
|
1804
|
+
width: OUTPUT_WIDTH,
|
|
1805
|
+
height: OUTPUT_HEIGHT,
|
|
1806
|
+
slides: pptxSlides,
|
|
1807
|
+
};
|
|
1808
|
+
window.__presentationPptxModel = JSON.parse(JSON.stringify(model));
|
|
1809
|
+
document.body.classList.add("pptx-artwork-mode");
|
|
1810
|
+
document.body.setAttribute("data-pptx-artwork", "ready");
|
|
1811
|
+
document.body.classList.remove("mermaid-loading");
|
|
1812
|
+
document.documentElement.setAttribute("data-pptx-ready", "true");
|
|
1813
|
+
return {
|
|
1814
|
+
model: window.__presentationPptxModel,
|
|
1815
|
+
layout: collectDeckLayout(rendered),
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
async function initPptx(params) {
|
|
1820
|
+
const token = params.get("token") || "";
|
|
1821
|
+
if (!token) throw new Error("Missing PowerPoint export token.");
|
|
1822
|
+
try {
|
|
1823
|
+
const response = await fetch(`./export-data?token=${encodeURIComponent(token)}`, {
|
|
1824
|
+
cache: "no-store",
|
|
1825
|
+
});
|
|
1826
|
+
if (!response.ok) throw new Error(`Could not load PowerPoint export data (${response.status}).`);
|
|
1827
|
+
const data = await response.json();
|
|
1828
|
+
if (
|
|
1829
|
+
!Array.isArray(data.slides) ||
|
|
1830
|
+
data.slides.length === 0 ||
|
|
1831
|
+
!data.slides.every((slide) => typeof slide === "string")
|
|
1832
|
+
) {
|
|
1833
|
+
throw new Error("PowerPoint export data does not contain a valid deck.");
|
|
1834
|
+
}
|
|
1835
|
+
const output = await renderPptxDeck(
|
|
1836
|
+
data.slides,
|
|
1837
|
+
data.theme,
|
|
1838
|
+
data.customThemeCss,
|
|
1839
|
+
data.customThemeMeta,
|
|
1840
|
+
data.themeLocked,
|
|
1841
|
+
);
|
|
1842
|
+
await reportOutputStatus(token, "ready", "", output.layout);
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
const message = error?.message || "PowerPoint rendering failed.";
|
|
1845
|
+
console.error(message);
|
|
1846
|
+
document.body.classList.remove("mermaid-loading");
|
|
1847
|
+
document.documentElement.setAttribute("data-pptx-error", "true");
|
|
1848
|
+
await reportOutputStatus(token, "error", message).catch(() => {});
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
860
1852
|
async function renderPrintDeck(
|
|
861
1853
|
slides,
|
|
862
1854
|
theme,
|
|
@@ -1048,6 +2040,13 @@ function reportCaptureBootstrapFailure(error) {
|
|
|
1048
2040
|
document.documentElement.setAttribute("data-capture-error", "true");
|
|
1049
2041
|
}
|
|
1050
2042
|
|
|
2043
|
+
function reportPptxBootstrapFailure(error) {
|
|
2044
|
+
const message = error?.message || "PowerPoint rendering failed.";
|
|
2045
|
+
console.error(message);
|
|
2046
|
+
document.body.classList.remove("mermaid-loading");
|
|
2047
|
+
document.documentElement.setAttribute("data-pptx-error", "true");
|
|
2048
|
+
}
|
|
2049
|
+
|
|
1051
2050
|
// --- live update -----------------------------------------------------------
|
|
1052
2051
|
// /state is the single source of truth for *what to show* (latest slide markdown
|
|
1053
2052
|
// + a monotonic version + the deck position). SSE is just a low-latency "version
|
|
@@ -1066,13 +2065,21 @@ let importOpen = false;
|
|
|
1066
2065
|
let importPending = false;
|
|
1067
2066
|
let importFiles = [];
|
|
1068
2067
|
let sourceBacked = false;
|
|
2068
|
+
let sourceModeAvailable = false;
|
|
1069
2069
|
let sourceMode = "snapshot";
|
|
1070
2070
|
let sourceWatchStatus = "inactive";
|
|
1071
2071
|
let sourceWatchError = "";
|
|
1072
2072
|
let presenterRequestPending = false;
|
|
1073
2073
|
let presenterRunning = false;
|
|
2074
|
+
let presenterWindowAvailable = false;
|
|
2075
|
+
let presenterViewAvailable = false;
|
|
2076
|
+
let pdfExportAvailable = false;
|
|
2077
|
+
let pptxExportAvailable = false;
|
|
2078
|
+
let markdownImportAvailable = false;
|
|
1074
2079
|
let presenterViewOpen = false;
|
|
2080
|
+
let presenterViewRequested = false;
|
|
1075
2081
|
let pdfExportPending = false;
|
|
2082
|
+
let pptxExportPending = false;
|
|
1076
2083
|
|
|
1077
2084
|
// Derive a short overview title from a slide fragment: first heading, else first
|
|
1078
2085
|
// non-empty body line, trimmed. Mirrors the skill's title rule.
|
|
@@ -1119,7 +2126,7 @@ async function fetchDeck() {
|
|
|
1119
2126
|
* early in init and never reaches this code. Return true only when the state changes.
|
|
1120
2127
|
*/
|
|
1121
2128
|
function setArchitectureEditMode(enabled) {
|
|
1122
|
-
const next = Boolean(enabled) && !presenterMode;
|
|
2129
|
+
const next = Boolean(enabled) && architectureEditAvailable && !presenterMode;
|
|
1123
2130
|
if (next === architectureEditMode) return false;
|
|
1124
2131
|
architectureEditMode = next;
|
|
1125
2132
|
document.body.classList.toggle("architecture-edit-mode", next);
|
|
@@ -1130,7 +2137,7 @@ function setArchitectureEditMode(enabled) {
|
|
|
1130
2137
|
function updateArchitectureEditButton(enabled = architectureEditMode) {
|
|
1131
2138
|
const button = document.getElementById("navEdit");
|
|
1132
2139
|
if (!button) return;
|
|
1133
|
-
button.hidden = presenterMode;
|
|
2140
|
+
button.hidden = presenterMode || !architectureEditAvailable;
|
|
1134
2141
|
button.dataset.state = enabled && !presenterMode ? "active" : "";
|
|
1135
2142
|
button.title = enabled ? "Exit shape editing mode" : "Shape editing mode";
|
|
1136
2143
|
button.setAttribute("aria-label", button.title);
|
|
@@ -1149,7 +2156,7 @@ function updateSourceModeButton() {
|
|
|
1149
2156
|
const button = document.getElementById("navSourceMode");
|
|
1150
2157
|
const status = document.getElementById("sourceStatus");
|
|
1151
2158
|
if (!button) return;
|
|
1152
|
-
button.hidden = presenterMode || !sourceBacked;
|
|
2159
|
+
button.hidden = presenterMode || !sourceBacked || !sourceModeAvailable;
|
|
1153
2160
|
if (!sourceBacked) {
|
|
1154
2161
|
button.dataset.state = "";
|
|
1155
2162
|
if (status) status.textContent = "";
|
|
@@ -1197,7 +2204,7 @@ async function requestSourceMode(mode) {
|
|
|
1197
2204
|
}
|
|
1198
2205
|
|
|
1199
2206
|
async function toggleSourceMode() {
|
|
1200
|
-
if (presenterMode || !sourceBacked) return;
|
|
2207
|
+
if (presenterMode || !sourceBacked || !sourceModeAvailable) return;
|
|
1201
2208
|
await requestSourceMode(sourceMode === "live" ? "snapshot" : "live");
|
|
1202
2209
|
}
|
|
1203
2210
|
|
|
@@ -1218,7 +2225,7 @@ async function requestArchitectureEditMode(enabled) {
|
|
|
1218
2225
|
}
|
|
1219
2226
|
|
|
1220
2227
|
async function toggleArchitectureEditMode() {
|
|
1221
|
-
if (presenterMode) return;
|
|
2228
|
+
if (presenterMode || !architectureEditAvailable) return;
|
|
1222
2229
|
await requestArchitectureEditMode(!architectureEditMode);
|
|
1223
2230
|
await fetchState();
|
|
1224
2231
|
}
|
|
@@ -1230,7 +2237,23 @@ async function toggleArchitectureEditMode() {
|
|
|
1230
2237
|
* Always return save success or failure to the caller. Swallowing it would make
|
|
1231
2238
|
* an unsaved edit look successful, recreating the silent-ignore behavior fixed in Phase 5.
|
|
1232
2239
|
*/
|
|
1233
|
-
|
|
2240
|
+
function saveArchitectureBlock(index, block, source, editorRenderToken) {
|
|
2241
|
+
const pending = architectureSaveQueue
|
|
2242
|
+
.catch(() => {})
|
|
2243
|
+
.then(() => {
|
|
2244
|
+
if (editorRenderToken !== renderToken) {
|
|
2245
|
+
return {
|
|
2246
|
+
ok: false,
|
|
2247
|
+
message: "The displayed deck was replaced. Select the diagram again",
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
return saveArchitectureBlockNow(index, block, source);
|
|
2251
|
+
});
|
|
2252
|
+
architectureSaveQueue = pending;
|
|
2253
|
+
return pending;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
async function saveArchitectureBlockNow(index, block, source) {
|
|
1234
2257
|
let res;
|
|
1235
2258
|
try {
|
|
1236
2259
|
res = await fetch("./edit", {
|
|
@@ -1283,6 +2306,9 @@ async function saveArchitectureBlock(index, block, source) {
|
|
|
1283
2306
|
}
|
|
1284
2307
|
|
|
1285
2308
|
async function openDetailedArchitectureEditor(index, block) {
|
|
2309
|
+
const pendingWindow =
|
|
2310
|
+
architectureDetailedEditTarget === "window" ? window.open("", "_blank") : null;
|
|
2311
|
+
if (pendingWindow) pendingWindow.opener = null;
|
|
1286
2312
|
let response;
|
|
1287
2313
|
try {
|
|
1288
2314
|
response = await fetch("./architecture-editor/open", {
|
|
@@ -1291,10 +2317,22 @@ async function openDetailedArchitectureEditor(index, block) {
|
|
|
1291
2317
|
body: JSON.stringify({ index, block }),
|
|
1292
2318
|
});
|
|
1293
2319
|
} catch (_) {
|
|
2320
|
+
pendingWindow?.close();
|
|
1294
2321
|
return { ok: false, message: "Could not connect to the server." };
|
|
1295
2322
|
}
|
|
1296
2323
|
const result = await response.json().catch(() => ({}));
|
|
1297
|
-
if (response.ok && result.ok === true)
|
|
2324
|
+
if (response.ok && result.ok === true) {
|
|
2325
|
+
if (typeof result.url === "string" && result.url) {
|
|
2326
|
+
if (pendingWindow) pendingWindow.location.replace(result.url);
|
|
2327
|
+
else if (!window.open(result.url, "_blank", "noopener")) {
|
|
2328
|
+
return { ok: false, message: "Allow pop-ups to open the Architecture Editor." };
|
|
2329
|
+
}
|
|
2330
|
+
} else {
|
|
2331
|
+
pendingWindow?.close();
|
|
2332
|
+
}
|
|
2333
|
+
return result;
|
|
2334
|
+
}
|
|
2335
|
+
pendingWindow?.close();
|
|
1298
2336
|
if (result.error === "source_not_available") {
|
|
1299
2337
|
return {
|
|
1300
2338
|
ok: false,
|
|
@@ -1320,10 +2358,29 @@ async function fetchState() {
|
|
|
1320
2358
|
data.customThemeMeta && typeof data.customThemeMeta === "object"
|
|
1321
2359
|
? data.customThemeMeta
|
|
1322
2360
|
: null;
|
|
2361
|
+
if (typeof data.presenterWindowAvailable === "boolean") {
|
|
2362
|
+
presenterWindowAvailable = data.presenterWindowAvailable;
|
|
2363
|
+
}
|
|
2364
|
+
if (typeof data.presenterViewAvailable === "boolean") {
|
|
2365
|
+
presenterViewAvailable = data.presenterViewAvailable;
|
|
2366
|
+
}
|
|
2367
|
+
if (typeof data.pdfExportAvailable === "boolean") {
|
|
2368
|
+
pdfExportAvailable = data.pdfExportAvailable;
|
|
2369
|
+
}
|
|
2370
|
+
if (typeof data.pptxExportAvailable === "boolean") {
|
|
2371
|
+
pptxExportAvailable = data.pptxExportAvailable;
|
|
2372
|
+
}
|
|
2373
|
+
if (typeof data.markdownImportAvailable === "boolean") {
|
|
2374
|
+
markdownImportAvailable = data.markdownImportAvailable;
|
|
2375
|
+
}
|
|
1323
2376
|
if (typeof data.presenterRunning === "boolean") {
|
|
1324
2377
|
updatePresenterButton(data.presenterRunning);
|
|
1325
2378
|
}
|
|
2379
|
+
updateHostActionButtons();
|
|
1326
2380
|
if (typeof data.sourceBacked === "boolean") sourceBacked = data.sourceBacked;
|
|
2381
|
+
if (typeof data.sourceModeAvailable === "boolean") {
|
|
2382
|
+
sourceModeAvailable = data.sourceModeAvailable;
|
|
2383
|
+
}
|
|
1327
2384
|
sourceMode = data.sourceMode === "live" ? "live" : "snapshot";
|
|
1328
2385
|
sourceWatchStatus =
|
|
1329
2386
|
data.sourceWatchStatus === "watching" || data.sourceWatchStatus === "error"
|
|
@@ -1331,6 +2388,14 @@ async function fetchState() {
|
|
|
1331
2388
|
: "inactive";
|
|
1332
2389
|
sourceWatchError = typeof data.sourceWatchError === "string" ? data.sourceWatchError : "";
|
|
1333
2390
|
updateSourceModeButton();
|
|
2391
|
+
const editAvailabilityChanged =
|
|
2392
|
+
typeof data.architectureEditAvailable === "boolean" &&
|
|
2393
|
+
data.architectureEditAvailable !== architectureEditAvailable;
|
|
2394
|
+
if (typeof data.architectureEditAvailable === "boolean") {
|
|
2395
|
+
architectureEditAvailable = data.architectureEditAvailable;
|
|
2396
|
+
}
|
|
2397
|
+
architectureDetailedEditTarget =
|
|
2398
|
+
data.architectureDetailedEditTarget === "window" ? "window" : "canvas";
|
|
1334
2399
|
const detailedEditChanged =
|
|
1335
2400
|
typeof data.architectureDetailedEdit === "boolean" &&
|
|
1336
2401
|
data.architectureDetailedEdit !== architectureDetailedEdit;
|
|
@@ -1338,7 +2403,15 @@ async function fetchState() {
|
|
|
1338
2403
|
architectureDetailedEdit = data.architectureDetailedEdit;
|
|
1339
2404
|
}
|
|
1340
2405
|
// Editing-mode changes do not increment the version, so process them before the version guard.
|
|
2406
|
+
let availabilityDisabledEditMode = false;
|
|
2407
|
+
if (editAvailabilityChanged) {
|
|
2408
|
+
if (!architectureEditAvailable) {
|
|
2409
|
+
availabilityDisabledEditMode = setArchitectureEditMode(false);
|
|
2410
|
+
}
|
|
2411
|
+
updateArchitectureEditButton();
|
|
2412
|
+
}
|
|
1341
2413
|
if (
|
|
2414
|
+
availabilityDisabledEditMode ||
|
|
1342
2415
|
(typeof data.architectureEdit === "boolean" &&
|
|
1343
2416
|
setArchitectureEditMode(data.architectureEdit)) ||
|
|
1344
2417
|
(architectureEditMode && detailedEditChanged)
|
|
@@ -1360,6 +2433,10 @@ async function fetchState() {
|
|
|
1360
2433
|
navMode = data.mode === "adhoc" ? "adhoc" : "deck";
|
|
1361
2434
|
renderSlide(typeof data.markdown === "string" ? data.markdown : "");
|
|
1362
2435
|
updateNav();
|
|
2436
|
+
if (presenterViewRequested) {
|
|
2437
|
+
presenterViewRequested = false;
|
|
2438
|
+
openPresenterView();
|
|
2439
|
+
}
|
|
1363
2440
|
}
|
|
1364
2441
|
|
|
1365
2442
|
// --- navigation ------------------------------------------------------------
|
|
@@ -1391,7 +2468,7 @@ function goToIndex(i) {
|
|
|
1391
2468
|
}
|
|
1392
2469
|
|
|
1393
2470
|
async function setPresenterRunning(running) {
|
|
1394
|
-
if (presenterRequestPending) return;
|
|
2471
|
+
if (!presenterWindowAvailable || presenterRequestPending) return;
|
|
1395
2472
|
presenterRequestPending = true;
|
|
1396
2473
|
const button = document.getElementById("navPresent");
|
|
1397
2474
|
const status = document.getElementById("presentStatus");
|
|
@@ -1445,6 +2522,23 @@ function togglePresenterWindow() {
|
|
|
1445
2522
|
return setPresenterRunning(!presenterRunning);
|
|
1446
2523
|
}
|
|
1447
2524
|
|
|
2525
|
+
function updateHostActionButtons() {
|
|
2526
|
+
const present = document.getElementById("navPresent");
|
|
2527
|
+
if (present) present.hidden = presenterMode || !presenterWindowAvailable;
|
|
2528
|
+
const presenterView = document.getElementById("navPresenterView");
|
|
2529
|
+
if (presenterView) presenterView.hidden = presenterMode || !presenterViewAvailable;
|
|
2530
|
+
const presenterToggle = document.getElementById("presenterToggleButton");
|
|
2531
|
+
if (presenterToggle) presenterToggle.hidden = !presenterWindowAvailable;
|
|
2532
|
+
const exportButton = document.getElementById("navExport");
|
|
2533
|
+
if (exportButton) exportButton.hidden = presenterMode || !pdfExportAvailable;
|
|
2534
|
+
const pptxButton = document.getElementById("navExportPptx");
|
|
2535
|
+
if (pptxButton) pptxButton.hidden = presenterMode || !pptxExportAvailable;
|
|
2536
|
+
const importButton = document.getElementById("navImport");
|
|
2537
|
+
if (importButton) importButton.hidden = presenterMode || !markdownImportAvailable;
|
|
2538
|
+
if (!presenterViewAvailable && presenterViewOpen) closePresenterView();
|
|
2539
|
+
if (!markdownImportAvailable && importOpen) closeImportPicker();
|
|
2540
|
+
}
|
|
2541
|
+
|
|
1448
2542
|
function updatePresenterButton(running, message = "") {
|
|
1449
2543
|
presenterRunning = running;
|
|
1450
2544
|
const button = document.getElementById("navPresent");
|
|
@@ -1462,11 +2556,13 @@ function updatePresenterButton(running, message = "") {
|
|
|
1462
2556
|
}
|
|
1463
2557
|
|
|
1464
2558
|
async function exportPdfFromCanvas() {
|
|
1465
|
-
if (pdfExportPending) return;
|
|
2559
|
+
if (!pdfExportAvailable || pdfExportPending || pptxExportPending) return;
|
|
1466
2560
|
pdfExportPending = true;
|
|
1467
2561
|
const button = document.getElementById("navExport");
|
|
2562
|
+
const pptxButton = document.getElementById("navExportPptx");
|
|
1468
2563
|
const status = document.getElementById("exportStatus");
|
|
1469
2564
|
if (button) button.disabled = true;
|
|
2565
|
+
if (pptxButton) pptxButton.disabled = true;
|
|
1470
2566
|
if (status) status.textContent = "Saving PDF.";
|
|
1471
2567
|
|
|
1472
2568
|
try {
|
|
@@ -1497,6 +2593,51 @@ async function exportPdfFromCanvas() {
|
|
|
1497
2593
|
} finally {
|
|
1498
2594
|
pdfExportPending = false;
|
|
1499
2595
|
if (button) button.disabled = false;
|
|
2596
|
+
if (pptxButton) pptxButton.disabled = false;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
async function exportPptxFromCanvas() {
|
|
2601
|
+
if (!pptxExportAvailable || pdfExportPending || pptxExportPending) return;
|
|
2602
|
+
pptxExportPending = true;
|
|
2603
|
+
const button = document.getElementById("navExportPptx");
|
|
2604
|
+
const pdfButton = document.getElementById("navExport");
|
|
2605
|
+
const status = document.getElementById("exportStatus");
|
|
2606
|
+
if (button) button.disabled = true;
|
|
2607
|
+
if (pdfButton) pdfButton.disabled = true;
|
|
2608
|
+
if (status) status.textContent = "Saving editable PowerPoint.";
|
|
2609
|
+
|
|
2610
|
+
try {
|
|
2611
|
+
const response = await fetch("./export-pptx", {
|
|
2612
|
+
method: "POST",
|
|
2613
|
+
headers: { Accept: "application/json" },
|
|
2614
|
+
cache: "no-store",
|
|
2615
|
+
});
|
|
2616
|
+
const data = await response.json().catch(() => ({}));
|
|
2617
|
+
if (!response.ok) {
|
|
2618
|
+
throw new Error(data.message || `PowerPoint export failed (${response.status}).`);
|
|
2619
|
+
}
|
|
2620
|
+
const filename = data.path ? data.path.split(/[\\/]/).pop() : "PowerPoint";
|
|
2621
|
+
const fallback =
|
|
2622
|
+
data.fallbackCount > 0 ? ` ${data.fallbackCount} fallback item(s) preserved.` : "";
|
|
2623
|
+
const message = `Saved ${filename}.${fallback}`;
|
|
2624
|
+
if (status) status.textContent = message;
|
|
2625
|
+
if (button) {
|
|
2626
|
+
button.dataset.state = "active";
|
|
2627
|
+
button.title = message;
|
|
2628
|
+
}
|
|
2629
|
+
} catch (error) {
|
|
2630
|
+
const message = error?.message || "Could not save the PowerPoint presentation.";
|
|
2631
|
+
console.error("PowerPoint export failed", error);
|
|
2632
|
+
if (status) status.textContent = message;
|
|
2633
|
+
if (button) {
|
|
2634
|
+
button.dataset.state = "error";
|
|
2635
|
+
button.title = message;
|
|
2636
|
+
}
|
|
2637
|
+
} finally {
|
|
2638
|
+
pptxExportPending = false;
|
|
2639
|
+
if (button) button.disabled = false;
|
|
2640
|
+
if (pdfButton) pdfButton.disabled = false;
|
|
1500
2641
|
}
|
|
1501
2642
|
}
|
|
1502
2643
|
|
|
@@ -1529,10 +2670,10 @@ function toggleFixedPreviewMode() {
|
|
|
1529
2670
|
function updateNav() {
|
|
1530
2671
|
const nav = document.getElementById("nav");
|
|
1531
2672
|
if (!nav) return;
|
|
1532
|
-
// Outside presenter view, show only the load button
|
|
1533
|
-
//
|
|
2673
|
+
// Outside presenter view, show only the load button when the host supports
|
|
2674
|
+
// Markdown import before any slide has been loaded.
|
|
1534
2675
|
const empty = navTotal <= 0;
|
|
1535
|
-
nav.hidden = previewMode || (empty && presenterMode);
|
|
2676
|
+
nav.hidden = previewMode || (empty && (presenterMode || !markdownImportAvailable));
|
|
1536
2677
|
nav.classList.toggle("nav-empty", empty);
|
|
1537
2678
|
const counter = document.getElementById("navCounter");
|
|
1538
2679
|
if (counter) {
|
|
@@ -1549,7 +2690,7 @@ function updateNav() {
|
|
|
1549
2690
|
}
|
|
1550
2691
|
|
|
1551
2692
|
function openPresenterView() {
|
|
1552
|
-
if (presenterMode || navTotal <= 0) return;
|
|
2693
|
+
if (presenterMode || !presenterViewAvailable || navTotal <= 0) return;
|
|
1553
2694
|
presenterViewOpen = true;
|
|
1554
2695
|
document.body.classList.add("presenter-view-mode");
|
|
1555
2696
|
const view = document.getElementById("presenterView");
|
|
@@ -1770,7 +2911,7 @@ async function importMarkdown(path) {
|
|
|
1770
2911
|
}
|
|
1771
2912
|
|
|
1772
2913
|
function openImportPicker() {
|
|
1773
|
-
if (presenterMode) return;
|
|
2914
|
+
if (presenterMode || !markdownImportAvailable) return;
|
|
1774
2915
|
importOpen = true;
|
|
1775
2916
|
const el = document.getElementById("importPicker");
|
|
1776
2917
|
if (el) el.hidden = false;
|
|
@@ -1911,6 +3052,7 @@ function wireControls() {
|
|
|
1911
3052
|
bind("navPresenterView", openPresenterView);
|
|
1912
3053
|
bind("navFixedPreview", toggleFixedPreviewMode);
|
|
1913
3054
|
bind("navExport", exportPdfFromCanvas);
|
|
3055
|
+
bind("navExportPptx", exportPptxFromCanvas);
|
|
1914
3056
|
bind("navImport", toggleImportPicker);
|
|
1915
3057
|
bind("navSourceMode", toggleSourceMode);
|
|
1916
3058
|
bind("navList", toggleOverview);
|
|
@@ -2014,6 +3156,11 @@ function init() {
|
|
|
2014
3156
|
} catch (_) {}
|
|
2015
3157
|
|
|
2016
3158
|
const params = new URLSearchParams(window.location.search);
|
|
3159
|
+
presenterViewRequested = params.get("presenter") === "1";
|
|
3160
|
+
if (params.get("pptx") === "1") {
|
|
3161
|
+
initPptx(params).catch(reportPptxBootstrapFailure);
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
2017
3164
|
if (params.get("capture") === "1") {
|
|
2018
3165
|
initCapture(params).catch(reportCaptureBootstrapFailure);
|
|
2019
3166
|
return;
|