@markdstage/markdstage 3.4.0 → 3.8.1

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
+ }
@@ -0,0 +1,48 @@
1
+ export const OUTPUT_WIDTH = 1280;
2
+ export const OUTPUT_HEIGHT = 720;
3
+
4
+ // Scale the browsing context, not its contents: SVG text measurement and viewport
5
+ // units must see the same untransformed geometry at every display size.
6
+ export function createSlideViewport(host, { id, title, onNavigate, onKey, onPointer, onLayout, onError }) {
7
+ const frame = document.createElement("iframe");
8
+ frame.id = id;
9
+ frame.title = title;
10
+ frame.className = "slide-viewport-frame";
11
+ frame.width = OUTPUT_WIDTH;
12
+ frame.height = OUTPUT_HEIGHT;
13
+ frame.src = "./?surface=1";
14
+ let state = null;
15
+ let loaded = false;
16
+ const render = () => {
17
+ if (loaded && state) frame.contentWindow.__markdstageSurface.render(state);
18
+ };
19
+ frame.addEventListener("load", () => {
20
+ loaded = true;
21
+ render();
22
+ });
23
+ frame.addEventListener("slide-navigate", (event) => onNavigate?.(event.detail));
24
+ frame.addEventListener("slide-key", (event) => onKey?.(event.detail));
25
+ frame.addEventListener("slide-pointer", () => onPointer?.());
26
+ frame.addEventListener("slide-layout", (event) => onLayout?.(event.detail));
27
+ frame.addEventListener("slide-error", (event) => onError?.(event.detail));
28
+ const resize = () => {
29
+ const scale = Math.min(host.clientWidth / OUTPUT_WIDTH, host.clientHeight / OUTPUT_HEIGHT);
30
+ frame.style.transform = `translate(-50%, -50%) scale(${scale})`;
31
+ };
32
+ const observer = new ResizeObserver(resize);
33
+ host.appendChild(frame);
34
+ observer.observe(host);
35
+ resize();
36
+ return {
37
+ setState(next) {
38
+ state = next;
39
+ frame.tabIndex = next.navigationEnabled ? 0 : -1;
40
+ render();
41
+ },
42
+ dispose() {
43
+ observer.disconnect();
44
+ frame.remove();
45
+ state = null;
46
+ },
47
+ };
48
+ }
@@ -95,10 +95,14 @@
95
95
  html,body{height:100%;margin:0;overflow:hidden}
96
96
  #stage{height:100%}
97
97
  body.fixed-preview-mode #stage{
98
- position:fixed;left:50%;top:calc(50% - 24px);width:1280px;height:720px;
99
- transform:translate(-50%,-50%) scale(var(--fixed-preview-scale,1));
100
- transform-origin:center;box-shadow:0 18px 60px rgba(0,0,0,.38);
98
+ position:fixed;inset:12px 12px 76px;height:auto;
101
99
  }
100
+ .slide-viewport{position:relative;overflow:hidden;}
101
+ .slide-viewport-frame{position:absolute;left:50%;top:50%;width:1280px;height:720px;
102
+ max-width:none;max-height:none;border:0;transform-origin:center;display:block;}
103
+ body.presenter-mode.fixed-preview-mode #stage{inset:0;background:#000;}
104
+ body.surface-mode #stage{width:1280px;height:720px;}
105
+ body.surface-mode #nav,body.surface-mode .layout-warning{display:none!important;}
102
106
  .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
103
107
  clip:rect(0,0,0,0);white-space:nowrap;border:0;}
