@weasel-js/svg 1.9.3 → 1.10.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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, resolveScreenLength, scriptMetricsFor, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, DEFAULT_TEXT_STYLE, PathBuilder, resolveTextStyle, layoutRuns, resolveRuns, resolveAlign, PATH_C, PATH_Q, resolveRunFace, parseColor, rgbaToHex, resolveMarkerSize, getPaintKind } from '@weasel-js/core';
2
- import { identity, multiply, invert } from '@weasel-js/geom';
1
+ import { identity, multiply, invert, fillsOpenSubpaths, splitByClosure, splitSubpaths, rotateAboutPoint, hasOpenSubpath } from '@weasel-js/geom';
2
+ import { isTileKind, boundsOfPath, transformPath, isSolidPaint, resolveScreenLength, scriptMetricsFor, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, poseLinearOf, poseFrameOf, pathThroughPoseFrame, DEFAULT_TEXT_STYLE, pathFromD, isPatternPaint, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, resolveAlign, paragraphDirection, resolveRunFace, resolveTextStyle, parseColor, rgbaToHex, layoutRuns, resolveRuns, PathBuilder, getPaintKind, resolveMarkerSize, isTilePaint, tileSpecOf } from '@weasel-js/core';
3
3
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
4
4
 
5
- // src/parse.ts
5
+ // src/types.ts
6
6
  var IDENTITY_MATRIX = identity();
7
7
  function applyMatrix(m, x, y) {
8
8
  return { x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] };
@@ -96,130 +96,134 @@ function trimNumber(n) {
96
96
  return String(r);
97
97
  }
98
98
 
