@weasel-js/svg 1.4.4 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,11 @@
1
- import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, getMarker, SCRIPT_METRICS, dwarn, createInsertOp, PathBuilder, parseColor, rgbaToHex, PATH_C, PATH_Q, solid, fillToBoundsFrame, resolveTextStyle, getPaintKind } from '@weasel-js/core';
1
+ import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, resolveScreenLength, SCRIPT_METRICS, dwarn, getMarker, registerMarker, createInsertOp, PathBuilder, parseColor, rgbaToHex, resolveTextStyle, layoutRuns, resolveRuns, resolveAlign, PATH_C, PATH_Q, solid, fillToBoundsFrame, getPaintKind } from '@weasel-js/core';
2
+ import { invert } from '@weasel-js/geom';
2
3
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
3
4
 
4
5
  // src/parse.ts
5
6
 
6
7
  // src/types.ts
7
8
  var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
8
- var UNBOUNDED_TEXT_WIDTH = 99999;
9
-
10
- // src/transform.ts
11
9
  function multiply(a, b) {
12
10
  return [
13
11
  a[0] * b[0] + a[2] * b[1],
@@ -78,15 +76,11 @@ function parseTransform(attr, onWarn) {
78
76
  }
79
77
  return result;
80
78
  }
81
- function invertMatrix(m) {
82
- const det = m[0] * m[3] - m[1] * m[2];
83
- if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;
84
- const a = m[3] / det, b = -m[1] / det, c = -m[2] / det, d = m[0] / det;
85
- return [a, b, c, d, -(a * m[4] + c * m[5]), -(b * m[4] + d * m[5])];
86
- }
87
79
  function rebaseTransform(ctm, local) {
88
- const inv = invertMatrix(ctm);
89
- return inv ? multiply(multiply(ctm, local), inv) : local;
80
+ const inv = invert([...ctm]);
81
+ if (!inv) return null;
82
+ const [a, b, c, d, e, f] = inv;
83
+ return multiply(multiply(ctm, local), [a, b, c, d, e, f]);
90
84
  }
91
85
  function decomposeRotation(m, cx, cy, eps = 1e-4) {
92
86
  const [a, b, c, d, e, f] = m;
@@ -236,6 +230,10 @@ function polygonToPath(points, m = IDENTITY_MATRIX) {
236
230
  b.close();
237
231
  return transformPath(b.build(), m);
238
232
  }
233
+ function anchorOffset(style, width) {
234
+ const edge = resolveAlign(style?.align ?? "left", style?.direction ?? "ltr");
235
+ return edge === "center" ? width / 2 : edge === "right" ? width : 0;
236
+ }
239
237
  function parsePaintAttr(raw) {
240
238
  if (raw == null) return null;
241
239
  const s = raw.trim();
@@ -244,8 +242,18 @@ function parsePaintAttr(raw) {
244
242
  if (s === "currentColor" || s === "currentcolor") {
245
243
  return { kind: "solid", color: "#000000", alpha: 1 };
246
244
  }
247
- const urlMatch = /^url\(\s*#([^)\s]+)\s*\)/.exec(s);
248
- if (urlMatch) return { kind: "ref", id: urlMatch[1] };
245
+ const urlMatch = /^url\(\s*#([^)\s]+)\s*\)(.*)$/.exec(s);
246
+ if (urlMatch) {
247
+ const out = { kind: "ref", id: urlMatch[1] };
248
+ const rest = urlMatch[2].trim();
249
+ if (rest !== "" && rest !== "none") {
250
+ const fallback = parsePaintAttr(rest);
251
+ if (fallback?.kind === "solid") {
252
+ out.fallback = { color: fallback.color, alpha: fallback.alpha };
253
+ }
254
+ }
255
+ return out;
256
+ }
249
257
  try {
250
258
  const [r, g, b, a] = parseColor(s);
251
259
  return { kind: "solid", color: rgbaToHex([r, g, b]), alpha: a };
@@ -389,13 +397,45 @@ function serializePathD(path) {
389
397
  }
390
398
 
391
399
  // src/gradients.ts
392
- var GRADIENT_TAGS = /* @__PURE__ */ new Set(["lineargradient", "radialgradient"]);
400
+ var WEASEL_NS = "urn:weasel-js:svg";
401
+ var WEASEL_NS_PREFIX = "wzl";
402
+ function hasNativeSvgForm(paint) {
403
+ const kind = paint.fill ?? "solid";
404
+ return kind === "solid" || kind === "linear-gradient" || kind === "radial-gradient" || kind === "pattern";
405
+ }
406
+ var INTERPOLATE_SPACES = /* @__PURE__ */ new Set(["rgb", "oklab", "oklch"]);
407
+ function interpolateAttr(space) {
408
+ if (!space || space === "rgb") return "";
409
+ return ` ${WEASEL_NS_PREFIX}:interpolate="${space}"`;
410
+ }
411
+ function readInterpolate(el, elements) {
412
+ const raw = inheritedAttr(el, elements, `${WEASEL_NS_PREFIX}:interpolate`) ?? el.getAttributeNS(WEASEL_NS, "interpolate");
413
+ if (raw === "oklab" || raw === "oklch") return raw;
414
+ return void 0;
415
+ }
416
+ function usesInterpolateAttr(paint) {
417
+ const space = paint.interpolate;
418
+ return space != null && space !== "rgb" && INTERPOLATE_SPACES.has(space);
419
+ }
420
+ function localName(tag) {
421
+ const i = tag.indexOf(":");
422
+ return (i < 0 ? tag : tag.slice(i + 1)).toLowerCase();
423
+ }
424
+ var GRADIENT_TAGS = /* @__PURE__ */ new Set([
425
+ "lineargradient",
426
+ "radialgradient",
427
+ "conicgradient",
428
+ `${WEASEL_NS_PREFIX}:conicgradient`,
429
+ "meshgradient",
430
+ `${WEASEL_NS_PREFIX}:meshgradient`
431
+ ]);
393
432
  var XLINK_NS = "http://www.w3.org/1999/xlink";
394
433
  function collectGradients(svg, onWarn) {
395
434
  const out = /* @__PURE__ */ new Map();
396
435
  const elements = collectElementsByTag(svg, GRADIENT_TAGS);
397
436
  for (const [id, el] of elements) {
398
- const paint = el.tagName.toLowerCase() === "lineargradient" ? readLinearGradient(el, elements, onWarn) : readRadialGradient(el, elements, onWarn);
437
+ const tag = localName(el.tagName);
438
+ const paint = tag === "lineargradient" ? readLinearGradient(el, elements, onWarn) : tag === "conicgradient" ? readConicGradient(el, elements, onWarn) : tag === "meshgradient" ? readMeshGradient(el, onWarn) : readRadialGradient(el, elements, onWarn);
399
439
  if (paint) out.set(id, paint);
400
440
  }
401
441
  warnUnsupportedDefsChildren(svg, onWarn);
@@ -409,7 +449,8 @@ function warnUnsupportedDefsChildren(svg, onWarn) {
409
449
  for (let i = 0; i < root.children.length; i++) {
410
450
  const child = root.children[i];
411
451
  const tag = child.tagName.toLowerCase();
412
- if (GRADIENT_TAGS.has(tag) || tag === "pattern" || tag === "marker") continue;
452
+ if (GRADIENT_TAGS.has(tag) || GRADIENT_TAGS.has(localName(tag))) continue;
453
+ if (tag === "pattern" || tag === "marker" || tag === "clippath") continue;
413
454
  onWarn(`unsupported <defs> child: <${child.tagName}>`);
414
455
  }
415
456
  }
@@ -491,7 +532,8 @@ function readLinearGradient(el, elements, onWarn) {
491
532
  y: num(inheritedAttr(el, elements, "y2"), 0)
492
533
  },
493
534
  stops: inheritedStops(el, elements, onWarn),
494
- units: readGradientUnits(el, elements)
535
+ units: readGradientUnits(el, elements),
536
+ interpolate: readInterpolate(el, elements)
495
537
  };
496
538
  }
497
539
  function readRadialGradient(el, elements, onWarn) {
@@ -504,7 +546,48 @@ function readRadialGradient(el, elements, onWarn) {
504
546
  },
505
547
  radius: num(inheritedAttr(el, elements, "r"), 0.5),
506
548
  stops: inheritedStops(el, elements, onWarn),
507
- units: readGradientUnits(el, elements)
549
+ units: readGradientUnits(el, elements),
550
+ interpolate: readInterpolate(el, elements)
551
+ };
552
+ }
553
+ function readConicGradient(el, elements, onWarn) {
554
+ warnGradientTransform(el, elements, onWarn);
555
+ return {
556
+ fill: "conic-gradient",
557
+ center: {
558
+ x: num(inheritedAttr(el, elements, "cx"), 0.5),
559
+ y: num(inheritedAttr(el, elements, "cy"), 0.5)
560
+ },
561
+ angle: num(inheritedAttr(el, elements, "angle"), 0),
562
+ stops: inheritedStops(el, elements, onWarn),
563
+ units: readGradientUnits(el, elements),
564
+ interpolate: readInterpolate(el, elements)
565
+ };
566
+ }
567
+ function readMeshGradient(el, onWarn) {
568
+ const patches = [];
569
+ for (let i = 0; i < el.children.length; i++) {
570
+ const child = el.children[i];
571
+ if (localName(child.tagName) !== "patch") continue;
572
+ const points = (child.getAttribute("points") ?? "").trim().split(/\s+/).filter(Boolean).map((pair) => {
573
+ const [x, y] = pair.split(",").map(Number);
574
+ return { x, y };
575
+ });
576
+ const colors = (child.getAttribute("colors") ?? "").trim().split(/\s+/).filter(Boolean);
577
+ const sane = (points.length === 12 || points.length === 16) && points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y)) && colors.length === 4;
578
+ if (!sane) {
579
+ onWarn?.(`<${WEASEL_NS_PREFIX}:patch> with ${points.length} points and ${colors.length} colors \u2014 dropped`);
580
+ continue;
581
+ }
582
+ patches.push({ points, colors });
583
+ }
584
+ if (patches.length === 0) return null;
585
+ const space = el.getAttribute("interpolate") ?? el.getAttributeNS(WEASEL_NS, "interpolate");
586
+ return {
587
+ fill: "mesh-gradient",
588
+ patches,
589
+ units: el.getAttribute("gradientUnits") === "objectBoundingBox" ? "bounds" : "world",
590
+ ...space === "oklab" || space === "oklch" ? { interpolate: space } : {}
508
591
  };
509
592
  }
510
593
  var PaintServerRegistry = class {
@@ -513,11 +596,14 @@ var PaintServerRegistry = class {
513
596
  counter = 0;
514
597
  // Marker keys referenced by any stroke, in first-use order. The `<defs>`
515
598
  // id is the marker key itself, not a minted counter id: `parseSvg` reads a
516
- // `marker-end="url(#id)"` fragment back as the marker key directly (it
517
- // never inspects the `<marker>` element), so a synthetic id would come
518
- // back unresolvable and warn on round-trip.
599
+ // `marker-end="url(#id)"` fragment back as the marker key directly when the
600
+ // registry knows it, so a synthetic id would come back as a stranger.
519
601
  markerKeys = [];
520
602
  markerKeySet = /* @__PURE__ */ new Set();
603
+ // Clip paths, in first-use order. Identity, not value: two groups clipped by
604
+ // equal outlines get a def each, which costs bytes and misleads nobody.
605
+ clips = [];
606
+ clipIds = /* @__PURE__ */ new Map();
521
607
  register(paint) {
522
608
  const existing = this.byPaint.get(paint);
523
609
  if (existing) return existing;
@@ -526,6 +612,23 @@ var PaintServerRegistry = class {
526
612
  this.order.push(paint);
527
613
  return id;
528
614
  }
615
+ /**
616
+ * The `fill` / `stroke` attribute value referencing this paint server: a
617
+ * `url(#id)` reference, plus the fallback paint SVG's own grammar allows
618
+ * after one for any kind SVG cannot express. A renderer that cannot resolve
619
+ * the reference takes the fallback instead of dropping the fill, which is
620
+ * the difference between a flat shape and a vanished one.
621
+ */
622
+ ref(paint) {
623
+ const id = this.register(paint);
624
+ if (hasNativeSvgForm(paint)) return `url(#${id})`;
625
+ return `url(#${id}) ${paintFallback(paint)}`;
626
+ }
627
+ /** Whether any registered paint serializes outside SVG's own vocabulary, so
628
+ * the root has to declare {@link WEASEL_NS}. */
629
+ usesPrivateNamespace() {
630
+ return this.order.some((paint) => !hasNativeSvgForm(paint) || usesInterpolateAttr(paint));
631
+ }
529
632
  /** The `<defs>` id for a marker key — the key itself, minting nothing.
530
633
  * `undefined` when nothing is registered under `key`, so a caller emits no
531
634
  * attribute at all rather than a `url(#…)` pointing at a def that will
@@ -538,9 +641,21 @@ var PaintServerRegistry = class {
538
641
  }
539
642
  return key;
540
643
  }
541
- /** Emit `<defs>...</defs>` XML for every registered paint server and marker. */
644
+ /** Mint (or recall) the `<defs>` id for a clip outline. */
645
+ clipId(path) {
646
+ const existing = this.clipIds.get(path);
647
+ if (existing) return existing;
648
+ const id = `clip${this.clips.length}`;
649
+ this.clipIds.set(path, id);
650
+ this.clips.push(path);
651
+ return id;
652
+ }
653
+ /** Emit `<defs>...</defs>` XML for every registered paint server, marker
654
+ * and clip path. */
542
655
  toDefsXml(onWarn) {
543
- if (this.order.length === 0 && this.markerKeys.length === 0) return "";
656
+ if (this.order.length === 0 && this.markerKeys.length === 0 && this.clips.length === 0) {
657
+ return "";
658
+ }
544
659
  const parts = ["<defs>"];
545
660
  for (const paint of this.order) {
546
661
  parts.push(paintServerXml(this.byPaint.get(paint), paint, onWarn));
@@ -548,18 +663,33 @@ var PaintServerRegistry = class {
548
663
  for (const key of this.markerKeys) {
549
664
  const entry = getMarker(key);
550
665
  if (!entry) continue;
551
- parts.push(entry.toSvg ? entry.toSvg(key, entry) : defaultMarkerXml(key, entry));
666
+ parts.push(entry.toSvg ? entry.toSvg(key, entry) : defaultMarkerXml(key, entry, onWarn));
667
+ }
668
+ for (const path of this.clips) {
669
+ parts.push(
670
+ `<clipPath id="${this.clipIds.get(path)}"><path d="${serializePathD(path)}"/></clipPath>`
671
+ );
552
672
  }
553
673
  parts.push("</defs>");
554
674
  return parts.join("");
555
675
  }
556
676
  };
557
- function defaultMarkerXml(id, entry) {
677
+ function defaultMarkerXml(id, entry, onWarn) {
558
678
  const path = entry.path({ size: 1, stroke: { paint: { fill: "solid", color: "#000" } } });
559
679
  const d = serializePathD(path);
560
- const fill = entry.fill === "none" ? "none" : "context-stroke";
561
- const outline = entry.outline ? ` stroke="context-stroke" stroke-width="${entry.outline.width}" stroke-linecap="round" stroke-linejoin="round"` : "";
562
- return `<marker id="${id}" markerUnits="strokeWidth" markerWidth="8" markerHeight="8" refX="0" refY="0" orient="auto" overflow="visible"><path d="${d}" fill="${fill}"${outline}/></marker>`;
680
+ const fill = markerPaintAttrs("fill", entry.fill, id, onWarn);
681
+ const outline = entry.outline ? ` ${markerPaintAttrs("stroke", entry.outline.paint, id, onWarn)} stroke-width="${trimNumber(entry.outline.width)}" stroke-linecap="round" stroke-linejoin="round"` : "";
682
+ const orient = typeof entry.orient === "number" ? trimNumber(entry.orient * 180 / Math.PI) : "auto-start-reverse";
683
+ return `<marker id="${id}" markerUnits="strokeWidth" markerWidth="8" markerHeight="8" refX="0" refY="0" orient="${orient}" overflow="visible"><path d="${d}" ${fill}${outline}/></marker>`;
684
+ }
685
+ function markerPaintAttrs(attr, paint, id, onWarn) {
686
+ if (paint === "none") return `${attr}="none"`;
687
+ if (paint === void 0 || paint === "line") return `${attr}="context-stroke"`;
688
+ if ("color" in paint && (paint.fill === void 0 || paint.fill === "solid")) {
689
+ return paint.opacity != null && paint.opacity !== 1 ? `${attr}="${paint.color}" ${attr}-opacity="${trimNumber(paint.opacity)}"` : `${attr}="${paint.color}"`;
690
+ }
691
+ onWarn?.(`marker "${id}" ${attr} is a paint server, which a <marker> def cannot reference here; written as the line's paint`);
692
+ return `${attr}="context-stroke"`;
563
693
  }
564
694
  function paintServerXml(id, paint, onWarn) {
565
695
  if (paint.fill === "pattern") return patternXml(id, paint, onWarn);
@@ -576,14 +706,25 @@ function gradientUnitsAttr(units) {
576
706
  function gradientXml(id, paint) {
577
707
  if (paint.fill === "linear-gradient") {
578
708
  const stops = paint.stops.map(stopXml).join("");
579
- return `<linearGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" x1="${trimNumber(paint.from.x)}" y1="${trimNumber(paint.from.y)}" x2="${trimNumber(paint.to.x)}" y2="${trimNumber(paint.to.y)}">${stops}</linearGradient>`;
709
+ return `<linearGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" x1="${trimNumber(paint.from.x)}" y1="${trimNumber(paint.from.y)}" x2="${trimNumber(paint.to.x)}" y2="${trimNumber(paint.to.y)}"${interpolateAttr(paint.interpolate)}>${stops}</linearGradient>`;
710
+ }
711
+ if (paint.fill === "conic-gradient") {
712
+ const stops = paint.stops.map(stopXml).join("");
713
+ const P = WEASEL_NS_PREFIX;
714
+ return `<${P}:conicGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" angle="${trimNumber(paint.angle)}"${interpolateAttr(paint.interpolate)}>${stops}</${P}:conicGradient>`;
580
715
  }
581
716
  if (paint.fill === "radial-gradient") {
582
717
  const stops = paint.stops.map(stopXml).join("");
583
- return `<radialGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" r="${trimNumber(paint.radius)}">${stops}</radialGradient>`;
718
+ return `<radialGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" r="${trimNumber(paint.radius)}"${interpolateAttr(paint.interpolate)}>${stops}</radialGradient>`;
584
719
  }
585
720
  return "";
586
721
  }
722
+ function paintFallback(paint) {
723
+ const color = getPaintKind(paint.fill)?.colorOf(paint);
724
+ if (!color) return "none";
725
+ const packed = /^#([0-9a-f]{6})[0-9a-f]{2}$/i.exec(color);
726
+ return packed ? `#${packed[1].toLowerCase()}` : color;
727
+ }
587
728
  function stopXml(s) {
588
729
  const m = /^#([0-9a-f]{6})([0-9a-f]{2})$/i.exec(s.color);
589
730
  if (m) {
@@ -663,6 +804,7 @@ var IGNORED_TAGS = /* @__PURE__ */ new Set([
663
804
  "radialgradient",
664
805
  "pattern",
665
806
  "marker",
807
+ "clippath",
666
808
  "title",
667
809
  "desc",
668
810
  "metadata"
@@ -695,8 +837,11 @@ function parseSvg(svg, opts = {}) {
695
837
  const gradients = collectGradients(root, onWarn);
696
838
  for (const [id, paint] of collectPatterns(root, onWarn)) gradients.set(id, paint);
697
839
  const rootStyle = deriveStyle(EMPTY_STYLE, root);
698
- const nodes = parseChildren(root, IDENTITY_MATRIX, rootStyle, gradients, onWarn, uriToPrefix);
840
+ const clips = collectClipPaths(root, onWarn);
841
+ const nodes = parseChildren(root, IDENTITY_MATRIX, rootStyle, gradients, clips, onWarn, uriToPrefix);
842
+ const markers = ingestMarkers(root, nodes, gradients, onWarn);
699
843
  const result = { nodes, warnings };
844
+ if (markers.length > 0) result.markers = markers;
700
845
  if (documentMeta) result.documentMeta = documentMeta;
701
846
  const viewBox = parseViewBoxAttr(root.getAttribute("viewBox"));
702
847
  if (viewBox) result.viewBox = viewBox;
@@ -798,13 +943,53 @@ function collectElementMeta(el, uriToPrefix) {
798
943
  }
799
944
  return Object.keys(meta).length > 0 ? meta : void 0;
800
945
  }
801
- function parseChildren(parent, ctm, style, gradients, onWarn, uriToPrefix) {
946
+ function collectClipPaths(root, onWarn) {
947
+ const out = /* @__PURE__ */ new Map();
948
+ for (const [id, el] of collectElementsByTag(root, /* @__PURE__ */ new Set(["clippath"]))) {
949
+ const units = el.getAttribute("clipPathUnits");
950
+ if (units && units !== "userSpaceOnUse") {
951
+ onWarn(`<clipPath id="${id}"> uses clipPathUnits="${units}", which is not modeled`);
952
+ continue;
953
+ }
954
+ const shapes = [];
955
+ for (let i = 0; i < el.children.length; i++) {
956
+ if (SUPPORTED_LEAF_TAGS.has(el.children[i].tagName.toLowerCase())) shapes.push(el.children[i]);
957
+ }
958
+ if (shapes.length !== 1) {
959
+ onWarn(
960
+ `<clipPath id="${id}"> holds ${shapes.length} shapes the kit can lower; only a single-shape clip is modeled, so it clips nothing`
961
+ );
962
+ continue;
963
+ }
964
+ const shape = shapes[0];
965
+ const local = parseTransform(shape.getAttribute("transform"), onWarn);
966
+ const path = lowerLeaf(shape, shape.tagName.toLowerCase(), local, onWarn);
967
+ if (path) out.set(id, path);
968
+ }
969
+ return out;
970
+ }
971
+ function clipFor(el, ctm, clips, onWarn) {
972
+ const attr = el.getAttribute("clip-path");
973
+ if (!attr || attr === "none") return void 0;
974
+ const ref = /^url\(\s*#([^)\s]+)\s*\)$/.exec(attr.trim());
975
+ if (!ref) {
976
+ onWarn(`clip-path="${attr}" is not a url(#id) reference; the clip is dropped`);
977
+ return void 0;
978
+ }
979
+ const path = clips.get(ref[1]);
980
+ if (!path) {
981
+ onWarn(`clip-path references #${ref[1]}, which no <clipPath> in the document defines`);
982
+ return void 0;
983
+ }
984
+ return transformPath(path, ctm);
985
+ }
986
+ function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix) {
802
987
  const out = [];
803
988
  for (let i = 0; i < parent.children.length; i++) {
804
989
  const el = parent.children[i];
805
990
  const ns = el.namespaceURI;
806
991
  if (ns && ns !== "http://www.w3.org/2000/svg") continue;
807
- const node = parseElement(el, ctm, style, gradients, onWarn, uriToPrefix);
992
+ const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix);
808
993
  if (node) {
809
994
  if (Array.isArray(node)) out.push(...node);
810
995
  else out.push(node);
@@ -812,16 +997,26 @@ function parseChildren(parent, ctm, style, gradients, onWarn, uriToPrefix) {
812
997
  }
813
998
  return out;
814
999
  }
815
- function parseElement(el, ctm, style, gradients, onWarn, uriToPrefix) {
1000
+ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
816
1001
  const tag = el.tagName.toLowerCase();
817
1002
  if (IGNORED_TAGS.has(tag)) return null;
1003
+ if (tag === "g" && el.hasAttribute("data-weasel-image")) {
1004
+ const image = parseCroppedImage(el, ctm, onWarn);
1005
+ if (image) {
1006
+ const meta2 = collectElementMeta(el, uriToPrefix);
1007
+ if (meta2) image.meta = meta2;
1008
+ return image;
1009
+ }
1010
+ }
818
1011
  if (tag === "g") {
819
1012
  const local = parseTransform(el.getAttribute("transform"), onWarn);
820
1013
  const childCtm = multiply(ctm, local);
821
1014
  const childStyle = deriveStyle(style, el);
822
- const children = parseChildren(el, childCtm, childStyle, gradients, onWarn, uriToPrefix);
1015
+ const children = parseChildren(el, childCtm, childStyle, gradients, clips, onWarn, uriToPrefix);
823
1016
  const opacity2 = readOpacityAttr(el, "opacity");
824
1017
  const group = { kind: "group", children };
1018
+ const clip = clipFor(el, childCtm, clips, onWarn);
1019
+ if (clip) group.clip = clip;
825
1020
  if (opacity2 != null) group.opacity = opacity2;
826
1021
  const meta2 = collectElementMeta(el, uriToPrefix);
827
1022
  if (meta2) group.meta = meta2;
@@ -835,7 +1030,7 @@ function parseElement(el, ctm, style, gradients, onWarn, uriToPrefix) {
835
1030
  if (el.hasAttribute("viewBox")) {
836
1031
  onWarn("nested <svg> viewBox is not modeled; its children are not rescaled");
837
1032
  }
838
- return parseChildren(el, multiply(ctm, offset), childStyle, gradients, onWarn, uriToPrefix);
1033
+ return parseChildren(el, multiply(ctm, offset), childStyle, gradients, clips, onWarn, uriToPrefix);
839
1034
  }
840
1035
  if (tag === "text") {
841
1036
  const textNode = parseTextElement(el, ctm, style, gradients, onWarn);
@@ -866,8 +1061,10 @@ function parseElement(el, ctm, style, gradients, onWarn, uriToPrefix) {
866
1061
  const bounds = pathAabb(path);
867
1062
  const cx = bounds.x + bounds.width / 2;
868
1063
  const cy = bounds.y + bounds.height / 2;
869
- const angle = decomposeRotation(worldLocal, cx, cy);
870
- if (angle != null) {
1064
+ const angle = worldLocal && decomposeRotation(worldLocal, cx, cy);
1065
+ if (!worldLocal) {
1066
+ onWarn("leaf transform sits under a singular parent transform; dropped");
1067
+ } else if (angle != null) {
871
1068
  rotation = angle;
872
1069
  } else {
873
1070
  path = transformPath(path, worldLocal);
@@ -978,6 +1175,12 @@ function readPaint(style, attr, defaultColor, gradients, onWarn) {
978
1175
  if (parsed.kind === "ref") {
979
1176
  const paint = gradients.get(parsed.id);
980
1177
  if (!paint) {
1178
+ if (parsed.fallback) {
1179
+ const flat = { kind: "solid", color: parsed.fallback.color };
1180
+ const a2 = opacity ?? (parsed.fallback.alpha < 1 ? parsed.fallback.alpha : void 0);
1181
+ if (a2 != null) flat.opacity = a2;
1182
+ return flat;
1183
+ }
981
1184
  onWarn(`${attr} references unknown gradient #${parsed.id}`);
982
1185
  return { kind: "solid", color: defaultColor };
983
1186
  }
@@ -1034,12 +1237,7 @@ function readStroke(style, gradients, onWarn, nonScaling = false) {
1034
1237
  ["marker-end", "markerEnd"]
1035
1238
  ]) {
1036
1239
  const id = parseMarkerRef(style[attr] ?? null);
1037
- if (id === void 0) continue;
1038
- if (getMarker(id) === void 0) {
1039
- onWarn(`${attr} references a marker this kit has no entry for: #${id}`);
1040
- continue;
1041
- }
1042
- stroke[field] = id;
1240
+ if (id !== void 0) stroke[field] = id;
1043
1241
  }
1044
1242
  return stroke;
1045
1243
  }
@@ -1155,20 +1353,30 @@ function parseRunFontSize(raw, onWarn) {
1155
1353
  }
1156
1354
  var XLINK_NS2 = "http://www.w3.org/1999/xlink";
1157
1355
  function parseImageElement(el, ctm, onWarn) {
1158
- const href = el.getAttribute("href") ?? el.getAttributeNS(XLINK_NS2, "href") ?? el.getAttribute("xlink:href");
1356
+ const href = imageHref(el);
1159
1357
  if (!href) {
1160
1358
  onWarn("<image> without href; dropped");
1161
1359
  return null;
1162
1360
  }
1361
+ const node = imageNodeAt(el, el, href, ctm, onWarn);
1362
+ if (el.hasAttribute("preserveAspectRatio") && el.getAttribute("preserveAspectRatio") !== "none") {
1363
+ onWarn("<image> preserveAspectRatio is not modeled; the box is taken literally");
1364
+ }
1365
+ return node;
1366
+ }
1367
+ function imageHref(el) {
1368
+ return el.getAttribute("href") ?? el.getAttributeNS(XLINK_NS2, "href") ?? el.getAttribute("xlink:href");
1369
+ }
1370
+ function imageNodeAt(boxEl, ownerEl, href, ctm, onWarn) {
1163
1371
  const num2 = (raw, fallback) => {
1164
1372
  if (raw == null) return fallback;
1165
1373
  const n = parseFloat(raw);
1166
1374
  return Number.isFinite(n) ? n : fallback;
1167
1375
  };
1168
- const rawX = num2(el.getAttribute("x"), 0);
1169
- const rawY = num2(el.getAttribute("y"), 0);
1170
- const rawW = num2(el.getAttribute("width"), 0);
1171
- const rawH = num2(el.getAttribute("height"), 0);
1376
+ const rawX = num2(boxEl.getAttribute("x"), 0);
1377
+ const rawY = num2(boxEl.getAttribute("y"), 0);
1378
+ const rawW = num2(boxEl.getAttribute("width"), 0);
1379
+ const rawH = num2(boxEl.getAttribute("height"), 0);
1172
1380
  const px = (x, y) => [ctm[0] * x + ctm[2] * y + ctm[4], ctm[1] * x + ctm[3] * y + ctm[5]];
1173
1381
  const [x0, y0] = px(rawX, rawY);
1174
1382
  const [x1, y1] = px(rawX + rawW, rawY + rawH);
@@ -1180,15 +1388,17 @@ function parseImageElement(el, ctm, onWarn) {
1180
1388
  width: Math.abs(x1 - x0),
1181
1389
  height: Math.abs(y1 - y0)
1182
1390
  };
1183
- const opacity = readOpacityAttr(el, "opacity");
1391
+ const opacity = readOpacityAttr(ownerEl, "opacity");
1184
1392
  if (opacity != null) node.opacity = opacity;
1185
- const localTransform = parseTransform(el.getAttribute("transform"), onWarn);
1393
+ const localTransform = parseTransform(ownerEl.getAttribute("transform"), onWarn);
1186
1394
  if (!isIdentity(localTransform)) {
1187
1395
  const worldLocal = rebaseTransform(ctm, localTransform);
1188
1396
  const cx = node.x + node.width / 2;
1189
1397
  const cy = node.y + node.height / 2;
1190
- const angle = decomposeRotation(worldLocal, cx, cy);
1191
- if (angle != null) {
1398
+ const angle = worldLocal && decomposeRotation(worldLocal, cx, cy);
1399
+ if (!worldLocal) {
1400
+ onWarn("<image> transform sits under a singular parent transform; dropped");
1401
+ } else if (angle != null) {
1192
1402
  node.rotation = angle;
1193
1403
  } else {
1194
1404
  const rotComp = rotationComponent(worldLocal);
@@ -1197,9 +1407,21 @@ function parseImageElement(el, ctm, onWarn) {
1197
1407
  }
1198
1408
  }
1199
1409
  }
1200
- if (el.hasAttribute("preserveAspectRatio") && el.getAttribute("preserveAspectRatio") !== "none") {
1201
- onWarn("<image> preserveAspectRatio is not modeled; the box is taken literally");
1202
- }
1410
+ return node;
1411
+ }
1412
+ function parseCroppedImage(g, ctm, onWarn) {
1413
+ const viewport = [...g.children].find((c) => c.tagName.toLowerCase() === "svg");
1414
+ const img = viewport && [...viewport.children].find((c) => c.tagName.toLowerCase() === "image");
1415
+ const href = img && imageHref(img);
1416
+ const source = viewport && parseViewBoxAttr(viewport.getAttribute("viewBox"));
1417
+ if (!img || !href || !source) return null;
1418
+ const node = imageNodeAt(viewport, g, href, ctm, onWarn);
1419
+ if (source.x !== 0 || source.y !== 0 || source.width !== 1 || source.height !== 1) {
1420
+ node.source = source;
1421
+ }
1422
+ const flip = parseTransform(img.getAttribute("transform"), onWarn);
1423
+ if (flip[0] < 0) node.flipX = true;
1424
+ if (flip[3] < 0) node.flipY = true;
1203
1425
  return node;
1204
1426
  }
1205
1427
  var XML_NS = "http://www.w3.org/XML/1998/namespace";
@@ -1238,6 +1460,19 @@ function collapseRunWhitespace(runs) {
1238
1460
  if (runs[i].text === "") runs.splice(i, 1);
1239
1461
  }
1240
1462
  }
1463
+ var ESTIMATED_GLYPH_ADVANCE_EM = 0.6;
1464
+ function laidOutWidth(runs, style, plain) {
1465
+ const resolved = resolveTextStyle(style);
1466
+ const { bounds } = layoutRuns(resolveRuns(runs.length > 0 ? runs : [{ text: plain }], resolved), {
1467
+ maxWidth: Infinity,
1468
+ lineHeight: resolved.lineHeight,
1469
+ align: resolved.align,
1470
+ direction: resolved.direction
1471
+ });
1472
+ if (bounds.width > 0) return bounds.width;
1473
+ const longest = plain.split("\n").reduce((max, line) => Math.max(max, line.length), 0);
1474
+ return longest * resolved.fontSize * ESTIMATED_GLYPH_ADVANCE_EM;
1475
+ }
1241
1476
  function parseTextElement(el, ctm, style, gradients, onWarn) {
1242
1477
  const num2 = (raw, fallback) => {
1243
1478
  if (raw == null) return fallback;
@@ -1253,7 +1488,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1253
1488
  const leafStyle = deriveStyle(style, el);
1254
1489
  const textStyle = readTextStyle(leafStyle, onWarn);
1255
1490
  const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
1256
- const fontSize = textStyle.fontSize ?? 16;
1491
+ const fontSize = resolveScreenLength(textStyle.fontSize ?? 16, 1);
1257
1492
  const lineHeight = textStyle.lineHeight ?? 1.2;
1258
1493
  const dominantBaseline = el.getAttribute("dominant-baseline");
1259
1494
  const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
@@ -1283,13 +1518,13 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1283
1518
  const plain = runs.map((r) => r.text).join("");
1284
1519
  const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
1285
1520
  const dataH = num2(el.getAttribute("data-weasel-height"), NaN);
1286
- const width = Number.isFinite(dataW) ? dataW : UNBOUNDED_TEXT_WIDTH;
1521
+ const width = Number.isFinite(dataW) ? dataW : laidOutWidth(runs, textStyle, plain);
1287
1522
  const lines = (plain.match(/\n/g)?.length ?? 0) + 1;
1288
1523
  const height = Number.isFinite(dataH) ? dataH : fontSize * lineHeight * lines;
1289
1524
  const opacity = readOpacityAttr(el, "opacity");
1290
1525
  const node = {
1291
1526
  kind: "text",
1292
- x: ax,
1527
+ x: ax - anchorOffset(textStyle, width),
1293
1528
  y: topY,
1294
1529
  width,
1295
1530
  height,
@@ -1299,6 +1534,9 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1299
1534
  (r) => Object.entries(r).some(([k, v]) => k !== "text" && v !== void 0)
1300
1535
  );
1301
1536
  if (hasStyling) node.runs = runs;
1537
+ if (el.getAttribute("data-weasel-wrap") === "true") textStyle.wrap = true;
1538
+ const va = el.getAttribute("data-weasel-vertical-align");
1539
+ if (va === "center" || va === "bottom") node.verticalAlign = va;
1302
1540
  if (Object.keys(textStyle).length > 0) node.style = textStyle;
1303
1541
  if (textPaint.fill !== void 0) node.fill = textPaint.fill;
1304
1542
  if (textPaint.stroke != null) node.stroke = textPaint.stroke;
@@ -1307,8 +1545,10 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1307
1545
  const worldLocal = rebaseTransform(ctm, localTransform);
1308
1546
  const cx = node.x + node.width / 2;
1309
1547
  const cy = node.y + node.height / 2;
1310
- const angle = decomposeRotation(worldLocal, cx, cy);
1311
- if (angle != null) {
1548
+ const angle = worldLocal && decomposeRotation(worldLocal, cx, cy);
1549
+ if (!worldLocal) {
1550
+ onWarn("<text> transform sits under a singular parent transform; dropped");
1551
+ } else if (angle != null) {
1312
1552
  node.rotation = angle;
1313
1553
  } else {
1314
1554
  const rotComp = rotationComponent(worldLocal);
@@ -1358,6 +1598,7 @@ function readTspanRun(el, gradients, style, onWarn) {
1358
1598
  } else if (parsed?.kind === "ref") {
1359
1599
  const paint = gradients.get(parsed.id);
1360
1600
  if (paint) run.fill = paint;
1601
+ else if (parsed.fallback) run.fill = { fill: "solid", color: parsed.fallback.color };
1361
1602
  }
1362
1603
  }
1363
1604
  const stroke = coreStroke(readStroke(ownStrokeStyle(el), gradients, onWarn, isNonScalingStroke(el)));
@@ -1407,6 +1648,9 @@ function readTextPaint(style, gradients, onWarn, nonScalingStroke = false) {
1407
1648
  } else if (parsed?.kind === "ref") {
1408
1649
  const paint = gradients.get(parsed.id);
1409
1650
  if (paint) out.fill = paint;
1651
+ else if (parsed.fallback) {
1652
+ out.fill = { fill: "solid", color: parsed.fallback.color };
1653
+ }
1410
1654
  } else if (parsed?.kind === "none") {
1411
1655
  out.fill = null;
1412
1656
  }
@@ -1415,6 +1659,196 @@ function readTextPaint(style, gradients, onWarn, nonScalingStroke = false) {
1415
1659
  if (stroke) out.stroke = stroke;
1416
1660
  return out;
1417
1661
  }
1662
+ var MARKER_FIELDS = [
1663
+ ["markerStart", "start", "marker-start"],
1664
+ ["markerMid", "mid", "marker-mid"],
1665
+ ["markerEnd", "end", "marker-end"]
1666
+ ];
1667
+ function ingestMarkers(root, nodes, gradients, onWarn) {
1668
+ const defs = /* @__PURE__ */ new Map();
1669
+ const all = root.getElementsByTagName("marker");
1670
+ for (let i = 0; i < all.length; i++) {
1671
+ const id = all[i].getAttribute("id");
1672
+ if (id && !defs.has(id)) defs.set(id, all[i]);
1673
+ }
1674
+ const minted = /* @__PURE__ */ new Map();
1675
+ const resolve = (stroke, width) => {
1676
+ const w = width === void 0 ? 1 : typeof width === "number" ? width : width.px;
1677
+ for (const [field, role, attr] of MARKER_FIELDS) {
1678
+ const id = stroke[field];
1679
+ if (typeof id !== "string" || getMarker(id) !== void 0) continue;
1680
+ const el = defs.get(id);
1681
+ if (!el) {
1682
+ onWarn(`${attr} references #${id}, which is neither a kit marker nor defined in the document`);
1683
+ delete stroke[field];
1684
+ continue;
1685
+ }
1686
+ const cacheKey = `${id}\0${role}\0${w}`;
1687
+ if (!minted.has(cacheKey)) minted.set(cacheKey, markerEntryFrom(el, id, role, w, gradients, onWarn));
1688
+ const entry = minted.get(cacheKey);
1689
+ if (entry) stroke[field] = entry.id;
1690
+ else delete stroke[field];
1691
+ }
1692
+ };
1693
+ const visit = (n) => {
1694
+ if (n.kind === "group") n.children.forEach(visit);
1695
+ else if (n.kind === "path" && n.stroke) resolve(n.stroke, n.stroke.width);
1696
+ else if (n.kind === "text") {
1697
+ if (n.stroke) resolve(n.stroke, n.stroke.width);
1698
+ for (const run of n.runs ?? []) {
1699
+ if (run.stroke) resolve(run.stroke, run.stroke.width ?? n.stroke?.width);
1700
+ }
1701
+ }
1702
+ };
1703
+ nodes.forEach(visit);
1704
+ const out = /* @__PURE__ */ new Map();
1705
+ for (const entry of minted.values()) if (entry) out.set(entry.id, entry);
1706
+ return [...out.values()];
1707
+ }
1708
+ function styleAt(el) {
1709
+ const chain = [];
1710
+ for (let e = el; e; e = e.parentElement) chain.unshift(e);
1711
+ return chain.reduce((style, e) => deriveStyle(style, e), EMPTY_STYLE);
1712
+ }
1713
+ function markerRef(raw, lo, span) {
1714
+ const v = raw?.trim();
1715
+ if (v === "left" || v === "top") return lo;
1716
+ if (v === "center") return lo + span / 2;
1717
+ if (v === "right" || v === "bottom") return lo + span;
1718
+ const n = v ? parseFloat(v) : 0;
1719
+ return Number.isFinite(n) ? n : 0;
1720
+ }
1721
+ function markerOrient(raw) {
1722
+ const v = raw?.trim() ?? "";
1723
+ if (v === "auto") return { orient: "auto", reverseAtStart: true };
1724
+ if (v === "auto-start-reverse") return { orient: "auto", reverseAtStart: false };
1725
+ const m = /^(-?[\d.]+(?:e-?\d+)?)(deg|rad|grad|turn)?$/i.exec(v);
1726
+ if (!m) return { orient: 0, reverseAtStart: false };
1727
+ const n = parseFloat(m[1]);
1728
+ const unit = (m[2] ?? "deg").toLowerCase();
1729
+ const rad = unit === "rad" ? n : unit === "grad" ? n * Math.PI / 200 : unit === "turn" ? n * 2 * Math.PI : n * Math.PI / 180;
1730
+ return { orient: rad, reverseAtStart: false };
1731
+ }
1732
+ function markerPaint(style, attr, gradients, onWarn) {
1733
+ const raw = style[attr]?.trim();
1734
+ if (raw === "context-stroke") return "line";
1735
+ if (raw === "context-fill") {
1736
+ onWarn(`a marker's ${attr}="context-fill" is read as the line's stroke paint`);
1737
+ return "line";
1738
+ }
1739
+ const paint = readPaint(style, attr, "#000000", gradients, onWarn);
1740
+ if (paint.kind === "none") return "none";
1741
+ if (paint.kind === "gradient") return paint.paint;
1742
+ return paint.opacity != null ? { ...solid(paint.color), opacity: paint.opacity } : solid(paint.color);
1743
+ }
1744
+ function asPolygon(path) {
1745
+ if (path.kind === "polygon") return path;
1746
+ const { x, y, width: w, height: h } = path;
1747
+ return {
1748
+ kind: "polygon",
1749
+ commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),
1750
+ coords: new Float32Array([x, y, x + w, y, x + w, y + h, x, y + h]),
1751
+ fillRule: "nonzero"
1752
+ };
1753
+ }
1754
+ function shortHash(s) {
1755
+ let h = 2166136261;
1756
+ for (let i = 0; i < s.length; i++) {
1757
+ h ^= s.charCodeAt(i);
1758
+ h = Math.imul(h, 16777619);
1759
+ }
1760
+ return (h >>> 0).toString(36);
1761
+ }
1762
+ function markerEntryFrom(el, id, role, strokeWidth, gradients, onWarn) {
1763
+ const num2 = (name, fallback) => {
1764
+ const n = parseFloat(el.getAttribute(name) ?? "");
1765
+ return Number.isFinite(n) ? n : fallback;
1766
+ };
1767
+ const vb = parseViewBoxAttr(el.getAttribute("viewBox"));
1768
+ let sx = 1, sy = 1;
1769
+ if (vb && vb.width > 0 && vb.height > 0) {
1770
+ sx = num2("markerWidth", 3) / vb.width;
1771
+ sy = num2("markerHeight", 3) / vb.height;
1772
+ const par = el.getAttribute("preserveAspectRatio")?.trim() ?? "";
1773
+ if (!par.startsWith("none")) {
1774
+ const s = par.endsWith("slice") ? Math.max(sx, sy) : Math.min(sx, sy);
1775
+ sx = s;
1776
+ sy = s;
1777
+ }
1778
+ }
1779
+ const refX = markerRef(el.getAttribute("refX"), vb?.x ?? 0, vb?.width ?? 0);
1780
+ const refY = markerRef(el.getAttribute("refY"), vb?.y ?? 0, vb?.height ?? 0);
1781
+ const k = el.getAttribute("markerUnits") === "userSpaceOnUse" && strokeWidth > 0 ? 1 / strokeWidth : 1;
1782
+ const { orient, reverseAtStart } = markerOrient(el.getAttribute("orient"));
1783
+ const turn = role === "start" && reverseAtStart ? -1 : 1;
1784
+ const base = [turn * sx * k, 0, 0, turn * sy * k, -turn * sx * k * refX, -turn * sy * k * refY];
1785
+ const parts = [];
1786
+ const collect = (parent, m, style) => {
1787
+ for (let i = 0; i < parent.children.length; i++) {
1788
+ const child = parent.children[i];
1789
+ const tag = child.tagName.toLowerCase();
1790
+ const cm = multiply(m, parseTransform(child.getAttribute("transform"), onWarn));
1791
+ const cs = deriveStyle(style, child);
1792
+ if (tag === "g") collect(child, cm, cs);
1793
+ else if (SUPPORTED_LEAF_TAGS.has(tag)) {
1794
+ const path = lowerLeaf(child, tag, cm, onWarn);
1795
+ if (path) parts.push({ path: asPolygon(path), style: cs });
1796
+ } else if (!IGNORED_TAGS.has(tag)) {
1797
+ onWarn(`<marker id="${id}"> child <${child.tagName}> is not supported; skipped`);
1798
+ }
1799
+ }
1800
+ };
1801
+ collect(el, base, styleAt(el));
1802
+ if (parts.length === 0) {
1803
+ onWarn(`<marker id="${id}"> draws nothing this kit can read; dropped`);
1804
+ return null;
1805
+ }
1806
+ const first = parts[0].style;
1807
+ const fill = markerPaint(first, "fill", gradients, onWarn);
1808
+ const outlinePaint = markerPaint(first, "stroke", gradients, onWarn);
1809
+ const outlineWidth = parseFloat(first["stroke-width"] ?? "1") * Math.sqrt(sx * sy) * k;
1810
+ const outline = outlinePaint !== "none" && outlineWidth > 0 ? { width: outlineWidth, paint: outlinePaint } : false;
1811
+ if (parts.some((p) => JSON.stringify(p.style) !== JSON.stringify(first))) {
1812
+ onWarn(`<marker id="${id}"> paints its parts differently; all take the first part's paint`);
1813
+ }
1814
+ let commandCount = 0, coordCount = 0;
1815
+ for (const p of parts) {
1816
+ commandCount += p.path.commands.length;
1817
+ coordCount += p.path.coords.length;
1818
+ }
1819
+ const commands = new Uint8Array(commandCount);
1820
+ const coords = new Float32Array(coordCount);
1821
+ let ci = 0, pi = 0;
1822
+ for (const p of parts) {
1823
+ commands.set(p.path.commands, ci);
1824
+ ci += p.path.commands.length;
1825
+ coords.set(p.path.coords, pi);
1826
+ pi += p.path.coords.length;
1827
+ }
1828
+ const fillRule = parts[0].path.fillRule ?? "nonzero";
1829
+ const round = (v) => Math.round(v * 1e4) / 1e4 + 0;
1830
+ const hash = shortHash(JSON.stringify([
1831
+ Array.from(commands),
1832
+ Array.from(coords, round),
1833
+ fillRule,
1834
+ fill,
1835
+ outline,
1836
+ typeof orient === "number" ? round(orient) : orient
1837
+ ]));
1838
+ const key = id.endsWith(`-${hash}`) ? id : `${id}-${hash}`;
1839
+ return {
1840
+ id: key,
1841
+ path: ({ size }) => {
1842
+ const scaled = new Float32Array(coords.length);
1843
+ for (let i = 0; i < coords.length; i++) scaled[i] = coords[i] * size;
1844
+ return { kind: "polygon", commands, coords: scaled, fillRule };
1845
+ },
1846
+ fill,
1847
+ outline,
1848
+ orient,
1849
+ inset: 0
1850
+ };
1851
+ }
1418
1852
  function serializeSvg(nodes, opts = {}) {
1419
1853
  const registry = new PaintServerRegistry();
1420
1854
  registerPaintServers(nodes, registry);
@@ -1422,6 +1856,9 @@ function serializeSvg(nodes, opts = {}) {
1422
1856
  const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
1423
1857
  const namespaces = opts.namespaces ?? {};
1424
1858
  const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
1859
+ if (registry.usesPrivateNamespace() && namespaces[WEASEL_NS_PREFIX] == null) {
1860
+ rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
1861
+ }
1425
1862
  for (const [prefix, uri] of Object.entries(namespaces)) {
1426
1863
  rootAttrs.push(`xmlns:${prefix}="${escapeAttr2(uri)}"`);
1427
1864
  }
@@ -1442,22 +1879,28 @@ function serializeSvg(nodes, opts = {}) {
1442
1879
  for (const prefix of Object.keys(namespaces)) {
1443
1880
  const bucket = opts.documentMeta[prefix];
1444
1881
  if (!bucket?.elements) continue;
1445
- for (const [localName, list] of Object.entries(bucket.elements)) {
1446
- for (const el of list) docMetaXml += namespacedElementXml(prefix, localName, el);
1882
+ for (const [localName2, list] of Object.entries(bucket.elements)) {
1883
+ for (const el of list) docMetaXml += namespacedElementXml(prefix, localName2, el);
1447
1884
  }
1448
1885
  }
1449
1886
  }
1450
1887
  const defsXml = registry.toDefsXml(opts.onWarn);
1451
- const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces)).join("");
1888
+ const said = /* @__PURE__ */ new Set();
1889
+ const warn = (m) => {
1890
+ if (!opts.onWarn || said.has(m)) return;
1891
+ said.add(m);
1892
+ opts.onWarn(m);
1893
+ };
1894
+ const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn)).join("");
1452
1895
  const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
1453
1896
  return `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
1454
1897
  }
1455
- function namespacedElementXml(prefix, localName, el) {
1898
+ function namespacedElementXml(prefix, localName2, el) {
1456
1899
  const attrs = [];
1457
1900
  for (const [name, value] of Object.entries(el.attrs)) {
1458
1901
  attrs.push(`${name}="${escapeAttr2(value)}"`);
1459
1902
  }
1460
- const head = attrs.length > 0 ? `<${prefix}:${localName} ${attrs.join(" ")}>` : `<${prefix}:${localName}>`;
1903
+ const head = attrs.length > 0 ? `<${prefix}:${localName2} ${attrs.join(" ")}>` : `<${prefix}:${localName2}>`;
1461
1904
  let body = "";
1462
1905
  if (el.children) {
1463
1906
  for (const [childName, list] of Object.entries(el.children)) {
@@ -1466,7 +1909,7 @@ function namespacedElementXml(prefix, localName, el) {
1466
1909
  } else if (el.text != null) {
1467
1910
  body = escapeText(el.text);
1468
1911
  }
1469
- return `${head}${body}</${prefix}:${localName}>`;
1912
+ return `${head}${body}</${prefix}:${localName2}>`;
1470
1913
  }
1471
1914
  function metaAttrsXml(meta, namespaces) {
1472
1915
  if (!meta) return "";
@@ -1486,8 +1929,8 @@ function metaElementsXml(meta, namespaces) {
1486
1929
  for (const prefix of Object.keys(namespaces)) {
1487
1930
  const bucket = meta[prefix];
1488
1931
  if (!bucket?.elements) continue;
1489
- for (const [localName, list] of Object.entries(bucket.elements)) {
1490
- for (const el of list) out += namespacedElementXml(prefix, localName, el);
1932
+ for (const [localName2, list] of Object.entries(bucket.elements)) {
1933
+ for (const el of list) out += namespacedElementXml(prefix, localName2, el);
1491
1934
  }
1492
1935
  }
1493
1936
  return out;
@@ -1495,6 +1938,7 @@ function metaElementsXml(meta, namespaces) {
1495
1938
  function registerPaintServers(nodes, registry) {
1496
1939
  for (const n of nodes) {
1497
1940
  if (n.kind === "group") {
1941
+ if (n.clip) registry.clipId(n.clip);
1498
1942
  registerPaintServers(n.children, registry);
1499
1943
  } else if (n.kind === "path") {
1500
1944
  if (n.fill.kind === "gradient") registry.register(n.fill.paint);
@@ -1526,50 +1970,61 @@ function registerMarkers(stroke, registry) {
1526
1970
  if (key) registry.markerId(key);
1527
1971
  }
1528
1972
  }
1529
- function nodeXml(node, registry, namespaces) {
1530
- if (node.kind === "group") return groupXml(node, registry, namespaces);
1531
- if (node.kind === "text") return textXml(node, registry, namespaces);
1973
+ function nodeXml(node, registry, namespaces, warn) {
1974
+ if (node.kind === "group") return groupXml(node, registry, namespaces, warn);
1975
+ if (node.kind === "text") return textXml(node, registry, namespaces, warn);
1532
1976
  if (node.kind === "image") return imageXml(node, namespaces);
1533
- return pathXml(node, registry, namespaces);
1977
+ return pathXml(node, registry, namespaces, warn);
1978
+ }
1979
+ function warnStrokeAlign(align, warn) {
1980
+ if (align && align !== "center") {
1981
+ warn(`a stroke aligned "${align}" of its edge is written centered: SVG has no stroke alignment`);
1982
+ }
1534
1983
  }
1535
1984
  function imageXml(node, namespaces) {
1536
- const attrs = [
1537
- `href="${escapeAttr2(node.href)}"`,
1538
- `x="${trimNumber(node.x)}"`,
1539
- `y="${trimNumber(node.y)}"`,
1540
- `width="${trimNumber(node.width)}"`,
1541
- `height="${trimNumber(node.height)}"`,
1542
- `preserveAspectRatio="none"`
1543
- ];
1985
+ const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
1986
+ const outer = [];
1544
1987
  if (node.opacity != null && node.opacity !== 1) {
1545
- attrs.push(`opacity="${trimNumber(node.opacity)}"`);
1988
+ outer.push(`opacity="${trimNumber(node.opacity)}"`);
1546
1989
  }
1547
1990
  if (node.rotation != null && node.rotation !== 0) {
1548
1991
  const cx = node.x + node.width / 2;
1549
1992
  const cy = node.y + node.height / 2;
1550
1993
  const deg = node.rotation * 180 / Math.PI;
1551
- attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
1994
+ outer.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
1552
1995
  }
1553
1996
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
1554
1997
  const metaEls = metaElementsXml(node.meta, namespaces);
1555
- if (metaEls) return `<image ${attrs.join(" ")}${metaAttrs}>${metaEls}</image>`;
1556
- return `<image ${attrs.join(" ")}${metaAttrs}/>`;
1557
- }
1558
- function groupXml(node, registry, namespaces) {
1998
+ const href = `href="${escapeAttr2(node.href)}"`;
1999
+ if (!node.source && !node.flipX && !node.flipY) {
2000
+ const attrs = [href, box, 'preserveAspectRatio="none"', ...outer].join(" ");
2001
+ if (metaEls) return `<image ${attrs}${metaAttrs}>${metaEls}</image>`;
2002
+ return `<image ${attrs}${metaAttrs}/>`;
2003
+ }
2004
+ const src = node.source ?? { x: 0, y: 0, width: 1, height: 1 };
2005
+ const viewBox = [src.x, src.y, src.width, src.height].map(trimNumber).join(" ");
2006
+ const flip = node.flipX || node.flipY ? ` transform="matrix(${node.flipX ? -1 : 1} 0 0 ${node.flipY ? -1 : 1} ${trimNumber(node.flipX ? 2 * src.x + src.width : 0)} ${trimNumber(node.flipY ? 2 * src.y + src.height : 0)})"` : "";
2007
+ const head = ['<g data-weasel-image="true"', ...outer].join(" ");
2008
+ return `${head}${metaAttrs}><svg ${box} viewBox="${viewBox}" preserveAspectRatio="none"><image ${href} x="0" y="0" width="1" height="1" preserveAspectRatio="none"${flip}/></svg>${metaEls}</g>`;
2009
+ }
2010
+ function groupXml(node, registry, namespaces, warn) {
1559
2011
  const attrs = [];
1560
2012
  if (node.transform) {
1561
2013
  const m = formatMatrix(node.transform);
1562
2014
  if (m) attrs.push(`transform="${m}"`);
1563
2015
  }
2016
+ if (node.clip) {
2017
+ attrs.push(`clip-path="url(#${registry.clipId(node.clip)})"`);
2018
+ }
1564
2019
  if (node.opacity != null && node.opacity !== 1) {
1565
2020
  attrs.push(`opacity="${trimNumber(node.opacity)}"`);
1566
2021
  }
1567
2022
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
1568
2023
  const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
1569
- const body = node.children.map((c) => nodeXml(c, registry, namespaces)).join("");
2024
+ const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn)).join("");
1570
2025
  return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
1571
2026
  }
1572
- function pathXml(node, registry, namespaces) {
2027
+ function pathXml(node, registry, namespaces, warn) {
1573
2028
  const attrs = [`d="${serializePathD(node.path)}"`];
1574
2029
  const fillAttrs = paintAttrs(node.fill, "fill", registry);
1575
2030
  for (const a of fillAttrs) attrs.push(a);
@@ -1577,7 +2032,7 @@ function pathXml(node, registry, namespaces) {
1577
2032
  attrs.push(`fill-rule="evenodd"`);
1578
2033
  }
1579
2034
  if (node.stroke) {
1580
- const strokeAttrs = strokeAttrsFor(node.stroke, registry);
2035
+ const strokeAttrs = strokeAttrsFor(node.stroke, registry, warn);
1581
2036
  for (const a of strokeAttrs) attrs.push(a);
1582
2037
  } else {
1583
2038
  attrs.push('stroke="none"');
@@ -1608,18 +2063,18 @@ function paintAttrs(paint, name, registry, includeOpacity = true) {
1608
2063
  }
1609
2064
  return out;
1610
2065
  }
1611
- const id = registry.register(paint.paint);
1612
- return [`${name}="url(#${id})"`];
2066
+ return [`${name}="${registry.ref(paint.paint)}"`];
1613
2067
  }
1614
2068
  function strokeWidthAttrs(width) {
1615
2069
  if (typeof width === "number") return [`stroke-width="${trimNumber(width)}"`];
1616
2070
  return [`stroke-width="${trimNumber(width.px)}"`, 'vector-effect="non-scaling-stroke"'];
1617
2071
  }
1618
- function coreStrokeAttrs(stroke, registry) {
2072
+ function coreStrokeAttrs(stroke, registry, warn) {
1619
2073
  const paint = stroke?.paint;
1620
2074
  if (!stroke || !paint) return [];
1621
2075
  const width = stroke.width ?? 1;
1622
2076
  if (!((typeof width === "object" ? width.px : width) > 0)) return [];
2077
+ warnStrokeAlign(stroke.align, warn);
1623
2078
  const attrs = [];
1624
2079
  if ("color" in paint) {
1625
2080
  attrs.push(`stroke="${paint.color}"`);
@@ -1627,7 +2082,7 @@ function coreStrokeAttrs(stroke, registry) {
1627
2082
  attrs.push(`stroke-opacity="${trimNumber(paint.opacity)}"`);
1628
2083
  }
1629
2084
  } else {
1630
- attrs.push(`stroke="url(#${registry.register(paint)})"`);
2085
+ attrs.push(`stroke="${registry.ref(paint)}"`);
1631
2086
  }
1632
2087
  attrs.push(...strokeWidthAttrs(width));
1633
2088
  if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
@@ -1650,7 +2105,8 @@ function coreStrokeAttrs(stroke, registry) {
1650
2105
  }
1651
2106
  return attrs;
1652
2107
  }
1653
- function strokeAttrsFor(stroke, registry) {
2108
+ function strokeAttrsFor(stroke, registry, warn) {
2109
+ warnStrokeAlign(stroke.align, warn);
1654
2110
  const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
1655
2111
  attrs.push(...strokeWidthAttrs(stroke.width));
1656
2112
  const opacity = stroke.opacity ?? (stroke.paint.kind === "solid" ? stroke.paint.opacity : void 0);
@@ -1717,9 +2173,9 @@ function rotateAboutCenter(ctm, b, angle) {
1717
2173
  const s = Math.sin(angle);
1718
2174
  return multiply(ctm, [c, s, -s, c, cx - cx * c + cy * s, cy - cx * s - cy * c]);
1719
2175
  }
1720
- function textXml(node, registry, namespaces) {
2176
+ function textXml(node, registry, namespaces, warn) {
1721
2177
  const attrs = [
1722
- `x="${trimNumber(node.x)}"`,
2178
+ `x="${trimNumber(node.x + anchorOffset(node.style, node.width))}"`,
1723
2179
  `y="${trimNumber(node.y)}"`,
1724
2180
  `dominant-baseline="text-before-edge"`,
1725
2181
  // The runs model stores real line breaks; SVG's default whitespace
@@ -1729,7 +2185,7 @@ function textXml(node, registry, namespaces) {
1729
2185
  `data-weasel-height="${trimNumber(node.height)}"`
1730
2186
  ];
1731
2187
  const style = node.style;
1732
- if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(style.fontSize)}"`);
2188
+ if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(style.fontSize, 1))}"`);
1733
2189
  if (style?.fontFamily) attrs.push(`font-family="${escapeAttr2(style.fontFamily)}"`);
1734
2190
  if (style?.fontWeight != null) attrs.push(`font-weight="${String(style.fontWeight)}"`);
1735
2191
  if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
@@ -1741,10 +2197,18 @@ function textXml(node, registry, namespaces) {
1741
2197
  if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
1742
2198
  }
1743
2199
  if (style?.letterSpacing != null && style.letterSpacing !== 0) {
1744
- attrs.push(`letter-spacing="${trimNumber(style.letterSpacing)}"`);
2200
+ attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(style.letterSpacing, 1))}"`);
1745
2201
  }
1746
2202
  const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
1747
2203
  if (decoration) attrs.push(`text-decoration="${decoration}"`);
2204
+ if (style?.wrap) {
2205
+ attrs.push('data-weasel-wrap="true"');
2206
+ 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)");
2207
+ }
2208
+ if (node.verticalAlign && node.verticalAlign !== "top") {
2209
+ attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
2210
+ 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)`);
2211
+ }
1748
2212
  if (node.fill === null) {
1749
2213
  attrs.push('fill="none"');
1750
2214
  } else if (node.fill) {
@@ -1754,11 +2218,10 @@ function textXml(node, registry, namespaces) {
1754
2218
  attrs.push(`fill-opacity="${trimNumber(node.fill.opacity)}"`);
1755
2219
  }
1756
2220
  } else {
1757
- const id = registry.register(node.fill);
1758
- attrs.push(`fill="url(#${id})"`);
2221
+ attrs.push(`fill="${registry.ref(node.fill)}"`);
1759
2222
  }
1760
2223
  }
1761
- for (const a of coreStrokeAttrs(node.stroke, registry)) attrs.push(a);
2224
+ for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
1762
2225
  if (node.opacity != null && node.opacity !== 1) {
1763
2226
  attrs.push(`opacity="${trimNumber(node.opacity)}"`);
1764
2227
  }
@@ -1768,19 +2231,19 @@ function textXml(node, registry, namespaces) {
1768
2231
  const deg = node.rotation * 180 / Math.PI;
1769
2232
  attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
1770
2233
  }
1771
- const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, registry)).join("") : escapeText(node.text);
2234
+ const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, registry, warn)).join("") : escapeText(node.text);
1772
2235
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
1773
2236
  const metaEls = metaElementsXml(node.meta, namespaces);
1774
2237
  return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
1775
2238
  }
1776
- function runXml(run, registry) {
2239
+ function runXml(run, registry, warn) {
1777
2240
  const attrs = [];
1778
2241
  if (run.bold) attrs.push(`font-weight="700"`);
1779
2242
  if (run.italic) attrs.push(`font-style="italic"`);
1780
2243
  if (run.fontFamily) attrs.push(`font-family="${escapeAttr2(run.fontFamily)}"`);
1781
2244
  const presetScale = run.script != null && run.baselineShift != null ? SCRIPT_METRICS[run.script].size : void 0;
1782
2245
  const scale = run.fontScale ?? presetScale;
1783
- if (run.fontSize != null) attrs.push(`font-size="${trimNumber(run.fontSize)}"`);
2246
+ if (run.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(run.fontSize, 1))}"`);
1784
2247
  else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
1785
2248
  if (run.baselineShift != null) {
1786
2249
  attrs.push(`baseline-shift="${trimNumber(run.baselineShift * 100)}%"`);
@@ -1788,7 +2251,7 @@ function runXml(run, registry) {
1788
2251
  attrs.push(`baseline-shift="${run.script}"`);
1789
2252
  }
1790
2253
  if (run.letterSpacing != null) {
1791
- attrs.push(`letter-spacing="${trimNumber(run.letterSpacing)}"`);
2254
+ attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(run.letterSpacing, 1))}"`);
1792
2255
  }
1793
2256
  const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
1794
2257
  if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
@@ -1796,11 +2259,10 @@ function runXml(run, registry) {
1796
2259
  if ("color" in run.fill) {
1797
2260
  attrs.push(`fill="${run.fill.color}"`);
1798
2261
  } else {
1799
- const id = registry.register(run.fill);
1800
- attrs.push(`fill="url(#${id})"`);
2262
+ attrs.push(`fill="${registry.ref(run.fill)}"`);
1801
2263
  }
1802
2264
  }
1803
- for (const a of coreStrokeAttrs(run.stroke, registry)) attrs.push(a);
2265
+ for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
1804
2266
  const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
1805
2267
  return `${head}${escapeText(run.text)}</tspan>`;
1806
2268
  }
@@ -1909,7 +2371,7 @@ function svgNodesToKitDrafts(nodes, nextId) {
1909
2371
  const pose2 = {
1910
2372
  x: n.x,
1911
2373
  y: n.y,
1912
- width: textBoxWidth(n),
2374
+ width: n.width,
1913
2375
  height: n.height
1914
2376
  };
1915
2377
  if (n.rotation) pose2.rotation = n.rotation;
@@ -1927,6 +2389,7 @@ function svgNodesToKitDrafts(nodes, nextId) {
1927
2389
  data: {
1928
2390
  text: n.text,
1929
2391
  ...n.style ? { style: n.style } : {},
2392
+ ...n.verticalAlign ? { verticalAlign: n.verticalAlign } : {},
1930
2393
  ...n.runs ? { runs: n.runs.map((r) => runInBoxFrame(r, box)) } : {},
1931
2394
  // `!== undefined`, not a truthiness test: `null` is the document
1932
2395
  // saying `fill="none"`, and absent takes the painter's default.
@@ -1974,23 +2437,17 @@ function svgNodesToKitDrafts(nodes, nextId) {
1974
2437
  for (const n of nodes) visit(n, null);
1975
2438
  return drafts;
1976
2439
  }
1977
- var ESTIMATED_GLYPH_ADVANCE_EM = 0.6;
1978
- function textBoxWidth(n) {
1979
- if (n.width !== UNBOUNDED_TEXT_WIDTH) return n.width;
1980
- const fontSize = resolveTextStyle(n.style).fontSize;
1981
- const longest = n.text.split("\n").reduce((max, line) => Math.max(max, line.length), 0);
1982
- return longest * fontSize * ESTIMATED_GLYPH_ADVANCE_EM;
1983
- }
1984
2440
  function scaleTextData(data, scale) {
1985
2441
  if (scale === 1 || typeof data.text !== "string") return data;
1986
2442
  const style = data.style ?? {};
2443
+ const scaleSize = (v) => typeof v === "number" ? v * scale : v;
1987
2444
  const runs = data.runs;
1988
2445
  return {
1989
2446
  ...data,
1990
2447
  ...runs ? {
1991
- runs: runs.map((r) => r.fontSize !== void 0 ? { ...r, fontSize: r.fontSize * scale } : r)
2448
+ runs: runs.map((r) => r.fontSize !== void 0 ? { ...r, fontSize: scaleSize(r.fontSize) } : r)
1992
2449
  } : {},
1993
- style: { ...style, fontSize: resolveTextStyle(style).fontSize * scale }
2450
+ style: { ...style, fontSize: scaleSize(style.fontSize ?? resolveTextStyle(style).fontSize) }
1994
2451
  };
1995
2452
  }
1996
2453
  function unionRect(a, b) {
@@ -2008,6 +2465,7 @@ async function unpackSvgFiles(files, ctx) {
2008
2465
  const text = await readFileText(file);
2009
2466
  const parsed = parseSvg(text);
2010
2467
  for (const w of parsed.warnings) dwarn("ingest", `svg "${file.name}":`, w);
2468
+ for (const m of parsed.markers ?? []) if (getMarker(m.id) === void 0) registerMarker(m);
2011
2469
  let drafts = svgNodesToKitDrafts(parsed.nodes, freshSvgNodeId);
2012
2470
  if (drafts.length === 0) {
2013
2471
  console.warn(`weasel ingest: svg "${file.name}" parsed to no drawable nodes`);
@@ -2064,6 +2522,6 @@ async function unpackSvgFiles(files, ctx) {
2064
2522
  }
2065
2523
  }
2066
2524
 
2067
- export { IDENTITY_MATRIX, UNBOUNDED_TEXT_WIDTH, parseSvg, serializeSvg, strokeDataFromSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
2525
+ export { IDENTITY_MATRIX, parseSvg, serializeSvg, strokeDataFromSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
2068
2526
  //# sourceMappingURL=index.js.map
2069
2527
  //# sourceMappingURL=index.js.map