@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/README.md +43 -0
- package/dist/index.d.ts +53 -17
- package/dist/index.js +579 -121
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath,
|
|
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 =
|
|
89
|
-
|
|
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*\)
|
|
248
|
-
if (urlMatch)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
517
|
-
//
|
|
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
|
-
/**
|
|
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)
|
|
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
|
|
561
|
-
const outline = entry.outline ? `
|
|
562
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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 =
|
|
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(
|
|
1169
|
-
const rawY = num2(
|
|
1170
|
-
const rawW = num2(
|
|
1171
|
-
const rawH = num2(
|
|
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(
|
|
1391
|
+
const opacity = readOpacityAttr(ownerEl, "opacity");
|
|
1184
1392
|
if (opacity != null) node.opacity = opacity;
|
|
1185
|
-
const localTransform = parseTransform(
|
|
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 (
|
|
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
|
-
|
|
1201
|
-
|
|
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 :
|
|
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 (
|
|
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 [
|
|
1446
|
-
for (const el of list) docMetaXml += namespacedElementXml(prefix,
|
|
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
|
|
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,
|
|
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}:${
|
|
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}:${
|
|
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 [
|
|
1490
|
-
for (const el of list) out += namespacedElementXml(prefix,
|
|
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
|
|
1537
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
2448
|
+
runs: runs.map((r) => r.fontSize !== void 0 ? { ...r, fontSize: scaleSize(r.fontSize) } : r)
|
|
1992
2449
|
} : {},
|
|
1993
|
-
style: { ...style, fontSize: resolveTextStyle(style).fontSize
|
|
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,
|
|
2525
|
+
export { IDENTITY_MATRIX, parseSvg, serializeSvg, strokeDataFromSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
|
|
2068
2526
|
//# sourceMappingURL=index.js.map
|
|
2069
2527
|
//# sourceMappingURL=index.js.map
|