104
108
  body{
@@ -336,8 +340,12 @@ footer .page{color:var(--accent-strong);font-weight:600;background:var(--accent-
336
340
  letter-spacing:-.045em;text-wrap:balance;}
337
341
  .deck.markdstage-placeholder p{
338
342
  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;}
343
+ .slide-background{position:absolute;inset:0;width:100%;height:100%;max-width:none;
344
+ max-height:none;object-fit:cover;object-position:center;border-radius:0;box-shadow:none;
345
+ z-index:0;pointer-events:none;}
346
+ .deck.has-slide-background>header,.deck.has-slide-background>.body,
347
+ .deck.has-slide-background>footer,.deck.has-slide-background>.theme-backcover-logo,
348
+ .deck.has-slide-background>.theme-backcover-copyright{z-index:1;}
341
349
  .theme-cover-logo{position:absolute;top:var(--deck-pad-y);left:var(--deck-pad-x);
342
350
  width:var(--cover-logo-width,clamp(130px,16vw,230px));height:auto;max-height:none;
343
351
  border-radius:0;box-shadow:none;z-index:2;}
@@ -442,6 +450,23 @@ footer .page{color:var(--accent-strong);font-weight:600;background:var(--accent-
442
450
  font-size:.95em;font-weight:700;}
443
451
  .nav-more-icon.nav-fixed-preview{font-size:.72em;}
444
452
  .nav-more-label{min-width:0;font-size:.88em;font-weight:600;line-height:1.25;}
453
+ .pptx-export-dialog{box-sizing:border-box;width:min(460px,calc(100vw - 32px));
454
+ max-height:calc(100dvh - 32px);overflow:auto;padding:24px;border:1px solid var(--border);
455
+ border-radius:12px;background:var(--surface);color:var(--fg);font-size:16px;
456
+ box-shadow:0 12px 34px rgba(0,0,0,.3);}
457
+ .pptx-export-dialog::backdrop{background:rgba(0,0,0,.55);}
458
+ .pptx-export-dialog h2{margin:0 0 24px;font-size:22px;}
459
+ .pptx-export-dialog fieldset{margin:0;padding:0;border:0;min-width:0;}
460
+ .pptx-export-dialog legend{padding:0;margin-bottom:12px;font-weight:600;}
461
+ .pptx-export-choice{display:flex;align-items:center;gap:10px;min-height:44px;cursor:pointer;font-weight:600;}
462
+ .pptx-export-choice input{margin:0;accent-color:var(--accent-strong);}
463
+ .pptx-export-choice input:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
464
+ .pptx-export-dialog p{margin:0 0 16px 24px;font-size:14px;line-height:1.5;}
465
+ .pptx-export-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:12px;margin-top:16px;}
466
+ .pptx-export-actions .presenter-button{min-height:44px;}
467
+ .pptx-export-actions .pptx-export-submit{font-weight:700;color:var(--accent-strong);
468
+ background:var(--accent-soft);border-color:var(--accent-strong);}
469
+ @media print{.pptx-export-dialog{display:none!important;}}
445
470
  .layout-warning{position:fixed;top:14px;right:14px;z-index:55;
446
471
  max-width:min(420px,calc(100vw - 28px));padding:.55em .8em;border-radius:8px;
447
472
  color:#fff;background:#a4262c;border:1px solid #f1aeb5;
@@ -472,6 +497,7 @@ body.presenter-mode #navPresenterView{display:none;}
472
497
  body.presenter-mode #navExport,body.presenter-mode #navExportPptx{display:none;}
473
498
  body.presenter-mode #navImport{display:none;}
474
499
  body.presenter-mode #navSourceMode{display:none;}
500
+ body.presenter-mode #navFixedPreview{display:none;}
475
501
  body.preview-mode .nav,body.preview-mode .overview{display:none!important;}
476
502
  /* When no deck is loaded, leave only the load button as the available action. */
477
503
  .nav.nav-empty .nav-main,.nav.nav-empty #navMore{display:none;}
@@ -613,7 +639,7 @@ body.presenter-view-mode .layout-warning{display:none;}
613
639
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
614
640
 
615
641
  /* ===== Fixed 16:9 output surface ===== */
616
- /* PDF, PNG capture, and the in-canvas fixed preview share these rules. At the
642
+ /* PDF, PNG capture, and all fixed slide viewports share these rules. At the
617
643
  CSS reference pixel density, 1280x720 equals the PDF's 13.333333in x 7.5in. */
618
644
  body.fixed-output-mode .deck{
619
645
  width:1280px;height:720px;min-width:1280px;max-width:1280px;
@@ -642,7 +668,7 @@ body.fixed-output-mode .deck.backcover-slide{background:var(--backcover-bg,var(-
642
668
  body.fixed-output-mode .deck::before{position:absolute;}
643
669
  body.fixed-output-mode .body{overflow:hidden;}
644
670
  body.fixed-output-mode .kicker{font-size:12px;}
645
- body.fixed-output-mode img:not(.theme-cover-background){max-height:346px;}
671
+ body.fixed-output-mode img:not(.slide-background){max-height:346px;}
646
672
  body.fixed-output-mode pre.mermaid svg{max-height:317px;}
647
673
  body.fixed-output-mode .architecture-svg{max-height:504px;}
648
674
  body.fixed-output-mode footer{gap:.65em;font-size:11px;margin-top:16px;}
@@ -697,17 +723,22 @@ body.pptx-artwork-mode svg [data-pptx-native="table"]{
697
723
  color:transparent!important;
698
724
  -webkit-text-fill-color:transparent!important;
699
725
  }
700
- body.pptx-artwork-mode svg path[data-pptx-native="connector"]{marker-end:none!important;}
726
+ body.pptx-artwork-mode svg [data-pptx-native="connector"]{
727
+ marker-start:none!important;
728
+ marker-mid:none!important;
729
+ marker-end:none!important;
730
+ }
701
731
  body.pptx-artwork-mode svg [data-pptx-native="image"]{opacity:0!important;}
702
732
  body.pptx-artwork-mode [data-pptx-shadow-fallback]{box-shadow:none!important;}
703
- body.pptx-artwork-mode .pptx-fallback-hidden{visibility:hidden!important;}
733
+ body.pptx-artwork-mode .pptx-fallback-hidden,
734
+ body.pptx-artwork-mode .pptx-fallback-hidden *{visibility:hidden!important;}
704
735
  body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template){
705
736
  background:transparent!important;
706
737
  }
707
738
  body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)::before{
708
739
  display:none!important;
709
740
  }
710
- body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)>.theme-cover-background,
741
+ body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)>.slide-background,
711
742
  body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)>.theme-cover-logo{
712
743
  visibility:hidden!important;
713
744
  }