@markdstage/markdstage 3.4.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.
@@ -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
- return capture(element, 0, true);
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
- if (CSS_PROPERTIES.has(property) && safeCss(content)) element.style?.setProperty(property, String(content));
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
- parent.appendChild(label);
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
- .theme-cover-background{position:absolute;inset:0;width:100%;height:100%;max-width:none;
340
- max-height:none;object-fit:cover;border-radius:0;box-shadow:none;z-index:0;}
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(.theme-cover-background){max-height:346px;}
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 path[data-pptx-native="connector"]{marker-end:none!important;}
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{visibility:hidden!important;}
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)>.theme-cover-background,
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
  }