@markdstage/markdstage 3.3.0 → 3.8.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 +25 -13
- package/package.json +1 -1
- package/shared/README.md +47 -10
- package/shared/architecture-editor/editor.css +9 -5
- package/shared/architecture-editor/editor.js +440 -75
- package/shared/architecture-editor/index.html +2 -2
- package/shared/docs/custom-theme-authoring.md +61 -4
- package/shared/markdown-deck.mjs +9 -5
- package/shared/renderer/architecture-document.mjs +169 -10
- package/shared/renderer/index.html +24 -1
- package/shared/renderer/mermaid-scene.mjs +6725 -197
- package/shared/renderer/renderer.js +260 -55
- package/shared/renderer/scene-graph.mjs +83 -13
- package/shared/renderer/scene-pptx.mjs +154 -1
- package/shared/renderer/scene-svg.mjs +227 -11
- package/shared/renderer/slide-background.mjs +22 -0
- package/shared/renderer/slides.css +32 -6
- package/shared/renderer/theme.mjs +328 -12
- package/shared/runtime/browser.mjs +75 -5
- package/shared/runtime/deck-session.mjs +35 -9
- package/shared/runtime/output-paths.mjs +7 -0
- package/shared/runtime/output.mjs +4 -2
- package/shared/runtime/pptx-package.mjs +103 -22
- package/shared/runtime/presentation-server.mjs +410 -95
- package/shared/runtime/slide-backgrounds.mjs +44 -0
- package/shared/schema/theme-metadata-v1.schema.json +25 -0
- package/shared/schema/theme-v1.json +3 -3
- package/src/cli.mjs +101 -21
- package/src/commands/export.mjs +2 -0
- package/src/commands/present.mjs +133 -143
- package/src/deck.mjs +4 -0
- package/src/skills.mjs +12 -8
|
@@ -6,10 +6,18 @@ const MATH_NS = "http://www.w3.org/1998/Math/MathML";
|
|
|
6
6
|
const SVG_TAGS = new Set("svg g defs symbol switch title desc path rect circle ellipse polygon polyline line text tspan textPath image use marker clipPath mask pattern linearGradient radialGradient stop filter feDropShadow feGaussianBlur feOffset feBlend feColorMatrix feComposite feFlood feMerge feMergeNode feComponentTransfer feFuncR feFuncG feFuncB feFuncA feConvolveMatrix feDisplacementMap feTurbulence feMorphology feImage feDiffuseLighting feSpecularLighting feDistantLight fePointLight feSpotLight feTile foreignObject".split(" "));
|
|
7
7
|
const HTML_TAGS = new Set("div span p br b strong i em s u small sub sup ul ol li code pre img table thead tbody tr th td".split(" "));
|
|
8
8
|
const MATH_TAGS = new Set("math mrow mi mn mo mtext mspace ms mfrac msqrt mroot mstyle merror mpadded mphantom mfenced menclose msub msup msubsup munder mover munderover mmultiscripts mprescripts none mtable mtr mtd semantics annotation".split(" "));
|
|
9
|
-
const ATTRIBUTES = new Set("id class x y x1 y1 x2 y2 dx dy width height cx cy r rx ry d points viewBox preserveAspectRatio transform fill fill-opacity fill-rule stroke stroke-width stroke-opacity stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit opacity color font-family font-size font-weight font-style text-anchor dominant-baseline alignment-baseline textLength lengthAdjust letter-spacing word-spacing text-decoration visibility display overflow pointer-events role tabindex focusable marker-start marker-mid marker-end markerWidth markerHeight markerUnits refX refY orient clip-path clipPathUnits mask maskUnits maskContentUnits filter filterUnits primitiveUnits in in2 result stdDeviation mode type values operator k1 k2 k3 k4 flood-color flood-opacity offset stop-color stop-opacity gradientUnits gradientTransform spreadMethod patternUnits patternContentUnits patternTransform".split(" "));
|
|
10
|
-
const CSS_PROPERTIES = new Set("fill fill-opacity fill-rule stroke stroke-width stroke-opacity stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit opacity color font-family font-size font-weight font-style font-variant line-height text-align text-anchor dominant-baseline alignment-baseline text-decoration letter-spacing word-spacing white-space overflow-wrap word-break display visibility overflow box-sizing width height min-width min-height max-width max-height padding padding-top padding-right padding-bottom padding-left margin margin-top margin-right margin-bottom margin-left border border-radius background-color vertical-align marker-start marker-mid marker-end clip-path filter".split(" "));
|
|
9
|
+
const ATTRIBUTES = new Set("id class name x y x1 y1 x2 y2 dx dy width height cx cy r rx ry d points viewBox preserveAspectRatio transform fill fill-opacity fill-rule stroke stroke-width stroke-opacity stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit opacity color font-family font-size font-weight font-style text-anchor dominant-baseline alignment-baseline textLength lengthAdjust letter-spacing word-spacing text-decoration visibility display overflow pointer-events role tabindex focusable marker-start marker-mid marker-end markerWidth markerHeight markerUnits refX refY orient clip-path clipPathUnits mask maskUnits maskContentUnits filter filterUnits primitiveUnits in in2 result stdDeviation mode type values operator k1 k2 k3 k4 flood-color flood-opacity offset stop-color stop-opacity gradientUnits gradientTransform spreadMethod patternUnits patternContentUnits patternTransform".split(" "));
|
|
10
|
+
const CSS_PROPERTIES = new Set("fill fill-opacity fill-rule stroke stroke-width stroke-opacity stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit opacity color font-family font-size font-weight font-style font-variant line-height text-align text-anchor dominant-baseline alignment-baseline text-decoration text-transform letter-spacing word-spacing white-space overflow-wrap word-break display visibility overflow box-sizing width height min-width min-height max-width max-height padding padding-top padding-right padding-bottom padding-left margin margin-top margin-right margin-bottom margin-left border border-radius background-color vertical-align marker-start marker-mid marker-end clip-path filter".split(" "));
|
|
11
|
+
const SVG_GEOMETRY_PROPERTIES = new Set(["cx", "cy", "d", "r", "x", "y", "rx", "ry"]);
|
|
12
|
+
const SVG_GEOMETRY_BY_TAG = {
|
|
13
|
+
circle: ["cx", "cy", "r"],
|
|
14
|
+
path: ["d"],
|
|
15
|
+
rect: ["x", "y", "width", "height", "rx", "ry"],
|
|
16
|
+
};
|
|
11
17
|
for (const attribute of "alt colspan rowspan systemLanguage startOffset method spacing baseFrequency numOctaves seed stitchTiles scale xChannelSelector yChannelSelector radius order kernelMatrix divisor bias targetX targetY edgeMode preserveAlpha tableValues slope intercept amplitude exponent surfaceScale diffuseConstant specularConstant specularExponent azimuth elevation limitingConeAngle pointsAtX pointsAtY pointsAtZ z mathvariant mathsize mathcolor mathbackground columnalign rowalign columnspacing rowspacing stretchy fence separator accent accentunder largeop movablelimits lspace rspace encoding".split(" ")) ATTRIBUTES.add(attribute);
|
|
12
|
-
for (const property of "position top right bottom left z-index transform transform-origin border-top border-right border-bottom border-left border-top-width border-right-width border-bottom-width border-left-width border-top-style border-right-style border-bottom-style border-left-style border-top-color border-right-color border-bottom-color border-left-color object-fit object-position flex flex-direction flex-wrap align-items align-content justify-content gap float clear".split(" ")) CSS_PROPERTIES.add(property);
|
|
18
|
+
for (const property of "position top right bottom left z-index transform transform-box transform-origin outline outline-width outline-style outline-color outline-offset border-top border-right border-bottom border-left border-top-width border-right-width border-bottom-width border-left-width border-top-style border-right-style border-bottom-style border-left-style border-top-color border-right-color border-bottom-color border-left-color object-fit object-position flex flex-direction flex-wrap align-items align-content justify-content gap float clear".split(" ")) CSS_PROPERTIES.add(property);
|
|
19
|
+
// Local artwork must keep the effects that prevented native conversion.
|
|
20
|
+
for (const property of "text-shadow mask-image mask-mode mask-type mask-size mask-position mask-repeat mask-origin mask-clip mask-composite mix-blend-mode isolation paint-order vector-effect rotate scale translate".split(" ")) CSS_PROPERTIES.add(property);
|
|
13
21
|
let renderSequence = 0;
|
|
14
22
|
|
|
15
23
|
function portableString(value) {
|
|
@@ -64,6 +72,119 @@ function inlineGeometry(source, property) {
|
|
|
64
72
|
|| source.style?.getPropertyValue(property);
|
|
65
73
|
}
|
|
66
74
|
|
|
75
|
+
function simplePixelMetric(value) {
|
|
76
|
+
const match = /^([-+]?(?:\d*\.?\d+)(?:e[-+]?\d+)?)(?:px)?$/i.exec(
|
|
77
|
+
String(value || "").trim(),
|
|
78
|
+
);
|
|
79
|
+
return match ? Number(match[1]) : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pathGeometry(value) {
|
|
83
|
+
let text = String(value || "").trim();
|
|
84
|
+
if (!text || text === "none") return { commands: "", numbers: [] };
|
|
85
|
+
const cssPath = /^path\(["'](.*)["']\)$/s.exec(text);
|
|
86
|
+
if (cssPath) text = cssPath[1];
|
|
87
|
+
const tokenPattern = /[-+]?(?:\d*\.?\d+)(?:e[-+]?\d+)?|[a-z]/gi;
|
|
88
|
+
if (text.replace(tokenPattern, "").replace(/[\s,]/g, "")) return null;
|
|
89
|
+
const tokens = text.match(tokenPattern) || [];
|
|
90
|
+
return {
|
|
91
|
+
commands: tokens
|
|
92
|
+
.filter((token) => /^[a-z]$/i.test(token))
|
|
93
|
+
.map((command) => command.toLowerCase() === "z" ? "Z" : command)
|
|
94
|
+
.join(""),
|
|
95
|
+
numbers: tokens
|
|
96
|
+
.filter((token) => !/^[a-z]$/i.test(token))
|
|
97
|
+
.map(Number),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sameGeometryMetric(left, right, exact = true) {
|
|
102
|
+
if (exact) return left === right;
|
|
103
|
+
// CSSOM shortens computed path numbers. Treat sub-pixel serialization
|
|
104
|
+
// rounding as attribute-equivalent rather than rewriting precise paths.
|
|
105
|
+
return Math.abs(left - right) <= Math.max(
|
|
106
|
+
0.0001,
|
|
107
|
+
Math.max(Math.abs(left), Math.abs(right)) * 0.00001,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function pathCssCanonicalizer(root, computedStyle) {
|
|
112
|
+
const documentRef = root?.ownerDocument || globalThis.document;
|
|
113
|
+
let host;
|
|
114
|
+
let reference;
|
|
115
|
+
const connect = () => {
|
|
116
|
+
if (reference) return true;
|
|
117
|
+
const parent = documentRef?.body || documentRef?.documentElement;
|
|
118
|
+
if (!parent?.appendChild ||
|
|
119
|
+
!documentRef?.createElement ||
|
|
120
|
+
!documentRef?.createElementNS) return false;
|
|
121
|
+
host = documentRef.createElement("div");
|
|
122
|
+
if (!host?.attachShadow) return false;
|
|
123
|
+
host.setAttribute("aria-hidden", "true");
|
|
124
|
+
host.style.cssText = [
|
|
125
|
+
"position:fixed",
|
|
126
|
+
"left:-10000px",
|
|
127
|
+
"top:-10000px",
|
|
128
|
+
"width:0",
|
|
129
|
+
"height:0",
|
|
130
|
+
"overflow:hidden",
|
|
131
|
+
"visibility:hidden",
|
|
132
|
+
"pointer-events:none",
|
|
133
|
+
"contain:strict",
|
|
134
|
+
].join(";");
|
|
135
|
+
const shadow = host.attachShadow({ mode: "closed" });
|
|
136
|
+
const svg = documentRef.createElementNS(SVG_NS, "svg");
|
|
137
|
+
reference = documentRef.createElementNS(SVG_NS, "path");
|
|
138
|
+
svg.setAttribute("width", "0");
|
|
139
|
+
svg.setAttribute("height", "0");
|
|
140
|
+
svg.appendChild(reference);
|
|
141
|
+
shadow.appendChild(svg);
|
|
142
|
+
parent.appendChild(host);
|
|
143
|
+
return reference.isConnected;
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
value(attribute) {
|
|
147
|
+
if (typeof attribute !== "string" || !attribute) return "";
|
|
148
|
+
if (!connect()) return null;
|
|
149
|
+
reference.removeAttribute("style");
|
|
150
|
+
reference.setAttribute("d", attribute);
|
|
151
|
+
return computedStyle(reference).getPropertyValue("d") || "";
|
|
152
|
+
},
|
|
153
|
+
dispose() {
|
|
154
|
+
host?.remove();
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function hasComputedGeometryOverride(source, property, value, canonicalizePath) {
|
|
160
|
+
const attribute = source.getAttribute?.(property);
|
|
161
|
+
if (property === "d") {
|
|
162
|
+
const computed = pathGeometry(value);
|
|
163
|
+
const declared = pathGeometry(attribute);
|
|
164
|
+
if (computed && declared &&
|
|
165
|
+
computed.commands === declared.commands &&
|
|
166
|
+
computed.numbers.length === declared.numbers.length &&
|
|
167
|
+
computed.numbers.every((number, index) =>
|
|
168
|
+
sameGeometryMetric(number, declared.numbers[index], true))) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const canonical = canonicalizePath(attribute);
|
|
172
|
+
if (canonical !== null) return String(value).trim() !== canonical.trim();
|
|
173
|
+
const exact = Boolean(source.closest?.("marker"));
|
|
174
|
+
return computed === null ||
|
|
175
|
+
declared === null ||
|
|
176
|
+
computed.commands !== declared.commands ||
|
|
177
|
+
computed.numbers.length !== declared.numbers.length ||
|
|
178
|
+
computed.numbers.some((number, index) =>
|
|
179
|
+
!sameGeometryMetric(number, declared.numbers[index], exact));
|
|
180
|
+
}
|
|
181
|
+
const computed = simplePixelMetric(value);
|
|
182
|
+
const declared = simplePixelMetric(attribute);
|
|
183
|
+
return computed === null ||
|
|
184
|
+
declared === null ||
|
|
185
|
+
!sameGeometryMetric(computed, declared);
|
|
186
|
+
}
|
|
187
|
+
|
|
67
188
|
/** A JSON-serializable primitive builder; it never creates or parses DOM. */
|
|
68
189
|
export function svgPrimitive(tag, attributes = {}) {
|
|
69
190
|
const primitive = { tag, attributes: {}, children: [] };
|
|
@@ -83,6 +204,9 @@ export function svgPrimitive(tag, attributes = {}) {
|
|
|
83
204
|
*/
|
|
84
205
|
export function captureSvgTree(element, { slots = new Map(), computedStyle = globalThis.getComputedStyle } = {}) {
|
|
85
206
|
let count = 0;
|
|
207
|
+
const pathCanonicalizer = computedStyle
|
|
208
|
+
? pathCssCanonicalizer(element, computedStyle)
|
|
209
|
+
: null;
|
|
86
210
|
function capture(source, depth, root = false) {
|
|
87
211
|
if (++count > 50000 || depth > 128) throw new Error("SVG primitive limit exceeded");
|
|
88
212
|
if (source.nodeType === 3) return { text: portableString(source.textContent || "") };
|
|
@@ -104,6 +228,11 @@ export function captureSvgTree(element, { slots = new Map(), computedStyle = glo
|
|
|
104
228
|
// SVG geometry and the root's responsive CSS must not become screen pixels.
|
|
105
229
|
if (!html && !math && /^(?:min-|max-)?(?:width|height)$/.test(property)) continue;
|
|
106
230
|
let value = localPaint(style.getPropertyValue(property), source);
|
|
231
|
+
if ((html || math) && (property === "width" || property === "height")) {
|
|
232
|
+
// CSSOM rounds intrinsic used pixels; retaining auto avoids fallback layout drift.
|
|
233
|
+
const specified = source.computedStyleMap?.().get(property)?.toString?.();
|
|
234
|
+
if (specified === "auto") value = specified;
|
|
235
|
+
}
|
|
107
236
|
if (property === "transform") {
|
|
108
237
|
// Typed OM retains matrix precision and includes stylesheet overrides.
|
|
109
238
|
const transform = source.computedStyleMap?.().get("transform");
|
|
@@ -111,12 +240,36 @@ export function captureSvgTree(element, { slots = new Map(), computedStyle = glo
|
|
|
111
240
|
}
|
|
112
241
|
if (value && safeCss(value)) primitive.style[property] = value;
|
|
113
242
|
}
|
|
243
|
+
const geometryProperties = tag === "path"
|
|
244
|
+
? SVG_GEOMETRY_BY_TAG.path
|
|
245
|
+
: source.closest?.("marker") || source.closest?.("clipPath")
|
|
246
|
+
? SVG_GEOMETRY_BY_TAG[tag] || []
|
|
247
|
+
: [];
|
|
248
|
+
for (const property of geometryProperties) {
|
|
249
|
+
const value = style.getPropertyValue(property);
|
|
250
|
+
if (value && hasComputedGeometryOverride(
|
|
251
|
+
source,
|
|
252
|
+
property,
|
|
253
|
+
value,
|
|
254
|
+
(attribute) => pathCanonicalizer?.value(attribute) ?? null,
|
|
255
|
+
) &&
|
|
256
|
+
safeCss(value)) {
|
|
257
|
+
primitive.style[property] = portableString(value);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
114
260
|
if (root) {
|
|
115
261
|
for (const property of ["width", "height", "max-width", "max-height"]) {
|
|
116
262
|
const value = inlineGeometry(source, property);
|
|
117
263
|
if (value && safeCss(value)) primitive.style[property] = value;
|
|
118
264
|
}
|
|
119
265
|
}
|
|
266
|
+
if (tag === "foreignObject") {
|
|
267
|
+
// Class multiplicities override their measured SVG attributes with CSS sizing.
|
|
268
|
+
for (const property of ["width", "height"]) {
|
|
269
|
+
const value = inlineGeometry(source, property);
|
|
270
|
+
if (value && safeCss(value)) primitive.style[property] = value;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
120
273
|
}
|
|
121
274
|
for (const child of source.childNodes || []) {
|
|
122
275
|
const captured = capture(child, depth + 1);
|
|
@@ -124,7 +277,11 @@ export function captureSvgTree(element, { slots = new Map(), computedStyle = glo
|
|
|
124
277
|
}
|
|
125
278
|
return primitive;
|
|
126
279
|
}
|
|
127
|
-
|
|
280
|
+
try {
|
|
281
|
+
return capture(element, 0, true);
|
|
282
|
+
} finally {
|
|
283
|
+
pathCanonicalizer?.dispose();
|
|
284
|
+
}
|
|
128
285
|
}
|
|
129
286
|
|
|
130
287
|
function setAttributes(element, attributes = {}) {
|
|
@@ -175,7 +332,11 @@ export function sceneToSvg(scene, {
|
|
|
175
332
|
if (!(math ? MATH_TAGS : html ? HTML_TAGS : SVG_TAGS).has(value.tag)) throw new Error(`Unsupported SVG primitive: ${value.tag}`);
|
|
176
333
|
const element = dom(value.tag, value.attributes, math ? MATH_NS : html ? HTML_NS : SVG_NS);
|
|
177
334
|
for (const [property, content] of Object.entries(value.style || {})) {
|
|
178
|
-
|
|
335
|
+
const styleValue = stringValue(content);
|
|
336
|
+
if ((CSS_PROPERTIES.has(property) || SVG_GEOMETRY_PROPERTIES.has(property)) &&
|
|
337
|
+
safeCss(styleValue)) {
|
|
338
|
+
element.style?.setProperty(property, styleValue);
|
|
339
|
+
}
|
|
179
340
|
}
|
|
180
341
|
if (value.textContent !== undefined) element.textContent = String(value.textContent);
|
|
181
342
|
for (const child of value.children || []) {
|
|
@@ -188,6 +349,9 @@ export function sceneToSvg(scene, {
|
|
|
188
349
|
return {
|
|
189
350
|
fill: style.fill ?? "none", stroke: style.stroke ?? "none",
|
|
190
351
|
"stroke-width": style.strokeWidth ?? 0, opacity: style.opacity ?? 1,
|
|
352
|
+
"fill-opacity": style.fillOpacity ?? 1,
|
|
353
|
+
"stroke-opacity": style.strokeOpacity ?? 1,
|
|
354
|
+
"stroke-linecap": style.lineCap,
|
|
191
355
|
"stroke-dasharray": { dash: "8 5", dashDot: "8 4 2 4", dot: "2 4" }[style.dash],
|
|
192
356
|
};
|
|
193
357
|
}
|
|
@@ -204,15 +368,62 @@ export function sceneToSvg(scene, {
|
|
|
204
368
|
if (presets[node.preset]) return dom("polygon", {
|
|
205
369
|
points: presets[node.preset].map(([px, py]) => `${x + px * w},${y + py * h}`).join(" "), ...attrs,
|
|
206
370
|
});
|
|
371
|
+
const halfHeight = h / 2;
|
|
372
|
+
const heightBasedPresets = {
|
|
373
|
+
quarterHeightHexagon: [
|
|
374
|
+
[x + h / 4, y + h], [x + w - h / 4, y + h], [x + w, y + h / 2],
|
|
375
|
+
[x + w - h / 4, y], [x + h / 4, y], [x, y + h / 2],
|
|
376
|
+
],
|
|
377
|
+
reverseParallelogram: [
|
|
378
|
+
[x + halfHeight, y + h],
|
|
379
|
+
[x + w, y + h],
|
|
380
|
+
[x + w - halfHeight, y],
|
|
381
|
+
[x, y],
|
|
382
|
+
],
|
|
383
|
+
trapezoid: [
|
|
384
|
+
[x, y + h],
|
|
385
|
+
[x + w, y + h],
|
|
386
|
+
[x + w - halfHeight, y],
|
|
387
|
+
[x + halfHeight, y],
|
|
388
|
+
],
|
|
389
|
+
invertedTrapezoid: [
|
|
390
|
+
[x + halfHeight, y + h],
|
|
391
|
+
[x + w - halfHeight, y + h],
|
|
392
|
+
[x + w, y],
|
|
393
|
+
[x, y],
|
|
394
|
+
],
|
|
395
|
+
};
|
|
396
|
+
if (heightBasedPresets[node.preset]) return dom("polygon", {
|
|
397
|
+
points: heightBasedPresets[node.preset].map(([px, py]) => `${px},${py}`).join(" "), ...attrs,
|
|
398
|
+
});
|
|
207
399
|
if (node.preset === "cylinder") {
|
|
208
400
|
const r = Math.min(h / 4, w / 8);
|
|
209
401
|
return dom("path", { d: `M ${x} ${y + r} A ${w / 2} ${r} 0 0 1 ${x + w} ${y + r} L ${x + w} ${y + h - r} A ${w / 2} ${r} 0 0 1 ${x} ${y + h - r} Z M ${x} ${y + r} A ${w / 2} ${r} 0 0 0 ${x + w} ${y + r}`, ...attrs });
|
|
210
402
|
}
|
|
403
|
+
if (node.preset === "sequenceTab") {
|
|
404
|
+
return dom("path", {
|
|
405
|
+
d: `M ${x} ${y} L ${x + w} ${y} L ${x + w} ${y + h * 0.65} L ${x + w * 0.832} ${y + h} L ${x} ${y + h} Z`,
|
|
406
|
+
...attrs,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (node.preset === "topRoundedRect") {
|
|
410
|
+
const r = Math.min(node.style?.cornerRadius ?? 5, w / 2, h / 2);
|
|
411
|
+
return dom("path", {
|
|
412
|
+
d: `M ${x} ${y + h} V ${y + r} Q ${x} ${y} ${x + r} ${y} H ${x + w - r} Q ${x + w} ${y} ${x + w} ${y + r} V ${y + h} Z`,
|
|
413
|
+
...attrs,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
211
416
|
if (node.preset && !["rect", "roundedRect"].includes(node.preset)) throw new Error(`Unsupported scene SVG preset: ${node.preset}`);
|
|
212
417
|
return dom("rect", { x, y, width: w, height: h, rx: node.preset === "roundedRect" ? node.style?.cornerRadius ?? 8 : 0, ...attrs });
|
|
213
418
|
}
|
|
214
|
-
function text(parent, content, bounds, layout = {}) {
|
|
419
|
+
function text(parent, content, bounds, layout = {}, rotation) {
|
|
215
420
|
if (!content?.paragraphs?.length) return;
|
|
421
|
+
const textParent = rotation
|
|
422
|
+
? dom("g", {
|
|
423
|
+
transform: `rotate(${rotation} ${bounds.x + bounds.width / 2} ${bounds.y + bounds.height / 2})`,
|
|
424
|
+
})
|
|
425
|
+
: parent;
|
|
426
|
+
if (textParent !== parent) parent.appendChild(textParent);
|
|
216
427
|
const lines = content.paragraphs.flatMap((paragraph) => {
|
|
217
428
|
const output = [{ ...paragraph, runs: [] }];
|
|
218
429
|
for (const run of paragraph.runs) {
|
|
@@ -243,7 +454,7 @@ export function sceneToSvg(scene, {
|
|
|
243
454
|
span.textContent = run.text;
|
|
244
455
|
label.appendChild(span);
|
|
245
456
|
}
|
|
246
|
-
|
|
457
|
+
textParent.appendChild(label);
|
|
247
458
|
y += heights[index];
|
|
248
459
|
});
|
|
249
460
|
}
|
|
@@ -260,6 +471,11 @@ export function sceneToSvg(scene, {
|
|
|
260
471
|
element = primitive(fallback);
|
|
261
472
|
} else {
|
|
262
473
|
element = dom("g");
|
|
474
|
+
if (node.kind === "shape" && node.rotation) {
|
|
475
|
+
setAttributes(element, {
|
|
476
|
+
transform: `rotate(${node.rotation} ${node.bounds.x + node.bounds.width / 2} ${node.bounds.y + node.bounds.height / 2})`,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
263
479
|
if (node.kind === "shape" || node.kind === "group") element.appendChild(shape(node));
|
|
264
480
|
if (node.kind === "image") element.appendChild(dom("image", {
|
|
265
481
|
...node.bounds, href: node.src, opacity: node.opacity,
|
|
@@ -267,15 +483,15 @@ export function sceneToSvg(scene, {
|
|
|
267
483
|
role: "img", "aria-label": node.alt,
|
|
268
484
|
}));
|
|
269
485
|
if (node.kind === "connector") {
|
|
270
|
-
const attrs = { ...paint(node.style), fill: "none", "stroke-linejoin": "round", "stroke-linecap": "round" };
|
|
486
|
+
const attrs = { ...paint(node.style), fill: "none", "stroke-linejoin": "round", "stroke-linecap": node.style?.lineCap ?? "round" };
|
|
271
487
|
const defs = dom("defs");
|
|
272
488
|
for (const [end, arrow] of [["start", node.arrowStart], ["end", node.arrowEnd]]) {
|
|
273
489
|
if (!arrow || arrow === "none") continue;
|
|
274
490
|
const id = `scene-arrow-${renderId}-${index}-${end}`;
|
|
275
491
|
const marker = dom("marker", { id, viewBox: "0 0 10 10", refX: 9, refY: 5, markerWidth: 7, markerHeight: 7, orient: "auto-start-reverse", markerUnits: "strokeWidth" });
|
|
276
492
|
marker.appendChild(arrow === "oval"
|
|
277
|
-
? dom("circle", { cx: 5, cy: 5, r: 4, fill: node.style?.stroke })
|
|
278
|
-
: dom("path", { d: arrow === "diamond" ? "M 0 5 L 5 0 L 10 5 L 5 10 Z" : arrow === "stealth" ? "M 0 0 L 10 5 L 0 10 L 3 5 Z" : "M 0 0 L 10 5 L 0 10 Z", fill: node.style?.stroke }));
|
|
493
|
+
? dom("circle", { cx: 5, cy: 5, r: 4, fill: node.style?.stroke, "fill-opacity": node.style?.strokeOpacity ?? 1 })
|
|
494
|
+
: dom("path", { d: arrow === "diamond" ? "M 0 5 L 5 0 L 10 5 L 5 10 Z" : arrow === "stealth" ? "M 0 0 L 10 5 L 0 10 L 3 5 Z" : "M 0 0 L 10 5 L 0 10 Z", fill: node.style?.stroke, "fill-opacity": node.style?.strokeOpacity ?? 1 }));
|
|
279
495
|
defs.appendChild(marker);
|
|
280
496
|
attrs[`marker-${end}`] = `url(#${id})`;
|
|
281
497
|
}
|
|
@@ -283,7 +499,7 @@ export function sceneToSvg(scene, {
|
|
|
283
499
|
element.appendChild(dom("path", { d: node.points.map((point, i) => `${i ? "L" : "M"} ${point.x} ${point.y}`).join(" "), ...attrs }));
|
|
284
500
|
if (node.label) text(element, node.label.text, node.label.bounds);
|
|
285
501
|
}
|
|
286
|
-
if (node.text) text(element, node.text, node.bounds, node.textLayout);
|
|
502
|
+
if (node.text) text(element, node.text, node.bounds, node.textLayout, node.kind === "text" ? node.rotation : undefined);
|
|
287
503
|
if (node.accessibility?.label) setAttributes(element, { role: node.accessibility.role || "img", "aria-label": node.accessibility.label });
|
|
288
504
|
}
|
|
289
505
|
setAttributes(element, { "data-scene-node": node.kind, "data-scene-source-path": node.sourcePath, "data-scene-id": node.id });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Parse a local slide background without rewriting the source Markdown. */
|
|
2
|
+
export function parseSlideBackground(value) {
|
|
3
|
+
if (value === undefined) return "";
|
|
4
|
+
const invalid = (reason) => {
|
|
5
|
+
throw new Error(`Invalid background-image: ${reason}`);
|
|
6
|
+
};
|
|
7
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
8
|
+
invalid("expected a non-empty /assets/... image path.");
|
|
9
|
+
}
|
|
10
|
+
const path = value.trim().replace(/^assets\//, "/assets/");
|
|
11
|
+
if (!path.startsWith("/assets/") || /[\\?#\u0000-\u001f\u007f]/.test(path)) {
|
|
12
|
+
invalid("use a local /assets/... path, without URLs, query strings, or fragments.");
|
|
13
|
+
}
|
|
14
|
+
const segments = path.slice("/assets/".length).split("/");
|
|
15
|
+
if (segments.some((segment) => !segment || segment === "." || segment === ".." || segment.includes(":"))) {
|
|
16
|
+
invalid("the path must stay inside an assets folder.");
|
|
17
|
+
}
|
|
18
|
+
if (!/\.(?:svg|png|webp|jpg|jpeg)$/i.test(path)) {
|
|
19
|
+
invalid("supported image formats are SVG, PNG, WebP, JPG, and JPEG.");
|
|
20
|
+
}
|
|
21
|
+
return path;
|
|
22
|
+
}
|
|
@@ -336,8 +336,12 @@ footer .page{color:var(--accent-strong);font-weight:600;background:var(--accent-
|
|
|
336
336
|
letter-spacing:-.045em;text-wrap:balance;}
|
|
337
337
|
.deck.markdstage-placeholder p{
|
|
338
338
|
max-width:32em;margin-top:1.25em;color:#c8cedd;font-size:clamp(18px,2.2vw,30px);}
|
|
339
|
-
.
|
|
340
|
-
max-height:none;object-fit:cover;border-radius:0;box-shadow:none;
|
|
339
|
+
.slide-background{position:absolute;inset:0;width:100%;height:100%;max-width:none;
|
|
340
|
+
max-height:none;object-fit:cover;object-position:center;border-radius:0;box-shadow:none;
|
|
341
|
+
z-index:0;pointer-events:none;}
|
|
342
|
+
.deck.has-slide-background>header,.deck.has-slide-background>.body,
|
|
343
|
+
.deck.has-slide-background>footer,.deck.has-slide-background>.theme-backcover-logo,
|
|
344
|
+
.deck.has-slide-background>.theme-backcover-copyright{z-index:1;}
|
|
341
345
|
.theme-cover-logo{position:absolute;top:var(--deck-pad-y);left:var(--deck-pad-x);
|
|
342
346
|
width:var(--cover-logo-width,clamp(130px,16vw,230px));height:auto;max-height:none;
|
|
343
347
|
border-radius:0;box-shadow:none;z-index:2;}
|
|
@@ -442,6 +446,23 @@ footer .page{color:var(--accent-strong);font-weight:600;background:var(--accent-
|
|
|
442
446
|
font-size:.95em;font-weight:700;}
|
|
443
447
|
.nav-more-icon.nav-fixed-preview{font-size:.72em;}
|
|
444
448
|
.nav-more-label{min-width:0;font-size:.88em;font-weight:600;line-height:1.25;}
|
|
449
|
+
.pptx-export-dialog{box-sizing:border-box;width:min(460px,calc(100vw - 32px));
|
|
450
|
+
max-height:calc(100dvh - 32px);overflow:auto;padding:24px;border:1px solid var(--border);
|
|
451
|
+
border-radius:12px;background:var(--surface);color:var(--fg);font-size:16px;
|
|
452
|
+
box-shadow:0 12px 34px rgba(0,0,0,.3);}
|
|
453
|
+
.pptx-export-dialog::backdrop{background:rgba(0,0,0,.55);}
|
|
454
|
+
.pptx-export-dialog h2{margin:0 0 24px;font-size:22px;}
|
|
455
|
+
.pptx-export-dialog fieldset{margin:0;padding:0;border:0;min-width:0;}
|
|
456
|
+
.pptx-export-dialog legend{padding:0;margin-bottom:12px;font-weight:600;}
|
|
457
|
+
.pptx-export-choice{display:flex;align-items:center;gap:10px;min-height:44px;cursor:pointer;font-weight:600;}
|
|
458
|
+
.pptx-export-choice input{margin:0;accent-color:var(--accent-strong);}
|
|
459
|
+
.pptx-export-choice input:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
|
|
460
|
+
.pptx-export-dialog p{margin:0 0 16px 24px;font-size:14px;line-height:1.5;}
|
|
461
|
+
.pptx-export-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:12px;margin-top:16px;}
|
|
462
|
+
.pptx-export-actions .presenter-button{min-height:44px;}
|
|
463
|
+
.pptx-export-actions .pptx-export-submit{font-weight:700;color:var(--accent-strong);
|
|
464
|
+
background:var(--accent-soft);border-color:var(--accent-strong);}
|
|
465
|
+
@media print{.pptx-export-dialog{display:none!important;}}
|
|
445
466
|
.layout-warning{position:fixed;top:14px;right:14px;z-index:55;
|
|
446
467
|
max-width:min(420px,calc(100vw - 28px));padding:.55em .8em;border-radius:8px;
|
|
447
468
|
color:#fff;background:#a4262c;border:1px solid #f1aeb5;
|
|
@@ -642,7 +663,7 @@ body.fixed-output-mode .deck.backcover-slide{background:var(--backcover-bg,var(-
|
|
|
642
663
|
body.fixed-output-mode .deck::before{position:absolute;}
|
|
643
664
|
body.fixed-output-mode .body{overflow:hidden;}
|
|
644
665
|
body.fixed-output-mode .kicker{font-size:12px;}
|
|
645
|
-
body.fixed-output-mode img:not(.
|
|
666
|
+
body.fixed-output-mode img:not(.slide-background){max-height:346px;}
|
|
646
667
|
body.fixed-output-mode pre.mermaid svg{max-height:317px;}
|
|
647
668
|
body.fixed-output-mode .architecture-svg{max-height:504px;}
|
|
648
669
|
body.fixed-output-mode footer{gap:.65em;font-size:11px;margin-top:16px;}
|
|
@@ -697,17 +718,22 @@ body.pptx-artwork-mode svg [data-pptx-native="table"]{
|
|
|
697
718
|
color:transparent!important;
|
|
698
719
|
-webkit-text-fill-color:transparent!important;
|
|
699
720
|
}
|
|
700
|
-
body.pptx-artwork-mode svg
|
|
721
|
+
body.pptx-artwork-mode svg [data-pptx-native="connector"]{
|
|
722
|
+
marker-start:none!important;
|
|
723
|
+
marker-mid:none!important;
|
|
724
|
+
marker-end:none!important;
|
|
725
|
+
}
|
|
701
726
|
body.pptx-artwork-mode svg [data-pptx-native="image"]{opacity:0!important;}
|
|
702
727
|
body.pptx-artwork-mode [data-pptx-shadow-fallback]{box-shadow:none!important;}
|
|
703
|
-
body.pptx-artwork-mode .pptx-fallback-hidden
|
|
728
|
+
body.pptx-artwork-mode .pptx-fallback-hidden,
|
|
729
|
+
body.pptx-artwork-mode .pptx-fallback-hidden *{visibility:hidden!important;}
|
|
704
730
|
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template){
|
|
705
731
|
background:transparent!important;
|
|
706
732
|
}
|
|
707
733
|
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)::before{
|
|
708
734
|
display:none!important;
|
|
709
735
|
}
|
|
710
|
-
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)>.
|
|
736
|
+
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)>.slide-background,
|
|
711
737
|
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)>.theme-cover-logo{
|
|
712
738
|
visibility:hidden!important;
|
|
713
739
|
}
|