99
- // src/shapes.ts
100
- var KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
101
- function isAxisAlignedScale(m, eps = 1e-9) {
102
- return Math.abs(m[1]) < eps && Math.abs(m[2]) < eps;
103
- }
104
- function transformPath(path, m) {
105
- if (isIdentity(m)) return path;
106
- if (path.kind === "rect") {
107
- if (isAxisAlignedScale(m)) {
108
- const a = m[0], d = m[3], e = m[4], f = m[5];
109
- let x = path.x * a + e;
110
- let y = path.y * d + f;
111
- let w = path.width * a;
112
- let h = path.height * d;
113
- if (w < 0) {
114
- x += w;
115
- w = -w;
116
- }
117
- if (h < 0) {
118
- y += h;
119
- h = -h;
120
- }
121
- return { kind: "rect", x, y, width: w, height: h };
122
- }
123
- return transformPath(rectToPolygon(path), m);
124
- }
125
- const xs = path.coords;
126
- const out = new Float32Array(xs.length);
127
- for (let i = 0; i < xs.length; i += 2) {
128
- const p = applyMatrix(m, xs[i], xs[i + 1]);
129
- out[i] = p.x;
130
- out[i + 1] = p.y;
131
- }
132
- return {
133
- kind: "polygon",
134
- commands: new Uint8Array(path.commands),
135
- coords: out,
136
- fillRule: path.fillRule
137
- };
138
- }
139
- function rectToPolygon(r) {
140
- const b = new PathBuilder();
141
- b.moveTo(r.x, r.y);
142
- b.lineTo(r.x + r.width, r.y);
143
- b.lineTo(r.x + r.width, r.y + r.height);
144
- b.lineTo(r.x, r.y + r.height);
145
- b.close();
146
- return b.build();
147
- }
148
- function rectElementToPath(x, y, width2, height2, rx, ry, m = IDENTITY_MATRIX) {
149
- if (rx <= 0 && ry <= 0) {
150
- return transformPath({ kind: "rect", x, y, width: width2, height: height2 }, m);
151
- }
152
- const r = Math.min(Math.max(rx, ry), Math.min(width2, height2) / 2);
153
- const r2 = Math.min(rx > 0 ? rx : r, ry > 0 ? ry : r, Math.min(width2, height2) / 2);
154
- const rad = r2;
155
- const k = KAPPA * rad;
156
- const x2 = x + width2;
157
- const y2 = y + height2;
158
- const b = new PathBuilder();
159
- b.moveTo(x + rad, y);
160
- b.lineTo(x2 - rad, y);
161
- b.curveTo(x2 - rad + k, y, x2, y + rad - k, x2, y + rad);
162
- b.lineTo(x2, y2 - rad);
163
- b.curveTo(x2, y2 - rad + k, x2 - rad + k, y2, x2 - rad, y2);
164
- b.lineTo(x + rad, y2);
165
- b.curveTo(x + rad - k, y2, x, y2 - rad + k, x, y2 - rad);
166
- b.lineTo(x, y + rad);
167
- b.curveTo(x, y + rad - k, x + rad - k, y, x + rad, y);
168
- b.close();
169
- return transformPath(b.build(), m);
99
+ // src/absoluteUnits.ts
100
+ var ABSOLUTE_PX = Object.assign(/* @__PURE__ */ Object.create(null), {
101
+ px: 1,
102
+ in: 96,
103
+ cm: 96 / 2.54,
104
+ mm: 96 / 25.4,
105
+ q: 96 / 101.6,
106
+ pt: 96 / 72,
107
+ pc: 16
108
+ });
109
+
110
+ // src/fontSize.ts
111
+ var DEFAULT_FONT_SIZE = 16;
112
+ var EMS = Object.assign(/* @__PURE__ */ Object.create(null), { em: 1, ex: 0.5 });
113
+ function emsPer(unit) {
114
+ return EMS[unit.toLowerCase()];
115
+ }
116
+ var KEYWORD_PX = Object.assign(/* @__PURE__ */ Object.create(null), {
117
+ "xx-small": 9,
118
+ "x-small": 10,
119
+ small: 13,
120
+ medium: DEFAULT_FONT_SIZE,
121
+ large: 18,
122
+ "x-large": 24,
123
+ "xx-large": 32,
124
+ "xxx-large": 48
125
+ });
126
+ var KEYWORD_SCALE = Object.assign(/* @__PURE__ */ Object.create(null), {
127
+ larger: 1.2,
128
+ smaller: 1 / 1.2
129
+ });
130
+ function fontSizeKeyword(raw) {
131
+ const word = raw.trim().toLowerCase();
132
+ const size = KEYWORD_PX[word];
133
+ if (size != null) return { size };
134
+ const scale = KEYWORD_SCALE[word];
135
+ return scale == null ? void 0 : { scale };
136
+ }
137
+ var NUMBER = "[-+]?(?:\\d+\\.?\\d*|\\.\\d+)(?:e[-+]?\\d+)?";
138
+ var DIMENSION = new RegExp(`^\\s*(${NUMBER})\\s*([a-z%]*)\\s*$`, "i");
139
+ var FONT_RELATIVE = new RegExp(`(?<![\\w.])(${NUMBER})(em|ex)(?![\\w%])`, "gi");
140
+ var LENGTH_PROPERTIES = ["stroke-width", "stroke-dasharray", "letter-spacing"];
141
+ function computedFontSize(raw, parentSize) {
142
+ const word = fontSizeKeyword(raw);
143
+ if (word) return "size" in word ? word.size : word.scale * parentSize;
144
+ const m = DIMENSION.exec(raw);
145
+ if (!m) return void 0;
146
+ const n = Number(m[1]);
147
+ const unit = m[2].toLowerCase();
148
+ if (unit === "") return n;
149
+ if (unit === "%") return n / 100 * parentSize;
150
+ const ems = emsPer(unit);
151
+ if (ems != null) return n * ems * parentSize;
152
+ const scale = ABSOLUTE_PX[unit];
153
+ return scale == null ? void 0 : n * scale;
154
+ }
155
+ function fontSizeOf(style) {
156
+ const raw = style["font-size"];
157
+ if (raw == null) return DEFAULT_FONT_SIZE;
158
+ const size = computedFontSize(raw, DEFAULT_FONT_SIZE);
159
+ if (size != null) return size;
160
+ const n = parseFloat(raw);
161
+ return Number.isFinite(n) ? n : DEFAULT_FONT_SIZE;
162
+ }
163
+ function computeFontRelative(style, parentSize) {
164
+ const raw = style["font-size"];
165
+ if (raw != null && (/(?:%|em|ex)\s*$/i.test(raw) || fontSizeKeyword(raw))) {
166
+ const size2 = computedFontSize(raw, parentSize);
167
+ if (size2 != null) style["font-size"] = String(size2);
168
+ }
169
+ const size = fontSizeOf(style);
170
+ for (const prop of LENGTH_PROPERTIES) {
171
+ const value = style[prop];
172
+ if (value == null) continue;
173
+ style[prop] = value.replace(FONT_RELATIVE, (_, n, unit) => String(Number(n) * EMS[unit.toLowerCase()] * size));
174
+ }
175
+ }
176
+ function scaleLength(v, scale) {
177
+ return typeof v === "number" ? v * scale : v;
178
+ }
179
+ function scaleStroke(stroke, scale) {
180
+ const { width: width2 } = stroke;
181
+ if (scale === 1 || typeof width2 === "object") return stroke;
182
+ const out = { ...stroke };
183
+ if (width2 !== void 0) out.width = width2 * scale;
184
+ if (out.dash) out.dash = out.dash.map((n) => n * scale);
185
+ return out;
170
186
  }
171
- function ellipseToPath(cx, cy, rx, ry, m = IDENTITY_MATRIX) {
172
- const kx = KAPPA * rx;
173
- const ky = KAPPA * ry;
174
- const b = new PathBuilder();
175
- b.moveTo(cx + rx, cy);
176
- b.curveTo(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry);
177
- b.curveTo(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy);
178
- b.curveTo(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry);
179
- b.curveTo(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy);
180
- b.close();
181
- return transformPath(b.build(), m);
187
+ function scaleRuns(runs, scale) {
188
+ return runs.map((r) => {
189
+ const out = { ...r };
190
+ if (out.fontSize !== void 0) out.fontSize = scaleLength(out.fontSize, scale);
191
+ if (out.letterSpacing !== void 0) out.letterSpacing = scaleLength(out.letterSpacing, scale);
192
+ if (out.stroke) out.stroke = scaleStroke(out.stroke, scale);
193
+ return out;
194
+ });
182
195
  }
183
- function circleToPath(cx, cy, r, m = IDENTITY_MATRIX) {
184
- return ellipseToPath(cx, cy, r, r, m);
196
+ function scaleTextStyle(style, scale) {
197
+ const out = { ...style };
198
+ out.fontSize = scaleLength(out.fontSize ?? resolveTextStyle(out).fontSize, scale);
199
+ if (out.letterSpacing !== void 0) out.letterSpacing = scaleLength(out.letterSpacing, scale);
200
+ return out;
185
201
  }
186
- function lineToPath(x1, y1, x2, y2, m = IDENTITY_MATRIX) {
187
- const b = new PathBuilder();
188
- b.moveTo(x1, y1);
189
- b.lineTo(x2, y2);
190
- return transformPath(b.build(), m);
202
+
203
+ // src/paintThroughMatrix.ts
204
+ function uniformScaleOf(m) {
205
+ return Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
191
206
  }
192
- function parsePoints(raw) {
193
- const out = [];
194
- const nums = raw.match(/[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g);
195
- if (!nums) return out;
196
- for (let i = 0; i + 1 < nums.length; i += 2) {
197
- out.push({ x: parseFloat(nums[i]), y: parseFloat(nums[i + 1]) });
198
- }
199
- return out;
207
+ function isNonUniform(m) {
208
+ const [a, b, c, d] = m;
209
+ const n = Math.hypot(a, b, c, d);
210
+ return Math.abs(a * c + b * d) > 1e-4 * n * n || Math.abs(Math.hypot(a, b) - Math.hypot(c, d)) > 1e-4 * n;
200
211
  }
201
- function polylineToPath(points, m = IDENTITY_MATRIX) {
202
- if (points.length === 0) {
203
- return { kind: "polygon", commands: new Uint8Array(), coords: new Float32Array(), fillRule: "nonzero" };
212
+ function fillThroughMatrix(fill, m) {
213
+ if (!("units" in fill) || fill.units !== "world" && fill.units !== "local") return fill;
214
+ const at = (p) => applyMatrix(m, p.x, p.y);
215
+ if (fill.kind === "linear-gradient") return { ...fill, from: at(fill.from), to: at(fill.to) };
216
+ if (fill.kind === "radial-gradient") {
217
+ return { ...fill, center: at(fill.center), radius: fill.radius * uniformScaleOf(m) };
204
218
  }
205
- const b = new PathBuilder();
206
- b.moveTo(points[0].x, points[0].y);
207
- for (let i = 1; i < points.length; i++) b.lineTo(points[i].x, points[i].y);
208
- return transformPath(b.build(), m);
219
+ if (fill.kind === "conic-gradient") return { ...fill, center: at(fill.center) };
220
+ return fill;
209
221
  }
210
- function polygonToPath(points, m = IDENTITY_MATRIX) {
211
- if (points.length === 0) {
212
- return { kind: "polygon", commands: new Uint8Array(), coords: new Float32Array(), fillRule: "nonzero" };
213
- }
214
- const b = new PathBuilder();
215
- b.moveTo(points[0].x, points[0].y);
216
- for (let i = 1; i < points.length; i++) b.lineTo(points[i].x, points[i].y);
217
- b.close();
218
- return transformPath(b.build(), m);
222
+ function paintThroughMatrix(paint, m) {
223
+ return paint.kind === "gradient" ? { ...paint, paint: fillThroughMatrix(paint.paint, m) } : paint;
219
224
  }
220
- function anchorOffset(style, width2) {
221
- const edge = resolveAlign(style?.align ?? "left", style?.direction ?? "ltr");
222
- return edge === "center" ? width2 / 2 : edge === "right" ? width2 : 0;
225
+ function strokeThroughMatrix(stroke, m) {
226
+ return { ...scaleStroke(stroke, uniformScaleOf(m)), paint: paintThroughMatrix(stroke.paint, m) };
223
227
  }
224
228
 
225
229
  // src/cssScan.ts
@@ -417,15 +421,6 @@ var DEFAULT_MEDIA_ENVIRONMENT = {
417
421
  color: 8,
418
422
  monochrome: 0
419
423
  };
420
- var ABSOLUTE_PX = {
421
- px: 1,
422
- in: 96,
423
- cm: 96 / 2.54,
424
- mm: 96 / 25.4,
425
- q: 96 / 101.6,
426
- pt: 96 / 72,
427
- pc: 16
428
- };
429
424
  function absoluteLength(raw) {
430
425
  const m = raw == null ? null : /^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)\s*([a-z]*)\s*$/i.exec(raw);
431
426
  if (!m) return void 0;
@@ -640,6 +635,106 @@ function evaluateMediaQuery(list, env) {
640
635
  }
641
636
  });
642
637
  }
638
+
639
+ // src/container.ts
640
+ function queryContainerOf(declared, media) {
641
+ const [shortNames, shortType] = (declared("container") ?? "").split("/").map((part) => part.trim());
642
+ const type = (declared("container-type") ?? shortType ?? "").trim().toLowerCase();
643
+ if (type !== "size" && type !== "inline-size") return null;
644
+ const names = (declared("container-name") ?? shortNames ?? "").trim().split(/\s+/).filter((n) => n !== "" && n.toLowerCase() !== "none");
645
+ return { names, type, width: media.width, height: media.height };
646
+ }
647
+ var NAME = /^(-?[a-zA-Z_\u0080-￿][\w\u0080-￿-]*)(?![\w(-])\s*/;
648
+ var BLOCK_AXIS = /\b(?:height|block-size|aspect-ratio|orientation)\b/;
649
+ var NOT_A_SIZE_FEATURE = /\b(?:device-|resolution|color|monochrome|prefers-|update|hover|pointer)/;
650
+ function containerQueryHolds(prelude, container) {
651
+ if (!container) return false;
652
+ let condition2 = prelude.trim();
653
+ const named = NAME.exec(condition2);
654
+ if (named && named[1].toLowerCase() !== "not") {
655
+ if (!container.names.includes(named[1])) return false;
656
+ condition2 = condition2.slice(named[0].length);
657
+ }
658
+ condition2 = condition2.toLowerCase();
659
+ if (NOT_A_SIZE_FEATURE.test(condition2)) return false;
660
+ if (container.type === "inline-size" && BLOCK_AXIS.test(condition2)) return false;
661
+ const physical = condition2.replace(/\binline-size\b/g, "width").replace(/\bblock-size\b/g, "height");
662
+ return evaluateMediaQuery(physical, {
663
+ ...DEFAULT_MEDIA_ENVIRONMENT,
664
+ width: container.width,
665
+ height: container.height
666
+ });
667
+ }
668
+
669
+ // src/specificity.ts
670
+ function identEnd(s, i) {
671
+ while (i < s.length) {
672
+ const ch = s[i];
673
+ if (ch === "\\") i += 2;
674
+ else if (/[\w-]/.test(ch) || ch.charCodeAt(0) > 127) i++;
675
+ else break;
676
+ }
677
+ return i;
678
+ }
679
+ var LEGACY_PSEUDO_ELEMENTS = /* @__PURE__ */ new Set(["before", "after", "first-line", "first-letter"]);
680
+ var MAX_ARG_PSEUDOS = /* @__PURE__ */ new Set(["not", "is", "matches", "-webkit-any", "has"]);
681
+ function compareSpecificity(a, b) {
682
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
683
+ }
684
+ function maxSpecificity(list) {
685
+ let best = [0, 0, 0];
686
+ for (const sel of splitTopLevel(list, ",")) {
687
+ const s = specificity(sel);
688
+ if (compareSpecificity(s, best) > 0) best = s;
689
+ }
690
+ return best;
691
+ }
692
+ function specificity(selector) {
693
+ let a = 0, b = 0, c = 0;
694
+ const s = selector;
695
+ let i = 0;
696
+ while (i < s.length) {
697
+ const ch = s[i];
698
+ if (ch === "#") {
699
+ a++;
700
+ i = identEnd(s, i + 1);
701
+ } else if (ch === ".") {
702
+ b++;
703
+ i = identEnd(s, i + 1);
704
+ } else if (ch === "[") {
705
+ b++;
706
+ while (i < s.length && s[i] !== "]") {
707
+ i = s[i] === '"' || s[i] === "'" ? skipString(s, i) : i + 1;
708
+ }
709
+ i++;
710
+ } else if (ch === ":") {
711
+ const element = s[i + 1] === ":";
712
+ const nameStart = i + (element ? 2 : 1);
713
+ const nameEnd = identEnd(s, nameStart);
714
+ const name = s.slice(nameStart, nameEnd).toLowerCase();
715
+ let arg = null;
716
+ i = nameEnd;
717
+ if (s[i] === "(") {
718
+ const end = parenEnd(s, i);
719
+ arg = s.slice(i + 1, end - 1);
720
+ i = end;
721
+ }
722
+ if (element || LEGACY_PSEUDO_ELEMENTS.has(name)) c++;
723
+ else if (name === "where") ; else if (MAX_ARG_PSEUDOS.has(name) && arg != null) {
724
+ const m = maxSpecificity(arg);
725
+ a += m[0];
726
+ b += m[1];
727
+ c += m[2];
728
+ } else b++;
729
+ } else if (/[a-zA-Z_\\-]/.test(ch) || ch.charCodeAt(0) > 127) {
730
+ c++;
731
+ i = identEnd(s, i);
732
+ } else {
733
+ i++;
734
+ }
735
+ }
736
+ return [a, b, c];
737
+ }
643
738
  function parsePaintAttr(raw) {
644
739
  if (raw == null) return null;
645
740
  const s = raw.trim();
@@ -670,21 +765,28 @@ function parsePaintAttr(raw) {
670
765
 
671
766
  // src/properties.ts
672
767
  var TEXT_TRANSFORMS = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
673
- var DIMENSION = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z%]*)$/i;
768
+ var DIMENSION2 = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z%]*)$/i;
674
769
  function dimension(v) {
675
- const m = DIMENSION.exec(v);
770
+ const m = DIMENSION2.exec(v);
676
771
  return m ? { n: Number(m[1]), unit: m[2].toLowerCase() } : void 0;
677
772
  }
678
773
  var clamp01 = (n) => n < 0 ? 0 : n > 1 ? 1 : n;
679
- var length = (native, coercedAs) => (v, warn, prop) => {
774
+ function pxPer(unit, fontSize) {
775
+ if (unit === "") return 1;
776
+ const ems = emsPer(unit);
777
+ return ems != null ? ems * fontSize : ABSOLUTE_PX[unit];
778
+ }
779
+ var length = (native, coercedAs) => (v, warn, prop, fontSize) => {
680
780
  const d = dimension(v);
681
781
  if (!d) return void 0;
682
- if (d.unit && !native.includes(d.unit)) {
782
+ const scale = native === "px" ? pxPer(d.unit, fontSize) : emsPer(d.unit);
783
+ if (scale != null) return d.n * scale;
784
+ if (d.unit) {
683
785
  warn(`${prop} "${v}" uses unit "${d.unit}", which is not converted; treated as ${d.n} ${coercedAs}`);
684
786
  }
685
787
  return d.n;
686
788
  };
687
- var worldLength = length(["px"], "world units");
789
+ var worldLength = length("px", "world units");
688
790
  var keyword2 = (...words) => (v) => words.includes(v) ? v : void 0;
689
791
  var isCurrentColor = (v) => v.toLowerCase() === "currentcolor";
690
792
  function readPaint(v) {
@@ -722,16 +824,17 @@ function readLinejoin(v, warn) {
722
824
  warn(`unsupported stroke-linejoin: ${v}`);
723
825
  return void 0;
724
826
  }
725
- function readDashArray(v, warn) {
827
+ function readDashArray(v, warn, _prop, fontSize) {
726
828
  if (v === "none" || v === "") return null;
727
829
  const nums = [];
728
830
  for (const t of v.split(/[\s,]+/).filter(Boolean)) {
729
831
  const d = dimension(t);
730
- if (!d || d.unit !== "" && d.unit !== "px" || d.n < 0) {
832
+ const scale = d && pxPer(d.unit, fontSize);
833
+ if (!d || scale == null || d.n < 0) {
731
834
  warn(`unrecognized stroke-dasharray: ${v}`);
732
835
  return void 0;
733
836
  }
734
- nums.push(d.n);
837
+ nums.push(d.n * scale);
735
838
  }
736
839
  return nums.length % 2 === 1 ? [...nums, ...nums] : nums;
737
840
  }
@@ -744,10 +847,14 @@ function readMiterLimit(v, warn) {
744
847
  function readColor(v) {
745
848
  return isCurrentColor(v) || parsePaintAttr(v)?.kind === "solid" ? v : void 0;
746
849
  }
747
- function readFontSize(v, warn, prop) {
850
+ function readFontSize(v, warn, prop, fontSize) {
851
+ const word = fontSizeKeyword(v);
852
+ if (word) return "size" in word ? { fontSize: word.size } : { fontScale: word.scale };
748
853
  const d = dimension(v);
749
854
  if (d?.unit === "%") return { fontScale: d.n / 100 };
750
- const n = worldLength(v, warn, prop);
855
+ const ems = d && emsPer(d.unit);
856
+ if (d && ems != null) return { fontScale: d.n * ems };
857
+ const n = worldLength(v, warn, prop, fontSize);
751
858
  return n == null ? void 0 : { fontSize: n };
752
859
  }
753
860
  function readFontWeight(v) {
@@ -763,8 +870,8 @@ function readFontStyle(v, warn) {
763
870
  }
764
871
  return void 0;
765
872
  }
766
- function readLetterSpacing(v, warn, prop) {
767
- return v === "normal" ? null : worldLength(v, warn, prop);
873
+ function readLetterSpacing(v, warn, prop, fontSize) {
874
+ return v === "normal" ? null : worldLength(v, warn, prop, fontSize);
768
875
  }
769
876
  function readDecoration(v, warn) {
770
877
  const out = {};
@@ -788,17 +895,22 @@ function readTextTransform(v) {
788
895
  const t = v.toLowerCase();
789
896
  return TEXT_TRANSFORMS.has(t) ? t : void 0;
790
897
  }
791
- var emLength = length(["em"], "em");
898
+ var emLength = length("em", "em");
792
899
  function readBaselineShift(v, warn, prop) {
793
900
  const k = v.toLowerCase();
794
901
  if (k === "super" || k === "sub") return { script: k };
795
902
  if (k === "baseline") return {};
796
903
  const d = dimension(v);
797
904
  if (d?.unit === "%") return d.n === 0 ? {} : { baselineShift: d.n / 100 };
798
- const n = emLength(v, warn, prop);
905
+ const n = emLength(v, warn, prop, DEFAULT_FONT_SIZE);
799
906
  if (n == null) return void 0;
800
907
  return n === 0 ? {} : { baselineShift: n };
801
908
  }
909
+ function readClipRef(v) {
910
+ if (v === "none") return "none";
911
+ const ref = /^url\(\s*["']?#([^)\s"']+)["']?\s*\)$/.exec(v);
912
+ return ref ? { id: ref[1] } : void 0;
913
+ }
802
914
  var inherited = (read) => ({ inherits: true, read });
803
915
  var own = (read) => ({ inherits: false, read });
804
916
  var PROPERTIES = {
@@ -830,16 +942,19 @@ var PROPERTIES = {
830
942
  "stop-color": own(readSolidColor),
831
943
  "stop-opacity": own(readRatio),
832
944
  "baseline-shift": own(readBaselineShift),
833
- "vector-effect": own(keyword2("none", "non-scaling-stroke"))
945
+ "vector-effect": own(keyword2("none", "non-scaling-stroke")),
946
+ "clip-path": own(readClipRef),
947
+ "display": own(keyword2("none", "inline")),
948
+ "visibility": inherited(keyword2("visible", "hidden", "collapse"))
834
949
  };
835
950
  var INHERITED_PROPERTIES = Object.keys(PROPERTIES).filter((p) => PROPERTIES[p].inherits);
836
951
  var silent = () => {
837
952
  };
838
- function readProperty(prop, raw, warn = silent) {
953
+ function readProperty(prop, raw, warn = silent, fontSize = DEFAULT_FONT_SIZE) {
839
954
  if (raw == null) return void 0;
840
955
  const v = raw.trim();
841
956
  if (v === "inherit") return void 0;
842
- return PROPERTIES[prop].read(v, warn, prop);
957
+ return PROPERTIES[prop].read(v, warn, prop, fontSize);
843
958
  }
844
959
  function isPropertyName(prop) {
845
960
  return Object.prototype.hasOwnProperty.call(PROPERTIES, prop);
@@ -873,74 +988,6 @@ function evaluateSupports(condition2, opts = {}) {
873
988
 
874
989
  // src/cascade.ts
875
990
  var EMPTY_STYLE = {};
876
- function identEnd(s, i) {
877
- while (i < s.length) {
878
- const ch = s[i];
879
- if (ch === "\\") i += 2;
880
- else if (/[\w-]/.test(ch) || ch.charCodeAt(0) > 127) i++;
881
- else break;
882
- }
883
- return i;
884
- }
885
- var LEGACY_PSEUDO_ELEMENTS = /* @__PURE__ */ new Set(["before", "after", "first-line", "first-letter"]);
886
- var MAX_ARG_PSEUDOS = /* @__PURE__ */ new Set(["not", "is", "matches", "-webkit-any", "has"]);
887
- function compareSpecificity(a, b) {
888
- return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
889
- }
890
- function maxSpecificity(list) {
891
- let best = [0, 0, 0];
892
- for (const sel of splitTopLevel(list, ",")) {
893
- const s = specificity(sel);
894
- if (compareSpecificity(s, best) > 0) best = s;
895
- }
896
- return best;
897
- }
898
- function specificity(selector) {
899
- let a = 0, b = 0, c = 0;
900
- const s = selector;
901
- let i = 0;
902
- while (i < s.length) {
903
- const ch = s[i];
904
- if (ch === "#") {
905
- a++;
906
- i = identEnd(s, i + 1);
907
- } else if (ch === ".") {
908
- b++;
909
- i = identEnd(s, i + 1);
910
- } else if (ch === "[") {
911
- b++;
912
- while (i < s.length && s[i] !== "]") {
913
- i = s[i] === '"' || s[i] === "'" ? skipString(s, i) : i + 1;
914
- }
915
- i++;
916
- } else if (ch === ":") {
917
- const element = s[i + 1] === ":";
918
- const nameStart = i + (element ? 2 : 1);
919
- const nameEnd = identEnd(s, nameStart);
920
- const name = s.slice(nameStart, nameEnd).toLowerCase();
921
- let arg = null;
922
- i = nameEnd;
923
- if (s[i] === "(") {
924
- const end = parenEnd(s, i);
925
- arg = s.slice(i + 1, end - 1);
926
- i = end;
927
- }
928
- if (element || LEGACY_PSEUDO_ELEMENTS.has(name)) c++;
929
- else if (name === "where") ; else if (MAX_ARG_PSEUDOS.has(name) && arg != null) {
930
- const m = maxSpecificity(arg);
931
- a += m[0];
932
- b += m[1];
933
- c += m[2];
934
- } else b++;
935
- } else if (/[a-zA-Z_\\-]/.test(ch) || ch.charCodeAt(0) > 127) {
936
- c++;
937
- i = identEnd(s, i);
938
- } else {
939
- i++;
940
- }
941
- }
942
- return [a, b, c];
943
- }
944
991
  var CascadeLayers = class {
945
992
  children = /* @__PURE__ */ new Map();
946
993
  anonymous = 0;
@@ -1029,6 +1076,11 @@ function parseRuleList(src, from, to, layer, ctx, rules, state) {
1029
1076
  if (names && names.length <= 1) {
1030
1077
  const inner = names.length ? state.layers.declare(layer, names[0]) : state.layers.declareAnonymous(layer);
1031
1078
  parseRuleList(src, i + 1, end - 1, inner, ctx, rules, state);
1079
+ } else if (name === "container") {
1080
+ const outer = state.containers;
1081
+ state.containers = [...outer ?? [], at[2]];
1082
+ parseRuleList(src, i + 1, end - 1, layer, ctx, rules, state);
1083
+ state.containers = outer;
1032
1084
  } else if (conditionHolds(name, at[2], ctx)) {
1033
1085
  parseRuleList(src, i + 1, end - 1, layer, ctx, rules, state);
1034
1086
  }
@@ -1037,7 +1089,15 @@ function parseRuleList(src, from, to, layer, ctx, rules, state) {
1037
1089
  for (const raw of splitTopLevel(prelude, ",")) {
1038
1090
  const selector = raw.trim();
1039
1091
  if (selector) {
1040
- rules.push({ selector, declarations, specificity: specificity(selector), order: state.order++, layer });
1092
+ const { containers } = state;
1093
+ rules.push({
1094
+ selector,
1095
+ declarations,
1096
+ specificity: specificity(selector),
1097
+ order: state.order++,
1098
+ layer,
1099
+ ...containers ? { containers } : {}
1100
+ });
1041
1101
  }
1042
1102
  }
1043
1103
  }
@@ -1057,15 +1117,23 @@ function appliesAsCss(style, ctx) {
1057
1117
  }
1058
1118
  var sheetCache = /* @__PURE__ */ new WeakMap();
1059
1119
  function bindStylesheets(doc, media, onWarn) {
1060
- sheetCache.set(doc, collectRules(doc, media, onWarn));
1120
+ sheetCache.set(doc, { rules: collectRules(doc, media, onWarn), media });
1061
1121
  }
1062
- function documentRules(doc) {
1122
+ function documentSheet(doc) {
1063
1123
  const cached = sheetCache.get(doc);
1064
1124
  if (cached) return cached;
1065
1125
  const root = doc.documentElement;
1066
- const rules = collectRules(doc, root ? mediaEnvironmentFor(root) : DEFAULT_MEDIA_ENVIRONMENT);
1067
- sheetCache.set(doc, rules);
1068
- return rules;
1126
+ const media = root ? mediaEnvironmentFor(root) : DEFAULT_MEDIA_ENVIRONMENT;
1127
+ const sheet = { rules: collectRules(doc, media), media };
1128
+ sheetCache.set(doc, sheet);
1129
+ return sheet;
1130
+ }
1131
+ function containersHold(el, containers, media) {
1132
+ const root = el.ownerDocument?.documentElement;
1133
+ if (!root || el === root) return false;
1134
+ const declared = authorDeclarations(root);
1135
+ const container = queryContainerOf((prop) => declared.get(prop), media);
1136
+ return containers.every((prelude) => containerQueryHolds(prelude, container));
1069
1137
  }
1070
1138
  function collectRules(doc, media, onWarn) {
1071
1139
  const ctx = { media, onWarn, selector: (s) => isValidSelector(doc, s) };
@@ -1103,8 +1171,8 @@ var elementCache = /* @__PURE__ */ new WeakMap();
1103
1171
  function authorDeclarations(el) {
1104
1172
  const cached = elementCache.get(el);
1105
1173
  if (cached) return cached;
1106
- const rules = el.ownerDocument ? documentRules(el.ownerDocument) : [];
1107
- const matched = rules.filter((r) => matches(el, r.selector));
1174
+ const sheet = el.ownerDocument && documentSheet(el.ownerDocument);
1175
+ const matched = (sheet ? sheet.rules : []).filter((r) => matches(el, r.selector) && (!r.containers || containersHold(el, r.containers, sheet.media)));
1108
1176
  const inline = parseDeclarations(el.getAttribute("style") ?? "");
1109
1177
  const out = /* @__PURE__ */ new Map();
1110
1178
  const importantOrder = [...matched].sort((x, y) => y.rank - x.rank || compareCascade(x, y));
@@ -1128,6 +1196,7 @@ function deriveStyle(parent, el) {
1128
1196
  if (!next) next = { ...parent };
1129
1197
  next[prop] = own2;
1130
1198
  }
1199
+ if (next) computeFontRelative(next, fontSizeOf(parent));
1131
1200
  return next ?? parent;
1132
1201
  }
1133
1202
  function resolveCurrentColor(raw, style) {
@@ -1136,18 +1205,86 @@ function resolveCurrentColor(raw, style) {
1136
1205
  const color = readProperty("color", style["color"]);
1137
1206
  return color == null || color.toLowerCase() === "currentcolor" ? "#000000" : color;
1138
1207
  }
1139
- function ownValue(el, prop, warn) {
1140
- return readProperty(prop, ownProp(el, prop), warn);
1208
+ function ownValue(el, prop, warn, fontSize) {
1209
+ return readProperty(prop, ownProp(el, prop), warn, fontSize);
1141
1210
  }
1142
1211
  function styleValue(style, prop, warn) {
1143
1212
  return readProperty(prop, style[prop], warn);
1144
1213
  }
1145
1214
 
1146
- // src/elements.ts
1147
- function collectElementsByTag(root, tags) {
1148
- const out = /* @__PURE__ */ new Map();
1149
- const consider = (el) => {
1150
- if (!tags.has(el.tagName.toLowerCase())) return;
1215
+ // src/xmlEscape.ts
1216
+ var LINE_BREAK_CONTROLS = /[\u000B\u000C]/g;
1217
+ var NOT_XML_CHAR = /[\u0000-\u0008\u000E-\u001F￾￿]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
1218
+ function xmlChars(s) {
1219
+ return s.replace(LINE_BREAK_CONTROLS, "\n").replace(NOT_XML_CHAR, "");
1220
+ }
1221
+ function escapeAttr(s) {
1222
+ return xmlChars(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/\t/g, "&#9;").replace(/\n/g, "&#10;").replace(/\r/g, "&#13;");
1223
+ }
1224
+ function escapeText(s) {
1225
+ return xmlChars(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\r/g, "&#13;");
1226
+ }
1227
+
1228
+ // src/styleAt.ts
1229
+ function styleAt(el) {
1230
+ const chain = [];
1231
+ for (let e = el; e; e = e.parentElement) chain.unshift(e);
1232
+ return chain.reduce((style, e) => deriveStyle(style, e), EMPTY_STYLE);
1233
+ }
1234
+
1235
+ // src/lengths.ts
1236
+ var LENGTH = /^\s*([-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?)\s*([a-z%]*)\s*$/i;
1237
+ function viewportOf(el) {
1238
+ for (let e = el.parentElement; e; e = e.parentElement) {
1239
+ if (e.tagName.toLowerCase() !== "svg") continue;
1240
+ const box = (e.getAttribute("viewBox") ?? "").trim().split(/[\s,]+/).map(Number);
1241
+ if (box.length === 4 && box.every(Number.isFinite)) return { width: box[2], height: box[3] };
1242
+ const w = LENGTH.exec(e.getAttribute("width") ?? "");
1243
+ const h = LENGTH.exec(e.getAttribute("height") ?? "");
1244
+ if (w && h && w[2] !== "%" && h[2] !== "%") return { width: userUnits(w), height: userUnits(h) };
1245
+ return void 0;
1246
+ }
1247
+ return void 0;
1248
+ }
1249
+ function userUnits(m) {
1250
+ return Number(m[1]) * (ABSOLUTE_PX[m[2].toLowerCase()] ?? 1);
1251
+ }
1252
+ function readLength(el, name, axis, fallback, onWarn) {
1253
+ return resolveLength(el.getAttribute(name), el, name, axis, fallback, onWarn);
1254
+ }
1255
+ function resolveLength(raw, el, name, axis, fallback, onWarn) {
1256
+ if (raw == null) return fallback;
1257
+ const m = LENGTH.exec(raw);
1258
+ if (!m) {
1259
+ const n2 = parseFloat(raw);
1260
+ return Number.isFinite(n2) ? n2 : fallback;
1261
+ }
1262
+ const n = Number(m[1]);
1263
+ const unit = m[2].toLowerCase();
1264
+ if (unit === "") return n;
1265
+ const scale = ABSOLUTE_PX[unit];
1266
+ if (scale != null) return n * scale;
1267
+ const ems = emsPer(unit);
1268
+ if (ems != null) return n * ems * fontSizeOf(styleAt(el));
1269
+ const tag = el.tagName.toLowerCase();
1270
+ if (unit !== "%") {
1271
+ onWarn(`<${tag} ${name}="${raw}"> unit "${unit}" is not converted; read as ${n}`);
1272
+ return n;
1273
+ }
1274
+ const view = viewportOf(el);
1275
+ if (!view) {
1276
+ onWarn(`<${tag} ${name}="${raw}"> has no viewport to be a percentage of; read as ${n}`);
1277
+ return n;
1278
+ }
1279
+ const of = axis === "x" ? view.width : axis === "y" ? view.height : Math.sqrt((view.width * view.width + view.height * view.height) / 2);
1280
+ return n / 100 * of;
1281
+ }
1282
+
1283
+ // src/elements.ts
1284
+ function collectElementsByTag(root, tags) {
1285
+ const out = /* @__PURE__ */ new Map();
1286
+ const consider = (el) => {
1287
+ if (!tags.has(el.tagName.toLowerCase())) return;
1151
1288
  const id = el.getAttribute("id");
1152
1289
  if (id && !out.has(id)) out.set(id, el);
1153
1290
  };
@@ -1158,15 +1295,12 @@ function collectElementsByTag(root, tags) {
1158
1295
  }
1159
1296
 
1160
1297
  // src/patterns.ts
1161
- function patternSpecOf(paint) {
1162
- return "tile" in paint.pattern ? paint.pattern : null;
1163
- }
1164
1298
  function patternXml(id, paint, onWarn) {
1165
- const spec = patternSpecOf(paint);
1166
- if (!spec) {
1167
- onWarn?.("pattern fill carries a TextureHandle, which has no vector form \u2014 omitted from <defs>");
1299
+ if (!isTilePaint(paint)) {
1300
+ onWarn?.("texture fill carries a TextureHandle, which has no vector form \u2014 omitted from <defs>");
1168
1301
  return "";
1169
1302
  }
1303
+ const spec = tileSpecOf(paint);
1170
1304
  const geometry = tileGeometry(spec);
1171
1305
  const body = geometry.shapes.map(shapeXml).join("");
1172
1306
  const origin = paint.origin ?? { x: 0, y: 0 };
@@ -1189,7 +1323,7 @@ function patternXml(id, paint, onWarn) {
1189
1323
  function tilePreviewSvg(spec, background) {
1190
1324
  const geometry = tileGeometry(spec);
1191
1325
  const s = trimNumber(geometry.size);
1192
- const bg = background ? `<rect width="${s}" height="${s}" fill="${background}"/>` : "";
1326
+ const bg = background ? `<rect width="${s}" height="${s}" fill="${escapeAttr(background)}"/>` : "";
1193
1327
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 ${s} ${s}">${bg}${geometry.shapes.map(shapeXml).join("")}</svg>`;
1194
1328
  }
1195
1329
  function tilePreviewCssUrl(spec, background) {
@@ -1200,14 +1334,14 @@ function shapeXml(shape) {
1200
1334
  case "line":
1201
1335
  return `<line x1="${trimNumber(shape.x1)}" y1="${trimNumber(shape.y1)}" x2="${trimNumber(shape.x2)}" y2="${trimNumber(shape.y2)}" stroke="${shape.color}" stroke-width="${trimNumber(shape.width)}"/>`;
1202
1336
  case "circle":
1203
- return `<circle cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" r="${trimNumber(shape.r)}" fill="${shape.color}"/>`;
1337
+ return `<circle cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" r="${trimNumber(shape.r)}" fill="${escapeAttr(shape.color)}"/>`;
1204
1338
  case "ellipse": {
1205
1339
  const deg = shape.rotation * 180 / Math.PI;
1206
1340
  const transform = deg === 0 ? "" : ` transform="rotate(${trimNumber(deg)} ${trimNumber(shape.cx)} ${trimNumber(shape.cy)})"`;
1207
- return `<ellipse cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" rx="${trimNumber(shape.rx)}" ry="${trimNumber(shape.ry)}" fill="${shape.color}"${transform}/>`;
1341
+ return `<ellipse cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" rx="${trimNumber(shape.rx)}" ry="${trimNumber(shape.ry)}" fill="${escapeAttr(shape.color)}"${transform}/>`;
1208
1342
  }
1209
1343
  case "rect":
1210
- return `<rect x="${trimNumber(shape.x)}" y="${trimNumber(shape.y)}" width="${trimNumber(shape.width)}" height="${trimNumber(shape.height)}" fill="${shape.color}"/>`;
1344
+ return `<rect x="${trimNumber(shape.x)}" y="${trimNumber(shape.y)}" width="${trimNumber(shape.width)}" height="${trimNumber(shape.height)}" fill="${escapeAttr(shape.color)}"/>`;
1211
1345
  default:
1212
1346
  return "";
1213
1347
  }
@@ -1234,7 +1368,7 @@ function readPattern(el, onWarn) {
1234
1368
  onWarn?.(`<pattern id="${el.getAttribute("id")}"> has malformed data-weasel-tile \u2014 dropped`);
1235
1369
  return null;
1236
1370
  }
1237
- if (typeof spec?.tile !== "string" || typeof spec?.color !== "string") {
1371
+ if (!isTileKind(spec?.kind) || typeof spec?.color !== "string") {
1238
1372
  onWarn?.(`<pattern id="${el.getAttribute("id")}"> data-weasel-tile is not a tile spec \u2014 dropped`);
1239
1373
  return null;
1240
1374
  }
@@ -1242,14 +1376,11 @@ function readPattern(el, onWarn) {
1242
1376
  const y = parseFloat(el.getAttribute("y") ?? "0");
1243
1377
  const m = parseTransform(el.getAttribute("patternTransform"), onWarn);
1244
1378
  const origin = applyMatrix(m, x, y);
1245
- const paint = { fill: "pattern", pattern: spec };
1379
+ const paint = { ...spec };
1246
1380
  if (origin.x !== 0 || origin.y !== 0) paint.origin = origin;
1247
1381
  if (!isIdentity([m[0], m[1], m[2], m[3], 0, 0])) paint.transform = [m[0], m[1], m[2], m[3]];
1248
1382
  return paint;
1249
1383
  }
1250
- function escapeAttr(s) {
1251
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1252
- }
1253
1384
  function serializePathD(path) {
1254
1385
  if (path.kind === "rect") {
1255
1386
  const { x, y, width: width2, height: height2 } = path;
@@ -1288,10 +1419,10 @@ function serializePathD(path) {
1288
1419
  var WEASEL_NS = "urn:weasel-js:svg";
1289
1420
  var WEASEL_NS_PREFIX = "wzl";
1290
1421
  function nativeSvgKind(kind) {
1291
- return kind === "solid" || kind === "linear-gradient" || kind === "radial-gradient" || kind === "pattern";
1422
+ return kind === "solid" || kind === "linear-gradient" || kind === "radial-gradient" || kind === "texture" || isTileKind(kind);
1292
1423
  }
1293
1424
  function hasNativeSvgForm(paint) {
1294
- return nativeSvgKind(paint.fill ?? "solid");
1425
+ return nativeSvgKind(paint.kind ?? "solid");
1295
1426
  }
1296
1427
  var INTERPOLATE_SPACES = /* @__PURE__ */ new Set(["rgb", "oklab", "oklch"]);
1297
1428
  function nativeSvgSpace(space) {
@@ -1383,7 +1514,7 @@ function readStops(el, onWarn) {
1383
1514
  const colorAttr = ownProp(c, "stop-color") ?? "#000000";
1384
1515
  const parsed = readProperty("stop-color", colorAttr, onWarn);
1385
1516
  if (parsed) {
1386
- const alpha = ownValue(c, "stop-opacity") ?? parsed.alpha;
1517
+ const alpha = (ownValue(c, "stop-opacity") ?? 1) * parsed.alpha;
1387
1518
  stops.push({ offset, color: alpha < 1 ? applyAlpha(parsed.color, alpha) : parsed.color });
1388
1519
  } else {
1389
1520
  onWarn?.(`gradient stop has unrecognized stop-color: ${colorAttr}`);
@@ -1410,41 +1541,61 @@ function num(raw, fallback) {
1410
1541
  const n = parseRatio(raw);
1411
1542
  return Number.isFinite(n) ? n : fallback;
1412
1543
  }
1544
+ function warnUnmodeled(el, elements, onWarn) {
1545
+ for (const name of ["fx", "fy", "fr"]) {
1546
+ if (inheritedAttr(el, elements, name) != null) {
1547
+ onWarn?.(`gradient ${name} is not modeled; the gradient is centered on its cx, cy`);
1548
+ break;
1549
+ }
1550
+ }
1551
+ const spread = inheritedAttr(el, elements, "spreadMethod");
1552
+ if (spread === "reflect" || spread === "repeat") {
1553
+ onWarn?.(`spreadMethod="${spread}" is not modeled; the gradient pads`);
1554
+ }
1555
+ }
1556
+ function coord(el, elements, name, axis, fraction, units, onWarn) {
1557
+ const raw = inheritedAttr(el, elements, name);
1558
+ if (units === "bounds") return num(raw, fraction);
1559
+ if (raw != null) return resolveLength(raw, el, name, axis, fraction, onWarn ?? (() => {
1560
+ }));
1561
+ let unresolved = false;
1562
+ const v = resolveLength(`${fraction * 100}%`, el, name, axis, fraction, () => {
1563
+ unresolved = true;
1564
+ });
1565
+ return unresolved ? fraction : v;
1566
+ }
1413
1567
  function readLinearGradient(el, elements, onWarn) {
1414
1568
  warnGradientTransform(el, elements, onWarn);
1569
+ warnUnmodeled(el, elements, onWarn);
1570
+ const units = readGradientUnits(el, elements);
1571
+ const at = (name, axis, fraction) => coord(el, elements, name, axis, fraction, units, onWarn);
1415
1572
  return {
1416
- fill: "linear-gradient",
1417
- from: {
1418
- x: num(inheritedAttr(el, elements, "x1"), 0),
1419
- y: num(inheritedAttr(el, elements, "y1"), 0)
1420
- },
1421
- to: {
1422
- x: num(inheritedAttr(el, elements, "x2"), 1),
1423
- y: num(inheritedAttr(el, elements, "y2"), 0)
1424
- },
1573
+ kind: "linear-gradient",
1574
+ from: { x: at("x1", "x", 0), y: at("y1", "y", 0) },
1575
+ to: { x: at("x2", "x", 1), y: at("y2", "y", 0) },
1425
1576
  stops: inheritedStops(el, elements, onWarn),
1426
- units: readGradientUnits(el, elements),
1577
+ units,
1427
1578
  interpolate: readInterpolate(el, elements)
1428
1579
  };
1429
1580
  }
1430
1581
  function readRadialGradient(el, elements, onWarn) {
1431
1582
  warnGradientTransform(el, elements, onWarn);
1583
+ warnUnmodeled(el, elements, onWarn);
1584
+ const units = readGradientUnits(el, elements);
1585
+ const at = (name, axis, fraction) => coord(el, elements, name, axis, fraction, units, onWarn);
1432
1586
  return {
1433
- fill: "radial-gradient",
1434
- center: {
1435
- x: num(inheritedAttr(el, elements, "cx"), 0.5),
1436
- y: num(inheritedAttr(el, elements, "cy"), 0.5)
1437
- },
1438
- radius: num(inheritedAttr(el, elements, "r"), 0.5),
1587
+ kind: "radial-gradient",
1588
+ center: { x: at("cx", "x", 0.5), y: at("cy", "y", 0.5) },
1589
+ radius: at("r", "other", 0.5),
1439
1590
  stops: inheritedStops(el, elements, onWarn),
1440
- units: readGradientUnits(el, elements),
1591
+ units,
1441
1592
  interpolate: readInterpolate(el, elements)
1442
1593
  };
1443
1594
  }
1444
1595
  function readConicGradient(el, elements, onWarn) {
1445
1596
  warnGradientTransform(el, elements, onWarn);
1446
1597
  return {
1447
- fill: "conic-gradient",
1598
+ kind: "conic-gradient",
1448
1599
  center: {
1449
1600
  x: num(inheritedAttr(el, elements, "cx"), 0.5),
1450
1601
  y: num(inheritedAttr(el, elements, "cy"), 0.5)
@@ -1475,7 +1626,7 @@ function readMeshGradient(el, onWarn) {
1475
1626
  if (patches.length === 0) return null;
1476
1627
  const space = el.getAttribute("interpolate") ?? el.getAttributeNS(WEASEL_NS, "interpolate");
1477
1628
  return {
1478
- fill: "mesh-gradient",
1629
+ kind: "mesh-gradient",
1479
1630
  patches,
1480
1631
  units: el.getAttribute("gradientUnits") === "objectBoundingBox" ? "bounds" : "world",
1481
1632
  ...space === "oklab" || space === "oklch" ? { interpolate: space } : {}
@@ -1499,7 +1650,7 @@ var PaintServerRegistry = class {
1499
1650
  register(paint) {
1500
1651
  const existing = this.byPaint.get(paint);
1501
1652
  if (existing) return existing;
1502
- const id = paint.fill === "pattern" ? `pat${this.counter++}` : `grad${this.counter++}`;
1653
+ const id = isPatternPaint(paint) ? `pat${this.counter++}` : `grad${this.counter++}`;
1503
1654
  this.byPaint.set(paint, id);
1504
1655
  this.order.push(paint);
1505
1656
  return id;
@@ -1518,7 +1669,7 @@ var PaintServerRegistry = class {
1518
1669
  }
1519
1670
  /** The paint kind of each registered paint, once, in first-use order. */
1520
1671
  paintKinds() {
1521
- return [...new Set(this.order.map((paint) => paint.fill ?? "solid"))];
1672
+ return [...new Set(this.order.map((paint) => paint.kind ?? "solid"))];
1522
1673
  }
1523
1674
  /** Whether any registered paint serializes outside SVG's own vocabulary, so
1524
1675
  * the root has to declare {@link WEASEL_NS}. */
@@ -1579,56 +1730,57 @@ function writesInset(entry) {
1579
1730
  return entry !== void 0 && !entry.toSvg && (entry.inset ?? 0) !== 0;
1580
1731
  }
1581
1732
  function defaultMarkerXml(id, entry, onWarn, key, size) {
1582
- const unit = size === void 0 ? 1 : resolveMarkerSize({ key: key ?? id, size }, 1);
1583
- const path = entry.path({ size: unit, stroke: { paint: { fill: "solid", color: "#000" } } });
1733
+ const name = key ?? id;
1734
+ const unit = size === void 0 ? 1 : resolveMarkerSize({ key: name, size }, 1);
1735
+ const path = entry.path({ size: unit, stroke: { paint: { kind: "solid", color: "#000" } } });
1584
1736
  const d = serializePathD(path);
1585
1737
  const fill = markerPaintAttrs("fill", entry.fill, id, onWarn);
1586
1738
  const outline = entry.outline ? ` ${markerPaintAttrs("stroke", entry.outline.paint, id, onWarn)} stroke-width="${trimNumber(entry.outline.width * unit)}" stroke-linecap="round" stroke-linejoin="round"` : "";
1587
1739
  const orient = typeof entry.orient === "number" ? trimNumber(entry.orient * 180 / Math.PI) : "auto-start-reverse";
1588
- const units = size === void 0 ? 'markerUnits="strokeWidth" markerWidth="8" markerHeight="8"' : `markerUnits="userSpaceOnUse" markerWidth="${trimNumber(8 * unit)}" markerHeight="${trimNumber(8 * unit)}" ${WEASEL_NS_PREFIX}:key="${key}" ${WEASEL_NS_PREFIX}:size="${typeof size === "number" ? trimNumber(size) : `${trimNumber(size.px)}px`}"`;
1740
+ const units = size === void 0 ? 'markerUnits="strokeWidth" markerWidth="8" markerHeight="8"' : `markerUnits="userSpaceOnUse" markerWidth="${trimNumber(8 * unit)}" markerHeight="${trimNumber(8 * unit)}" ${WEASEL_NS_PREFIX}:key="${escapeAttr(name)}" ${WEASEL_NS_PREFIX}:size="${typeof size === "number" ? trimNumber(size) : `${trimNumber(size.px)}px`}"`;
1589
1741
  const inset = writesInset(entry) ? ` ${WEASEL_NS_PREFIX}:inset="${trimNumber(entry.inset)}"` : "";
1590
1742
  return `<marker id="${id}" ${units}${inset} refX="0" refY="0" orient="${orient}" overflow="visible"><path d="${d}" ${fill}${outline}/></marker>`;
1591
1743
  }
1592
1744
  function markerPaintAttrs(attr, paint, id, onWarn) {
1593
1745
  if (paint === "none") return `${attr}="none"`;
1594
1746
  if (paint === void 0 || paint === "line") return `${attr}="context-stroke"`;
1595
- if ("color" in paint && (paint.fill === void 0 || paint.fill === "solid")) {
1747
+ if ("color" in paint && (paint.kind === void 0 || paint.kind === "solid")) {
1596
1748
  return paint.opacity != null && paint.opacity !== 1 ? `${attr}="${paint.color}" ${attr}-opacity="${trimNumber(paint.opacity)}"` : `${attr}="${paint.color}"`;
1597
1749
  }
1598
1750
  onWarn?.(`marker "${id}" ${attr} is a paint server, which a <marker> def cannot reference here; written as the line's paint`);
1599
1751
  return `${attr}="context-stroke"`;
1600
1752
  }
1601
1753
  function paintServerXml(id, paint, onWarn) {
1602
- if (paint.fill === "pattern") return patternXml(id, paint, onWarn);
1754
+ if (isPatternPaint(paint)) return patternXml(id, paint, onWarn);
1603
1755
  const builtin = gradientXml(id, paint);
1604
1756
  if (builtin) return builtin;
1605
- const kind = getPaintKind(paint.fill);
1757
+ const kind = getPaintKind(paint.kind);
1606
1758
  const custom = kind?.toSvg?.(id, paint);
1607
1759
  if (custom) return custom;
1608
- onWarn?.(kind ? `${paint.fill} fill has no vector form \u2014 omitted from <defs>` : `${paint.fill} fill is not registered \u2014 omitted from <defs>; a kind loaded on demand needs \`await warmSvg(nodes)\` before a synchronous export`);
1760
+ onWarn?.(kind ? `${paint.kind} fill has no vector form \u2014 omitted from <defs>` : `${paint.kind} fill is not registered \u2014 omitted from <defs>; a kind loaded on demand needs \`await warmSvg(nodes)\` before a synchronous export`);
1609
1761
  return "";
1610
1762
  }
1611
1763
  function gradientUnitsAttr(units) {
1612
1764
  return units === "bounds" ? "objectBoundingBox" : "userSpaceOnUse";
1613
1765
  }
1614
1766
  function gradientXml(id, paint) {
1615
- if (paint.fill === "linear-gradient") {
1767
+ if (paint.kind === "linear-gradient") {
1616
1768
  const stops = paint.stops.map(stopXml).join("");
1617
1769
  return `<linearGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" x1="${trimNumber(paint.from.x)}" y1="${trimNumber(paint.from.y)}" x2="${trimNumber(paint.to.x)}" y2="${trimNumber(paint.to.y)}"${interpolateAttr(paint.interpolate)}>${stops}</linearGradient>`;
1618
1770
  }
1619
- if (paint.fill === "conic-gradient") {
1771
+ if (paint.kind === "conic-gradient") {
1620
1772
  const stops = paint.stops.map(stopXml).join("");
1621
1773
  const P = WEASEL_NS_PREFIX;
1622
1774
  return `<${P}:conicGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" angle="${trimNumber(paint.angle)}"${interpolateAttr(paint.interpolate)}>${stops}</${P}:conicGradient>`;
1623
1775
  }
1624
- if (paint.fill === "radial-gradient") {
1776
+ if (paint.kind === "radial-gradient") {
1625
1777
  const stops = paint.stops.map(stopXml).join("");
1626
1778
  return `<radialGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" r="${trimNumber(paint.radius)}"${interpolateAttr(paint.interpolate)}>${stops}</radialGradient>`;
1627
1779
  }
1628
1780
  return "";
1629
1781
  }
1630
1782
  function paintFallback(paint) {
1631
- const color = getPaintKind(paint.fill)?.colorOf(paint);
1783
+ const color = getPaintKind(paint.kind)?.colorOf(paint);
1632
1784
  if (!color) return "none";
1633
1785
  const packed = /^#([0-9a-f]{6})[0-9a-f]{2}$/i.exec(color);
1634
1786
  return packed ? `#${packed[1].toLowerCase()}` : color;
@@ -1639,10 +1791,81 @@ function stopXml(s) {
1639
1791
  const alpha = parseInt(m[2], 16) / 255;
1640
1792
  return `<stop offset="${trimNumber(s.offset)}" stop-color="#${m[1].toLowerCase()}" stop-opacity="${trimNumber(alpha)}"/>`;
1641
1793
  }
1642
- return `<stop offset="${trimNumber(s.offset)}" stop-color="${s.color}"/>`;
1794
+ return `<stop offset="${trimNumber(s.offset)}" stop-color="${escapeAttr(s.color)}"/>`;
1795
+ }
1796
+ var KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
1797
+ function rectElementToPath(x, y, width2, height2, rx, ry, m = IDENTITY_MATRIX) {
1798
+ if (rx <= 0 && ry <= 0) {
1799
+ return transformPath({ kind: "rect", x, y, width: width2, height: height2 }, m);
1800
+ }
1801
+ const r = Math.min(Math.max(rx, ry), Math.min(width2, height2) / 2);
1802
+ const r2 = Math.min(rx > 0 ? rx : r, ry > 0 ? ry : r, Math.min(width2, height2) / 2);
1803
+ const rad = r2;
1804
+ const k = KAPPA * rad;
1805
+ const x2 = x + width2;
1806
+ const y2 = y + height2;
1807
+ const b = new PathBuilder();
1808
+ b.moveTo(x + rad, y);
1809
+ b.lineTo(x2 - rad, y);
1810
+ b.curveTo(x2 - rad + k, y, x2, y + rad - k, x2, y + rad);
1811
+ b.lineTo(x2, y2 - rad);
1812
+ b.curveTo(x2, y2 - rad + k, x2 - rad + k, y2, x2 - rad, y2);
1813
+ b.lineTo(x + rad, y2);
1814
+ b.curveTo(x + rad - k, y2, x, y2 - rad + k, x, y2 - rad);
1815
+ b.lineTo(x, y + rad);
1816
+ b.curveTo(x, y + rad - k, x + rad - k, y, x + rad, y);
1817
+ b.close();
1818
+ return transformPath(b.build(), m);
1819
+ }
1820
+ function ellipseToPath(cx, cy, rx, ry, m = IDENTITY_MATRIX) {
1821
+ const kx = KAPPA * rx;
1822
+ const ky = KAPPA * ry;
1823
+ const b = new PathBuilder();
1824
+ b.moveTo(cx + rx, cy);
1825
+ b.curveTo(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry);
1826
+ b.curveTo(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy);
1827
+ b.curveTo(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry);
1828
+ b.curveTo(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy);
1829
+ b.close();
1830
+ return transformPath(b.build(), m);
1831
+ }
1832
+ function circleToPath(cx, cy, r, m = IDENTITY_MATRIX) {
1833
+ return ellipseToPath(cx, cy, r, r, m);
1834
+ }
1835
+ function lineToPath(x1, y1, x2, y2, m = IDENTITY_MATRIX) {
1836
+ const b = new PathBuilder();
1837
+ b.moveTo(x1, y1);
1838
+ b.lineTo(x2, y2);
1839
+ return transformPath(b.build(), m);
1840
+ }
1841
+ function parsePoints(raw) {
1842
+ const out = [];
1843
+ const nums = raw.match(/[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g);
1844
+ if (!nums) return out;
1845
+ for (let i = 0; i + 1 < nums.length; i += 2) {
1846
+ out.push({ x: parseFloat(nums[i]), y: parseFloat(nums[i + 1]) });
1847
+ }
1848
+ return out;
1849
+ }
1850
+ function polylineToPath(points, m = IDENTITY_MATRIX) {
1851
+ if (points.length === 0) {
1852
+ return { kind: "polygon", commands: new Uint8Array(), coords: new Float32Array(), fillRule: "nonzero" };
1853
+ }
1854
+ const b = new PathBuilder();
1855
+ b.moveTo(points[0].x, points[0].y);
1856
+ for (let i = 1; i < points.length; i++) b.lineTo(points[i].x, points[i].y);
1857
+ return transformPath(b.build(), m);
1858
+ }
1859
+ function polygonToPath(points, m = IDENTITY_MATRIX) {
1860
+ if (points.length === 0) {
1861
+ return { kind: "polygon", commands: new Uint8Array(), coords: new Float32Array(), fillRule: "nonzero" };
1862
+ }
1863
+ const b = new PathBuilder();
1864
+ b.moveTo(points[0].x, points[0].y);
1865
+ for (let i = 1; i < points.length; i++) b.lineTo(points[i].x, points[i].y);
1866
+ b.close();
1867
+ return transformPath(b.build(), m);
1643
1868
  }
1644
-
1645
- // src/parse.ts
1646
1869
  var SUPPORTED_LEAF_TAGS = /* @__PURE__ */ new Set([
1647
1870
  "rect",
1648
1871
  "circle",
@@ -1652,7 +1875,6 @@ var SUPPORTED_LEAF_TAGS = /* @__PURE__ */ new Set([
1652
1875
  "polygon",
1653
1876
  "path"
1654
1877
  ]);
1655
- var SUPPORTED_GROUP_TAGS = /* @__PURE__ */ new Set(["g", "svg"]);
1656
1878
  var IGNORED_TAGS = /* @__PURE__ */ new Set([
1657
1879
  "defs",
1658
1880
  "lineargradient",
@@ -1665,310 +1887,17 @@ var IGNORED_TAGS = /* @__PURE__ */ new Set([
1665
1887
  "metadata",
1666
1888
  "style"
1667
1889
  ]);
1668
- function parseSvg(svg, opts = {}) {
1669
- const warnings = [];
1670
- const onWarn = (m) => {
1671
- warnings.push(m);
1672
- };
1673
- let doc;
1674
- try {
1675
- doc = new DOMParser().parseFromString(svg, "image/svg+xml");
1676
- } catch (e) {
1677
- return { nodes: [], warnings: [`failed to parse SVG: ${e.message}`] };
1678
- }
1679
- const errEl = doc.getElementsByTagName("parsererror")[0];
1680
- if (errEl) {
1681
- return { nodes: [], warnings: [`SVG parse error: ${errEl.textContent ?? "unknown"}`] };
1682
- }
1683
- const root = doc.documentElement;
1684
- if (!root || root.tagName.toLowerCase() !== "svg") {
1685
- return { nodes: [], warnings: ["root element is not <svg>"] };
1686
- }
1687
- bindStylesheets(doc, mediaEnvironmentFor(root, opts.media), onWarn);
1688
- const namespaces = opts.namespaces ?? {};
1689
- const uriToPrefix = /* @__PURE__ */ new Map();
1690
- for (const [prefix, uri] of Object.entries(namespaces)) {
1691
- uriToPrefix.set(uri, prefix);
1692
- }
1693
- const documentMeta = collectDocumentMeta(root, uriToPrefix);
1694
- const gradients = collectGradients(root, onWarn);
1695
- for (const [id, paint] of collectPatterns(root, onWarn)) gradients.set(id, paint);
1696
- const rootStyle = deriveStyle(EMPTY_STYLE, root);
1697
- const clips = collectClipPaths(root, onWarn);
1698
- const nodes = parseChildren(root, IDENTITY_MATRIX, rootStyle, gradients, clips, onWarn, uriToPrefix);
1699
- const markers = ingestMarkers(root, nodes, gradients, onWarn);
1700
- const result = { nodes, warnings };
1701
- if (markers.length > 0) result.markers = markers;
1702
- if (documentMeta) result.documentMeta = documentMeta;
1703
- const viewBox = parseViewBoxAttr(root.getAttribute("viewBox"));
1704
- if (viewBox) result.viewBox = viewBox;
1705
- const width2 = parseRootLength(root.getAttribute("width"), "width", onWarn);
1706
- if (width2 != null) result.width = width2;
1707
- const height2 = parseRootLength(root.getAttribute("height"), "height", onWarn);
1708
- if (height2 != null) result.height = height2;
1709
- for (let i = 0; i < root.children.length; i++) {
1710
- const c = root.children[i];
1711
- if (c.tagName.toLowerCase() === "title") {
1712
- result.title = c.textContent ?? "";
1713
- break;
1714
- }
1715
- }
1716
- return result;
1717
- }
1718
- function parseViewBoxAttr(raw) {
1719
- if (raw == null) return void 0;
1720
- const parts = raw.trim().split(/[\s,]+/).filter(Boolean);
1721
- if (parts.length !== 4) return void 0;
1722
- const nums = parts.map(parseFloat);
1723
- if (nums.some((n) => !Number.isFinite(n))) return void 0;
1724
- return { x: nums[0], y: nums[1], width: nums[2], height: nums[3] };
1725
- }
1726
- function parseRootLength(raw, name, onWarn) {
1727
- if (raw == null) return void 0;
1728
- const n = parseFloat(raw);
1729
- if (!Number.isFinite(n)) return void 0;
1730
- const unit = /^\s*[-+]?[\d.eE+-]+\s*([a-z%]+)\s*$/i.exec(raw)?.[1];
1731
- if (unit) onWarn(`<svg ${name}="${raw}"> unit "${unit}" is not converted; read as ${n}`);
1732
- return n;
1733
- }
1734
- function collectDocumentMeta(root, uriToPrefix) {
1735
- if (uriToPrefix.size === 0) return void 0;
1736
- const meta = {};
1737
- for (let i = 0; i < root.attributes.length; i++) {
1738
- const a = root.attributes[i];
1739
- if (!a.namespaceURI) continue;
1740
- const prefix = uriToPrefix.get(a.namespaceURI);
1741
- if (!prefix) continue;
1742
- const bucket = meta[prefix] ??= {};
1743
- (bucket.attrs ??= {})[a.localName] = a.value;
1744
- }
1745
- for (let i = 0; i < root.children.length; i++) {
1746
- const c = root.children[i];
1747
- if (!c.namespaceURI) continue;
1748
- const prefix = uriToPrefix.get(c.namespaceURI);
1749
- if (!prefix) continue;
1750
- const bucket = meta[prefix] ??= {};
1751
- const elements = bucket.elements ??= {};
1752
- const list = elements[c.localName] ??= [];
1753
- list.push(collectNamespacedElement(c, uriToPrefix));
1754
- }
1755
- return Object.keys(meta).length > 0 ? meta : void 0;
1756
- }
1757
- function collectNamespacedElement(el, uriToPrefix) {
1758
- const result = { attrs: {} };
1759
- for (let i = 0; i < el.attributes.length; i++) {
1760
- const a = el.attributes[i];
1761
- if (a.name.startsWith("xmlns")) continue;
1762
- result.attrs[a.localName] = a.value;
1763
- }
1764
- let hasChildElements = false;
1765
- for (let i = 0; i < el.children.length; i++) {
1766
- const c = el.children[i];
1767
- if (!c.namespaceURI) continue;
1768
- const prefix = uriToPrefix.get(c.namespaceURI);
1769
- if (!prefix) continue;
1770
- hasChildElements = true;
1771
- const children = result.children ??= {};
1772
- const list = children[c.localName] ??= [];
1773
- list.push(collectNamespacedElement(c, uriToPrefix));
1774
- }
1775
- if (!hasChildElements && el.textContent != null && el.textContent.trim() !== "") {
1776
- result.text = el.textContent;
1777
- }
1778
- return result;
1779
- }
1780
- function collectElementMeta(el, uriToPrefix) {
1781
- if (uriToPrefix.size === 0) return void 0;
1782
- const meta = {};
1783
- for (let i = 0; i < el.attributes.length; i++) {
1784
- const a = el.attributes[i];
1785
- if (!a.namespaceURI) continue;
1786
- const prefix = uriToPrefix.get(a.namespaceURI);
1787
- if (!prefix) continue;
1788
- const bucket = meta[prefix] ??= {};
1789
- (bucket.attrs ??= {})[a.localName] = a.value;
1790
- }
1791
- for (let i = 0; i < el.children.length; i++) {
1792
- const c = el.children[i];
1793
- if (!c.namespaceURI) continue;
1794
- const prefix = uriToPrefix.get(c.namespaceURI);
1795
- if (!prefix) continue;
1796
- const bucket = meta[prefix] ??= {};
1797
- const elements = bucket.elements ??= {};
1798
- const list = elements[c.localName] ??= [];
1799
- list.push(collectNamespacedElement(c, uriToPrefix));
1800
- }
1801
- return Object.keys(meta).length > 0 ? meta : void 0;
1802
- }
1803
- function collectClipPaths(root, onWarn) {
1804
- const out = /* @__PURE__ */ new Map();
1805
- for (const [id, el] of collectElementsByTag(root, /* @__PURE__ */ new Set(["clippath"]))) {
1806
- const units = el.getAttribute("clipPathUnits");
1807
- if (units && units !== "userSpaceOnUse") {
1808
- onWarn(`<clipPath id="${id}"> uses clipPathUnits="${units}", which is not modeled`);
1809
- continue;
1810
- }
1811
- const shapes = [];
1812
- for (let i = 0; i < el.children.length; i++) {
1813
- if (SUPPORTED_LEAF_TAGS.has(el.children[i].tagName.toLowerCase())) shapes.push(el.children[i]);
1814
- }
1815
- if (shapes.length !== 1) {
1816
- onWarn(
1817
- `<clipPath id="${id}"> holds ${shapes.length} shapes the kit can lower; only a single-shape clip is modeled, so it clips nothing`
1818
- );
1819
- continue;
1820
- }
1821
- const shape = shapes[0];
1822
- const local = parseTransform(shape.getAttribute("transform"), onWarn);
1823
- const path = lowerLeaf(shape, shape.tagName.toLowerCase(), local, onWarn);
1824
- if (path) out.set(id, path);
1825
- }
1826
- return out;
1827
- }
1828
- function clipFor(el, ctm, clips, onWarn) {
1829
- const attr = el.getAttribute("clip-path");
1830
- if (!attr || attr === "none") return void 0;
1831
- const ref = /^url\(\s*#([^)\s]+)\s*\)$/.exec(attr.trim());
1832
- if (!ref) {
1833
- onWarn(`clip-path="${attr}" is not a url(#id) reference; the clip is dropped`);
1834
- return void 0;
1835
- }
1836
- const path = clips.get(ref[1]);
1837
- if (!path) {
1838
- onWarn(`clip-path references #${ref[1]}, which no <clipPath> in the document defines`);
1839
- return void 0;
1840
- }
1841
- return transformPath(path, ctm);
1842
- }
1843
- function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix) {
1844
- const out = [];
1845
- for (let i = 0; i < parent.children.length; i++) {
1846
- const el = parent.children[i];
1847
- const ns = el.namespaceURI;
1848
- if (ns && ns !== "http://www.w3.org/2000/svg") continue;
1849
- const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix);
1850
- if (node) {
1851
- if (Array.isArray(node)) out.push(...node);
1852
- else out.push(node);
1853
- }
1854
- }
1855
- return out;
1856
- }
1857
- function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
1858
- const tag = el.tagName.toLowerCase();
1859
- if (IGNORED_TAGS.has(tag)) return null;
1860
- if (tag === "g" && el.hasAttribute("data-weasel-image")) {
1861
- const image = parseCroppedImage(el, ctm, onWarn);
1862
- if (image) {
1863
- const meta2 = collectElementMeta(el, uriToPrefix);
1864
- if (meta2) image.meta = meta2;
1865
- return image;
1866
- }
1867
- }
1868
- if (tag === "g") {
1869
- const local = parseTransform(el.getAttribute("transform"), onWarn);
1870
- const childCtm = multiply(ctm, local);
1871
- const childStyle = deriveStyle(style, el);
1872
- const children = parseChildren(el, childCtm, childStyle, gradients, clips, onWarn, uriToPrefix);
1873
- const opacity2 = ownValue(el, "opacity");
1874
- const group = { kind: "group", children };
1875
- const clip = clipFor(el, childCtm, clips, onWarn);
1876
- if (clip) group.clip = clip;
1877
- if (opacity2 != null) group.opacity = opacity2;
1878
- const meta2 = collectElementMeta(el, uriToPrefix);
1879
- if (meta2) group.meta = meta2;
1880
- return group;
1881
- }
1882
- if (SUPPORTED_GROUP_TAGS.has(tag)) {
1883
- const childStyle = deriveStyle(style, el);
1884
- const x = parseFloat(el.getAttribute("x") ?? "0");
1885
- const y = parseFloat(el.getAttribute("y") ?? "0");
1886
- const offset = [1, 0, 0, 1, Number.isFinite(x) ? x : 0, Number.isFinite(y) ? y : 0];
1887
- if (el.hasAttribute("viewBox")) {
1888
- onWarn("nested <svg> viewBox is not modeled; its children are not rescaled");
1889
- }
1890
- return parseChildren(el, multiply(ctm, offset), childStyle, gradients, clips, onWarn, uriToPrefix);
1891
- }
1892
- if (tag === "text") {
1893
- const textNode = parseTextElement(el, ctm, style, gradients, onWarn);
1894
- if (textNode && !Array.isArray(textNode) && textNode.kind === "text") {
1895
- const meta2 = collectElementMeta(el, uriToPrefix);
1896
- if (meta2) textNode.meta = meta2;
1897
- }
1898
- return textNode;
1899
- }
1900
- if (tag === "image") {
1901
- const imageNode = parseImageElement(el, ctm, onWarn);
1902
- if (imageNode) {
1903
- const meta2 = collectElementMeta(el, uriToPrefix);
1904
- if (meta2) imageNode.meta = meta2;
1905
- }
1906
- return imageNode;
1907
- }
1908
- if (!SUPPORTED_LEAF_TAGS.has(tag)) {
1909
- onWarn(`unsupported element: <${el.tagName}>`);
1910
- return null;
1911
- }
1912
- const localTransform = parseTransform(el.getAttribute("transform"), onWarn);
1913
- let path = lowerLeaf(el, tag, ctm, onWarn);
1914
- if (!path) return null;
1915
- let rotation;
1916
- if (!isIdentity(localTransform)) {
1917
- const worldLocal = rebaseTransform(ctm, localTransform);
1918
- const bounds = pathAabb(path);
1919
- const cx = bounds.x + bounds.width / 2;
1920
- const cy = bounds.y + bounds.height / 2;
1921
- const angle = worldLocal && decomposeRotation(worldLocal, cx, cy);
1922
- if (!worldLocal) {
1923
- onWarn("leaf transform sits under a singular parent transform; dropped");
1924
- } else if (angle != null) {
1925
- rotation = angle;
1926
- } else {
1927
- path = transformPath(path, worldLocal);
1928
- const rotComp = rotationComponent(worldLocal);
1929
- if (Math.abs(rotComp) > 1e-4) {
1930
- onWarn(`leaf transform has a rotational component that can't be cleanly stored as rotation; baked into geometry (may not round-trip identically)`);
1931
- }
1932
- }
1933
- }
1934
- const leafStyle = deriveStyle(style, el);
1935
- const fill = readPaint2(leafStyle, "fill", "#000000", gradients, onWarn);
1936
- const stroke = readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el));
1937
- const opacity = ownValue(el, "opacity");
1938
- if (styleValue(leafStyle, "fill-rule") === "evenodd" && path.kind === "polygon") {
1939
- path = { ...path, fillRule: "evenodd" };
1940
- }
1941
- const node = { kind: "path", path, fill };
1942
- if (stroke) node.stroke = stroke;
1943
- if (opacity != null) node.opacity = opacity;
1944
- if (rotation != null) node.rotation = rotation;
1945
- if (tag === "line" && (leafStyle["fill"] ?? null) == null) {
1946
- node.fill = { kind: "none" };
1947
- }
1948
- const meta = collectElementMeta(el, uriToPrefix);
1949
- if (meta) node.meta = meta;
1950
- return node;
1951
- }
1952
1890
  function lowerLeaf(el, tag, m, onWarn) {
1953
- const num2 = (name, fallback = 0) => {
1954
- const v = el.getAttribute(name);
1955
- if (v == null) return fallback;
1956
- const n = parseFloat(v);
1957
- return Number.isFinite(n) ? n : fallback;
1958
- };
1891
+ const x = (name) => readLength(el, name, "x", 0, onWarn);
1892
+ const y = (name) => readLength(el, name, "y", 0, onWarn);
1893
+ const r = (name) => readLength(el, name, "other", 0, onWarn);
1959
1894
  if (tag === "rect") {
1960
- const x = num2("x", 0);
1961
- const y = num2("y", 0);
1962
- const w = num2("width", 0);
1963
- const h = num2("height", 0);
1964
- const rx = num2("rx", 0);
1965
- const ry = num2("ry", 0);
1966
- return rectElementToPath(x, y, w, h, rx, ry, m);
1967
- }
1968
- if (tag === "circle") return circleToPath(num2("cx"), num2("cy"), num2("r"), m);
1969
- if (tag === "ellipse") return ellipseToPath(num2("cx"), num2("cy"), num2("rx"), num2("ry"), m);
1970
- if (tag === "line") return lineToPath(num2("x1"), num2("y1"), num2("x2"), num2("y2"), m);
1971
- if (tag === "polyline") return polylineToPath(parsePoints(el.getAttribute("points") ?? ""), m);
1895
+ return rectElementToPath(x("x"), y("y"), x("width"), y("height"), x("rx"), y("ry"), m);
1896
+ }
1897
+ if (tag === "circle") return circleToPath(x("cx"), y("cy"), r("r"), m);
1898
+ if (tag === "ellipse") return ellipseToPath(x("cx"), y("cy"), x("rx"), y("ry"), m);
1899
+ if (tag === "line") return lineToPath(x("x1"), y("y1"), x("x2"), y("y2"), m);
1900
+ if (tag === "polyline") return svgFilled(polylineToPath(parsePoints(el.getAttribute("points") ?? ""), m));
1972
1901
  if (tag === "polygon") return polygonToPath(parsePoints(el.getAttribute("points") ?? ""), m);
1973
1902
  if (tag === "path") {
1974
1903
  const d = el.getAttribute("d") ?? "";
@@ -1982,10 +1911,13 @@ function lowerLeaf(el, tag, m, onWarn) {
1982
1911
  const rect = polygonToRectIfAxisAligned(transformed);
1983
1912
  if (rect) return rect;
1984
1913
  }
1985
- return transformed;
1914
+ return svgFilled(transformed);
1986
1915
  }
1987
1916
  return null;
1988
1917
  }
1918
+ function svgFilled(path) {
1919
+ return hasOpenSubpath(path) ? { ...path, openFill: "close" } : path;
1920
+ }
1989
1921
  function polygonToRectIfAxisAligned(p) {
1990
1922
  const cmds = p.commands;
1991
1923
  if (cmds.length !== 5) return null;
@@ -2006,12 +1938,59 @@ function polygonToRectIfAxisAligned(p) {
2006
1938
  if (width2 === 0 || height2 === 0) return null;
2007
1939
  return { kind: "rect", x, y, width: width2, height: height2 };
2008
1940
  }
2009
- function pathAabb(path) {
2010
- if (path.kind === "rect") {
2011
- return { x: path.x, y: path.y, width: path.width, height: path.height };
1941
+
1942
+ // src/viewBox.ts
1943
+ function parseViewBoxAttr(raw) {
1944
+ if (raw == null) return void 0;
1945
+ const parts = raw.trim().split(/[\s,]+/).filter(Boolean);
1946
+ if (parts.length !== 4) return void 0;
1947
+ const nums = parts.map(parseFloat);
1948
+ if (nums.some((n) => !Number.isFinite(n))) return void 0;
1949
+ return { x: nums[0], y: nums[1], width: nums[2], height: nums[3] };
1950
+ }
1951
+ function collectClipPaths(root, onWarn) {
1952
+ const out = /* @__PURE__ */ new Map();
1953
+ for (const [id, el] of collectElementsByTag(root, /* @__PURE__ */ new Set(["clippath"]))) {
1954
+ const units = el.getAttribute("clipPathUnits");
1955
+ if (units && units !== "userSpaceOnUse") {
1956
+ onWarn(`<clipPath id="${id}"> uses clipPathUnits="${units}", which is not modeled`);
1957
+ continue;
1958
+ }
1959
+ const shapes = [];
1960
+ for (let i = 0; i < el.children.length; i++) {
1961
+ if (SUPPORTED_LEAF_TAGS.has(el.children[i].tagName.toLowerCase())) shapes.push(el.children[i]);
1962
+ }
1963
+ if (shapes.length !== 1) {
1964
+ onWarn(
1965
+ `<clipPath id="${id}"> holds ${shapes.length} shapes the kit can lower; only a single-shape clip is modeled, so it clips nothing`
1966
+ );
1967
+ continue;
1968
+ }
1969
+ const shape = shapes[0];
1970
+ const local = parseTransform(shape.getAttribute("transform"), onWarn);
1971
+ const path = lowerLeaf(shape, shape.tagName.toLowerCase(), local, onWarn);
1972
+ if (path) out.set(id, path);
1973
+ }
1974
+ return out;
1975
+ }
1976
+ function clipFor(el, ctm, clips, onWarn) {
1977
+ const raw = ownProp(el, "clip-path");
1978
+ if (raw == null) return void 0;
1979
+ const ref = readProperty("clip-path", raw);
1980
+ if (ref === "none") return void 0;
1981
+ if (!ref) {
1982
+ onWarn(`clip-path="${raw}" is not a url(#id) reference; the clip is dropped`);
1983
+ return void 0;
2012
1984
  }
2013
- return boundsOfPath(path);
1985
+ const path = clips.get(ref.id);
1986
+ if (!path) {
1987
+ onWarn(`clip-path references #${ref.id}, which no <clipPath> in the document defines`);
1988
+ return void 0;
1989
+ }
1990
+ return transformPath(path, ctm);
2014
1991
  }
1992
+
1993
+ // src/readPaint.ts
2015
1994
  function readPaint2(style, attr, defaultColor, gradients, onWarn) {
2016
1995
  const raw = resolveCurrentColor(style[attr] ?? null, style);
2017
1996
  const opacity = styleValue(style, `${attr}-opacity`, onWarn);
@@ -2089,11 +2068,11 @@ var STROKE_KEYS = [
2089
2068
  function isNonScalingStroke(el) {
2090
2069
  return ownValue(el, "vector-effect") === "non-scaling-stroke";
2091
2070
  }
2092
- function ownStrokeStyle(el) {
2071
+ function ownStrokeStyle(el, computed) {
2093
2072
  const out = {};
2094
2073
  for (const k of STROKE_KEYS) {
2095
2074
  const v = ownProp(el, k);
2096
- if (v != null) out[k] = v;
2075
+ if (v != null) out[k] = v === "inherit" ? v : computed[k];
2097
2076
  }
2098
2077
  return out;
2099
2078
  }
@@ -2103,7 +2082,7 @@ function coreStroke(stroke) {
2103
2082
  if (stroke.paint.kind === "gradient") {
2104
2083
  paint = stroke.paint.paint;
2105
2084
  } else if (stroke.paint.kind === "solid") {
2106
- paint = stroke.paint.opacity != null ? { fill: "solid", color: stroke.paint.color, opacity: stroke.paint.opacity } : { fill: "solid", color: stroke.paint.color };
2085
+ paint = stroke.paint.opacity != null ? { kind: "solid", color: stroke.paint.color, opacity: stroke.paint.opacity } : { kind: "solid", color: stroke.paint.color };
2107
2086
  } else {
2108
2087
  return void 0;
2109
2088
  }
@@ -2117,6 +2096,92 @@ function coreStroke(stroke) {
2117
2096
  if (stroke.markerEnd) out.markerEnd = stroke.markerEnd;
2118
2097
  return out;
2119
2098
  }
2099
+ var near = (v, target) => Math.abs(v - target) <= 1e-9;
2100
+ function placeBox(box, m, stretch) {
2101
+ const [a, b, c, d] = m;
2102
+ const scale = Math.sqrt(Math.abs(a * d - b * c));
2103
+ const eps = 1e-4 * Math.hypot(a, b, c, d);
2104
+ const uniform = Math.abs(a - d) <= eps;
2105
+ if (Math.abs(b) <= eps && Math.abs(c) <= eps && (stretch || uniform && a > 0)) {
2106
+ const p0 = applyMatrix(m, box.x, box.y);
2107
+ const p1 = applyMatrix(m, box.x + box.width, box.y + box.height);
2108
+ return {
2109
+ x: Math.min(p0.x, p1.x),
2110
+ y: Math.min(p0.y, p1.y),
2111
+ width: Math.abs(p1.x - p0.x),
2112
+ height: Math.abs(p1.y - p0.y),
2113
+ rotation: 0,
2114
+ scale,
2115
+ flipX: a < 0,
2116
+ flipY: d < 0
2117
+ };
2118
+ }
2119
+ const p = Math.hypot(a, b);
2120
+ const q = (a * c + b * d) / p;
2121
+ const r = (a * d - b * c) / p;
2122
+ const settle = (v) => Math.abs(v - 1) <= 1e-5 ? 1 : v;
2123
+ const sx = settle(stretch ? p : scale);
2124
+ const sy = settle(stretch ? Math.abs(r) : scale);
2125
+ const flipY = stretch && r < 0;
2126
+ const ry = flipY ? -sy : sy;
2127
+ const linear = [p / sx, 0, q / ry, r / ry];
2128
+ const center = applyMatrix(m, box.x + box.width / 2, box.y + box.height / 2);
2129
+ const width2 = box.width * sx;
2130
+ const height2 = box.height * sy;
2131
+ const placed = {
2132
+ x: center.x - width2 / 2,
2133
+ y: center.y - height2 / 2,
2134
+ width: width2,
2135
+ height: height2,
2136
+ rotation: Math.atan2(b, a),
2137
+ scale: settle(scale),
2138
+ flipX: false,
2139
+ flipY
2140
+ };
2141
+ if (!near(linear[0], 1) || !near(linear[2], 0) || !near(linear[3], 1)) placed.transform = linear;
2142
+ return placed;
2143
+ }
2144
+ function placeNode(node, tag, m, stretch, onWarn) {
2145
+ if (isIdentity(m)) return 1;
2146
+ const cx = node.x + node.width / 2;
2147
+ const cy = node.y + node.height / 2;
2148
+ if (!invert(m)) {
2149
+ onWarn(`<${tag}> sits under a singular transform; kept its size`);
2150
+ const center = applyMatrix(m, cx, cy);
2151
+ node.x = center.x - node.width / 2;
2152
+ node.y = center.y - node.height / 2;
2153
+ return 1;
2154
+ }
2155
+ const turn = decomposeRotation(m, cx, cy);
2156
+ if (turn != null) {
2157
+ node.rotation = turn;
2158
+ return 1;
2159
+ }
2160
+ const placed = placeBox(node, m, stretch);
2161
+ node.x = placed.x;
2162
+ node.y = placed.y;
2163
+ node.width = placed.width;
2164
+ node.height = placed.height;
2165
+ if (Math.abs(placed.rotation) > 1e-9) node.rotation = placed.rotation;
2166
+ if (placed.flipX) node.flipX = !node.flipX;
2167
+ if (placed.flipY) node.flipY = !node.flipY;
2168
+ if (placed.transform) node.transform = placed.transform;
2169
+ return placed.scale;
2170
+ }
2171
+ function boxTransformAttr(node) {
2172
+ if (poseLinearOf(node)) {
2173
+ const frame = poseFrameOf(node);
2174
+ const m = frame && formatMatrix(frame);
2175
+ return m ? `transform="${m}"` : null;
2176
+ }
2177
+ if (!node.rotation) return null;
2178
+ const cx = node.x + node.width / 2;
2179
+ const cy = node.y + node.height / 2;
2180
+ const deg = node.rotation * 180 / Math.PI;
2181
+ return `transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`;
2182
+ }
2183
+
2184
+ // src/readImage.ts
2120
2185
  var XLINK_NS2 = "http://www.w3.org/1999/xlink";
2121
2186
  function parseImageElement(el, ctm, onWarn) {
2122
2187
  const href = imageHref(el);
@@ -2134,45 +2199,15 @@ function imageHref(el) {
2134
2199
  return el.getAttribute("href") ?? el.getAttributeNS(XLINK_NS2, "href") ?? el.getAttribute("xlink:href");
2135
2200
  }
2136
2201
  function imageNodeAt(boxEl, ownerEl, href, ctm, onWarn) {
2137
- const num2 = (raw, fallback) => {
2138
- if (raw == null) return fallback;
2139
- const n = parseFloat(raw);
2140
- return Number.isFinite(n) ? n : fallback;
2141
- };
2142
- const rawX = num2(boxEl.getAttribute("x"), 0);
2143
- const rawY = num2(boxEl.getAttribute("y"), 0);
2144
- const rawW = num2(boxEl.getAttribute("width"), 0);
2145
- const rawH = num2(boxEl.getAttribute("height"), 0);
2146
- const px = (x, y) => [ctm[0] * x + ctm[2] * y + ctm[4], ctm[1] * x + ctm[3] * y + ctm[5]];
2147
- const [x0, y0] = px(rawX, rawY);
2148
- const [x1, y1] = px(rawX + rawW, rawY + rawH);
2149
- const node = {
2150
- kind: "image",
2151
- href,
2152
- x: Math.min(x0, x1),
2153
- y: Math.min(y0, y1),
2154
- width: Math.abs(x1 - x0),
2155
- height: Math.abs(y1 - y0)
2156
- };
2202
+ const rawX = readLength(boxEl, "x", "x", 0, onWarn);
2203
+ const rawY = readLength(boxEl, "y", "y", 0, onWarn);
2204
+ const rawW = readLength(boxEl, "width", "x", 0, onWarn);
2205
+ const rawH = readLength(boxEl, "height", "y", 0, onWarn);
2206
+ const node = { kind: "image", href, x: rawX, y: rawY, width: rawW, height: rawH };
2157
2207
  const opacity = ownValue(ownerEl, "opacity");
2158
2208
  if (opacity != null) node.opacity = opacity;
2159
2209
  const localTransform = parseTransform(ownerEl.getAttribute("transform"), onWarn);
2160
- if (!isIdentity(localTransform)) {
2161
- const worldLocal = rebaseTransform(ctm, localTransform);
2162
- const cx = node.x + node.width / 2;
2163
- const cy = node.y + node.height / 2;
2164
- const angle = worldLocal && decomposeRotation(worldLocal, cx, cy);
2165
- if (!worldLocal) {
2166
- onWarn("<image> transform sits under a singular parent transform; dropped");
2167
- } else if (angle != null) {
2168
- node.rotation = angle;
2169
- } else {
2170
- const rotComp = rotationComponent(worldLocal);
2171
- if (Math.abs(rotComp) > 1e-4) {
2172
- onWarn("<image> transform has a rotational component that can't be cleanly stored as rotation; dropped (may not round-trip identically)");
2173
- }
2174
- }
2175
- }
2210
+ placeNode(node, "image", multiply(ctm, localTransform), true, onWarn);
2176
2211
  return node;
2177
2212
  }
2178
2213
  function parseCroppedImage(g, ctm, onWarn) {
@@ -2186,10 +2221,102 @@ function parseCroppedImage(g, ctm, onWarn) {
2186
2221
  node.source = source;
2187
2222
  }
2188
2223
  const flip = parseTransform(img.getAttribute("transform"), onWarn);
2189
- if (flip[0] < 0) node.flipX = true;
2190
- if (flip[3] < 0) node.flipY = true;
2224
+ if (flip[0] < 0) toggle(node, "flipX");
2225
+ if (flip[3] < 0) toggle(node, "flipY");
2191
2226
  return node;
2192
2227
  }
2228
+ function toggle(node, key) {
2229
+ if (node[key]) delete node[key];
2230
+ else node[key] = true;
2231
+ }
2232
+ function svgDirection(style, text) {
2233
+ return paragraphDirection(text, style?.direction ?? "auto");
2234
+ }
2235
+ function anchorOffset(style, width2, text) {
2236
+ const edge = resolveAlign(style?.align ?? "left", svgDirection(style, text));
2237
+ return edge === "center" ? width2 / 2 : edge === "right" ? width2 : 0;
2238
+ }
2239
+
2240
+ // src/tspanLines.ts
2241
+ var DEFAULT_LINE_HEIGHT = 1.2;
2242
+ var LineTracker = class {
2243
+ constructor(text, onWarn) {
2244
+ this.onWarn = onWarn;
2245
+ this.y = text.getAttribute("y");
2246
+ this.at = readLength(text, "y", "y", 0, onWarn);
2247
+ }
2248
+ onWarn;
2249
+ y;
2250
+ /** Where the current line sits, in user units. */
2251
+ at;
2252
+ pitches = [];
2253
+ /** Whether `tspan` opens a new line. Call once per tspan, in order.
2254
+ * `afterText` is whether any text precedes it: the first line's own
2255
+ * placement is not a pitch. */
2256
+ startsLine(tspan, afterText) {
2257
+ const y = tspan.getAttribute("y");
2258
+ const movedY = y != null && y !== this.y;
2259
+ if (y != null) this.y = y;
2260
+ const from = this.at;
2261
+ const dy = readLength(tspan, "dy", "y", 0, this.onWarn);
2262
+ this.at = (y != null ? readLength(tspan, "y", "y", from, this.onWarn) : from) + dy;
2263
+ if (!tspan.hasAttribute("x")) return false;
2264
+ const starts = movedY || dy !== 0;
2265
+ if (starts && afterText && this.at > from) this.pitches.push(this.at - from);
2266
+ return starts;
2267
+ }
2268
+ /**
2269
+ * The line height the lines were pitched at, as a multiple of `fontSize`:
2270
+ * `undefined` for a single line, or for lines at the default pitch. The
2271
+ * model holds one line height, so uneven pitches give their mean, which
2272
+ * keeps the block's height.
2273
+ */
2274
+ lineHeight(fontSize) {
2275
+ const { pitches } = this;
2276
+ if (pitches.length === 0 || !(fontSize > 0)) return void 0;
2277
+ const mean = pitches.reduce((sum, p) => sum + p, 0) / pitches.length;
2278
+ if (pitches.some((p) => Math.abs(p - mean) > 1e-3 * fontSize)) {
2279
+ this.onWarn("<text> lines are pitched unevenly; they take the mean pitch as one line height");
2280
+ }
2281
+ const lineHeight = mean / fontSize;
2282
+ return Math.abs(lineHeight - DEFAULT_LINE_HEIGHT) < 1e-6 ? void 0 : lineHeight;
2283
+ }
2284
+ };
2285
+ function warnUnreadPositions(el, onWarn) {
2286
+ const tag = el.tagName.toLowerCase();
2287
+ for (const name of ["x", "y"]) {
2288
+ const raw = el.getAttribute(name)?.trim() ?? "";
2289
+ if (/[\s,]/.test(raw)) onWarn(`<${tag} ${name}="${raw}"> positions each glyph; only the first value is read`);
2290
+ }
2291
+ for (const name of ["dx", "rotate"]) {
2292
+ const raw = el.getAttribute(name)?.trim() ?? "";
2293
+ if (raw.split(/[\s,]+/).some((v) => parseFloat(v) !== 0 && v !== "")) {
2294
+ onWarn(`<${tag} ${name}="${raw}"> is not read; the glyphs keep their own advance and angle`);
2295
+ }
2296
+ }
2297
+ }
2298
+ function joinLines(lines) {
2299
+ const out = [];
2300
+ let breaks = 0;
2301
+ for (const line of lines) {
2302
+ if (line.length > 0) {
2303
+ if (breaks > 0) line[0] = { ...line[0], text: "\n".repeat(breaks) + line[0].text };
2304
+ out.push(...line);
2305
+ breaks = 0;
2306
+ }
2307
+ if (out.length > 0) breaks++;
2308
+ }
2309
+ return out;
2310
+ }
2311
+
2312
+ // src/fillPaint.ts
2313
+ function mapFillPaint(fill, f) {
2314
+ if (!fill.paint) return fill;
2315
+ const paint = f(fill.paint);
2316
+ return paint === fill.paint ? fill : { ...fill, paint };
2317
+ }
2318
+
2319
+ // src/readText.ts
2193
2320
  var XML_NS = "http://www.w3.org/XML/1998/namespace";
2194
2321
  function preservesSpace(el) {
2195
2322
  for (let e = el; e; e = e.parentElement) {
@@ -2233,12 +2360,39 @@ function laidOutWidth(runs, style, plain) {
2233
2360
  maxWidth: Infinity,
2234
2361
  lineHeight: resolved.lineHeight,
2235
2362
  align: resolved.align,
2236
- direction: resolved.direction
2363
+ direction: resolved.direction,
2364
+ tabSize: resolved.tabSize
2237
2365
  });
2238
2366
  if (bounds.width > 0) return bounds.width;
2239
2367
  const longest = plain.split("\n").reduce((max, line) => Math.max(max, line.length), 0);
2240
2368
  return longest * resolved.fontSize * ESTIMATED_GLYPH_ADVANCE_EM;
2241
2369
  }
2370
+ function placeTextNode(node, m, onWarn) {
2371
+ const cx = node.x + node.width / 2;
2372
+ const cy = node.y + node.height / 2;
2373
+ const scale = placeNode(node, "text", m, false, onWarn);
2374
+ const inBox = [
2375
+ scale,
2376
+ 0,
2377
+ 0,
2378
+ scale,
2379
+ node.x + node.width / 2 - scale * cx,
2380
+ node.y + node.height / 2 - scale * cy
2381
+ ];
2382
+ if (isIdentity(inBox)) return;
2383
+ if (Math.abs(scale - 1) > 1e-9) {
2384
+ node.style = scaleTextStyle(node.style, scale);
2385
+ if (node.runs) node.runs = scaleRuns(node.runs, scale);
2386
+ if (node.stroke) node.stroke = scaleStroke(node.stroke, scale);
2387
+ }
2388
+ if (node.fill) node.fill = mapFillPaint(node.fill, (p) => fillThroughMatrix(p, inBox));
2389
+ if (node.stroke?.paint) {
2390
+ node.stroke = { ...node.stroke, paint: fillThroughMatrix(node.stroke.paint, inBox) };
2391
+ }
2392
+ if (node.runs) {
2393
+ node.runs = node.runs.map((r) => r.fill ? { ...r, fill: mapFillPaint(r.fill, (p) => fillThroughMatrix(p, inBox)) } : r);
2394
+ }
2395
+ }
2242
2396
  function parseTextElement(el, ctm, style, gradients, onWarn) {
2243
2397
  const num2 = (raw, fallback) => {
2244
2398
  if (raw == null) return fallback;
@@ -2246,41 +2400,48 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2246
2400
  return Number.isFinite(n) ? n : fallback;
2247
2401
  };
2248
2402
  const localTransform = parseTransform(el.getAttribute("transform"), onWarn);
2249
- const m = ctm;
2250
- const rawX = num2(el.getAttribute("x"), 0);
2251
- const rawY = num2(el.getAttribute("y"), 0);
2252
- const ax = m[0] * rawX + m[2] * rawY + m[4];
2253
- const ay = m[1] * rawX + m[3] * rawY + m[5];
2403
+ warnUnreadPositions(el, onWarn);
2404
+ const ax = readLength(el, "x", "x", 0, onWarn);
2405
+ const ay = readLength(el, "y", "y", 0, onWarn);
2254
2406
  const leafStyle = deriveStyle(style, el);
2255
2407
  const textStyle = readTextStyle(leafStyle, onWarn);
2256
2408
  const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
2257
2409
  const fontSize = resolveScreenLength(textStyle.fontSize ?? 16, 1);
2258
- const lineHeight = textStyle.lineHeight ?? 1.2;
2259
2410
  const dominantBaseline = el.getAttribute("dominant-baseline");
2260
2411
  const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
2261
2412
  const topY = explicitTopAnchor ? ay : ay - fontSize;
2262
2413
  const inheritedShift = ownValue(el, "baseline-shift", onWarn) ?? {};
2263
- const runs = [];
2414
+ const runLines = [[]];
2415
+ const tracker = new LineTracker(el, onWarn);
2416
+ const push = (run) => {
2417
+ runLines[runLines.length - 1].push(run);
2418
+ };
2264
2419
  for (let i = 0; i < el.childNodes.length; i++) {
2265
2420
  const child = el.childNodes[i];
2266
2421
  if (child.nodeType === 3) {
2267
2422
  const t = child.textContent ?? "";
2268
2423
  if (!t) continue;
2269
- runs.push({ text: t, ...inheritedShift });
2424
+ push({ text: t, ...inheritedShift });
2270
2425
  } else if (child.nodeType === 1) {
2271
2426
  const sp = child;
2272
2427
  if (sp.tagName.toLowerCase() !== "tspan") {
2273
2428
  onWarn(`<text> child <${sp.tagName}> not supported; flattening text content`);
2274
2429
  const t = sp.textContent ?? "";
2275
- if (t) runs.push({ text: t, ...inheritedShift });
2430
+ if (t) push({ text: t, ...inheritedShift });
2276
2431
  continue;
2277
2432
  }
2278
- const run = readTspanRun(sp, gradients, leafStyle, onWarn);
2279
- runs.push(run);
2433
+ warnUnreadPositions(sp, onWarn);
2434
+ const afterText = runLines.some((line) => line.some((r) => r.text.trim() !== ""));
2435
+ if (tracker.startsLine(sp, afterText)) runLines.push([]);
2436
+ push(readTspanRun(sp, gradients, leafStyle, onWarn));
2280
2437
  }
2281
2438
  }
2282
- if (!preservesSpace(el)) collapseRunWhitespace(runs);
2439
+ if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
2440
+ const runs = joinLines(runLines);
2283
2441
  const plain = runs.map((r) => r.text).join("");
2442
+ const pitched = tracker.lineHeight(fontSize);
2443
+ if (pitched !== void 0) textStyle.lineHeight = pitched;
2444
+ const lineHeight = textStyle.lineHeight ?? DEFAULT_LINE_HEIGHT;
2284
2445
  const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
2285
2446
  const dataH = num2(el.getAttribute("data-weasel-height"), NaN);
2286
2447
  const width2 = Number.isFinite(dataW) ? dataW : laidOutWidth(runs, textStyle, plain);
@@ -2289,7 +2450,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2289
2450
  const opacity = ownValue(el, "opacity");
2290
2451
  const node = {
2291
2452
  kind: "text",
2292
- x: ax - anchorOffset(textStyle, width2),
2453
+ x: ax - anchorOffset(textStyle, width2, plain),
2293
2454
  y: topY,
2294
2455
  width: width2,
2295
2456
  height: height2,
@@ -2299,30 +2460,20 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2299
2460
  (r) => Object.entries(r).some(([k, v]) => k !== "text" && v !== void 0)
2300
2461
  );
2301
2462
  if (hasStyling) node.runs = runs;
2463
+ if (textStyle.direction === void 0 && !el.hasAttribute("data-weasel-width")) {
2464
+ textStyle.direction = "ltr";
2465
+ }
2302
2466
  if (el.getAttribute("data-weasel-wrap") === "true") textStyle.wrap = true;
2303
2467
  if (el.getAttribute("data-weasel-align") === "justify") textStyle.align = "justify";
2468
+ const tabSize = Number(el.getAttribute("data-weasel-tab-size") ?? NaN);
2469
+ if (Number.isFinite(tabSize)) textStyle.tabSize = tabSize;
2304
2470
  const va = el.getAttribute("data-weasel-vertical-align");
2305
2471
  if (va === "center" || va === "bottom") node.verticalAlign = va;
2306
2472
  if (Object.keys(textStyle).length > 0) node.style = textStyle;
2307
2473
  if (textPaint.fill !== void 0) node.fill = textPaint.fill;
2308
2474
  if (textPaint.stroke != null) node.stroke = textPaint.stroke;
2309
2475
  if (opacity != null) node.opacity = opacity;
2310
- if (!isIdentity(localTransform)) {
2311
- const worldLocal = rebaseTransform(ctm, localTransform);
2312
- const cx = node.x + node.width / 2;
2313
- const cy = node.y + node.height / 2;
2314
- const angle = worldLocal && decomposeRotation(worldLocal, cx, cy);
2315
- if (!worldLocal) {
2316
- onWarn("<text> transform sits under a singular parent transform; dropped");
2317
- } else if (angle != null) {
2318
- node.rotation = angle;
2319
- } else {
2320
- const rotComp = rotationComponent(worldLocal);
2321
- if (Math.abs(rotComp) > 1e-4) {
2322
- onWarn(`<text> transform has a rotational component that can't be cleanly stored as rotation; dropped (may not round-trip identically)`);
2323
- }
2324
- }
2325
- }
2476
+ placeTextNode(node, multiply(ctm, localTransform), onWarn);
2326
2477
  return node;
2327
2478
  }
2328
2479
  function readTspanRun(el, gradients, style, onWarn) {
@@ -2338,7 +2489,8 @@ function readTspanRun(el, gradients, style, onWarn) {
2338
2489
  const size = ownValue(el, "font-size", onWarn);
2339
2490
  if (size?.fontSize != null) run.fontSize = size.fontSize;
2340
2491
  if (size?.fontScale != null) run.fontScale = size.fontScale;
2341
- const ls = ownValue(el, "letter-spacing", onWarn);
2492
+ const tspanStyle = deriveStyle(style, el);
2493
+ const ls = ownValue(el, "letter-spacing", onWarn, fontSizeOf(tspanStyle));
2342
2494
  if (ls != null) run.letterSpacing = ls;
2343
2495
  const shift = ownValue(el, "baseline-shift", onWarn);
2344
2496
  if (shift?.script != null) run.script = shift.script;
@@ -2351,19 +2503,20 @@ function readTspanRun(el, gradients, style, onWarn) {
2351
2503
  if (transform) run.textTransform = transform;
2352
2504
  const variant = ownValue(el, "font-variant", onWarn);
2353
2505
  if (variant) run.fontVariantCaps = variant;
2354
- const tspanStyle = deriveStyle(style, el);
2355
2506
  const fillAttr = resolveCurrentColor(ownProp(el, "fill"), tspanStyle);
2356
2507
  if (fillAttr) {
2357
2508
  const parsed = readProperty("fill", fillAttr);
2358
- if (parsed?.kind === "solid") {
2359
- run.fill = { fill: "solid", color: parsed.color };
2509
+ if (parsed?.kind === "none") {
2510
+ run.fill = null;
2511
+ } else if (parsed?.kind === "solid") {
2512
+ run.fill = { paint: { kind: "solid", color: parsed.color } };
2360
2513
  } else if (parsed?.kind === "ref") {
2361
2514
  const paint = gradients.get(parsed.id);
2362
- if (paint) run.fill = paint;
2363
- else if (parsed.fallback) run.fill = { fill: "solid", color: parsed.fallback.color };
2515
+ if (paint) run.fill = { paint };
2516
+ else if (parsed.fallback) run.fill = { paint: { kind: "solid", color: parsed.fallback.color } };
2364
2517
  }
2365
2518
  }
2366
- const stroke = coreStroke(readStroke(ownStrokeStyle(el), gradients, onWarn, isNonScalingStroke(el)));
2519
+ const stroke = coreStroke(readStroke(ownStrokeStyle(el, tspanStyle), gradients, onWarn, isNonScalingStroke(el)));
2367
2520
  if (stroke) run.stroke = stroke;
2368
2521
  return run;
2369
2522
  }
@@ -2371,17 +2524,14 @@ function readTextStyle(style, onWarn) {
2371
2524
  const out = {};
2372
2525
  const size = styleValue(style, "font-size", onWarn);
2373
2526
  if (size?.fontSize != null) out.fontSize = size.fontSize;
2374
- else if (size?.fontScale != null) {
2375
- onWarn(`font-size "${style["font-size"]}" on <text> is resolved against the 16px default`);
2376
- out.fontSize = 16 * size.fontScale;
2377
- }
2378
2527
  const ff = styleValue(style, "font-family", onWarn);
2379
2528
  if (ff) out.fontFamily = ff;
2380
2529
  const fw = styleValue(style, "font-weight", onWarn);
2381
2530
  if (fw != null) out.fontWeight = fw;
2382
2531
  const fs = styleValue(style, "font-style", onWarn);
2383
2532
  if (fs) out.fontStyle = fs;
2384
- if (styleValue(style, "direction") === "rtl") out.direction = "rtl";
2533
+ const direction = styleValue(style, "direction");
2534
+ if (direction === "rtl" || direction === "ltr") out.direction = direction;
2385
2535
  const ltr = out.direction !== "rtl";
2386
2536
  const anchor = styleValue(style, "text-anchor") ?? (ltr ? void 0 : "start");
2387
2537
  if (anchor === "start") out.align = ltr ? "left" : "right";
@@ -2405,12 +2555,12 @@ function readTextPaint(style, gradients, onWarn, nonScalingStroke = false) {
2405
2555
  if (fillRaw) {
2406
2556
  const parsed = readProperty("fill", fillRaw);
2407
2557
  if (parsed?.kind === "solid") {
2408
- out.fill = { fill: "solid", color: parsed.color };
2558
+ out.fill = { paint: { kind: "solid", color: parsed.color } };
2409
2559
  } else if (parsed?.kind === "ref") {
2410
2560
  const paint = gradients.get(parsed.id);
2411
- if (paint) out.fill = paint;
2561
+ if (paint) out.fill = { paint };
2412
2562
  else if (parsed.fallback) {
2413
- out.fill = { fill: "solid", color: parsed.fallback.color };
2563
+ out.fill = { paint: { kind: "solid", color: parsed.fallback.color } };
2414
2564
  }
2415
2565
  } else if (parsed?.kind === "none") {
2416
2566
  out.fill = null;
@@ -2492,11 +2642,6 @@ function markerInsetAttr(el) {
2492
2642
  const n = parseFloat(weaselAttr(el, "inset") ?? "");
2493
2643
  return Number.isFinite(n) && n > 0 ? n : 0;
2494
2644
  }
2495
- function styleAt(el) {
2496
- const chain = [];
2497
- for (let e = el; e; e = e.parentElement) chain.unshift(e);
2498
- return chain.reduce((style, e) => deriveStyle(style, e), EMPTY_STYLE);
2499
- }
2500
2645
  function markerRef(raw, lo, span) {
2501
2646
  const v = raw?.trim();
2502
2647
  if (v === "left" || v === "top") return lo;
@@ -2638,56 +2783,275 @@ function markerEntryFrom(el, id, role, strokeWidth, gradients, onWarn) {
2638
2783
  inset
2639
2784
  };
2640
2785
  }
2641
- function serializeSvg(nodes, opts = {}) {
2642
- const registry = new PaintServerRegistry();
2643
- registerPaintServers(nodes, registry);
2644
- const bounds = opts.viewBox ?? computeBounds(nodes);
2645
- const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
2786
+
2787
+ // src/parse.ts
2788
+ var SUPPORTED_GROUP_TAGS = /* @__PURE__ */ new Set(["g", "svg"]);
2789
+ function parseSvg(svg, opts = {}) {
2790
+ const warnings = [];
2791
+ const onWarn = (m) => {
2792
+ warnings.push(m);
2793
+ };
2794
+ let doc;
2795
+ try {
2796
+ doc = new DOMParser().parseFromString(svg, "image/svg+xml");
2797
+ } catch (e) {
2798
+ return { nodes: [], warnings: [`failed to parse SVG: ${e.message}`] };
2799
+ }
2800
+ const errEl = doc.getElementsByTagName("parsererror")[0];
2801
+ if (errEl) {
2802
+ return { nodes: [], warnings: [`SVG parse error: ${errEl.textContent ?? "unknown"}`] };
2803
+ }
2804
+ const root = doc.documentElement;
2805
+ if (!root || root.tagName.toLowerCase() !== "svg") {
2806
+ return { nodes: [], warnings: ["root element is not <svg>"] };
2807
+ }
2808
+ bindStylesheets(doc, mediaEnvironmentFor(root, opts.media), onWarn);
2646
2809
  const namespaces = opts.namespaces ?? {};
2647
- const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
2648
- if (registry.usesPrivateNamespace() && namespaces[WEASEL_NS_PREFIX] == null) {
2649
- rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
2810
+ const uriToPrefix = /* @__PURE__ */ new Map();
2811
+ for (const [prefix, uri] of Object.entries(namespaces)) {
2812
+ uriToPrefix.set(uri, prefix);
2813
+ }
2814
+ const documentMeta = collectDocumentMeta(root, uriToPrefix);
2815
+ const gradients = collectGradients(root, onWarn);
2816
+ for (const [id, paint] of collectPatterns(root, onWarn)) gradients.set(id, paint);
2817
+ const rootStyle = deriveStyle(EMPTY_STYLE, root);
2818
+ const clips = collectClipPaths(root, onWarn);
2819
+ const nodes = parseChildren(root, IDENTITY_MATRIX, rootStyle, gradients, clips, onWarn, uriToPrefix, opts.keepHidden === true);
2820
+ const markers = ingestMarkers(root, nodes, gradients, onWarn);
2821
+ const result = { nodes, warnings };
2822
+ if (markers.length > 0) result.markers = markers;
2823
+ if (documentMeta) result.documentMeta = documentMeta;
2824
+ const viewBox = parseViewBoxAttr(root.getAttribute("viewBox"));
2825
+ if (viewBox) result.viewBox = viewBox;
2826
+ const width2 = parseRootLength(root.getAttribute("width"), "width", fontSizeOf(rootStyle), onWarn);
2827
+ if (width2 != null) result.width = width2;
2828
+ const height2 = parseRootLength(root.getAttribute("height"), "height", fontSizeOf(rootStyle), onWarn);
2829
+ if (height2 != null) result.height = height2;
2830
+ for (let i = 0; i < root.children.length; i++) {
2831
+ const c = root.children[i];
2832
+ if (c.tagName.toLowerCase() === "title") {
2833
+ result.title = c.textContent ?? "";
2834
+ break;
2835
+ }
2836
+ }
2837
+ return result;
2838
+ }
2839
+ function parseRootLength(raw, name, fontSize, onWarn) {
2840
+ if (raw == null) return void 0;
2841
+ const n = parseFloat(raw);
2842
+ if (!Number.isFinite(n)) return void 0;
2843
+ const unit = /^\s*[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?\s*([a-z%]+)\s*$/i.exec(raw)?.[1];
2844
+ const scale = unit && ABSOLUTE_PX[unit.toLowerCase()];
2845
+ if (scale) return n * scale;
2846
+ const ems = unit && emsPer(unit);
2847
+ if (ems) return n * ems * fontSize;
2848
+ if (unit) onWarn(`<svg ${name}="${raw}"> unit "${unit}" is not converted; read as ${n}`);
2849
+ return n;
2850
+ }
2851
+ function collectDocumentMeta(root, uriToPrefix) {
2852
+ if (uriToPrefix.size === 0) return void 0;
2853
+ const meta = {};
2854
+ for (let i = 0; i < root.attributes.length; i++) {
2855
+ const a = root.attributes[i];
2856
+ if (!a.namespaceURI) continue;
2857
+ const prefix = uriToPrefix.get(a.namespaceURI);
2858
+ if (!prefix) continue;
2859
+ const bucket = meta[prefix] ??= {};
2860
+ (bucket.attrs ??= {})[a.localName] = a.value;
2861
+ }
2862
+ for (let i = 0; i < root.children.length; i++) {
2863
+ const c = root.children[i];
2864
+ if (!c.namespaceURI) continue;
2865
+ const prefix = uriToPrefix.get(c.namespaceURI);
2866
+ if (!prefix) continue;
2867
+ const bucket = meta[prefix] ??= {};
2868
+ const elements = bucket.elements ??= {};
2869
+ const list = elements[c.localName] ??= [];
2870
+ list.push(collectNamespacedElement(c, uriToPrefix));
2871
+ }
2872
+ return Object.keys(meta).length > 0 ? meta : void 0;
2873
+ }
2874
+ function collectNamespacedElement(el, uriToPrefix) {
2875
+ const result = { attrs: {} };
2876
+ for (let i = 0; i < el.attributes.length; i++) {
2877
+ const a = el.attributes[i];
2878
+ if (a.name.startsWith("xmlns")) continue;
2879
+ result.attrs[a.localName] = a.value;
2880
+ }
2881
+ let hasChildElements = false;
2882
+ for (let i = 0; i < el.children.length; i++) {
2883
+ const c = el.children[i];
2884
+ if (!c.namespaceURI) continue;
2885
+ const prefix = uriToPrefix.get(c.namespaceURI);
2886
+ if (!prefix) continue;
2887
+ hasChildElements = true;
2888
+ const children = result.children ??= {};
2889
+ const list = children[c.localName] ??= [];
2890
+ list.push(collectNamespacedElement(c, uriToPrefix));
2891
+ }
2892
+ if (!hasChildElements && el.textContent != null && el.textContent.trim() !== "") {
2893
+ result.text = el.textContent;
2894
+ }
2895
+ return result;
2896
+ }
2897
+ function collectElementMeta(el, uriToPrefix) {
2898
+ if (uriToPrefix.size === 0) return void 0;
2899
+ const meta = {};
2900
+ for (let i = 0; i < el.attributes.length; i++) {
2901
+ const a = el.attributes[i];
2902
+ if (!a.namespaceURI) continue;
2903
+ const prefix = uriToPrefix.get(a.namespaceURI);
2904
+ if (!prefix) continue;
2905
+ const bucket = meta[prefix] ??= {};
2906
+ (bucket.attrs ??= {})[a.localName] = a.value;
2907
+ }
2908
+ for (let i = 0; i < el.children.length; i++) {
2909
+ const c = el.children[i];
2910
+ if (!c.namespaceURI) continue;
2911
+ const prefix = uriToPrefix.get(c.namespaceURI);
2912
+ if (!prefix) continue;
2913
+ const bucket = meta[prefix] ??= {};
2914
+ const elements = bucket.elements ??= {};
2915
+ const list = elements[c.localName] ??= [];
2916
+ list.push(collectNamespacedElement(c, uriToPrefix));
2917
+ }
2918
+ return Object.keys(meta).length > 0 ? meta : void 0;
2919
+ }
2920
+ function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden) {
2921
+ const out = [];
2922
+ for (let i = 0; i < parent.children.length; i++) {
2923
+ const el = parent.children[i];
2924
+ const ns = el.namespaceURI;
2925
+ if (ns && ns !== "http://www.w3.org/2000/svg") continue;
2926
+ const hidden = isHiddenElement(el, style);
2927
+ if (hidden && !keepHidden) continue;
2928
+ const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden);
2929
+ if (!node) continue;
2930
+ const parsed = Array.isArray(node) ? node : [node];
2931
+ if (hidden) for (const n of parsed) n.hidden = true;
2932
+ out.push(...parsed);
2933
+ }
2934
+ return out;
2935
+ }
2936
+ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden) {
2937
+ const tag = el.tagName.toLowerCase();
2938
+ if (IGNORED_TAGS.has(tag)) return null;
2939
+ if (tag === "g" && el.hasAttribute("data-weasel-image")) {
2940
+ const image = parseCroppedImage(el, ctm, onWarn);
2941
+ if (image) {
2942
+ const meta2 = collectElementMeta(el, uriToPrefix);
2943
+ if (meta2) image.meta = meta2;
2944
+ return image;
2945
+ }
2946
+ }
2947
+ const clipRaw = tag === "g" ? null : ownProp(el, "clip-path");
2948
+ if (clipRaw != null && readProperty("clip-path", clipRaw) !== "none") {
2949
+ onWarn(`clip-path on <${tag}> is not modeled; only a <g> clips, so it is drawn unclipped`);
2950
+ }
2951
+ if (tag === "g") {
2952
+ const local = parseTransform(el.getAttribute("transform"), onWarn);
2953
+ const childCtm = multiply(ctm, local);
2954
+ const childStyle = deriveStyle(style, el);
2955
+ const children = parseChildren(el, childCtm, childStyle, gradients, clips, onWarn, uriToPrefix, keepHidden);
2956
+ const opacity2 = ownValue(el, "opacity");
2957
+ const group = { kind: "group", children };
2958
+ const clip = clipFor(el, childCtm, clips, onWarn);
2959
+ if (clip) group.clip = clip;
2960
+ if (opacity2 != null) group.opacity = opacity2;
2961
+ const meta2 = collectElementMeta(el, uriToPrefix);
2962
+ if (meta2) group.meta = meta2;
2963
+ return group;
2964
+ }
2965
+ if (SUPPORTED_GROUP_TAGS.has(tag)) {
2966
+ const childStyle = deriveStyle(style, el);
2967
+ const x = parseFloat(el.getAttribute("x") ?? "0");
2968
+ const y = parseFloat(el.getAttribute("y") ?? "0");
2969
+ const offset = [1, 0, 0, 1, Number.isFinite(x) ? x : 0, Number.isFinite(y) ? y : 0];
2970
+ if (el.hasAttribute("viewBox")) {
2971
+ onWarn("nested <svg> viewBox is not modeled; its children are not rescaled");
2972
+ }
2973
+ return parseChildren(el, multiply(ctm, offset), childStyle, gradients, clips, onWarn, uriToPrefix, keepHidden);
2650
2974
  }
2651
- for (const [prefix, uri] of Object.entries(namespaces)) {
2652
- rootAttrs.push(`xmlns:${prefix}="${escapeAttr2(uri)}"`);
2975
+ if (tag === "text") {
2976
+ const textNode = parseTextElement(el, ctm, style, gradients, onWarn);
2977
+ if (textNode && !Array.isArray(textNode) && textNode.kind === "text") {
2978
+ const meta2 = collectElementMeta(el, uriToPrefix);
2979
+ if (meta2) textNode.meta = meta2;
2980
+ }
2981
+ return textNode;
2653
2982
  }
2654
- rootAttrs.push(`viewBox="${vb}"`);
2655
- if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
2656
- if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
2657
- if (opts.documentMeta) {
2658
- for (const prefix of Object.keys(namespaces)) {
2659
- const bucket = opts.documentMeta[prefix];
2660
- if (!bucket?.attrs) continue;
2661
- for (const [name, value] of Object.entries(bucket.attrs)) {
2662
- rootAttrs.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
2663
- }
2983
+ if (tag === "image") {
2984
+ const imageNode = parseImageElement(el, ctm, onWarn);
2985
+ if (imageNode) {
2986
+ const meta2 = collectElementMeta(el, uriToPrefix);
2987
+ if (meta2) imageNode.meta = meta2;
2664
2988
  }
2989
+ return imageNode;
2665
2990
  }
2666
- let docMetaXml = "";
2667
- if (opts.documentMeta) {
2668
- for (const prefix of Object.keys(namespaces)) {
2669
- const bucket = opts.documentMeta[prefix];
2670
- if (!bucket?.elements) continue;
2671
- for (const [localName2, list] of Object.entries(bucket.elements)) {
2672
- for (const el of list) docMetaXml += namespacedElementXml(prefix, localName2, el);
2991
+ if (!SUPPORTED_LEAF_TAGS.has(tag)) {
2992
+ onWarn(`unsupported element: <${el.tagName}>`);
2993
+ return null;
2994
+ }
2995
+ const localTransform = parseTransform(el.getAttribute("transform"), onWarn);
2996
+ let path = lowerLeaf(el, tag, ctm, onWarn);
2997
+ if (!path) return null;
2998
+ let rotation;
2999
+ let baked = ctm;
3000
+ if (!isIdentity(localTransform)) {
3001
+ const worldLocal = rebaseTransform(ctm, localTransform);
3002
+ const bounds = boundsOfPath(path);
3003
+ const cx = bounds.x + bounds.width / 2;
3004
+ const cy = bounds.y + bounds.height / 2;
3005
+ const angle = worldLocal && decomposeRotation(worldLocal, cx, cy);
3006
+ if (!worldLocal) {
3007
+ onWarn("leaf transform sits under a singular parent transform; dropped");
3008
+ } else if (angle != null) {
3009
+ rotation = angle;
3010
+ } else {
3011
+ path = transformPath(path, worldLocal);
3012
+ baked = multiply(ctm, localTransform);
3013
+ const rotComp = rotationComponent(worldLocal);
3014
+ if (Math.abs(rotComp) > 1e-4) {
3015
+ onWarn(`leaf transform has a rotational component that can't be cleanly stored as rotation; baked into geometry (may not round-trip identically)`);
2673
3016
  }
2674
3017
  }
2675
3018
  }
2676
- const defsXml = registry.toDefsXml(opts.onWarn);
2677
- const said = /* @__PURE__ */ new Set();
2678
- const warn = (m) => {
2679
- if (!opts.onWarn || said.has(m)) return;
2680
- said.add(m);
2681
- opts.onWarn(m);
2682
- };
2683
- const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn)).join("");
2684
- const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
2685
- return `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
3019
+ const leafStyle = deriveStyle(style, el);
3020
+ const fill = paintThroughMatrix(readPaint2(leafStyle, "fill", "#000000", gradients, onWarn), baked);
3021
+ const ownStroke = readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el));
3022
+ const stroke = ownStroke && strokeThroughMatrix(ownStroke, baked);
3023
+ if (stroke && typeof stroke.width === "number" && isNonUniform(baked)) {
3024
+ onWarn(`<${tag}> stroke sits under a skew or non-uniform scale; its width takes the uniform part only`);
3025
+ }
3026
+ const opacity = ownValue(el, "opacity");
3027
+ if (styleValue(leafStyle, "fill-rule") === "evenodd" && path.kind === "polygon") {
3028
+ path = { ...path, fillRule: "evenodd" };
3029
+ }
3030
+ const node = { kind: "path", path, fill };
3031
+ if (stroke) node.stroke = stroke;
3032
+ if (opacity != null) node.opacity = opacity;
3033
+ if (rotation != null) node.rotation = rotation;
3034
+ if (tag === "line" && (leafStyle["fill"] ?? null) == null) {
3035
+ node.fill = { kind: "none" };
3036
+ }
3037
+ const meta = collectElementMeta(el, uriToPrefix);
3038
+ if (meta) node.meta = meta;
3039
+ return node;
2686
3040
  }
3041
+ function isHiddenElement(el, style) {
3042
+ const tag = el.tagName.toLowerCase();
3043
+ if (IGNORED_TAGS.has(tag)) return false;
3044
+ if (ownValue(el, "display") === "none") return true;
3045
+ if (SUPPORTED_GROUP_TAGS.has(tag)) return false;
3046
+ const v = styleValue(deriveStyle(style, el), "visibility");
3047
+ return v === "hidden" || v === "collapse";
3048
+ }
3049
+
3050
+ // src/metaXml.ts
2687
3051
  function namespacedElementXml(prefix, localName2, el) {
2688
3052
  const attrs = [];
2689
3053
  for (const [name, value] of Object.entries(el.attrs)) {
2690
- attrs.push(`${name}="${escapeAttr2(value)}"`);
3054
+ attrs.push(`${name}="${escapeAttr(value)}"`);
2691
3055
  }
2692
3056
  const head = attrs.length > 0 ? `<${prefix}:${localName2} ${attrs.join(" ")}>` : `<${prefix}:${localName2}>`;
2693
3057
  let body = "";
@@ -2707,7 +3071,7 @@ function metaAttrsXml(meta, namespaces) {
2707
3071
  const bucket = meta[prefix];
2708
3072
  if (!bucket?.attrs) continue;
2709
3073
  for (const [name, value] of Object.entries(bucket.attrs)) {
2710
- parts.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
3074
+ parts.push(`${prefix}:${name}="${escapeAttr(value)}"`);
2711
3075
  }
2712
3076
  }
2713
3077
  return parts.length > 0 ? ` ${parts.join(" ")}` : "";
@@ -2724,6 +3088,158 @@ function metaElementsXml(meta, namespaces) {
2724
3088
  }
2725
3089
  return out;
2726
3090
  }
3091
+ function computeBounds(nodes) {
3092
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3093
+ const visit = (n, ctm) => {
3094
+ if (n.hidden) return;
3095
+ if (n.kind === "group") {
3096
+ const childCtm = n.transform ? multiply(ctm, n.transform) : ctm;
3097
+ n.children.forEach((c) => visit(c, childCtm));
3098
+ return;
3099
+ }
3100
+ const b = n.kind === "text" || n.kind === "image" ? { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height } : pathBounds(n.path);
3101
+ const frame = n.kind === "path" ? null : poseFrameOf(n);
3102
+ const local = frame ? multiply(ctm, frame) : n.rotation ? rotateAboutCenter(ctm, b, n.rotation) : ctm;
3103
+ for (const [x, y] of corners(b)) {
3104
+ const px = local[0] * x + local[2] * y + local[4];
3105
+ const py = local[1] * x + local[3] * y + local[5];
3106
+ if (px < minX) minX = px;
3107
+ if (py < minY) minY = py;
3108
+ if (px > maxX) maxX = px;
3109
+ if (py > maxY) maxY = py;
3110
+ }
3111
+ };
3112
+ nodes.forEach((n) => visit(n, IDENTITY_MATRIX));
3113
+ if (!Number.isFinite(minX)) {
3114
+ return { x: 0, y: 0, width: 0, height: 0 };
3115
+ }
3116
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
3117
+ }
3118
+ function corners(b) {
3119
+ return [[b.minX, b.minY], [b.maxX, b.minY], [b.maxX, b.maxY], [b.minX, b.maxY]];
3120
+ }
3121
+ function rotateAboutCenter(ctm, b, angle) {
3122
+ const cx = (b.minX + b.maxX) / 2;
3123
+ const cy = (b.minY + b.maxY) / 2;
3124
+ const c = Math.cos(angle);
3125
+ const s = Math.sin(angle);
3126
+ return multiply(ctm, [c, s, -s, c, cx - cx * c + cy * s, cy - cx * s - cy * c]);
3127
+ }
3128
+ function pathBounds(path) {
3129
+ const b = boundsOfPath(path);
3130
+ return { minX: b.x, minY: b.y, maxX: b.x + b.width, maxY: b.y + b.height };
3131
+ }
3132
+
3133
+ // src/openSubpathFill.ts
3134
+ var capsEnds = (stroke) => stroke?.markerStart !== void 0 || stroke?.markerEnd !== void 0;
3135
+ function withoutEndMarkers(stroke) {
3136
+ const { markerStart: _start, markerEnd: _end, ...rest } = stroke;
3137
+ return rest;
3138
+ }
3139
+ function withOpenSubpathsUnfilled(node) {
3140
+ const path = node.path;
3141
+ if (path.kind !== "polygon" || node.fill.kind === "none" || fillsOpenSubpaths(path)) return node;
3142
+ const { closed, open } = splitByClosure(path);
3143
+ if (!open) return node;
3144
+ const { fill, stroke } = node;
3145
+ const opens = capsEnds(stroke) ? splitSubpaths(open) : [open];
3146
+ if (!closed && opens.length === 1) return { ...node, fill: { kind: "none" } };
3147
+ const piece = (part, paint, line = stroke) => ({ kind: "path", path: part, fill: paint, ...line ? { stroke: line } : {} });
3148
+ const group = {
3149
+ kind: "group",
3150
+ children: [
3151
+ ...closed ? [piece(closed, fill, stroke && withoutEndMarkers(stroke))] : [],
3152
+ ...opens.map((part) => piece(part, { kind: "none" }))
3153
+ ]
3154
+ };
3155
+ if (node.rotation != null && node.rotation !== 0) {
3156
+ const b = pathBounds(path);
3157
+ group.transform = rotateAboutPoint((b.minX + b.maxX) / 2, (b.minY + b.maxY) / 2, node.rotation);
3158
+ }
3159
+ if (node.opacity != null) group.opacity = node.opacity;
3160
+ if (node.hidden) group.hidden = true;
3161
+ if (node.meta) group.meta = node.meta;
3162
+ return group;
3163
+ }
3164
+
3165
+ // src/prettyXml.ts
3166
+ var TOKEN2 = /<(?:[^>"]|"[^"]*")*>|[^<]+/g;
3167
+ var isTag = (t) => t !== void 0 && t.startsWith("<");
3168
+ var isClose = (t) => t.startsWith("</");
3169
+ var isSelfClosing = (t) => t.endsWith("/>");
3170
+ var tagName = (t) => /^<\/?([^\s/>]+)/.exec(t)[1];
3171
+ function matchingClose(tokens, open) {
3172
+ let depth = 0;
3173
+ for (let i = open; i < tokens.length; i++) {
3174
+ const t = tokens[i];
3175
+ if (!isTag(t) || isSelfClosing(t)) continue;
3176
+ depth += isClose(t) ? -1 : 1;
3177
+ if (depth === 0) return i;
3178
+ }
3179
+ return tokens.length - 1;
3180
+ }
3181
+ function prettyXml(xml) {
3182
+ const tokens = xml.match(TOKEN2) ?? [];
3183
+ const lines = [];
3184
+ let depth = 0;
3185
+ for (let i = 0; i < tokens.length; i++) {
3186
+ const t = tokens[i];
3187
+ if (!isTag(t) || isSelfClosing(t)) {
3188
+ lines.push(" ".repeat(depth) + t);
3189
+ } else if (isClose(t)) {
3190
+ depth--;
3191
+ lines.push(" ".repeat(depth) + t);
3192
+ } else if (tagName(t) === "text" || !isTag(tokens[i + 1])) {
3193
+ const close2 = matchingClose(tokens, i);
3194
+ lines.push(" ".repeat(depth) + tokens.slice(i, close2 + 1).join(""));
3195
+ i = close2;
3196
+ } else {
3197
+ lines.push(" ".repeat(depth) + t);
3198
+ depth++;
3199
+ }
3200
+ }
3201
+ return lines.join("\n");
3202
+ }
3203
+
3204
+ // src/serialize.ts
3205
+ function serializeSvg(nodes, opts = {}) {
3206
+ const registry = new PaintServerRegistry();
3207
+ registerPaintServers(nodes, registry);
3208
+ const bounds = opts.viewBox ?? computeBounds(nodes);
3209
+ const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
3210
+ const namespaces = opts.namespaces ?? {};
3211
+ const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
3212
+ if (registry.usesPrivateNamespace() && namespaces[WEASEL_NS_PREFIX] == null) {
3213
+ rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
3214
+ }
3215
+ for (const [prefix, uri] of Object.entries(namespaces)) {
3216
+ rootAttrs.push(`xmlns:${prefix}="${escapeAttr(uri)}"`);
3217
+ }
3218
+ rootAttrs.push(`viewBox="${vb}"`);
3219
+ if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
3220
+ if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
3221
+ if (opts.documentMeta) {
3222
+ for (const prefix of Object.keys(namespaces)) {
3223
+ const bucket = opts.documentMeta[prefix];
3224
+ if (!bucket?.attrs) continue;
3225
+ for (const [name, value] of Object.entries(bucket.attrs)) {
3226
+ rootAttrs.push(`${prefix}:${name}="${escapeAttr(value)}"`);
3227
+ }
3228
+ }
3229
+ }
3230
+ const docMetaXml = metaElementsXml(opts.documentMeta, namespaces);
3231
+ const defsXml = registry.toDefsXml(opts.onWarn);
3232
+ const said = /* @__PURE__ */ new Set();
3233
+ const warn = (m) => {
3234
+ if (!opts.onWarn || said.has(m)) return;
3235
+ said.add(m);
3236
+ opts.onWarn(m);
3237
+ };
3238
+ const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn)).join("");
3239
+ const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
3240
+ const xml = `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
3241
+ return opts.pretty ? prettyXml(xml) : xml;
3242
+ }
2727
3243
  function registerPaintServers(nodes, registry) {
2728
3244
  for (const n of nodes) {
2729
3245
  if (n.kind === "group") {
@@ -2734,18 +3250,18 @@ function registerPaintServers(nodes, registry) {
2734
3250
  if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
2735
3251
  registerMarkers(n.stroke, registry);
2736
3252
  } else if (n.kind === "text") {
2737
- registerTextPaint(n.fill, registry);
3253
+ registerTextPaint(n.fill?.paint, registry);
2738
3254
  registerTextPaint(n.stroke?.paint, registry);
2739
3255
  registerMarkers(n.stroke, registry);
2740
3256
  for (const run of n.runs ?? []) {
2741
- registerTextPaint(run.fill, registry);
3257
+ registerTextPaint(run.fill?.paint, registry);
2742
3258
  registerTextPaint(run.stroke?.paint, registry);
2743
3259
  }
2744
3260
  }
2745
3261
  }
2746
3262
  }
2747
3263
  function registerTextPaint(paint, registry) {
2748
- if (paint && !("color" in paint)) registry.register(paint);
3264
+ if (paint && !isSolidPaint(paint)) registry.register(paint);
2749
3265
  }
2750
3266
  function registerMarkers(stroke, registry) {
2751
3267
  if (!stroke) return;
@@ -2754,10 +3270,15 @@ function registerMarkers(stroke, registry) {
2754
3270
  }
2755
3271
  }
2756
3272
  function nodeXml(node, registry, namespaces, warn) {
3273
+ const xml = kindXml(node, registry, namespaces, warn);
3274
+ return node.hidden ? xml.replace(/^<[\w:]+/, (open) => `${open} display="none"`) : xml;
3275
+ }
3276
+ function kindXml(node, registry, namespaces, warn) {
2757
3277
  if (node.kind === "group") return groupXml(node, registry, namespaces, warn);
2758
3278
  if (node.kind === "text") return textXml(node, registry, namespaces, warn);
2759
3279
  if (node.kind === "image") return imageXml(node, namespaces);
2760
- return pathXml(node, registry, namespaces, warn);
3280
+ const drawn = withOpenSubpathsUnfilled(node);
3281
+ return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn) : pathXml(drawn, registry, namespaces, warn);
2761
3282
  }
2762
3283
  function warnStrokeAlign(align, warn) {
2763
3284
  if (align && align !== "center") {
@@ -2770,15 +3291,11 @@ function imageXml(node, namespaces) {
2770
3291
  if (node.opacity != null && node.opacity !== 1) {
2771
3292
  outer.push(`opacity="${trimNumber(node.opacity)}"`);
2772
3293
  }
2773
- if (node.rotation != null && node.rotation !== 0) {
2774
- const cx = node.x + node.width / 2;
2775
- const cy = node.y + node.height / 2;
2776
- const deg = node.rotation * 180 / Math.PI;
2777
- outer.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
2778
- }
3294
+ const frame = boxTransformAttr(node);
3295
+ if (frame) outer.push(frame);
2779
3296
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
2780
3297
  const metaEls = metaElementsXml(node.meta, namespaces);
2781
- const href = `href="${escapeAttr2(node.href)}"`;
3298
+ const href = `href="${escapeAttr(node.href)}"`;
2782
3299
  if (!node.source && !node.flipX && !node.flipY) {
2783
3300
  const attrs = [href, box, 'preserveAspectRatio="none"', ...outer].join(" ");
2784
3301
  if (metaEls) return `<image ${attrs}${metaAttrs}>${metaEls}</image>`;
@@ -2839,7 +3356,7 @@ function pathXml(node, registry, namespaces, warn) {
2839
3356
  }
2840
3357
  function paintAttrs(paint, name, registry, includeOpacity = true) {
2841
3358
  if (paint.kind === "none") return [`${name}="none"`];
2842
- const out = [`${name}="${paint.kind === "solid" ? paint.color : registry.ref(paint.paint)}"`];
3359
+ const out = [`${name}="${paint.kind === "solid" ? escapeAttr(paint.color) : registry.ref(paint.paint)}"`];
2843
3360
  if (includeOpacity) out.push(...opacityAttr(`${name}-opacity`, svgPaintOpacity(paint)));
2844
3361
  return out;
2845
3362
  }
@@ -2860,7 +3377,7 @@ function coreStrokeAttrs(stroke, registry, warn) {
2860
3377
  const width2 = stroke.width ?? 1;
2861
3378
  if (!((typeof width2 === "object" ? width2.px : width2) > 0)) return [];
2862
3379
  warnStrokeAlign(stroke.align, warn);
2863
- const attrs = [`stroke="${"color" in paint ? paint.color : registry.ref(paint)}"`];
3380
+ const attrs = [`stroke="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`];
2864
3381
  attrs.push(...opacityAttr("stroke-opacity", paint.opacity));
2865
3382
  attrs.push(...strokeWidthAttrs(width2));
2866
3383
  if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
@@ -2912,44 +3429,9 @@ function strokeAttrsFor(stroke, registry, warn) {
2912
3429
  }
2913
3430
  return attrs;
2914
3431
  }
2915
- function computeBounds(nodes) {
2916
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2917
- const visit = (n, ctm) => {
2918
- if (n.kind === "group") {
2919
- const childCtm = n.transform ? multiply(ctm, n.transform) : ctm;
2920
- n.children.forEach((c) => visit(c, childCtm));
2921
- return;
2922
- }
2923
- const b = n.kind === "text" || n.kind === "image" ? { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height } : pathBounds(n.path);
2924
- const local = n.rotation ? rotateAboutCenter(ctm, b, n.rotation) : ctm;
2925
- for (const [x, y] of corners(b)) {
2926
- const px = local[0] * x + local[2] * y + local[4];
2927
- const py = local[1] * x + local[3] * y + local[5];
2928
- if (px < minX) minX = px;
2929
- if (py < minY) minY = py;
2930
- if (px > maxX) maxX = px;
2931
- if (py > maxY) maxY = py;
2932
- }
2933
- };
2934
- nodes.forEach((n) => visit(n, IDENTITY_MATRIX));
2935
- if (!Number.isFinite(minX)) {
2936
- return { x: 0, y: 0, width: 0, height: 0 };
2937
- }
2938
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
2939
- }
2940
- function corners(b) {
2941
- return [[b.minX, b.minY], [b.maxX, b.minY], [b.maxX, b.maxY], [b.minX, b.maxY]];
2942
- }
2943
- function rotateAboutCenter(ctm, b, angle) {
2944
- const cx = (b.minX + b.maxX) / 2;
2945
- const cy = (b.minY + b.maxY) / 2;
2946
- const c = Math.cos(angle);
2947
- const s = Math.sin(angle);
2948
- return multiply(ctm, [c, s, -s, c, cx - cx * c + cy * s, cy - cx * s - cy * c]);
2949
- }
2950
3432
  function textXml(node, registry, namespaces, warn) {
2951
3433
  const attrs = [
2952
- `x="${trimNumber(node.x + anchorOffset(node.style, node.width))}"`,
3434
+ `x="${trimNumber(node.x + anchorOffset(node.style, node.width, node.text))}"`,
2953
3435
  `y="${trimNumber(node.y)}"`,
2954
3436
  `dominant-baseline="text-before-edge"`,
2955
3437
  // The runs model stores real line breaks; SVG's default whitespace
@@ -2960,12 +3442,12 @@ function textXml(node, registry, namespaces, warn) {
2960
3442
  ];
2961
3443
  const style = node.style;
2962
3444
  if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(style.fontSize, 1))}"`);
2963
- if (style?.fontFamily) attrs.push(`font-family="${escapeAttr2(style.fontFamily)}"`);
2964
- if (style?.fontWeight != null) attrs.push(`font-weight="${String(style.fontWeight)}"`);
3445
+ if (style?.fontFamily) attrs.push(`font-family="${escapeAttr(style.fontFamily)}"`);
3446
+ if (style?.fontWeight != null) attrs.push(`font-weight="${escapeAttr(String(style.fontWeight))}"`);
2965
3447
  if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
2966
3448
  if (style?.fontVariantCaps === "small-caps") attrs.push('font-variant="small-caps"');
2967
- const direction = style?.direction ?? "ltr";
2968
- if (direction === "rtl") attrs.push('direction="rtl"');
3449
+ const direction = svgDirection(style, node.text);
3450
+ if (direction === "rtl" || style?.direction === "ltr") attrs.push(`direction="${direction}"`);
2969
3451
  if (style?.align != null || direction === "rtl") {
2970
3452
  const align = style?.align ?? "left";
2971
3453
  const anchor = align === "center" ? "middle" : align === "start" || align === "justify" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
@@ -2985,26 +3467,25 @@ function textXml(node, registry, namespaces, warn) {
2985
3467
  attrs.push('data-weasel-wrap="true"');
2986
3468
  warn("text that wraps at its box width is written unwrapped: SVG text does not wrap, so other readers draw one line per hard break (weasel reads the wrap back)");
2987
3469
  }
3470
+ if (style?.tabSize != null) attrs.push(`data-weasel-tab-size="${trimNumber(style.tabSize)}"`);
2988
3471
  if (node.verticalAlign && node.verticalAlign !== "top") {
2989
3472
  attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
2990
3473
  warn(`text aligned to the ${node.verticalAlign} of its box is written top-aligned: SVG text has no box, so other readers draw it at the top (weasel reads the alignment back)`);
2991
3474
  }
2992
- if (node.fill === null) {
2993
- attrs.push('fill="none"');
2994
- } else if (node.fill) {
2995
- attrs.push(`fill="${"color" in node.fill ? node.fill.color : registry.ref(node.fill)}"`);
2996
- attrs.push(...opacityAttr("fill-opacity", node.fill.opacity));
3475
+ if (node.fill !== void 0) {
3476
+ const paint = node.fill?.paint;
3477
+ if (!paint) attrs.push('fill="none"');
3478
+ else {
3479
+ attrs.push(`fill="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`);
3480
+ attrs.push(...opacityAttr("fill-opacity", paint.opacity));
3481
+ }
2997
3482
  }
2998
3483
  for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
2999
3484
  if (node.opacity != null && node.opacity !== 1) {
3000
3485
  attrs.push(`opacity="${trimNumber(node.opacity)}"`);
3001
3486
  }
3002
- if (node.rotation != null && node.rotation !== 0) {
3003
- const cx = node.x + node.width / 2;
3004
- const cy = node.y + node.height / 2;
3005
- const deg = node.rotation * 180 / Math.PI;
3006
- attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
3007
- }
3487
+ const frame = boxTransformAttr(node);
3488
+ if (frame) attrs.push(frame);
3008
3489
  const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, style, registry, warn)).join("") : escapeText(node.text);
3009
3490
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
3010
3491
  const metaEls = metaElementsXml(node.meta, namespaces);
@@ -3021,7 +3502,7 @@ function runXml(run, style, registry, warn) {
3021
3502
  else if (run.bold) attrs.push(`font-weight="700"`);
3022
3503
  if (run.italic) attrs.push(`font-style="italic"`);
3023
3504
  if (run.fontVariantCaps) attrs.push(`font-variant="${run.fontVariantCaps}"`);
3024
- if (run.fontFamily) attrs.push(`font-family="${escapeAttr2(run.fontFamily)}"`);
3505
+ if (run.fontFamily) attrs.push(`font-family="${escapeAttr(run.fontFamily)}"`);
3025
3506
  const face = scriptSizeFace(run, style);
3026
3507
  const presetScale = face && run.script ? scriptMetricsFor(face.family, face.weight, face.style)[run.script].size : void 0;
3027
3508
  const scale = run.fontScale ?? presetScale;
@@ -3037,12 +3518,11 @@ function runXml(run, style, registry, warn) {
3037
3518
  }
3038
3519
  const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
3039
3520
  if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
3040
- if (run.fill) {
3041
- if ("color" in run.fill) {
3042
- attrs.push(`fill="${run.fill.color}"`);
3043
- } else {
3044
- attrs.push(`fill="${registry.ref(run.fill)}"`);
3045
- }
3521
+ if (run.fill !== void 0) {
3522
+ const paint = run.fill?.paint;
3523
+ if (!paint) attrs.push('fill="none"');
3524
+ else if (isSolidPaint(paint)) attrs.push(`fill="${escapeAttr(paint.color)}"`);
3525
+ else attrs.push(`fill="${registry.ref(paint)}"`);
3046
3526
  }
3047
3527
  for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
3048
3528
  if (run.textTransform) attrs.push(`style="text-transform:${run.textTransform}"`);
@@ -3056,24 +3536,6 @@ function textDecorationValue(underline, strikethrough, overline) {
3056
3536
  if (overline) tokens.push("overline");
3057
3537
  return tokens.length > 0 ? tokens.join(" ") : null;
3058
3538
  }
3059
- function escapeAttr2(s) {
3060
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
3061
- }
3062
- function escapeText(s) {
3063
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3064
- }
3065
- function pathBounds(path) {
3066
- if (path.kind === "rect") {
3067
- return {
3068
- minX: path.x,
3069
- minY: path.y,
3070
- maxX: path.x + path.width,
3071
- maxY: path.y + path.height
3072
- };
3073
- }
3074
- const b = boundsOfPath(path);
3075
- return { minX: b.x, minY: b.y, maxX: b.x + b.width, maxY: b.y + b.height };
3076
- }
3077
3539
  function svgNeeds(nodes) {
3078
3540
  const registry = new PaintServerRegistry();
3079
3541
  registerPaintServers(nodes, registry);
@@ -3099,6 +3561,22 @@ function warmSvg(nodes) {
3099
3561
  warmPaintKinds(needs.paintKinds.filter(isPaintKindKnown))
3100
3562
  ]).then(() => void 0);
3101
3563
  }
3564
+
3565
+ // src/fadePaint.ts
3566
+ function fadeFill(fill, k) {
3567
+ return k === 1 ? fill : { ...fill, opacity: (fill.opacity ?? 1) * k };
3568
+ }
3569
+ function fadeStroke(stroke, k) {
3570
+ return k === 1 || !stroke.paint ? stroke : { ...stroke, paint: fadeFill(stroke.paint, k) };
3571
+ }
3572
+ function fadeRun(run, k) {
3573
+ if (k === 1 || !run.fill && !run.stroke) return run;
3574
+ return {
3575
+ ...run,
3576
+ ...run.fill ? { fill: mapFillPaint(run.fill, (p) => fadeFill(p, k)) } : {},
3577
+ ...run.stroke ? { stroke: fadeStroke(run.stroke, k) } : {}
3578
+ };
3579
+ }
3102
3580
  var CASCADE_OFFSET_PX = 24;
3103
3581
  var VIEWPORT_FIT = 0.9;
3104
3582
  function readFileText(file) {
@@ -3109,10 +3587,6 @@ function readFileText(file) {
3109
3587
  reader.readAsText(file);
3110
3588
  });
3111
3589
  }
3112
- var svgIdCounter = 0;
3113
- function freshSvgNodeId() {
3114
- return `n${(svgIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
3115
- }
3116
3590
  function fillDataFromSvg(paint, box) {
3117
3591
  if (!paint) return void 0;
3118
3592
  if (paint.kind === "none") return null;
@@ -3121,20 +3595,6 @@ function fillDataFromSvg(paint, box) {
3121
3595
  }
3122
3596
  return fillInBoxFrame(paint.paint, box);
3123
3597
  }
3124
- function fadeFill(fill, k) {
3125
- return k === 1 ? fill : { ...fill, opacity: (fill.opacity ?? 1) * k };
3126
- }
3127
- function fadeStroke(stroke, k) {
3128
- return k === 1 || !stroke.paint ? stroke : { ...stroke, paint: fadeFill(stroke.paint, k) };
3129
- }
3130
- function fadeRun(run, k) {
3131
- if (k === 1 || !run.fill && !run.stroke) return run;
3132
- return {
3133
- ...run,
3134
- ...run.fill ? { fill: fadeFill(run.fill, k) } : {},
3135
- ...run.stroke ? { stroke: fadeStroke(run.stroke, k) } : {}
3136
- };
3137
- }
3138
3598
  function strokeDataFromSvg(stroke, box) {
3139
3599
  if (!stroke || stroke.paint.kind === "none") return void 0;
3140
3600
  const paint = fillDataFromSvg(stroke.paint, box);
@@ -3156,7 +3616,7 @@ function fillInBoxFrame(fill, box) {
3156
3616
  return units === "world" ? fillToBoundsFrame(fill, box) : fill;
3157
3617
  }
3158
3618
  function runInBoxFrame(run, box) {
3159
- const fill = run.fill ? fillInBoxFrame(run.fill, box) : run.fill;
3619
+ const fill = run.fill ? mapFillPaint(run.fill, (p) => fillInBoxFrame(p, box)) : run.fill;
3160
3620
  const stroke = run.stroke ? strokeInBoxFrame(run.stroke, box) : run.stroke;
3161
3621
  if (fill === run.fill && stroke === run.stroke) return run;
3162
3622
  return { ...run, ...fill ? { fill } : {}, ...stroke ? { stroke } : {} };
@@ -3176,7 +3636,7 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3176
3636
  const id = nextId(source);
3177
3637
  const out = options.leaf ? options.leaf(id, { pose, data }, source) : data;
3178
3638
  if (out === null) return null;
3179
- drafts.push({ kind: "leaf", id, parentId, pose, data: out });
3639
+ drafts.push({ kind: "leaf", id, parentId, pose, data: out, ...source.hidden ? { hidden: true } : {} });
3180
3640
  return pose;
3181
3641
  };
3182
3642
  const visit = (n, parentId, inherited2) => {
@@ -3186,7 +3646,9 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3186
3646
  kind: "container",
3187
3647
  id: nextId(n),
3188
3648
  parentId,
3189
- pose: { x: 0, y: 0, width: 0, height: 0 }
3649
+ pose: { x: 0, y: 0, width: 0, height: 0 },
3650
+ data: n.clip ? { path: n.clip, fill: null } : {},
3651
+ ...n.hidden ? { hidden: true } : {}
3190
3652
  };
3191
3653
  drafts.push(draft);
3192
3654
  const acc = unionAABB(n.children.map((c) => visit(c, draft.id, k)));
@@ -3194,7 +3656,7 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3194
3656
  drafts.splice(drafts.indexOf(draft), 1);
3195
3657
  return null;
3196
3658
  }
3197
- draft.pose = acc;
3659
+ draft.pose = n.clip ? boxOfPath(n.clip) : acc;
3198
3660
  return draft.pose;
3199
3661
  }
3200
3662
  if (n.kind === "text") {
@@ -3205,6 +3667,7 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3205
3667
  height: n.height
3206
3668
  };
3207
3669
  if (n.rotation) pose2.rotation = n.rotation;
3670
+ if (n.transform) pose2.transform = n.transform;
3208
3671
  const box = {
3209
3672
  x: pose2.x,
3210
3673
  y: pose2.y,
@@ -3219,13 +3682,14 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3219
3682
  ...n.runs ? { runs: n.runs.map((r) => fadeRun(runInBoxFrame(r, box), k)) } : {},
3220
3683
  // `!== undefined`, not a truthiness test: `null` is the document
3221
3684
  // saying `fill="none"`, and absent takes the painter's default.
3222
- ...fill2 !== void 0 ? { fill: fill2 === null ? null : fadeFill(fillInBoxFrame(fill2, box), k) } : {},
3685
+ ...fill2 !== void 0 ? { fill: fill2 && mapFillPaint(fill2, (p) => fadeFill(fillInBoxFrame(p, box), k)) } : {},
3223
3686
  ...n.stroke !== void 0 ? { stroke: fadeStroke(strokeInBoxFrame(n.stroke, box), k) } : {}
3224
3687
  });
3225
3688
  }
3226
3689
  if (n.kind === "image") {
3227
3690
  const pose2 = { x: n.x, y: n.y, width: n.width, height: n.height };
3228
3691
  if (n.rotation) pose2.rotation = n.rotation;
3692
+ if (n.transform) pose2.transform = n.transform;
3229
3693
  return pushLeaf(n, parentId, pose2, {
3230
3694
  image: {
3231
3695
  src: n.href,
@@ -3236,35 +3700,36 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3236
3700
  }
3237
3701
  });
3238
3702
  }
3239
- const b = n.path.kind === "rect" ? { x: n.path.x, y: n.path.y, width: n.path.width, height: n.path.height } : boundsOfPath(n.path);
3703
+ const b = boxOfPath(n.path);
3240
3704
  const pose = { x: b.x, y: b.y, width: b.width, height: b.height };
3241
3705
  if (n.rotation) pose.rotation = n.rotation;
3242
3706
  const fill = fillDataFromSvg(n.fill, b);
3243
3707
  const strokeFromSvg = strokeDataFromSvg(n.stroke, b);
3244
3708
  return pushLeaf(n, parentId, pose, {
3245
3709
  path: n.path,
3246
- ...fill !== void 0 ? { fill: fill && fadeFill(fill, k) } : {},
3710
+ ...fill !== void 0 ? { fill: fill && { paint: fadeFill(fill, k) } } : {},
3247
3711
  ...strokeFromSvg !== void 0 ? { stroke: fadeStroke(strokeFromSvg, k) } : {}
3248
3712
  });
3249
3713
  };
3250
3714
  for (const n of nodes) visit(n, null, 1);
3251
3715
  return drafts;
3252
3716
  }
3717
+ function boxOfPath(path) {
3718
+ const b = boundsOfPath(path);
3719
+ return { x: b.x, y: b.y, width: b.width, height: b.height };
3720
+ }
3253
3721
  function isNodeList(input) {
3254
3722
  return Array.isArray(input);
3255
3723
  }
3256
- function scaleTextData(data, scale) {
3257
- if (scale === 1 || typeof data.text !== "string") return data;
3258
- const style = data.style ?? {};
3259
- const scaleSize = (v) => typeof v === "number" ? v * scale : v;
3260
- const runs = data.runs;
3261
- return {
3262
- ...data,
3263
- ...runs ? {
3264
- runs: runs.map((r) => r.fontSize !== void 0 ? { ...r, fontSize: scaleSize(r.fontSize) } : r)
3265
- } : {},
3266
- style: { ...style, fontSize: scaleSize(style.fontSize ?? resolveTextStyle(style).fontSize) }
3267
- };
3724
+ function scaleLeafData(data, scale) {
3725
+ if (scale === 1) return data;
3726
+ const out = { ...data };
3727
+ if (out.stroke) out.stroke = scaleStroke(out.stroke, scale);
3728
+ if (typeof out.text === "string") {
3729
+ out.style = scaleTextStyle(out.style, scale);
3730
+ if (out.runs) out.runs = scaleRuns(out.runs, scale);
3731
+ }
3732
+ return out;
3268
3733
  }
3269
3734
  async function unpackSvgFiles(files, ctx) {
3270
3735
  const layer = ctx.scene.layers[0]?.id ?? "default";
@@ -3274,17 +3739,17 @@ async function unpackSvgFiles(files, ctx) {
3274
3739
  const text = await readFileText(file);
3275
3740
  const parsed = parseSvg(text);
3276
3741
  for (const w of parsed.warnings) dwarn("ingest", `svg "${file.name}":`, w);
3277
- let drafts = svgNodesToKitDrafts(parsed, freshSvgNodeId);
3742
+ let drafts = svgNodesToKitDrafts(parsed, () => ctx.scene.mintId());
3278
3743
  if (drafts.length === 0) {
3279
3744
  console.warn(`weasel ingest: svg "${file.name}" parsed to no drawable nodes`);
3280
3745
  continue;
3281
3746
  }
3282
3747
  const roots = drafts.filter((d) => d.parentId === null);
3283
3748
  if (roots.length > 1) {
3284
- const wrapperId = freshSvgNodeId();
3749
+ const wrapperId = ctx.scene.mintId();
3285
3750
  const union2 = unionAABB(roots.map((d) => d.pose));
3286
3751
  drafts = [
3287
- { kind: "container", id: wrapperId, parentId: null, pose: union2 },
3752
+ { kind: "container", id: wrapperId, parentId: null, pose: union2, data: {} },
3288
3753
  ...drafts.map((d) => d.parentId === null ? { ...d, parentId: wrapperId } : d)
3289
3754
  ];
3290
3755
  }
@@ -3316,7 +3781,7 @@ async function unpackSvgFiles(files, ctx) {
3316
3781
  kind: d.kind,
3317
3782
  layer,
3318
3783
  pose: place(d.pose),
3319
- data: d.kind === "leaf" ? scaleTextData(d.data, scale) : {},
3784
+ data: d.kind === "leaf" ? scaleLeafData(d.data, scale) : d.data,
3320
3785
  parent: d.parentId
3321
3786
  },
3322
3787
  label: "Insert SVG"
@@ -3331,7 +3796,7 @@ async function unpackSvgFiles(files, ctx) {
3331
3796
  }
3332
3797
  function svgPaintFromKit(fill, box) {
3333
3798
  if (fill === null) return { kind: "none" };
3334
- if (fill.fill === void 0 || fill.fill === "solid") {
3799
+ if (fill.kind === void 0 || fill.kind === "solid") {
3335
3800
  return { kind: "solid", color: fill.color, ...fill.opacity != null ? { opacity: fill.opacity } : {} };
3336
3801
  }
3337
3802
  return { kind: "gradient", paint: fillInPoseFrame(fill, box) };
@@ -3369,6 +3834,7 @@ function svgImageFromKit(image, pose) {
3369
3834
  if (image.flipY) node.flipY = true;
3370
3835
  if (image.opacity != null) node.opacity = image.opacity;
3371
3836
  if (pose.rotation) node.rotation = pose.rotation;
3837
+ if (pose.transform) node.transform = pose.transform;
3372
3838
  return node;
3373
3839
  }
3374
3840
  function strokeInPoseFrame(stroke, box) {
@@ -3378,7 +3844,7 @@ function runInPoseFrame(run, box) {
3378
3844
  if (!run.fill && !run.stroke) return run;
3379
3845
  return {
3380
3846
  ...run,
3381
- ...run.fill ? { fill: fillInPoseFrame(run.fill, box) } : {},
3847
+ ...run.fill ? { fill: mapFillPaint(run.fill, (p) => fillInPoseFrame(p, box)) } : {},
3382
3848
  ...run.stroke ? { stroke: strokeInPoseFrame(run.stroke, box) } : {}
3383
3849
  };
3384
3850
  }
@@ -3396,9 +3862,10 @@ function svgLeafFromKit(data, pose) {
3396
3862
  if (data.style) node2.style = data.style;
3397
3863
  if (data.runs) node2.runs = data.runs.map((r) => runInPoseFrame(r, pose));
3398
3864
  if (data.verticalAlign) node2.verticalAlign = data.verticalAlign;
3399
- if (data.fill !== void 0) node2.fill = data.fill && fillInPoseFrame(data.fill, pose);
3865
+ if (data.fill !== void 0) node2.fill = data.fill && mapFillPaint(data.fill, (p) => fillInPoseFrame(p, pose));
3400
3866
  if (data.stroke) node2.stroke = strokeInPoseFrame(data.stroke, pose);
3401
3867
  if (pose.rotation) node2.rotation = pose.rotation;
3868
+ if (pose.transform) node2.transform = pose.transform;
3402
3869
  return node2;
3403
3870
  }
3404
3871
  if (!data.path) return null;
@@ -3407,10 +3874,15 @@ function svgLeafFromKit(data, pose) {
3407
3874
  const node = {
3408
3875
  kind: "path",
3409
3876
  path: pathInPoseFrame(data.path, pose),
3410
- fill: svgPaintFromKit(fill, pose)
3877
+ fill: svgPaintFromKit(fill?.paint ?? null, pose)
3411
3878
  };
3412
3879
  if (stroke) node.stroke = stroke;
3413
- if (pose.rotation) node.rotation = pose.rotation;
3880
+ const frame = poseLinearOf(pose) && poseFrameOf(pose);
3881
+ if (frame) {
3882
+ node.path = pathThroughPoseFrame(node.path, pose);
3883
+ node.fill = paintThroughMatrix(node.fill, frame);
3884
+ if (stroke) node.stroke = strokeThroughMatrix(stroke, frame);
3885
+ } else if (pose.rotation) node.rotation = pose.rotation;
3414
3886
  return node;
3415
3887
  }
3416
3888
  function svgNodesFromKit(tree, options = {}) {
@@ -3426,6 +3898,8 @@ function svgNodesFromKit(tree, options = {}) {
3426
3898
  if (child) children.push(child);
3427
3899
  }
3428
3900
  const g = { kind: "group", children };
3901
+ const clip = node.data?.path;
3902
+ if (clip) g.clip = pathThroughPoseFrame(pathInPoseFrame(clip, node.pose), node.pose);
3429
3903
  return group ? group(id, g) : g;
3430
3904
  }
3431
3905
  return leaf ? leaf(id, node) : svgLeafFromKit(node.data, node.pose);