@weasel-js/svg 1.9.3 → 1.9.4

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
1
  import { identity, multiply, invert } from '@weasel-js/geom';
2
+ import { boundsOfPath, transformPath, resolveScreenLength, scriptMetricsFor, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, DEFAULT_TEXT_STYLE, pathFromD, poseFrameOf, poseLinearOf, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, resolveAlign, paragraphDirection, resolveRunFace, resolveTextStyle, parseColor, rgbaToHex, layoutRuns, resolveRuns, PathBuilder, getPaintKind, resolveMarkerSize } 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") return fill;
214
+ const at = (p) => applyMatrix(m, p.x, p.y);
215
+ if (fill.fill === "linear-gradient") return { ...fill, from: at(fill.from), to: at(fill.to) };
216
+ if (fill.fill === "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.fill === "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;
@@ -670,21 +665,28 @@ function parsePaintAttr(raw) {
670
665
 
671
666
  // src/properties.ts
672
667
  var TEXT_TRANSFORMS = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
673
- var DIMENSION = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z%]*)$/i;
668
+ var DIMENSION2 = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z%]*)$/i;
674
669
  function dimension(v) {
675
- const m = DIMENSION.exec(v);
670
+ const m = DIMENSION2.exec(v);
676
671
  return m ? { n: Number(m[1]), unit: m[2].toLowerCase() } : void 0;
677
672
  }
678
673
  var clamp01 = (n) => n < 0 ? 0 : n > 1 ? 1 : n;
679
- var length = (native, coercedAs) => (v, warn, prop) => {
674
+ function pxPer(unit, fontSize) {
675
+ if (unit === "") return 1;
676
+ const ems = emsPer(unit);
677
+ return ems != null ? ems * fontSize : ABSOLUTE_PX[unit];
678
+ }
679
+ var length = (native, coercedAs) => (v, warn, prop, fontSize) => {
680
680
  const d = dimension(v);
681
681
  if (!d) return void 0;
682
- if (d.unit && !native.includes(d.unit)) {
682
+ const scale = native === "px" ? pxPer(d.unit, fontSize) : emsPer(d.unit);
683
+ if (scale != null) return d.n * scale;
684
+ if (d.unit) {
683
685
  warn(`${prop} "${v}" uses unit "${d.unit}", which is not converted; treated as ${d.n} ${coercedAs}`);
684
686
  }
685
687
  return d.n;
686
688
  };
687
- var worldLength = length(["px"], "world units");
689
+ var worldLength = length("px", "world units");
688
690
  var keyword2 = (...words) => (v) => words.includes(v) ? v : void 0;
689
691
  var isCurrentColor = (v) => v.toLowerCase() === "currentcolor";
690
692
  function readPaint(v) {
@@ -722,16 +724,17 @@ function readLinejoin(v, warn) {
722
724
  warn(`unsupported stroke-linejoin: ${v}`);
723
725
  return void 0;
724
726
  }
725
- function readDashArray(v, warn) {
727
+ function readDashArray(v, warn, _prop, fontSize) {
726
728
  if (v === "none" || v === "") return null;
727
729
  const nums = [];
728
730
  for (const t of v.split(/[\s,]+/).filter(Boolean)) {
729
731
  const d = dimension(t);
730
- if (!d || d.unit !== "" && d.unit !== "px" || d.n < 0) {
732
+ const scale = d && pxPer(d.unit, fontSize);
733
+ if (!d || scale == null || d.n < 0) {
731
734
  warn(`unrecognized stroke-dasharray: ${v}`);
732
735
  return void 0;
733
736
  }
734
- nums.push(d.n);
737
+ nums.push(d.n * scale);
735
738
  }
736
739
  return nums.length % 2 === 1 ? [...nums, ...nums] : nums;
737
740
  }
@@ -744,10 +747,14 @@ function readMiterLimit(v, warn) {
744
747
  function readColor(v) {
745
748
  return isCurrentColor(v) || parsePaintAttr(v)?.kind === "solid" ? v : void 0;
746
749
  }
747
- function readFontSize(v, warn, prop) {
750
+ function readFontSize(v, warn, prop, fontSize) {
751
+ const word = fontSizeKeyword(v);
752
+ if (word) return "size" in word ? { fontSize: word.size } : { fontScale: word.scale };
748
753
  const d = dimension(v);
749
754
  if (d?.unit === "%") return { fontScale: d.n / 100 };
750
- const n = worldLength(v, warn, prop);
755
+ const ems = d && emsPer(d.unit);
756
+ if (d && ems != null) return { fontScale: d.n * ems };
757
+ const n = worldLength(v, warn, prop, fontSize);
751
758
  return n == null ? void 0 : { fontSize: n };
752
759
  }
753
760
  function readFontWeight(v) {
@@ -763,8 +770,8 @@ function readFontStyle(v, warn) {
763
770
  }
764
771
  return void 0;
765
772
  }
766
- function readLetterSpacing(v, warn, prop) {
767
- return v === "normal" ? null : worldLength(v, warn, prop);
773
+ function readLetterSpacing(v, warn, prop, fontSize) {
774
+ return v === "normal" ? null : worldLength(v, warn, prop, fontSize);
768
775
  }
769
776
  function readDecoration(v, warn) {
770
777
  const out = {};
@@ -788,17 +795,22 @@ function readTextTransform(v) {
788
795
  const t = v.toLowerCase();
789
796
  return TEXT_TRANSFORMS.has(t) ? t : void 0;
790
797
  }
791
- var emLength = length(["em"], "em");
798
+ var emLength = length("em", "em");
792
799
  function readBaselineShift(v, warn, prop) {
793
800
  const k = v.toLowerCase();
794
801
  if (k === "super" || k === "sub") return { script: k };
795
802
  if (k === "baseline") return {};
796
803
  const d = dimension(v);
797
804
  if (d?.unit === "%") return d.n === 0 ? {} : { baselineShift: d.n / 100 };
798
- const n = emLength(v, warn, prop);
805
+ const n = emLength(v, warn, prop, DEFAULT_FONT_SIZE);
799
806
  if (n == null) return void 0;
800
807
  return n === 0 ? {} : { baselineShift: n };
801
808
  }
809
+ function readClipRef(v) {
810
+ if (v === "none") return "none";
811
+ const ref = /^url\(\s*["']?#([^)\s"']+)["']?\s*\)$/.exec(v);
812
+ return ref ? { id: ref[1] } : void 0;
813
+ }
802
814
  var inherited = (read) => ({ inherits: true, read });
803
815
  var own = (read) => ({ inherits: false, read });
804
816
  var PROPERTIES = {
@@ -830,16 +842,19 @@ var PROPERTIES = {
830
842
  "stop-color": own(readSolidColor),
831
843
  "stop-opacity": own(readRatio),
832
844
  "baseline-shift": own(readBaselineShift),
833
- "vector-effect": own(keyword2("none", "non-scaling-stroke"))
845
+ "vector-effect": own(keyword2("none", "non-scaling-stroke")),
846
+ "clip-path": own(readClipRef),
847
+ "display": own(keyword2("none", "inline")),
848
+ "visibility": inherited(keyword2("visible", "hidden", "collapse"))
834
849
  };
835
850
  var INHERITED_PROPERTIES = Object.keys(PROPERTIES).filter((p) => PROPERTIES[p].inherits);
836
851
  var silent = () => {
837
852
  };
838
- function readProperty(prop, raw, warn = silent) {
853
+ function readProperty(prop, raw, warn = silent, fontSize = DEFAULT_FONT_SIZE) {
839
854
  if (raw == null) return void 0;
840
855
  const v = raw.trim();
841
856
  if (v === "inherit") return void 0;
842
- return PROPERTIES[prop].read(v, warn, prop);
857
+ return PROPERTIES[prop].read(v, warn, prop, fontSize);
843
858
  }
844
859
  function isPropertyName(prop) {
845
860
  return Object.prototype.hasOwnProperty.call(PROPERTIES, prop);
@@ -1128,6 +1143,7 @@ function deriveStyle(parent, el) {
1128
1143
  if (!next) next = { ...parent };
1129
1144
  next[prop] = own2;
1130
1145
  }
1146
+ if (next) computeFontRelative(next, fontSizeOf(parent));
1131
1147
  return next ?? parent;
1132
1148
  }
1133
1149
  function resolveCurrentColor(raw, style) {
@@ -1136,13 +1152,81 @@ function resolveCurrentColor(raw, style) {
1136
1152
  const color = readProperty("color", style["color"]);
1137
1153
  return color == null || color.toLowerCase() === "currentcolor" ? "#000000" : color;
1138
1154
  }
1139
- function ownValue(el, prop, warn) {
1140
- return readProperty(prop, ownProp(el, prop), warn);
1155
+ function ownValue(el, prop, warn, fontSize) {
1156
+ return readProperty(prop, ownProp(el, prop), warn, fontSize);
1141
1157
  }
1142
1158
  function styleValue(style, prop, warn) {
1143
1159
  return readProperty(prop, style[prop], warn);
1144
1160
  }
1145
1161
 
1162
+ // src/xmlEscape.ts
1163
+ var LINE_BREAK_CONTROLS = /[\u000B\u000C]/g;
1164
+ var NOT_XML_CHAR = /[\u0000-\u0008\u000E-\u001F￾￿]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
1165
+ function xmlChars(s) {
1166
+ return s.replace(LINE_BREAK_CONTROLS, "\n").replace(NOT_XML_CHAR, "");
1167
+ }
1168
+ function escapeAttr(s) {
1169
+ return xmlChars(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/\t/g, "&#9;").replace(/\n/g, "&#10;").replace(/\r/g, "&#13;");
1170
+ }
1171
+ function escapeText(s) {
1172
+ return xmlChars(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\r/g, "&#13;");
1173
+ }
1174
+
1175
+ // src/styleAt.ts
1176
+ function styleAt(el) {
1177
+ const chain = [];
1178
+ for (let e = el; e; e = e.parentElement) chain.unshift(e);
1179
+ return chain.reduce((style, e) => deriveStyle(style, e), EMPTY_STYLE);
1180
+ }
1181
+
1182
+ // src/lengths.ts
1183
+ var LENGTH = /^\s*([-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?)\s*([a-z%]*)\s*$/i;
1184
+ function viewportOf(el) {
1185
+ for (let e = el.parentElement; e; e = e.parentElement) {
1186
+ if (e.tagName.toLowerCase() !== "svg") continue;
1187
+ const box = (e.getAttribute("viewBox") ?? "").trim().split(/[\s,]+/).map(Number);
1188
+ if (box.length === 4 && box.every(Number.isFinite)) return { width: box[2], height: box[3] };
1189
+ const w = LENGTH.exec(e.getAttribute("width") ?? "");
1190
+ const h = LENGTH.exec(e.getAttribute("height") ?? "");
1191
+ if (w && h && w[2] !== "%" && h[2] !== "%") return { width: userUnits(w), height: userUnits(h) };
1192
+ return void 0;
1193
+ }
1194
+ return void 0;
1195
+ }
1196
+ function userUnits(m) {
1197
+ return Number(m[1]) * (ABSOLUTE_PX[m[2].toLowerCase()] ?? 1);
1198
+ }
1199
+ function readLength(el, name, axis, fallback, onWarn) {
1200
+ return resolveLength(el.getAttribute(name), el, name, axis, fallback, onWarn);
1201
+ }
1202
+ function resolveLength(raw, el, name, axis, fallback, onWarn) {
1203
+ if (raw == null) return fallback;
1204
+ const m = LENGTH.exec(raw);
1205
+ if (!m) {
1206
+ const n2 = parseFloat(raw);
1207
+ return Number.isFinite(n2) ? n2 : fallback;
1208
+ }
1209
+ const n = Number(m[1]);
1210
+ const unit = m[2].toLowerCase();
1211
+ if (unit === "") return n;
1212
+ const scale = ABSOLUTE_PX[unit];
1213
+ if (scale != null) return n * scale;
1214
+ const ems = emsPer(unit);
1215
+ if (ems != null) return n * ems * fontSizeOf(styleAt(el));
1216
+ const tag = el.tagName.toLowerCase();
1217
+ if (unit !== "%") {
1218
+ onWarn(`<${tag} ${name}="${raw}"> unit "${unit}" is not converted; read as ${n}`);
1219
+ return n;
1220
+ }
1221
+ const view = viewportOf(el);
1222
+ if (!view) {
1223
+ onWarn(`<${tag} ${name}="${raw}"> has no viewport to be a percentage of; read as ${n}`);
1224
+ return n;
1225
+ }
1226
+ const of = axis === "x" ? view.width : axis === "y" ? view.height : Math.sqrt((view.width * view.width + view.height * view.height) / 2);
1227
+ return n / 100 * of;
1228
+ }
1229
+
1146
1230
  // src/elements.ts
1147
1231
  function collectElementsByTag(root, tags) {
1148
1232
  const out = /* @__PURE__ */ new Map();
@@ -1189,7 +1273,7 @@ function patternXml(id, paint, onWarn) {
1189
1273
  function tilePreviewSvg(spec, background) {
1190
1274
  const geometry = tileGeometry(spec);
1191
1275
  const s = trimNumber(geometry.size);
1192
- const bg = background ? `<rect width="${s}" height="${s}" fill="${background}"/>` : "";
1276
+ const bg = background ? `<rect width="${s}" height="${s}" fill="${escapeAttr(background)}"/>` : "";
1193
1277
  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
1278
  }
1195
1279
  function tilePreviewCssUrl(spec, background) {
@@ -1200,14 +1284,14 @@ function shapeXml(shape) {
1200
1284
  case "line":
1201
1285
  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
1286
  case "circle":
1203
- return `<circle cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" r="${trimNumber(shape.r)}" fill="${shape.color}"/>`;
1287
+ return `<circle cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" r="${trimNumber(shape.r)}" fill="${escapeAttr(shape.color)}"/>`;
1204
1288
  case "ellipse": {
1205
1289
  const deg = shape.rotation * 180 / Math.PI;
1206
1290
  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}/>`;
1291
+ return `<ellipse cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" rx="${trimNumber(shape.rx)}" ry="${trimNumber(shape.ry)}" fill="${escapeAttr(shape.color)}"${transform}/>`;
1208
1292
  }
1209
1293
  case "rect":
1210
- return `<rect x="${trimNumber(shape.x)}" y="${trimNumber(shape.y)}" width="${trimNumber(shape.width)}" height="${trimNumber(shape.height)}" fill="${shape.color}"/>`;
1294
+ return `<rect x="${trimNumber(shape.x)}" y="${trimNumber(shape.y)}" width="${trimNumber(shape.width)}" height="${trimNumber(shape.height)}" fill="${escapeAttr(shape.color)}"/>`;
1211
1295
  default:
1212
1296
  return "";
1213
1297
  }
@@ -1247,9 +1331,6 @@ function readPattern(el, onWarn) {
1247
1331
  if (!isIdentity([m[0], m[1], m[2], m[3], 0, 0])) paint.transform = [m[0], m[1], m[2], m[3]];
1248
1332
  return paint;
1249
1333
  }
1250
- function escapeAttr(s) {
1251
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1252
- }
1253
1334
  function serializePathD(path) {
1254
1335
  if (path.kind === "rect") {
1255
1336
  const { x, y, width: width2, height: height2 } = path;
@@ -1383,7 +1464,7 @@ function readStops(el, onWarn) {
1383
1464
  const colorAttr = ownProp(c, "stop-color") ?? "#000000";
1384
1465
  const parsed = readProperty("stop-color", colorAttr, onWarn);
1385
1466
  if (parsed) {
1386
- const alpha = ownValue(c, "stop-opacity") ?? parsed.alpha;
1467
+ const alpha = (ownValue(c, "stop-opacity") ?? 1) * parsed.alpha;
1387
1468
  stops.push({ offset, color: alpha < 1 ? applyAlpha(parsed.color, alpha) : parsed.color });
1388
1469
  } else {
1389
1470
  onWarn?.(`gradient stop has unrecognized stop-color: ${colorAttr}`);
@@ -1410,34 +1491,54 @@ function num(raw, fallback) {
1410
1491
  const n = parseRatio(raw);
1411
1492
  return Number.isFinite(n) ? n : fallback;
1412
1493
  }
1494
+ function warnUnmodeled(el, elements, onWarn) {
1495
+ for (const name of ["fx", "fy", "fr"]) {
1496
+ if (inheritedAttr(el, elements, name) != null) {
1497
+ onWarn?.(`gradient ${name} is not modeled; the gradient is centered on its cx, cy`);
1498
+ break;
1499
+ }
1500
+ }
1501
+ const spread = inheritedAttr(el, elements, "spreadMethod");
1502
+ if (spread === "reflect" || spread === "repeat") {
1503
+ onWarn?.(`spreadMethod="${spread}" is not modeled; the gradient pads`);
1504
+ }
1505
+ }
1506
+ function coord(el, elements, name, axis, fraction, units, onWarn) {
1507
+ const raw = inheritedAttr(el, elements, name);
1508
+ if (units === "bounds") return num(raw, fraction);
1509
+ if (raw != null) return resolveLength(raw, el, name, axis, fraction, onWarn ?? (() => {
1510
+ }));
1511
+ let unresolved = false;
1512
+ const v = resolveLength(`${fraction * 100}%`, el, name, axis, fraction, () => {
1513
+ unresolved = true;
1514
+ });
1515
+ return unresolved ? fraction : v;
1516
+ }
1413
1517
  function readLinearGradient(el, elements, onWarn) {
1414
1518
  warnGradientTransform(el, elements, onWarn);
1519
+ warnUnmodeled(el, elements, onWarn);
1520
+ const units = readGradientUnits(el, elements);
1521
+ const at = (name, axis, fraction) => coord(el, elements, name, axis, fraction, units, onWarn);
1415
1522
  return {
1416
1523
  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
- },
1524
+ from: { x: at("x1", "x", 0), y: at("y1", "y", 0) },
1525
+ to: { x: at("x2", "x", 1), y: at("y2", "y", 0) },
1425
1526
  stops: inheritedStops(el, elements, onWarn),
1426
- units: readGradientUnits(el, elements),
1527
+ units,
1427
1528
  interpolate: readInterpolate(el, elements)
1428
1529
  };
1429
1530
  }
1430
1531
  function readRadialGradient(el, elements, onWarn) {
1431
1532
  warnGradientTransform(el, elements, onWarn);
1533
+ warnUnmodeled(el, elements, onWarn);
1534
+ const units = readGradientUnits(el, elements);
1535
+ const at = (name, axis, fraction) => coord(el, elements, name, axis, fraction, units, onWarn);
1432
1536
  return {
1433
1537
  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),
1538
+ center: { x: at("cx", "x", 0.5), y: at("cy", "y", 0.5) },
1539
+ radius: at("r", "other", 0.5),
1439
1540
  stops: inheritedStops(el, elements, onWarn),
1440
- units: readGradientUnits(el, elements),
1541
+ units,
1441
1542
  interpolate: readInterpolate(el, elements)
1442
1543
  };
1443
1544
  }
@@ -1579,13 +1680,14 @@ function writesInset(entry) {
1579
1680
  return entry !== void 0 && !entry.toSvg && (entry.inset ?? 0) !== 0;
1580
1681
  }
1581
1682
  function defaultMarkerXml(id, entry, onWarn, key, size) {
1582
- const unit = size === void 0 ? 1 : resolveMarkerSize({ key: key ?? id, size }, 1);
1683
+ const name = key ?? id;
1684
+ const unit = size === void 0 ? 1 : resolveMarkerSize({ key: name, size }, 1);
1583
1685
  const path = entry.path({ size: unit, stroke: { paint: { fill: "solid", color: "#000" } } });
1584
1686
  const d = serializePathD(path);
1585
1687
  const fill = markerPaintAttrs("fill", entry.fill, id, onWarn);
1586
1688
  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
1689
  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`}"`;
1690
+ 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
1691
  const inset = writesInset(entry) ? ` ${WEASEL_NS_PREFIX}:inset="${trimNumber(entry.inset)}"` : "";
1590
1692
  return `<marker id="${id}" ${units}${inset} refX="0" refY="0" orient="${orient}" overflow="visible"><path d="${d}" ${fill}${outline}/></marker>`;
1591
1693
  }
@@ -1639,10 +1741,83 @@ function stopXml(s) {
1639
1741
  const alpha = parseInt(m[2], 16) / 255;
1640
1742
  return `<stop offset="${trimNumber(s.offset)}" stop-color="#${m[1].toLowerCase()}" stop-opacity="${trimNumber(alpha)}"/>`;
1641
1743
  }
1642
- return `<stop offset="${trimNumber(s.offset)}" stop-color="${s.color}"/>`;
1744
+ return `<stop offset="${trimNumber(s.offset)}" stop-color="${escapeAttr(s.color)}"/>`;
1745
+ }
1746
+ var KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
1747
+ function rectElementToPath(x, y, width2, height2, rx, ry, m = IDENTITY_MATRIX) {
1748
+ if (rx <= 0 && ry <= 0) {
1749
+ return transformPath({ kind: "rect", x, y, width: width2, height: height2 }, m);
1750
+ }
1751
+ const r = Math.min(Math.max(rx, ry), Math.min(width2, height2) / 2);
1752
+ const r2 = Math.min(rx > 0 ? rx : r, ry > 0 ? ry : r, Math.min(width2, height2) / 2);
1753
+ const rad = r2;
1754
+ const k = KAPPA * rad;
1755
+ const x2 = x + width2;
1756
+ const y2 = y + height2;
1757
+ const b = new PathBuilder();
1758
+ b.moveTo(x + rad, y);
1759
+ b.lineTo(x2 - rad, y);
1760
+ b.curveTo(x2 - rad + k, y, x2, y + rad - k, x2, y + rad);
1761
+ b.lineTo(x2, y2 - rad);
1762
+ b.curveTo(x2, y2 - rad + k, x2 - rad + k, y2, x2 - rad, y2);
1763
+ b.lineTo(x + rad, y2);
1764
+ b.curveTo(x + rad - k, y2, x, y2 - rad + k, x, y2 - rad);
1765
+ b.lineTo(x, y + rad);
1766
+ b.curveTo(x, y + rad - k, x + rad - k, y, x + rad, y);
1767
+ b.close();
1768
+ return transformPath(b.build(), m);
1769
+ }
1770
+ function ellipseToPath(cx, cy, rx, ry, m = IDENTITY_MATRIX) {
1771
+ const kx = KAPPA * rx;
1772
+ const ky = KAPPA * ry;
1773
+ const b = new PathBuilder();
1774
+ b.moveTo(cx + rx, cy);
1775
+ b.curveTo(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry);
1776
+ b.curveTo(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy);
1777
+ b.curveTo(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry);
1778
+ b.curveTo(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy);
1779
+ b.close();
1780
+ return transformPath(b.build(), m);
1781
+ }
1782
+ function circleToPath(cx, cy, r, m = IDENTITY_MATRIX) {
1783
+ return ellipseToPath(cx, cy, r, r, m);
1784
+ }
1785
+ function lineToPath(x1, y1, x2, y2, m = IDENTITY_MATRIX) {
1786
+ const b = new PathBuilder();
1787
+ b.moveTo(x1, y1);
1788
+ b.lineTo(x2, y2);
1789
+ return transformPath(b.build(), m);
1790
+ }
1791
+ function parsePoints(raw) {
1792
+ const out = [];
1793
+ const nums = raw.match(/[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g);
1794
+ if (!nums) return out;
1795
+ for (let i = 0; i + 1 < nums.length; i += 2) {
1796
+ out.push({ x: parseFloat(nums[i]), y: parseFloat(nums[i + 1]) });
1797
+ }
1798
+ return out;
1799
+ }
1800
+ function polylineToPath(points, m = IDENTITY_MATRIX) {
1801
+ if (points.length === 0) {
1802
+ return { kind: "polygon", commands: new Uint8Array(), coords: new Float32Array(), fillRule: "nonzero" };
1803
+ }
1804
+ const b = new PathBuilder();
1805
+ b.moveTo(points[0].x, points[0].y);
1806
+ for (let i = 1; i < points.length; i++) b.lineTo(points[i].x, points[i].y);
1807
+ return transformPath(b.build(), m);
1808
+ }
1809
+ function polygonToPath(points, m = IDENTITY_MATRIX) {
1810
+ if (points.length === 0) {
1811
+ return { kind: "polygon", commands: new Uint8Array(), coords: new Float32Array(), fillRule: "nonzero" };
1812
+ }
1813
+ const b = new PathBuilder();
1814
+ b.moveTo(points[0].x, points[0].y);
1815
+ for (let i = 1; i < points.length; i++) b.lineTo(points[i].x, points[i].y);
1816
+ b.close();
1817
+ return transformPath(b.build(), m);
1643
1818
  }
1644
1819
 
1645
- // src/parse.ts
1820
+ // src/lowerLeaf.ts
1646
1821
  var SUPPORTED_LEAF_TAGS = /* @__PURE__ */ new Set([
1647
1822
  "rect",
1648
1823
  "circle",
@@ -1652,7 +1827,6 @@ var SUPPORTED_LEAF_TAGS = /* @__PURE__ */ new Set([
1652
1827
  "polygon",
1653
1828
  "path"
1654
1829
  ]);
1655
- var SUPPORTED_GROUP_TAGS = /* @__PURE__ */ new Set(["g", "svg"]);
1656
1830
  var IGNORED_TAGS = /* @__PURE__ */ new Set([
1657
1831
  "defs",
1658
1832
  "lineargradient",
@@ -1665,56 +1839,56 @@ var IGNORED_TAGS = /* @__PURE__ */ new Set([
1665
1839
  "metadata",
1666
1840
  "style"
1667
1841
  ]);
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}`] };
1842
+ function lowerLeaf(el, tag, m, onWarn) {
1843
+ const x = (name) => readLength(el, name, "x", 0, onWarn);
1844
+ const y = (name) => readLength(el, name, "y", 0, onWarn);
1845
+ const r = (name) => readLength(el, name, "other", 0, onWarn);
1846
+ if (tag === "rect") {
1847
+ return rectElementToPath(x("x"), y("y"), x("width"), y("height"), x("rx"), y("ry"), m);
1678
1848
  }
1679
- const errEl = doc.getElementsByTagName("parsererror")[0];
1680
- if (errEl) {
1681
- return { nodes: [], warnings: [`SVG parse error: ${errEl.textContent ?? "unknown"}`] };
1849
+ if (tag === "circle") return circleToPath(x("cx"), y("cy"), r("r"), m);
1850
+ if (tag === "ellipse") return ellipseToPath(x("cx"), y("cy"), x("rx"), y("ry"), m);
1851
+ if (tag === "line") return lineToPath(x("x1"), y("y1"), x("x2"), y("y2"), m);
1852
+ if (tag === "polyline") return polylineToPath(parsePoints(el.getAttribute("points") ?? ""), m);
1853
+ if (tag === "polygon") return polygonToPath(parsePoints(el.getAttribute("points") ?? ""), m);
1854
+ if (tag === "path") {
1855
+ const d = el.getAttribute("d") ?? "";
1856
+ if (!d.trim()) {
1857
+ onWarn("<path> with empty d=");
1858
+ return null;
1859
+ }
1860
+ const raw = pathFromD(d, onWarn);
1861
+ const transformed = transformPath(raw, m);
1862
+ if (transformed.kind === "polygon") {
1863
+ const rect = polygonToRectIfAxisAligned(transformed);
1864
+ if (rect) return rect;
1865
+ }
1866
+ return transformed;
1682
1867
  }
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;
1868
+ return null;
1717
1869
  }
1870
+ function polygonToRectIfAxisAligned(p) {
1871
+ const cmds = p.commands;
1872
+ if (cmds.length !== 5) return null;
1873
+ if (cmds[0] !== PATH_M) return null;
1874
+ if (cmds[1] !== PATH_L || cmds[2] !== PATH_L || cmds[3] !== PATH_L) return null;
1875
+ if (cmds[4] !== PATH_Z) return null;
1876
+ const c = p.coords;
1877
+ if (c.length !== 8) return null;
1878
+ const x0 = c[0], y0 = c[1];
1879
+ const x1 = c[2], y1 = c[3];
1880
+ const x2 = c[4], y2 = c[5];
1881
+ const x3 = c[6], y3 = c[7];
1882
+ if (y0 !== y1 || x1 !== x2 || y2 !== y3 || x3 !== x0) return null;
1883
+ const x = Math.min(x0, x1);
1884
+ const y = Math.min(y0, y2);
1885
+ const width2 = Math.abs(x1 - x0);
1886
+ const height2 = Math.abs(y2 - y1);
1887
+ if (width2 === 0 || height2 === 0) return null;
1888
+ return { kind: "rect", x, y, width: width2, height: height2 };
1889
+ }
1890
+
1891
+ // src/viewBox.ts
1718
1892
  function parseViewBoxAttr(raw) {
1719
1893
  if (raw == null) return void 0;
1720
1894
  const parts = raw.trim().split(/[\s,]+/).filter(Boolean);
@@ -1723,83 +1897,6 @@ function parseViewBoxAttr(raw) {
1723
1897
  if (nums.some((n) => !Number.isFinite(n))) return void 0;
1724
1898
  return { x: nums[0], y: nums[1], width: nums[2], height: nums[3] };
1725
1899
  }
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
1900
  function collectClipPaths(root, onWarn) {
1804
1901
  const out = /* @__PURE__ */ new Map();
1805
1902
  for (const [id, el] of collectElementsByTag(root, /* @__PURE__ */ new Set(["clippath"]))) {
@@ -1826,192 +1923,23 @@ function collectClipPaths(root, onWarn) {
1826
1923
  return out;
1827
1924
  }
1828
1925
  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());
1926
+ const raw = ownProp(el, "clip-path");
1927
+ if (raw == null) return void 0;
1928
+ const ref = readProperty("clip-path", raw);
1929
+ if (ref === "none") return void 0;
1832
1930
  if (!ref) {
1833
- onWarn(`clip-path="${attr}" is not a url(#id) reference; the clip is dropped`);
1931
+ onWarn(`clip-path="${raw}" is not a url(#id) reference; the clip is dropped`);
1834
1932
  return void 0;
1835
1933
  }
1836
- const path = clips.get(ref[1]);
1934
+ const path = clips.get(ref.id);
1837
1935
  if (!path) {
1838
- onWarn(`clip-path references #${ref[1]}, which no <clipPath> in the document defines`);
1936
+ onWarn(`clip-path references #${ref.id}, which no <clipPath> in the document defines`);
1839
1937
  return void 0;
1840
1938
  }
1841
1939
  return transformPath(path, ctm);
1842
1940
  }
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
- 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
- };
1959
- 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);
1972
- if (tag === "polygon") return polygonToPath(parsePoints(el.getAttribute("points") ?? ""), m);
1973
- if (tag === "path") {
1974
- const d = el.getAttribute("d") ?? "";
1975
- if (!d.trim()) {
1976
- onWarn("<path> with empty d=");
1977
- return null;
1978
- }
1979
- const raw = pathFromD(d, onWarn);
1980
- const transformed = transformPath(raw, m);
1981
- if (transformed.kind === "polygon") {
1982
- const rect = polygonToRectIfAxisAligned(transformed);
1983
- if (rect) return rect;
1984
- }
1985
- return transformed;
1986
- }
1987
- return null;
1988
- }
1989
- function polygonToRectIfAxisAligned(p) {
1990
- const cmds = p.commands;
1991
- if (cmds.length !== 5) return null;
1992
- if (cmds[0] !== PATH_M) return null;
1993
- if (cmds[1] !== PATH_L || cmds[2] !== PATH_L || cmds[3] !== PATH_L) return null;
1994
- if (cmds[4] !== PATH_Z) return null;
1995
- const c = p.coords;
1996
- if (c.length !== 8) return null;
1997
- const x0 = c[0], y0 = c[1];
1998
- const x1 = c[2], y1 = c[3];
1999
- const x2 = c[4], y2 = c[5];
2000
- const x3 = c[6], y3 = c[7];
2001
- if (y0 !== y1 || x1 !== x2 || y2 !== y3 || x3 !== x0) return null;
2002
- const x = Math.min(x0, x1);
2003
- const y = Math.min(y0, y2);
2004
- const width2 = Math.abs(x1 - x0);
2005
- const height2 = Math.abs(y2 - y1);
2006
- if (width2 === 0 || height2 === 0) return null;
2007
- return { kind: "rect", x, y, width: width2, height: height2 };
2008
- }
2009
- function pathAabb(path) {
2010
- if (path.kind === "rect") {
2011
- return { x: path.x, y: path.y, width: path.width, height: path.height };
2012
- }
2013
- return boundsOfPath(path);
2014
- }
1941
+
1942
+ // src/readPaint.ts
2015
1943
  function readPaint2(style, attr, defaultColor, gradients, onWarn) {
2016
1944
  const raw = resolveCurrentColor(style[attr] ?? null, style);
2017
1945
  const opacity = styleValue(style, `${attr}-opacity`, onWarn);
@@ -2089,11 +2017,11 @@ var STROKE_KEYS = [
2089
2017
  function isNonScalingStroke(el) {
2090
2018
  return ownValue(el, "vector-effect") === "non-scaling-stroke";
2091
2019
  }
2092
- function ownStrokeStyle(el) {
2020
+ function ownStrokeStyle(el, computed) {
2093
2021
  const out = {};
2094
2022
  for (const k of STROKE_KEYS) {
2095
2023
  const v = ownProp(el, k);
2096
- if (v != null) out[k] = v;
2024
+ if (v != null) out[k] = v === "inherit" ? v : computed[k];
2097
2025
  }
2098
2026
  return out;
2099
2027
  }
@@ -2117,6 +2045,92 @@ function coreStroke(stroke) {
2117
2045
  if (stroke.markerEnd) out.markerEnd = stroke.markerEnd;
2118
2046
  return out;
2119
2047
  }
2048
+ var near = (v, target) => Math.abs(v - target) <= 1e-9;
2049
+ function placeBox(box, m, stretch) {
2050
+ const [a, b, c, d] = m;
2051
+ const scale = Math.sqrt(Math.abs(a * d - b * c));
2052
+ const eps = 1e-4 * Math.hypot(a, b, c, d);
2053
+ const uniform = Math.abs(a - d) <= eps;
2054
+ if (Math.abs(b) <= eps && Math.abs(c) <= eps && (stretch || uniform && a > 0)) {
2055
+ const p0 = applyMatrix(m, box.x, box.y);
2056
+ const p1 = applyMatrix(m, box.x + box.width, box.y + box.height);
2057
+ return {
2058
+ x: Math.min(p0.x, p1.x),
2059
+ y: Math.min(p0.y, p1.y),
2060
+ width: Math.abs(p1.x - p0.x),
2061
+ height: Math.abs(p1.y - p0.y),
2062
+ rotation: 0,
2063
+ scale,
2064
+ flipX: a < 0,
2065
+ flipY: d < 0
2066
+ };
2067
+ }
2068
+ const p = Math.hypot(a, b);
2069
+ const q = (a * c + b * d) / p;
2070
+ const r = (a * d - b * c) / p;
2071
+ const settle = (v) => Math.abs(v - 1) <= 1e-5 ? 1 : v;
2072
+ const sx = settle(stretch ? p : scale);
2073
+ const sy = settle(stretch ? Math.abs(r) : scale);
2074
+ const flipY = stretch && r < 0;
2075
+ const ry = flipY ? -sy : sy;
2076
+ const linear = [p / sx, 0, q / ry, r / ry];
2077
+ const center = applyMatrix(m, box.x + box.width / 2, box.y + box.height / 2);
2078
+ const width2 = box.width * sx;
2079
+ const height2 = box.height * sy;
2080
+ const placed = {
2081
+ x: center.x - width2 / 2,
2082
+ y: center.y - height2 / 2,
2083
+ width: width2,
2084
+ height: height2,
2085
+ rotation: Math.atan2(b, a),
2086
+ scale: settle(scale),
2087
+ flipX: false,
2088
+ flipY
2089
+ };
2090
+ if (!near(linear[0], 1) || !near(linear[2], 0) || !near(linear[3], 1)) placed.transform = linear;
2091
+ return placed;
2092
+ }
2093
+ function placeNode(node, tag, m, stretch, onWarn) {
2094
+ if (isIdentity(m)) return 1;
2095
+ const cx = node.x + node.width / 2;
2096
+ const cy = node.y + node.height / 2;
2097
+ if (!invert(m)) {
2098
+ onWarn(`<${tag}> sits under a singular transform; kept its size`);
2099
+ const center = applyMatrix(m, cx, cy);
2100
+ node.x = center.x - node.width / 2;
2101
+ node.y = center.y - node.height / 2;
2102
+ return 1;
2103
+ }
2104
+ const turn = decomposeRotation(m, cx, cy);
2105
+ if (turn != null) {
2106
+ node.rotation = turn;
2107
+ return 1;
2108
+ }
2109
+ const placed = placeBox(node, m, stretch);
2110
+ node.x = placed.x;
2111
+ node.y = placed.y;
2112
+ node.width = placed.width;
2113
+ node.height = placed.height;
2114
+ if (Math.abs(placed.rotation) > 1e-9) node.rotation = placed.rotation;
2115
+ if (placed.flipX) node.flipX = !node.flipX;
2116
+ if (placed.flipY) node.flipY = !node.flipY;
2117
+ if (placed.transform) node.transform = placed.transform;
2118
+ return placed.scale;
2119
+ }
2120
+ function boxTransformAttr(node) {
2121
+ if (poseLinearOf(node)) {
2122
+ const frame = poseFrameOf(node);
2123
+ const m = frame && formatMatrix(frame);
2124
+ return m ? `transform="${m}"` : null;
2125
+ }
2126
+ if (!node.rotation) return null;
2127
+ const cx = node.x + node.width / 2;
2128
+ const cy = node.y + node.height / 2;
2129
+ const deg = node.rotation * 180 / Math.PI;
2130
+ return `transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`;
2131
+ }
2132
+
2133
+ // src/readImage.ts
2120
2134
  var XLINK_NS2 = "http://www.w3.org/1999/xlink";
2121
2135
  function parseImageElement(el, ctm, onWarn) {
2122
2136
  const href = imageHref(el);
@@ -2134,45 +2148,15 @@ function imageHref(el) {
2134
2148
  return el.getAttribute("href") ?? el.getAttributeNS(XLINK_NS2, "href") ?? el.getAttribute("xlink:href");
2135
2149
  }
2136
2150
  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
- };
2151
+ const rawX = readLength(boxEl, "x", "x", 0, onWarn);
2152
+ const rawY = readLength(boxEl, "y", "y", 0, onWarn);
2153
+ const rawW = readLength(boxEl, "width", "x", 0, onWarn);
2154
+ const rawH = readLength(boxEl, "height", "y", 0, onWarn);
2155
+ const node = { kind: "image", href, x: rawX, y: rawY, width: rawW, height: rawH };
2157
2156
  const opacity = ownValue(ownerEl, "opacity");
2158
2157
  if (opacity != null) node.opacity = opacity;
2159
2158
  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
- }
2159
+ placeNode(node, "image", multiply(ctm, localTransform), true, onWarn);
2176
2160
  return node;
2177
2161
  }
2178
2162
  function parseCroppedImage(g, ctm, onWarn) {
@@ -2186,10 +2170,66 @@ function parseCroppedImage(g, ctm, onWarn) {
2186
2170
  node.source = source;
2187
2171
  }
2188
2172
  const flip = parseTransform(img.getAttribute("transform"), onWarn);
2189
- if (flip[0] < 0) node.flipX = true;
2190
- if (flip[3] < 0) node.flipY = true;
2173
+ if (flip[0] < 0) toggle(node, "flipX");
2174
+ if (flip[3] < 0) toggle(node, "flipY");
2191
2175
  return node;
2192
2176
  }
2177
+ function toggle(node, key) {
2178
+ if (node[key]) delete node[key];
2179
+ else node[key] = true;
2180
+ }
2181
+ function svgDirection(style, text) {
2182
+ return paragraphDirection(text, style?.direction ?? "auto");
2183
+ }
2184
+ function anchorOffset(style, width2, text) {
2185
+ const edge = resolveAlign(style?.align ?? "left", svgDirection(style, text));
2186
+ return edge === "center" ? width2 / 2 : edge === "right" ? width2 : 0;
2187
+ }
2188
+
2189
+ // src/tspanLines.ts
2190
+ var LineTracker = class {
2191
+ y;
2192
+ constructor(text) {
2193
+ this.y = text.getAttribute("y");
2194
+ }
2195
+ /** Whether `tspan` opens a new line. Call once per tspan, in order. */
2196
+ startsLine(tspan) {
2197
+ const y = tspan.getAttribute("y");
2198
+ const movedY = y != null && y !== this.y;
2199
+ if (y != null) this.y = y;
2200
+ if (!tspan.hasAttribute("x")) return false;
2201
+ const dy = parseFloat(tspan.getAttribute("dy") ?? "");
2202
+ return movedY || Number.isFinite(dy) && dy !== 0;
2203
+ }
2204
+ };
2205
+ function warnUnreadPositions(el, onWarn) {
2206
+ const tag = el.tagName.toLowerCase();
2207
+ for (const name of ["x", "y"]) {
2208
+ const raw = el.getAttribute(name)?.trim() ?? "";
2209
+ if (/[\s,]/.test(raw)) onWarn(`<${tag} ${name}="${raw}"> positions each glyph; only the first value is read`);
2210
+ }
2211
+ for (const name of ["dx", "rotate"]) {
2212
+ const raw = el.getAttribute(name)?.trim() ?? "";
2213
+ if (raw.split(/[\s,]+/).some((v) => parseFloat(v) !== 0 && v !== "")) {
2214
+ onWarn(`<${tag} ${name}="${raw}"> is not read; the glyphs keep their own advance and angle`);
2215
+ }
2216
+ }
2217
+ }
2218
+ function joinLines(lines) {
2219
+ const out = [];
2220
+ let breaks = 0;
2221
+ for (const line of lines) {
2222
+ if (line.length > 0) {
2223
+ if (breaks > 0) line[0] = { ...line[0], text: "\n".repeat(breaks) + line[0].text };
2224
+ out.push(...line);
2225
+ breaks = 0;
2226
+ }
2227
+ if (out.length > 0) breaks++;
2228
+ }
2229
+ return out;
2230
+ }
2231
+
2232
+ // src/readText.ts
2193
2233
  var XML_NS = "http://www.w3.org/XML/1998/namespace";
2194
2234
  function preservesSpace(el) {
2195
2235
  for (let e = el; e; e = e.parentElement) {
@@ -2233,12 +2273,39 @@ function laidOutWidth(runs, style, plain) {
2233
2273
  maxWidth: Infinity,
2234
2274
  lineHeight: resolved.lineHeight,
2235
2275
  align: resolved.align,
2236
- direction: resolved.direction
2276
+ direction: resolved.direction,
2277
+ tabSize: resolved.tabSize
2237
2278
  });
2238
2279
  if (bounds.width > 0) return bounds.width;
2239
2280
  const longest = plain.split("\n").reduce((max, line) => Math.max(max, line.length), 0);
2240
2281
  return longest * resolved.fontSize * ESTIMATED_GLYPH_ADVANCE_EM;
2241
2282
  }
2283
+ function placeTextNode(node, m, onWarn) {
2284
+ const cx = node.x + node.width / 2;
2285
+ const cy = node.y + node.height / 2;
2286
+ const scale = placeNode(node, "text", m, false, onWarn);
2287
+ const inBox = [
2288
+ scale,
2289
+ 0,
2290
+ 0,
2291
+ scale,
2292
+ node.x + node.width / 2 - scale * cx,
2293
+ node.y + node.height / 2 - scale * cy
2294
+ ];
2295
+ if (isIdentity(inBox)) return;
2296
+ if (Math.abs(scale - 1) > 1e-9) {
2297
+ node.style = scaleTextStyle(node.style, scale);
2298
+ if (node.runs) node.runs = scaleRuns(node.runs, scale);
2299
+ if (node.stroke) node.stroke = scaleStroke(node.stroke, scale);
2300
+ }
2301
+ if (node.fill) node.fill = fillThroughMatrix(node.fill, inBox);
2302
+ if (node.stroke?.paint) {
2303
+ node.stroke = { ...node.stroke, paint: fillThroughMatrix(node.stroke.paint, inBox) };
2304
+ }
2305
+ if (node.runs) {
2306
+ node.runs = node.runs.map((r) => r.fill ? { ...r, fill: fillThroughMatrix(r.fill, inBox) } : r);
2307
+ }
2308
+ }
2242
2309
  function parseTextElement(el, ctm, style, gradients, onWarn) {
2243
2310
  const num2 = (raw, fallback) => {
2244
2311
  if (raw == null) return fallback;
@@ -2246,11 +2313,9 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2246
2313
  return Number.isFinite(n) ? n : fallback;
2247
2314
  };
2248
2315
  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];
2316
+ warnUnreadPositions(el, onWarn);
2317
+ const ax = readLength(el, "x", "x", 0, onWarn);
2318
+ const ay = readLength(el, "y", "y", 0, onWarn);
2254
2319
  const leafStyle = deriveStyle(style, el);
2255
2320
  const textStyle = readTextStyle(leafStyle, onWarn);
2256
2321
  const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
@@ -2260,26 +2325,32 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2260
2325
  const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
2261
2326
  const topY = explicitTopAnchor ? ay : ay - fontSize;
2262
2327
  const inheritedShift = ownValue(el, "baseline-shift", onWarn) ?? {};
2263
- const runs = [];
2328
+ const runLines = [[]];
2329
+ const tracker = new LineTracker(el);
2330
+ const push = (run) => {
2331
+ runLines[runLines.length - 1].push(run);
2332
+ };
2264
2333
  for (let i = 0; i < el.childNodes.length; i++) {
2265
2334
  const child = el.childNodes[i];
2266
2335
  if (child.nodeType === 3) {
2267
2336
  const t = child.textContent ?? "";
2268
2337
  if (!t) continue;
2269
- runs.push({ text: t, ...inheritedShift });
2338
+ push({ text: t, ...inheritedShift });
2270
2339
  } else if (child.nodeType === 1) {
2271
2340
  const sp = child;
2272
2341
  if (sp.tagName.toLowerCase() !== "tspan") {
2273
2342
  onWarn(`<text> child <${sp.tagName}> not supported; flattening text content`);
2274
2343
  const t = sp.textContent ?? "";
2275
- if (t) runs.push({ text: t, ...inheritedShift });
2344
+ if (t) push({ text: t, ...inheritedShift });
2276
2345
  continue;
2277
2346
  }
2278
- const run = readTspanRun(sp, gradients, leafStyle, onWarn);
2279
- runs.push(run);
2347
+ warnUnreadPositions(sp, onWarn);
2348
+ if (tracker.startsLine(sp)) runLines.push([]);
2349
+ push(readTspanRun(sp, gradients, leafStyle, onWarn));
2280
2350
  }
2281
2351
  }
2282
- if (!preservesSpace(el)) collapseRunWhitespace(runs);
2352
+ if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
2353
+ const runs = joinLines(runLines);
2283
2354
  const plain = runs.map((r) => r.text).join("");
2284
2355
  const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
2285
2356
  const dataH = num2(el.getAttribute("data-weasel-height"), NaN);
@@ -2289,7 +2360,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2289
2360
  const opacity = ownValue(el, "opacity");
2290
2361
  const node = {
2291
2362
  kind: "text",
2292
- x: ax - anchorOffset(textStyle, width2),
2363
+ x: ax - anchorOffset(textStyle, width2, plain),
2293
2364
  y: topY,
2294
2365
  width: width2,
2295
2366
  height: height2,
@@ -2299,30 +2370,20 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2299
2370
  (r) => Object.entries(r).some(([k, v]) => k !== "text" && v !== void 0)
2300
2371
  );
2301
2372
  if (hasStyling) node.runs = runs;
2373
+ if (textStyle.direction === void 0 && !el.hasAttribute("data-weasel-width")) {
2374
+ textStyle.direction = "ltr";
2375
+ }
2302
2376
  if (el.getAttribute("data-weasel-wrap") === "true") textStyle.wrap = true;
2303
2377
  if (el.getAttribute("data-weasel-align") === "justify") textStyle.align = "justify";
2378
+ const tabSize = Number(el.getAttribute("data-weasel-tab-size") ?? NaN);
2379
+ if (Number.isFinite(tabSize)) textStyle.tabSize = tabSize;
2304
2380
  const va = el.getAttribute("data-weasel-vertical-align");
2305
2381
  if (va === "center" || va === "bottom") node.verticalAlign = va;
2306
2382
  if (Object.keys(textStyle).length > 0) node.style = textStyle;
2307
2383
  if (textPaint.fill !== void 0) node.fill = textPaint.fill;
2308
2384
  if (textPaint.stroke != null) node.stroke = textPaint.stroke;
2309
2385
  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
- }
2386
+ placeTextNode(node, multiply(ctm, localTransform), onWarn);
2326
2387
  return node;
2327
2388
  }
2328
2389
  function readTspanRun(el, gradients, style, onWarn) {
@@ -2338,7 +2399,8 @@ function readTspanRun(el, gradients, style, onWarn) {
2338
2399
  const size = ownValue(el, "font-size", onWarn);
2339
2400
  if (size?.fontSize != null) run.fontSize = size.fontSize;
2340
2401
  if (size?.fontScale != null) run.fontScale = size.fontScale;
2341
- const ls = ownValue(el, "letter-spacing", onWarn);
2402
+ const tspanStyle = deriveStyle(style, el);
2403
+ const ls = ownValue(el, "letter-spacing", onWarn, fontSizeOf(tspanStyle));
2342
2404
  if (ls != null) run.letterSpacing = ls;
2343
2405
  const shift = ownValue(el, "baseline-shift", onWarn);
2344
2406
  if (shift?.script != null) run.script = shift.script;
@@ -2351,11 +2413,12 @@ function readTspanRun(el, gradients, style, onWarn) {
2351
2413
  if (transform) run.textTransform = transform;
2352
2414
  const variant = ownValue(el, "font-variant", onWarn);
2353
2415
  if (variant) run.fontVariantCaps = variant;
2354
- const tspanStyle = deriveStyle(style, el);
2355
2416
  const fillAttr = resolveCurrentColor(ownProp(el, "fill"), tspanStyle);
2356
2417
  if (fillAttr) {
2357
2418
  const parsed = readProperty("fill", fillAttr);
2358
- if (parsed?.kind === "solid") {
2419
+ if (parsed?.kind === "none") {
2420
+ run.fill = null;
2421
+ } else if (parsed?.kind === "solid") {
2359
2422
  run.fill = { fill: "solid", color: parsed.color };
2360
2423
  } else if (parsed?.kind === "ref") {
2361
2424
  const paint = gradients.get(parsed.id);
@@ -2363,7 +2426,7 @@ function readTspanRun(el, gradients, style, onWarn) {
2363
2426
  else if (parsed.fallback) run.fill = { fill: "solid", color: parsed.fallback.color };
2364
2427
  }
2365
2428
  }
2366
- const stroke = coreStroke(readStroke(ownStrokeStyle(el), gradients, onWarn, isNonScalingStroke(el)));
2429
+ const stroke = coreStroke(readStroke(ownStrokeStyle(el, tspanStyle), gradients, onWarn, isNonScalingStroke(el)));
2367
2430
  if (stroke) run.stroke = stroke;
2368
2431
  return run;
2369
2432
  }
@@ -2371,17 +2434,14 @@ function readTextStyle(style, onWarn) {
2371
2434
  const out = {};
2372
2435
  const size = styleValue(style, "font-size", onWarn);
2373
2436
  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
2437
  const ff = styleValue(style, "font-family", onWarn);
2379
2438
  if (ff) out.fontFamily = ff;
2380
2439
  const fw = styleValue(style, "font-weight", onWarn);
2381
2440
  if (fw != null) out.fontWeight = fw;
2382
2441
  const fs = styleValue(style, "font-style", onWarn);
2383
2442
  if (fs) out.fontStyle = fs;
2384
- if (styleValue(style, "direction") === "rtl") out.direction = "rtl";
2443
+ const direction = styleValue(style, "direction");
2444
+ if (direction === "rtl" || direction === "ltr") out.direction = direction;
2385
2445
  const ltr = out.direction !== "rtl";
2386
2446
  const anchor = styleValue(style, "text-anchor") ?? (ltr ? void 0 : "start");
2387
2447
  if (anchor === "start") out.align = ltr ? "left" : "right";
@@ -2489,13 +2549,8 @@ function sizedMarkerRef(el) {
2489
2549
  return { key, size: px ? { px: n } : n };
2490
2550
  }
2491
2551
  function markerInsetAttr(el) {
2492
- const n = parseFloat(weaselAttr(el, "inset") ?? "");
2493
- return Number.isFinite(n) && n > 0 ? n : 0;
2494
- }
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);
2552
+ const n = parseFloat(weaselAttr(el, "inset") ?? "");
2553
+ return Number.isFinite(n) && n > 0 ? n : 0;
2499
2554
  }
2500
2555
  function markerRef(raw, lo, span) {
2501
2556
  const v = raw?.trim();
@@ -2638,6 +2693,266 @@ function markerEntryFrom(el, id, role, strokeWidth, gradients, onWarn) {
2638
2693
  inset
2639
2694
  };
2640
2695
  }
2696
+
2697
+ // src/parse.ts
2698
+ var SUPPORTED_GROUP_TAGS = /* @__PURE__ */ new Set(["g", "svg"]);
2699
+ function parseSvg(svg, opts = {}) {
2700
+ const warnings = [];
2701
+ const onWarn = (m) => {
2702
+ warnings.push(m);
2703
+ };
2704
+ let doc;
2705
+ try {
2706
+ doc = new DOMParser().parseFromString(svg, "image/svg+xml");
2707
+ } catch (e) {
2708
+ return { nodes: [], warnings: [`failed to parse SVG: ${e.message}`] };
2709
+ }
2710
+ const errEl = doc.getElementsByTagName("parsererror")[0];
2711
+ if (errEl) {
2712
+ return { nodes: [], warnings: [`SVG parse error: ${errEl.textContent ?? "unknown"}`] };
2713
+ }
2714
+ const root = doc.documentElement;
2715
+ if (!root || root.tagName.toLowerCase() !== "svg") {
2716
+ return { nodes: [], warnings: ["root element is not <svg>"] };
2717
+ }
2718
+ bindStylesheets(doc, mediaEnvironmentFor(root, opts.media), onWarn);
2719
+ const namespaces = opts.namespaces ?? {};
2720
+ const uriToPrefix = /* @__PURE__ */ new Map();
2721
+ for (const [prefix, uri] of Object.entries(namespaces)) {
2722
+ uriToPrefix.set(uri, prefix);
2723
+ }
2724
+ const documentMeta = collectDocumentMeta(root, uriToPrefix);
2725
+ const gradients = collectGradients(root, onWarn);
2726
+ for (const [id, paint] of collectPatterns(root, onWarn)) gradients.set(id, paint);
2727
+ const rootStyle = deriveStyle(EMPTY_STYLE, root);
2728
+ const clips = collectClipPaths(root, onWarn);
2729
+ const nodes = parseChildren(root, IDENTITY_MATRIX, rootStyle, gradients, clips, onWarn, uriToPrefix);
2730
+ const markers = ingestMarkers(root, nodes, gradients, onWarn);
2731
+ const result = { nodes, warnings };
2732
+ if (markers.length > 0) result.markers = markers;
2733
+ if (documentMeta) result.documentMeta = documentMeta;
2734
+ const viewBox = parseViewBoxAttr(root.getAttribute("viewBox"));
2735
+ if (viewBox) result.viewBox = viewBox;
2736
+ const width2 = parseRootLength(root.getAttribute("width"), "width", fontSizeOf(rootStyle), onWarn);
2737
+ if (width2 != null) result.width = width2;
2738
+ const height2 = parseRootLength(root.getAttribute("height"), "height", fontSizeOf(rootStyle), onWarn);
2739
+ if (height2 != null) result.height = height2;
2740
+ for (let i = 0; i < root.children.length; i++) {
2741
+ const c = root.children[i];
2742
+ if (c.tagName.toLowerCase() === "title") {
2743
+ result.title = c.textContent ?? "";
2744
+ break;
2745
+ }
2746
+ }
2747
+ return result;
2748
+ }
2749
+ function parseRootLength(raw, name, fontSize, onWarn) {
2750
+ if (raw == null) return void 0;
2751
+ const n = parseFloat(raw);
2752
+ if (!Number.isFinite(n)) return void 0;
2753
+ const unit = /^\s*[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?\s*([a-z%]+)\s*$/i.exec(raw)?.[1];
2754
+ const scale = unit && ABSOLUTE_PX[unit.toLowerCase()];
2755
+ if (scale) return n * scale;
2756
+ const ems = unit && emsPer(unit);
2757
+ if (ems) return n * ems * fontSize;
2758
+ if (unit) onWarn(`<svg ${name}="${raw}"> unit "${unit}" is not converted; read as ${n}`);
2759
+ return n;
2760
+ }
2761
+ function collectDocumentMeta(root, uriToPrefix) {
2762
+ if (uriToPrefix.size === 0) return void 0;
2763
+ const meta = {};
2764
+ for (let i = 0; i < root.attributes.length; i++) {
2765
+ const a = root.attributes[i];
2766
+ if (!a.namespaceURI) continue;
2767
+ const prefix = uriToPrefix.get(a.namespaceURI);
2768
+ if (!prefix) continue;
2769
+ const bucket = meta[prefix] ??= {};
2770
+ (bucket.attrs ??= {})[a.localName] = a.value;
2771
+ }
2772
+ for (let i = 0; i < root.children.length; i++) {
2773
+ const c = root.children[i];
2774
+ if (!c.namespaceURI) continue;
2775
+ const prefix = uriToPrefix.get(c.namespaceURI);
2776
+ if (!prefix) continue;
2777
+ const bucket = meta[prefix] ??= {};
2778
+ const elements = bucket.elements ??= {};
2779
+ const list = elements[c.localName] ??= [];
2780
+ list.push(collectNamespacedElement(c, uriToPrefix));
2781
+ }
2782
+ return Object.keys(meta).length > 0 ? meta : void 0;
2783
+ }
2784
+ function collectNamespacedElement(el, uriToPrefix) {
2785
+ const result = { attrs: {} };
2786
+ for (let i = 0; i < el.attributes.length; i++) {
2787
+ const a = el.attributes[i];
2788
+ if (a.name.startsWith("xmlns")) continue;
2789
+ result.attrs[a.localName] = a.value;
2790
+ }
2791
+ let hasChildElements = false;
2792
+ for (let i = 0; i < el.children.length; i++) {
2793
+ const c = el.children[i];
2794
+ if (!c.namespaceURI) continue;
2795
+ const prefix = uriToPrefix.get(c.namespaceURI);
2796
+ if (!prefix) continue;
2797
+ hasChildElements = true;
2798
+ const children = result.children ??= {};
2799
+ const list = children[c.localName] ??= [];
2800
+ list.push(collectNamespacedElement(c, uriToPrefix));
2801
+ }
2802
+ if (!hasChildElements && el.textContent != null && el.textContent.trim() !== "") {
2803
+ result.text = el.textContent;
2804
+ }
2805
+ return result;
2806
+ }
2807
+ function collectElementMeta(el, uriToPrefix) {
2808
+ if (uriToPrefix.size === 0) return void 0;
2809
+ const meta = {};
2810
+ for (let i = 0; i < el.attributes.length; i++) {
2811
+ const a = el.attributes[i];
2812
+ if (!a.namespaceURI) continue;
2813
+ const prefix = uriToPrefix.get(a.namespaceURI);
2814
+ if (!prefix) continue;
2815
+ const bucket = meta[prefix] ??= {};
2816
+ (bucket.attrs ??= {})[a.localName] = a.value;
2817
+ }
2818
+ for (let i = 0; i < el.children.length; i++) {
2819
+ const c = el.children[i];
2820
+ if (!c.namespaceURI) continue;
2821
+ const prefix = uriToPrefix.get(c.namespaceURI);
2822
+ if (!prefix) continue;
2823
+ const bucket = meta[prefix] ??= {};
2824
+ const elements = bucket.elements ??= {};
2825
+ const list = elements[c.localName] ??= [];
2826
+ list.push(collectNamespacedElement(c, uriToPrefix));
2827
+ }
2828
+ return Object.keys(meta).length > 0 ? meta : void 0;
2829
+ }
2830
+ function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix) {
2831
+ const out = [];
2832
+ for (let i = 0; i < parent.children.length; i++) {
2833
+ const el = parent.children[i];
2834
+ const ns = el.namespaceURI;
2835
+ if (ns && ns !== "http://www.w3.org/2000/svg") continue;
2836
+ const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix);
2837
+ if (node) {
2838
+ if (Array.isArray(node)) out.push(...node);
2839
+ else out.push(node);
2840
+ }
2841
+ }
2842
+ return out;
2843
+ }
2844
+ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
2845
+ const tag = el.tagName.toLowerCase();
2846
+ if (IGNORED_TAGS.has(tag)) return null;
2847
+ if (ownValue(el, "display") === "none") return null;
2848
+ const containers = tag === "g" || SUPPORTED_GROUP_TAGS.has(tag);
2849
+ if (!containers && isHidden(deriveStyle(style, el))) return null;
2850
+ if (tag === "g" && el.hasAttribute("data-weasel-image")) {
2851
+ const image = parseCroppedImage(el, ctm, onWarn);
2852
+ if (image) {
2853
+ const meta2 = collectElementMeta(el, uriToPrefix);
2854
+ if (meta2) image.meta = meta2;
2855
+ return image;
2856
+ }
2857
+ }
2858
+ const clipRaw = tag === "g" ? null : ownProp(el, "clip-path");
2859
+ if (clipRaw != null && readProperty("clip-path", clipRaw) !== "none") {
2860
+ onWarn(`clip-path on <${tag}> is not modeled; only a <g> clips, so it is drawn unclipped`);
2861
+ }
2862
+ if (tag === "g") {
2863
+ const local = parseTransform(el.getAttribute("transform"), onWarn);
2864
+ const childCtm = multiply(ctm, local);
2865
+ const childStyle = deriveStyle(style, el);
2866
+ const children = parseChildren(el, childCtm, childStyle, gradients, clips, onWarn, uriToPrefix);
2867
+ const opacity2 = ownValue(el, "opacity");
2868
+ const group = { kind: "group", children };
2869
+ const clip = clipFor(el, childCtm, clips, onWarn);
2870
+ if (clip) group.clip = clip;
2871
+ if (opacity2 != null) group.opacity = opacity2;
2872
+ const meta2 = collectElementMeta(el, uriToPrefix);
2873
+ if (meta2) group.meta = meta2;
2874
+ return group;
2875
+ }
2876
+ if (SUPPORTED_GROUP_TAGS.has(tag)) {
2877
+ const childStyle = deriveStyle(style, el);
2878
+ const x = parseFloat(el.getAttribute("x") ?? "0");
2879
+ const y = parseFloat(el.getAttribute("y") ?? "0");
2880
+ const offset = [1, 0, 0, 1, Number.isFinite(x) ? x : 0, Number.isFinite(y) ? y : 0];
2881
+ if (el.hasAttribute("viewBox")) {
2882
+ onWarn("nested <svg> viewBox is not modeled; its children are not rescaled");
2883
+ }
2884
+ return parseChildren(el, multiply(ctm, offset), childStyle, gradients, clips, onWarn, uriToPrefix);
2885
+ }
2886
+ if (tag === "text") {
2887
+ const textNode = parseTextElement(el, ctm, style, gradients, onWarn);
2888
+ if (textNode && !Array.isArray(textNode) && textNode.kind === "text") {
2889
+ const meta2 = collectElementMeta(el, uriToPrefix);
2890
+ if (meta2) textNode.meta = meta2;
2891
+ }
2892
+ return textNode;
2893
+ }
2894
+ if (tag === "image") {
2895
+ const imageNode = parseImageElement(el, ctm, onWarn);
2896
+ if (imageNode) {
2897
+ const meta2 = collectElementMeta(el, uriToPrefix);
2898
+ if (meta2) imageNode.meta = meta2;
2899
+ }
2900
+ return imageNode;
2901
+ }
2902
+ if (!SUPPORTED_LEAF_TAGS.has(tag)) {
2903
+ onWarn(`unsupported element: <${el.tagName}>`);
2904
+ return null;
2905
+ }
2906
+ const localTransform = parseTransform(el.getAttribute("transform"), onWarn);
2907
+ let path = lowerLeaf(el, tag, ctm, onWarn);
2908
+ if (!path) return null;
2909
+ let rotation;
2910
+ let baked = ctm;
2911
+ if (!isIdentity(localTransform)) {
2912
+ const worldLocal = rebaseTransform(ctm, localTransform);
2913
+ const bounds = boundsOfPath(path);
2914
+ const cx = bounds.x + bounds.width / 2;
2915
+ const cy = bounds.y + bounds.height / 2;
2916
+ const angle = worldLocal && decomposeRotation(worldLocal, cx, cy);
2917
+ if (!worldLocal) {
2918
+ onWarn("leaf transform sits under a singular parent transform; dropped");
2919
+ } else if (angle != null) {
2920
+ rotation = angle;
2921
+ } else {
2922
+ path = transformPath(path, worldLocal);
2923
+ baked = multiply(ctm, localTransform);
2924
+ const rotComp = rotationComponent(worldLocal);
2925
+ if (Math.abs(rotComp) > 1e-4) {
2926
+ onWarn(`leaf transform has a rotational component that can't be cleanly stored as rotation; baked into geometry (may not round-trip identically)`);
2927
+ }
2928
+ }
2929
+ }
2930
+ const leafStyle = deriveStyle(style, el);
2931
+ const fill = paintThroughMatrix(readPaint2(leafStyle, "fill", "#000000", gradients, onWarn), baked);
2932
+ const ownStroke = readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el));
2933
+ const stroke = ownStroke && strokeThroughMatrix(ownStroke, baked);
2934
+ if (stroke && typeof stroke.width === "number" && isNonUniform(baked)) {
2935
+ onWarn(`<${tag}> stroke sits under a skew or non-uniform scale; its width takes the uniform part only`);
2936
+ }
2937
+ const opacity = ownValue(el, "opacity");
2938
+ if (styleValue(leafStyle, "fill-rule") === "evenodd" && path.kind === "polygon") {
2939
+ path = { ...path, fillRule: "evenodd" };
2940
+ }
2941
+ const node = { kind: "path", path, fill };
2942
+ if (stroke) node.stroke = stroke;
2943
+ if (opacity != null) node.opacity = opacity;
2944
+ if (rotation != null) node.rotation = rotation;
2945
+ if (tag === "line" && (leafStyle["fill"] ?? null) == null) {
2946
+ node.fill = { kind: "none" };
2947
+ }
2948
+ const meta = collectElementMeta(el, uriToPrefix);
2949
+ if (meta) node.meta = meta;
2950
+ return node;
2951
+ }
2952
+ function isHidden(style) {
2953
+ const v = styleValue(style, "visibility");
2954
+ return v === "hidden" || v === "collapse";
2955
+ }
2641
2956
  function serializeSvg(nodes, opts = {}) {
2642
2957
  const registry = new PaintServerRegistry();
2643
2958
  registerPaintServers(nodes, registry);
@@ -2649,7 +2964,7 @@ function serializeSvg(nodes, opts = {}) {
2649
2964
  rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
2650
2965
  }
2651
2966
  for (const [prefix, uri] of Object.entries(namespaces)) {
2652
- rootAttrs.push(`xmlns:${prefix}="${escapeAttr2(uri)}"`);
2967
+ rootAttrs.push(`xmlns:${prefix}="${escapeAttr(uri)}"`);
2653
2968
  }
2654
2969
  rootAttrs.push(`viewBox="${vb}"`);
2655
2970
  if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
@@ -2659,7 +2974,7 @@ function serializeSvg(nodes, opts = {}) {
2659
2974
  const bucket = opts.documentMeta[prefix];
2660
2975
  if (!bucket?.attrs) continue;
2661
2976
  for (const [name, value] of Object.entries(bucket.attrs)) {
2662
- rootAttrs.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
2977
+ rootAttrs.push(`${prefix}:${name}="${escapeAttr(value)}"`);
2663
2978
  }
2664
2979
  }
2665
2980
  }
@@ -2687,7 +3002,7 @@ function serializeSvg(nodes, opts = {}) {
2687
3002
  function namespacedElementXml(prefix, localName2, el) {
2688
3003
  const attrs = [];
2689
3004
  for (const [name, value] of Object.entries(el.attrs)) {
2690
- attrs.push(`${name}="${escapeAttr2(value)}"`);
3005
+ attrs.push(`${name}="${escapeAttr(value)}"`);
2691
3006
  }
2692
3007
  const head = attrs.length > 0 ? `<${prefix}:${localName2} ${attrs.join(" ")}>` : `<${prefix}:${localName2}>`;
2693
3008
  let body = "";
@@ -2707,7 +3022,7 @@ function metaAttrsXml(meta, namespaces) {
2707
3022
  const bucket = meta[prefix];
2708
3023
  if (!bucket?.attrs) continue;
2709
3024
  for (const [name, value] of Object.entries(bucket.attrs)) {
2710
- parts.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
3025
+ parts.push(`${prefix}:${name}="${escapeAttr(value)}"`);
2711
3026
  }
2712
3027
  }
2713
3028
  return parts.length > 0 ? ` ${parts.join(" ")}` : "";
@@ -2770,15 +3085,11 @@ function imageXml(node, namespaces) {
2770
3085
  if (node.opacity != null && node.opacity !== 1) {
2771
3086
  outer.push(`opacity="${trimNumber(node.opacity)}"`);
2772
3087
  }
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
- }
3088
+ const frame = boxTransformAttr(node);
3089
+ if (frame) outer.push(frame);
2779
3090
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
2780
3091
  const metaEls = metaElementsXml(node.meta, namespaces);
2781
- const href = `href="${escapeAttr2(node.href)}"`;
3092
+ const href = `href="${escapeAttr(node.href)}"`;
2782
3093
  if (!node.source && !node.flipX && !node.flipY) {
2783
3094
  const attrs = [href, box, 'preserveAspectRatio="none"', ...outer].join(" ");
2784
3095
  if (metaEls) return `<image ${attrs}${metaAttrs}>${metaEls}</image>`;
@@ -2839,7 +3150,7 @@ function pathXml(node, registry, namespaces, warn) {
2839
3150
  }
2840
3151
  function paintAttrs(paint, name, registry, includeOpacity = true) {
2841
3152
  if (paint.kind === "none") return [`${name}="none"`];
2842
- const out = [`${name}="${paint.kind === "solid" ? paint.color : registry.ref(paint.paint)}"`];
3153
+ const out = [`${name}="${paint.kind === "solid" ? escapeAttr(paint.color) : registry.ref(paint.paint)}"`];
2843
3154
  if (includeOpacity) out.push(...opacityAttr(`${name}-opacity`, svgPaintOpacity(paint)));
2844
3155
  return out;
2845
3156
  }
@@ -2860,7 +3171,7 @@ function coreStrokeAttrs(stroke, registry, warn) {
2860
3171
  const width2 = stroke.width ?? 1;
2861
3172
  if (!((typeof width2 === "object" ? width2.px : width2) > 0)) return [];
2862
3173
  warnStrokeAlign(stroke.align, warn);
2863
- const attrs = [`stroke="${"color" in paint ? paint.color : registry.ref(paint)}"`];
3174
+ const attrs = [`stroke="${"color" in paint ? escapeAttr(paint.color) : registry.ref(paint)}"`];
2864
3175
  attrs.push(...opacityAttr("stroke-opacity", paint.opacity));
2865
3176
  attrs.push(...strokeWidthAttrs(width2));
2866
3177
  if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
@@ -2921,7 +3232,8 @@ function computeBounds(nodes) {
2921
3232
  return;
2922
3233
  }
2923
3234
  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;
3235
+ const frame = n.kind === "path" ? null : poseFrameOf(n);
3236
+ const local = frame ? multiply(ctm, frame) : n.rotation ? rotateAboutCenter(ctm, b, n.rotation) : ctm;
2925
3237
  for (const [x, y] of corners(b)) {
2926
3238
  const px = local[0] * x + local[2] * y + local[4];
2927
3239
  const py = local[1] * x + local[3] * y + local[5];
@@ -2949,7 +3261,7 @@ function rotateAboutCenter(ctm, b, angle) {
2949
3261
  }
2950
3262
  function textXml(node, registry, namespaces, warn) {
2951
3263
  const attrs = [
2952
- `x="${trimNumber(node.x + anchorOffset(node.style, node.width))}"`,
3264
+ `x="${trimNumber(node.x + anchorOffset(node.style, node.width, node.text))}"`,
2953
3265
  `y="${trimNumber(node.y)}"`,
2954
3266
  `dominant-baseline="text-before-edge"`,
2955
3267
  // The runs model stores real line breaks; SVG's default whitespace
@@ -2960,12 +3272,12 @@ function textXml(node, registry, namespaces, warn) {
2960
3272
  ];
2961
3273
  const style = node.style;
2962
3274
  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)}"`);
3275
+ if (style?.fontFamily) attrs.push(`font-family="${escapeAttr(style.fontFamily)}"`);
3276
+ if (style?.fontWeight != null) attrs.push(`font-weight="${escapeAttr(String(style.fontWeight))}"`);
2965
3277
  if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
2966
3278
  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"');
3279
+ const direction = svgDirection(style, node.text);
3280
+ if (direction === "rtl" || style?.direction === "ltr") attrs.push(`direction="${direction}"`);
2969
3281
  if (style?.align != null || direction === "rtl") {
2970
3282
  const align = style?.align ?? "left";
2971
3283
  const anchor = align === "center" ? "middle" : align === "start" || align === "justify" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
@@ -2985,6 +3297,7 @@ function textXml(node, registry, namespaces, warn) {
2985
3297
  attrs.push('data-weasel-wrap="true"');
2986
3298
  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
3299
  }
3300
+ if (style?.tabSize != null) attrs.push(`data-weasel-tab-size="${trimNumber(style.tabSize)}"`);
2988
3301
  if (node.verticalAlign && node.verticalAlign !== "top") {
2989
3302
  attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
2990
3303
  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)`);
@@ -2992,19 +3305,15 @@ function textXml(node, registry, namespaces, warn) {
2992
3305
  if (node.fill === null) {
2993
3306
  attrs.push('fill="none"');
2994
3307
  } else if (node.fill) {
2995
- attrs.push(`fill="${"color" in node.fill ? node.fill.color : registry.ref(node.fill)}"`);
3308
+ attrs.push(`fill="${"color" in node.fill ? escapeAttr(node.fill.color) : registry.ref(node.fill)}"`);
2996
3309
  attrs.push(...opacityAttr("fill-opacity", node.fill.opacity));
2997
3310
  }
2998
3311
  for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
2999
3312
  if (node.opacity != null && node.opacity !== 1) {
3000
3313
  attrs.push(`opacity="${trimNumber(node.opacity)}"`);
3001
3314
  }
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
- }
3315
+ const frame = boxTransformAttr(node);
3316
+ if (frame) attrs.push(frame);
3008
3317
  const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, style, registry, warn)).join("") : escapeText(node.text);
3009
3318
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
3010
3319
  const metaEls = metaElementsXml(node.meta, namespaces);
@@ -3021,7 +3330,7 @@ function runXml(run, style, registry, warn) {
3021
3330
  else if (run.bold) attrs.push(`font-weight="700"`);
3022
3331
  if (run.italic) attrs.push(`font-style="italic"`);
3023
3332
  if (run.fontVariantCaps) attrs.push(`font-variant="${run.fontVariantCaps}"`);
3024
- if (run.fontFamily) attrs.push(`font-family="${escapeAttr2(run.fontFamily)}"`);
3333
+ if (run.fontFamily) attrs.push(`font-family="${escapeAttr(run.fontFamily)}"`);
3025
3334
  const face = scriptSizeFace(run, style);
3026
3335
  const presetScale = face && run.script ? scriptMetricsFor(face.family, face.weight, face.style)[run.script].size : void 0;
3027
3336
  const scale = run.fontScale ?? presetScale;
@@ -3037,9 +3346,10 @@ function runXml(run, style, registry, warn) {
3037
3346
  }
3038
3347
  const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
3039
3348
  if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
3040
- if (run.fill) {
3349
+ if (run.fill === null) attrs.push('fill="none"');
3350
+ else if (run.fill) {
3041
3351
  if ("color" in run.fill) {
3042
- attrs.push(`fill="${run.fill.color}"`);
3352
+ attrs.push(`fill="${escapeAttr(run.fill.color)}"`);
3043
3353
  } else {
3044
3354
  attrs.push(`fill="${registry.ref(run.fill)}"`);
3045
3355
  }
@@ -3056,21 +3366,7 @@ function textDecorationValue(underline, strikethrough, overline) {
3056
3366
  if (overline) tokens.push("overline");
3057
3367
  return tokens.length > 0 ? tokens.join(" ") : null;
3058
3368
  }
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
3369
  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
3370
  const b = boundsOfPath(path);
3075
3371
  return { minX: b.x, minY: b.y, maxX: b.x + b.width, maxY: b.y + b.height };
3076
3372
  }
@@ -3109,10 +3405,6 @@ function readFileText(file) {
3109
3405
  reader.readAsText(file);
3110
3406
  });
3111
3407
  }
3112
- var svgIdCounter = 0;
3113
- function freshSvgNodeId() {
3114
- return `n${(svgIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
3115
- }
3116
3408
  function fillDataFromSvg(paint, box) {
3117
3409
  if (!paint) return void 0;
3118
3410
  if (paint.kind === "none") return null;
@@ -3205,6 +3497,7 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3205
3497
  height: n.height
3206
3498
  };
3207
3499
  if (n.rotation) pose2.rotation = n.rotation;
3500
+ if (n.transform) pose2.transform = n.transform;
3208
3501
  const box = {
3209
3502
  x: pose2.x,
3210
3503
  y: pose2.y,
@@ -3226,6 +3519,7 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3226
3519
  if (n.kind === "image") {
3227
3520
  const pose2 = { x: n.x, y: n.y, width: n.width, height: n.height };
3228
3521
  if (n.rotation) pose2.rotation = n.rotation;
3522
+ if (n.transform) pose2.transform = n.transform;
3229
3523
  return pushLeaf(n, parentId, pose2, {
3230
3524
  image: {
3231
3525
  src: n.href,
@@ -3253,18 +3547,15 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3253
3547
  function isNodeList(input) {
3254
3548
  return Array.isArray(input);
3255
3549
  }
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
- };
3550
+ function scaleLeafData(data, scale) {
3551
+ if (scale === 1) return data;
3552
+ const out = { ...data };
3553
+ if (out.stroke) out.stroke = scaleStroke(out.stroke, scale);
3554
+ if (typeof out.text === "string") {
3555
+ out.style = scaleTextStyle(out.style, scale);
3556
+ if (out.runs) out.runs = scaleRuns(out.runs, scale);
3557
+ }
3558
+ return out;
3268
3559
  }
3269
3560
  async function unpackSvgFiles(files, ctx) {
3270
3561
  const layer = ctx.scene.layers[0]?.id ?? "default";
@@ -3274,14 +3565,14 @@ async function unpackSvgFiles(files, ctx) {
3274
3565
  const text = await readFileText(file);
3275
3566
  const parsed = parseSvg(text);
3276
3567
  for (const w of parsed.warnings) dwarn("ingest", `svg "${file.name}":`, w);
3277
- let drafts = svgNodesToKitDrafts(parsed, freshSvgNodeId);
3568
+ let drafts = svgNodesToKitDrafts(parsed, () => ctx.scene.mintId());
3278
3569
  if (drafts.length === 0) {
3279
3570
  console.warn(`weasel ingest: svg "${file.name}" parsed to no drawable nodes`);
3280
3571
  continue;
3281
3572
  }
3282
3573
  const roots = drafts.filter((d) => d.parentId === null);
3283
3574
  if (roots.length > 1) {
3284
- const wrapperId = freshSvgNodeId();
3575
+ const wrapperId = ctx.scene.mintId();
3285
3576
  const union2 = unionAABB(roots.map((d) => d.pose));
3286
3577
  drafts = [
3287
3578
  { kind: "container", id: wrapperId, parentId: null, pose: union2 },
@@ -3316,7 +3607,7 @@ async function unpackSvgFiles(files, ctx) {
3316
3607
  kind: d.kind,
3317
3608
  layer,
3318
3609
  pose: place(d.pose),
3319
- data: d.kind === "leaf" ? scaleTextData(d.data, scale) : {},
3610
+ data: d.kind === "leaf" ? scaleLeafData(d.data, scale) : {},
3320
3611
  parent: d.parentId
3321
3612
  },
3322
3613
  label: "Insert SVG"
@@ -3369,6 +3660,7 @@ function svgImageFromKit(image, pose) {
3369
3660
  if (image.flipY) node.flipY = true;
3370
3661
  if (image.opacity != null) node.opacity = image.opacity;
3371
3662
  if (pose.rotation) node.rotation = pose.rotation;
3663
+ if (pose.transform) node.transform = pose.transform;
3372
3664
  return node;
3373
3665
  }
3374
3666
  function strokeInPoseFrame(stroke, box) {
@@ -3399,6 +3691,7 @@ function svgLeafFromKit(data, pose) {
3399
3691
  if (data.fill !== void 0) node2.fill = data.fill && fillInPoseFrame(data.fill, pose);
3400
3692
  if (data.stroke) node2.stroke = strokeInPoseFrame(data.stroke, pose);
3401
3693
  if (pose.rotation) node2.rotation = pose.rotation;
3694
+ if (pose.transform) node2.transform = pose.transform;
3402
3695
  return node2;
3403
3696
  }
3404
3697
  if (!data.path) return null;