@weasel-js/svg 1.5.0 → 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 +47 -4
- package/dist/index.js +539 -93
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
2
|
import { invert } from '@weasel-js/geom';
|
|
3
3
|
import { tileGeometry } from '@weasel-js/core/patterns-builtin';
|
|
4
4
|
|
|
@@ -242,8 +242,18 @@ function parsePaintAttr(raw) {
|
|
|
242
242
|
if (s === "currentColor" || s === "currentcolor") {
|
|
243
243
|
return { kind: "solid", color: "#000000", alpha: 1 };
|
|
244
244
|
}
|
|
245
|
-
const urlMatch = /^url\(\s*#([^)\s]+)\s*\)
|
|
246
|
-
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
|
+
}
|
|
247
257
|
try {
|
|
248
258
|
const [r, g, b, a] = parseColor(s);
|
|
249
259
|
return { kind: "solid", color: rgbaToHex([r, g, b]), alpha: a };
|
|
@@ -387,13 +397,45 @@ function serializePathD(path) {
|
|
|
387
397
|
}
|
|
388
398
|
|
|
389
399
|
// src/gradients.ts
|
|
390
|
-
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
|
+
]);
|
|
391
432
|
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
392
433
|
function collectGradients(svg, onWarn) {
|
|
393
434
|
const out = /* @__PURE__ */ new Map();
|
|
394
435
|
const elements = collectElementsByTag(svg, GRADIENT_TAGS);
|
|
395
436
|
for (const [id, el] of elements) {
|
|
396
|
-
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);
|
|
397
439
|
if (paint) out.set(id, paint);
|
|
398
440
|
}
|
|
399
441
|
warnUnsupportedDefsChildren(svg, onWarn);
|
|
@@ -407,7 +449,8 @@ function warnUnsupportedDefsChildren(svg, onWarn) {
|
|
|
407
449
|
for (let i = 0; i < root.children.length; i++) {
|
|
408
450
|
const child = root.children[i];
|
|
409
451
|
const tag = child.tagName.toLowerCase();
|
|
410
|
-
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;
|
|
411
454
|
onWarn(`unsupported <defs> child: <${child.tagName}>`);
|
|
412
455
|
}
|
|
413
456
|
}
|
|
@@ -489,7 +532,8 @@ function readLinearGradient(el, elements, onWarn) {
|
|
|
489
532
|
y: num(inheritedAttr(el, elements, "y2"), 0)
|
|
490
533
|
},
|
|
491
534
|
stops: inheritedStops(el, elements, onWarn),
|
|
492
|
-
units: readGradientUnits(el, elements)
|
|
535
|
+
units: readGradientUnits(el, elements),
|
|
536
|
+
interpolate: readInterpolate(el, elements)
|
|
493
537
|
};
|
|
494
538
|
}
|
|
495
539
|
function readRadialGradient(el, elements, onWarn) {
|
|
@@ -502,7 +546,48 @@ function readRadialGradient(el, elements, onWarn) {
|
|
|
502
546
|
},
|
|
503
547
|
radius: num(inheritedAttr(el, elements, "r"), 0.5),
|
|
504
548
|
stops: inheritedStops(el, elements, onWarn),
|
|
505
|
-
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 } : {}
|
|
506
591
|
};
|
|
507
592
|
}
|
|
508
593
|
var PaintServerRegistry = class {
|
|
@@ -511,11 +596,14 @@ var PaintServerRegistry = class {
|
|
|
511
596
|
counter = 0;
|
|
512
597
|
// Marker keys referenced by any stroke, in first-use order. The `<defs>`
|
|
513
598
|
// id is the marker key itself, not a minted counter id: `parseSvg` reads a
|
|
514
|
-
// `marker-end="url(#id)"` fragment back as the marker key directly
|
|
515
|
-
//
|
|
516
|
-
// 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.
|
|
517
601
|
markerKeys = [];
|
|
518
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();
|
|
519
607
|
register(paint) {
|
|
520
608
|
const existing = this.byPaint.get(paint);
|
|
521
609
|
if (existing) return existing;
|
|
@@ -524,6 +612,23 @@ var PaintServerRegistry = class {
|
|
|
524
612
|
this.order.push(paint);
|
|
525
613
|
return id;
|
|
526
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
|
+
}
|
|
527
632
|
/** The `<defs>` id for a marker key — the key itself, minting nothing.
|
|
528
633
|
* `undefined` when nothing is registered under `key`, so a caller emits no
|
|
529
634
|
* attribute at all rather than a `url(#…)` pointing at a def that will
|
|
@@ -536,9 +641,21 @@ var PaintServerRegistry = class {
|
|
|
536
641
|
}
|
|
537
642
|
return key;
|
|
538
643
|
}
|
|
539
|
-
/**
|
|
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. */
|
|
540
655
|
toDefsXml(onWarn) {
|
|
541
|
-
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
|
+
}
|
|
542
659
|
const parts = ["<defs>"];
|
|
543
660
|
for (const paint of this.order) {
|
|
544
661
|
parts.push(paintServerXml(this.byPaint.get(paint), paint, onWarn));
|
|
@@ -546,18 +663,33 @@ var PaintServerRegistry = class {
|
|
|
546
663
|
for (const key of this.markerKeys) {
|
|
547
664
|
const entry = getMarker(key);
|
|
548
665
|
if (!entry) continue;
|
|
549
|
-
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
|
+
);
|
|
550
672
|
}
|
|
551
673
|
parts.push("</defs>");
|
|
552
674
|
return parts.join("");
|
|
553
675
|
}
|
|
554
676
|
};
|
|
555
|
-
function defaultMarkerXml(id, entry) {
|
|
677
|
+
function defaultMarkerXml(id, entry, onWarn) {
|
|
556
678
|
const path = entry.path({ size: 1, stroke: { paint: { fill: "solid", color: "#000" } } });
|
|
557
679
|
const d = serializePathD(path);
|
|
558
|
-
const fill = entry.fill
|
|
559
|
-
const outline = entry.outline ? `
|
|
560
|
-
|
|
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"`;
|
|
561
693
|
}
|
|
562
694
|
function paintServerXml(id, paint, onWarn) {
|
|
563
695
|
if (paint.fill === "pattern") return patternXml(id, paint, onWarn);
|
|
@@ -574,14 +706,25 @@ function gradientUnitsAttr(units) {
|
|
|
574
706
|
function gradientXml(id, paint) {
|
|
575
707
|
if (paint.fill === "linear-gradient") {
|
|
576
708
|
const stops = paint.stops.map(stopXml).join("");
|
|
577
|
-
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>`;
|
|
578
715
|
}
|
|
579
716
|
if (paint.fill === "radial-gradient") {
|
|
580
717
|
const stops = paint.stops.map(stopXml).join("");
|
|
581
|
-
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>`;
|
|
582
719
|
}
|
|
583
720
|
return "";
|
|
584
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
|
+
}
|
|
585
728
|
function stopXml(s) {
|
|
586
729
|
const m = /^#([0-9a-f]{6})([0-9a-f]{2})$/i.exec(s.color);
|
|
587
730
|
if (m) {
|
|
@@ -661,6 +804,7 @@ var IGNORED_TAGS = /* @__PURE__ */ new Set([
|
|
|
661
804
|
"radialgradient",
|
|
662
805
|
"pattern",
|
|
663
806
|
"marker",
|
|
807
|
+
"clippath",
|
|
664
808
|
"title",
|
|
665
809
|
"desc",
|
|
666
810
|
"metadata"
|
|
@@ -693,8 +837,11 @@ function parseSvg(svg, opts = {}) {
|
|
|
693
837
|
const gradients = collectGradients(root, onWarn);
|
|
694
838
|
for (const [id, paint] of collectPatterns(root, onWarn)) gradients.set(id, paint);
|
|
695
839
|
const rootStyle = deriveStyle(EMPTY_STYLE, root);
|
|
696
|
-
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);
|
|
697
843
|
const result = { nodes, warnings };
|
|
844
|
+
if (markers.length > 0) result.markers = markers;
|
|
698
845
|
if (documentMeta) result.documentMeta = documentMeta;
|
|
699
846
|
const viewBox = parseViewBoxAttr(root.getAttribute("viewBox"));
|
|
700
847
|
if (viewBox) result.viewBox = viewBox;
|
|
@@ -796,13 +943,53 @@ function collectElementMeta(el, uriToPrefix) {
|
|
|
796
943
|
}
|
|
797
944
|
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
798
945
|
}
|
|
799
|
-
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) {
|
|
800
987
|
const out = [];
|
|
801
988
|
for (let i = 0; i < parent.children.length; i++) {
|
|
802
989
|
const el = parent.children[i];
|
|
803
990
|
const ns = el.namespaceURI;
|
|
804
991
|
if (ns && ns !== "http://www.w3.org/2000/svg") continue;
|
|
805
|
-
const node = parseElement(el, ctm, style, gradients, onWarn, uriToPrefix);
|
|
992
|
+
const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix);
|
|
806
993
|
if (node) {
|
|
807
994
|
if (Array.isArray(node)) out.push(...node);
|
|
808
995
|
else out.push(node);
|
|
@@ -810,16 +997,26 @@ function parseChildren(parent, ctm, style, gradients, onWarn, uriToPrefix) {
|
|
|
810
997
|
}
|
|
811
998
|
return out;
|
|
812
999
|
}
|
|
813
|
-
function parseElement(el, ctm, style, gradients, onWarn, uriToPrefix) {
|
|
1000
|
+
function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
|
|
814
1001
|
const tag = el.tagName.toLowerCase();
|
|
815
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
|
+
}
|
|
816
1011
|
if (tag === "g") {
|
|
817
1012
|
const local = parseTransform(el.getAttribute("transform"), onWarn);
|
|
818
1013
|
const childCtm = multiply(ctm, local);
|
|
819
1014
|
const childStyle = deriveStyle(style, el);
|
|
820
|
-
const children = parseChildren(el, childCtm, childStyle, gradients, onWarn, uriToPrefix);
|
|
1015
|
+
const children = parseChildren(el, childCtm, childStyle, gradients, clips, onWarn, uriToPrefix);
|
|
821
1016
|
const opacity2 = readOpacityAttr(el, "opacity");
|
|
822
1017
|
const group = { kind: "group", children };
|
|
1018
|
+
const clip = clipFor(el, childCtm, clips, onWarn);
|
|
1019
|
+
if (clip) group.clip = clip;
|
|
823
1020
|
if (opacity2 != null) group.opacity = opacity2;
|
|
824
1021
|
const meta2 = collectElementMeta(el, uriToPrefix);
|
|
825
1022
|
if (meta2) group.meta = meta2;
|
|
@@ -833,7 +1030,7 @@ function parseElement(el, ctm, style, gradients, onWarn, uriToPrefix) {
|
|
|
833
1030
|
if (el.hasAttribute("viewBox")) {
|
|
834
1031
|
onWarn("nested <svg> viewBox is not modeled; its children are not rescaled");
|
|
835
1032
|
}
|
|
836
|
-
return parseChildren(el, multiply(ctm, offset), childStyle, gradients, onWarn, uriToPrefix);
|
|
1033
|
+
return parseChildren(el, multiply(ctm, offset), childStyle, gradients, clips, onWarn, uriToPrefix);
|
|
837
1034
|
}
|
|
838
1035
|
if (tag === "text") {
|
|
839
1036
|
const textNode = parseTextElement(el, ctm, style, gradients, onWarn);
|
|
@@ -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,9 +1388,9 @@ 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;
|
|
@@ -1199,9 +1407,21 @@ function parseImageElement(el, ctm, onWarn) {
|
|
|
1199
1407
|
}
|
|
1200
1408
|
}
|
|
1201
1409
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
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;
|
|
1205
1425
|
return node;
|
|
1206
1426
|
}
|
|
1207
1427
|
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
@@ -1268,7 +1488,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
1268
1488
|
const leafStyle = deriveStyle(style, el);
|
|
1269
1489
|
const textStyle = readTextStyle(leafStyle, onWarn);
|
|
1270
1490
|
const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
|
|
1271
|
-
const fontSize = textStyle.fontSize ?? 16;
|
|
1491
|
+
const fontSize = resolveScreenLength(textStyle.fontSize ?? 16, 1);
|
|
1272
1492
|
const lineHeight = textStyle.lineHeight ?? 1.2;
|
|
1273
1493
|
const dominantBaseline = el.getAttribute("dominant-baseline");
|
|
1274
1494
|
const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
|
|
@@ -1315,6 +1535,8 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
1315
1535
|
);
|
|
1316
1536
|
if (hasStyling) node.runs = runs;
|
|
1317
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;
|
|
1318
1540
|
if (Object.keys(textStyle).length > 0) node.style = textStyle;
|
|
1319
1541
|
if (textPaint.fill !== void 0) node.fill = textPaint.fill;
|
|
1320
1542
|
if (textPaint.stroke != null) node.stroke = textPaint.stroke;
|
|
@@ -1376,6 +1598,7 @@ function readTspanRun(el, gradients, style, onWarn) {
|
|
|
1376
1598
|
} else if (parsed?.kind === "ref") {
|
|
1377
1599
|
const paint = gradients.get(parsed.id);
|
|
1378
1600
|
if (paint) run.fill = paint;
|
|
1601
|
+
else if (parsed.fallback) run.fill = { fill: "solid", color: parsed.fallback.color };
|
|
1379
1602
|
}
|
|
1380
1603
|
}
|
|
1381
1604
|
const stroke = coreStroke(readStroke(ownStrokeStyle(el), gradients, onWarn, isNonScalingStroke(el)));
|
|
@@ -1425,6 +1648,9 @@ function readTextPaint(style, gradients, onWarn, nonScalingStroke = false) {
|
|
|
1425
1648
|
} else if (parsed?.kind === "ref") {
|
|
1426
1649
|
const paint = gradients.get(parsed.id);
|
|
1427
1650
|
if (paint) out.fill = paint;
|
|
1651
|
+
else if (parsed.fallback) {
|
|
1652
|
+
out.fill = { fill: "solid", color: parsed.fallback.color };
|
|
1653
|
+
}
|
|
1428
1654
|
} else if (parsed?.kind === "none") {
|
|
1429
1655
|
out.fill = null;
|
|
1430
1656
|
}
|
|
@@ -1433,6 +1659,196 @@ function readTextPaint(style, gradients, onWarn, nonScalingStroke = false) {
|
|
|
1433
1659
|
if (stroke) out.stroke = stroke;
|
|
1434
1660
|
return out;
|
|
1435
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
|
+
}
|
|
1436
1852
|
function serializeSvg(nodes, opts = {}) {
|
|
1437
1853
|
const registry = new PaintServerRegistry();
|
|
1438
1854
|
registerPaintServers(nodes, registry);
|
|
@@ -1440,6 +1856,9 @@ function serializeSvg(nodes, opts = {}) {
|
|
|
1440
1856
|
const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
|
|
1441
1857
|
const namespaces = opts.namespaces ?? {};
|
|
1442
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
|
+
}
|
|
1443
1862
|
for (const [prefix, uri] of Object.entries(namespaces)) {
|
|
1444
1863
|
rootAttrs.push(`xmlns:${prefix}="${escapeAttr2(uri)}"`);
|
|
1445
1864
|
}
|
|
@@ -1460,22 +1879,28 @@ function serializeSvg(nodes, opts = {}) {
|
|
|
1460
1879
|
for (const prefix of Object.keys(namespaces)) {
|
|
1461
1880
|
const bucket = opts.documentMeta[prefix];
|
|
1462
1881
|
if (!bucket?.elements) continue;
|
|
1463
|
-
for (const [
|
|
1464
|
-
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);
|
|
1465
1884
|
}
|
|
1466
1885
|
}
|
|
1467
1886
|
}
|
|
1468
1887
|
const defsXml = registry.toDefsXml(opts.onWarn);
|
|
1469
|
-
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("");
|
|
1470
1895
|
const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
|
|
1471
1896
|
return `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
|
|
1472
1897
|
}
|
|
1473
|
-
function namespacedElementXml(prefix,
|
|
1898
|
+
function namespacedElementXml(prefix, localName2, el) {
|
|
1474
1899
|
const attrs = [];
|
|
1475
1900
|
for (const [name, value] of Object.entries(el.attrs)) {
|
|
1476
1901
|
attrs.push(`${name}="${escapeAttr2(value)}"`);
|
|
1477
1902
|
}
|
|
1478
|
-
const head = attrs.length > 0 ? `<${prefix}:${
|
|
1903
|
+
const head = attrs.length > 0 ? `<${prefix}:${localName2} ${attrs.join(" ")}>` : `<${prefix}:${localName2}>`;
|
|
1479
1904
|
let body = "";
|
|
1480
1905
|
if (el.children) {
|
|
1481
1906
|
for (const [childName, list] of Object.entries(el.children)) {
|
|
@@ -1484,7 +1909,7 @@ function namespacedElementXml(prefix, localName, el) {
|
|
|
1484
1909
|
} else if (el.text != null) {
|
|
1485
1910
|
body = escapeText(el.text);
|
|
1486
1911
|
}
|
|
1487
|
-
return `${head}${body}</${prefix}:${
|
|
1912
|
+
return `${head}${body}</${prefix}:${localName2}>`;
|
|
1488
1913
|
}
|
|
1489
1914
|
function metaAttrsXml(meta, namespaces) {
|
|
1490
1915
|
if (!meta) return "";
|
|
@@ -1504,8 +1929,8 @@ function metaElementsXml(meta, namespaces) {
|
|
|
1504
1929
|
for (const prefix of Object.keys(namespaces)) {
|
|
1505
1930
|
const bucket = meta[prefix];
|
|
1506
1931
|
if (!bucket?.elements) continue;
|
|
1507
|
-
for (const [
|
|
1508
|
-
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);
|
|
1509
1934
|
}
|
|
1510
1935
|
}
|
|
1511
1936
|
return out;
|
|
@@ -1513,6 +1938,7 @@ function metaElementsXml(meta, namespaces) {
|
|
|
1513
1938
|
function registerPaintServers(nodes, registry) {
|
|
1514
1939
|
for (const n of nodes) {
|
|
1515
1940
|
if (n.kind === "group") {
|
|
1941
|
+
if (n.clip) registry.clipId(n.clip);
|
|
1516
1942
|
registerPaintServers(n.children, registry);
|
|
1517
1943
|
} else if (n.kind === "path") {
|
|
1518
1944
|
if (n.fill.kind === "gradient") registry.register(n.fill.paint);
|
|
@@ -1544,50 +1970,61 @@ function registerMarkers(stroke, registry) {
|
|
|
1544
1970
|
if (key) registry.markerId(key);
|
|
1545
1971
|
}
|
|
1546
1972
|
}
|
|
1547
|
-
function nodeXml(node, registry, namespaces) {
|
|
1548
|
-
if (node.kind === "group") return groupXml(node, registry, namespaces);
|
|
1549
|
-
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);
|
|
1550
1976
|
if (node.kind === "image") return imageXml(node, namespaces);
|
|
1551
|
-
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
|
+
}
|
|
1552
1983
|
}
|
|
1553
1984
|
function imageXml(node, namespaces) {
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
`x="${trimNumber(node.x)}"`,
|
|
1557
|
-
`y="${trimNumber(node.y)}"`,
|
|
1558
|
-
`width="${trimNumber(node.width)}"`,
|
|
1559
|
-
`height="${trimNumber(node.height)}"`,
|
|
1560
|
-
`preserveAspectRatio="none"`
|
|
1561
|
-
];
|
|
1985
|
+
const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
|
|
1986
|
+
const outer = [];
|
|
1562
1987
|
if (node.opacity != null && node.opacity !== 1) {
|
|
1563
|
-
|
|
1988
|
+
outer.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
1564
1989
|
}
|
|
1565
1990
|
if (node.rotation != null && node.rotation !== 0) {
|
|
1566
1991
|
const cx = node.x + node.width / 2;
|
|
1567
1992
|
const cy = node.y + node.height / 2;
|
|
1568
1993
|
const deg = node.rotation * 180 / Math.PI;
|
|
1569
|
-
|
|
1994
|
+
outer.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
1570
1995
|
}
|
|
1571
1996
|
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
1572
1997
|
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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) {
|
|
1577
2011
|
const attrs = [];
|
|
1578
2012
|
if (node.transform) {
|
|
1579
2013
|
const m = formatMatrix(node.transform);
|
|
1580
2014
|
if (m) attrs.push(`transform="${m}"`);
|
|
1581
2015
|
}
|
|
2016
|
+
if (node.clip) {
|
|
2017
|
+
attrs.push(`clip-path="url(#${registry.clipId(node.clip)})"`);
|
|
2018
|
+
}
|
|
1582
2019
|
if (node.opacity != null && node.opacity !== 1) {
|
|
1583
2020
|
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
1584
2021
|
}
|
|
1585
2022
|
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
1586
2023
|
const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
|
|
1587
|
-
const body = node.children.map((c) => nodeXml(c, registry, namespaces)).join("");
|
|
2024
|
+
const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn)).join("");
|
|
1588
2025
|
return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
|
|
1589
2026
|
}
|
|
1590
|
-
function pathXml(node, registry, namespaces) {
|
|
2027
|
+
function pathXml(node, registry, namespaces, warn) {
|
|
1591
2028
|
const attrs = [`d="${serializePathD(node.path)}"`];
|
|
1592
2029
|
const fillAttrs = paintAttrs(node.fill, "fill", registry);
|
|
1593
2030
|
for (const a of fillAttrs) attrs.push(a);
|
|
@@ -1595,7 +2032,7 @@ function pathXml(node, registry, namespaces) {
|
|
|
1595
2032
|
attrs.push(`fill-rule="evenodd"`);
|
|
1596
2033
|
}
|
|
1597
2034
|
if (node.stroke) {
|
|
1598
|
-
const strokeAttrs = strokeAttrsFor(node.stroke, registry);
|
|
2035
|
+
const strokeAttrs = strokeAttrsFor(node.stroke, registry, warn);
|
|
1599
2036
|
for (const a of strokeAttrs) attrs.push(a);
|
|
1600
2037
|
} else {
|
|
1601
2038
|
attrs.push('stroke="none"');
|
|
@@ -1626,18 +2063,18 @@ function paintAttrs(paint, name, registry, includeOpacity = true) {
|
|
|
1626
2063
|
}
|
|
1627
2064
|
return out;
|
|
1628
2065
|
}
|
|
1629
|
-
|
|
1630
|
-
return [`${name}="url(#${id})"`];
|
|
2066
|
+
return [`${name}="${registry.ref(paint.paint)}"`];
|
|
1631
2067
|
}
|
|
1632
2068
|
function strokeWidthAttrs(width) {
|
|
1633
2069
|
if (typeof width === "number") return [`stroke-width="${trimNumber(width)}"`];
|
|
1634
2070
|
return [`stroke-width="${trimNumber(width.px)}"`, 'vector-effect="non-scaling-stroke"'];
|
|
1635
2071
|
}
|
|
1636
|
-
function coreStrokeAttrs(stroke, registry) {
|
|
2072
|
+
function coreStrokeAttrs(stroke, registry, warn) {
|
|
1637
2073
|
const paint = stroke?.paint;
|
|
1638
2074
|
if (!stroke || !paint) return [];
|
|
1639
2075
|
const width = stroke.width ?? 1;
|
|
1640
2076
|
if (!((typeof width === "object" ? width.px : width) > 0)) return [];
|
|
2077
|
+
warnStrokeAlign(stroke.align, warn);
|
|
1641
2078
|
const attrs = [];
|
|
1642
2079
|
if ("color" in paint) {
|
|
1643
2080
|
attrs.push(`stroke="${paint.color}"`);
|
|
@@ -1645,7 +2082,7 @@ function coreStrokeAttrs(stroke, registry) {
|
|
|
1645
2082
|
attrs.push(`stroke-opacity="${trimNumber(paint.opacity)}"`);
|
|
1646
2083
|
}
|
|
1647
2084
|
} else {
|
|
1648
|
-
attrs.push(`stroke="
|
|
2085
|
+
attrs.push(`stroke="${registry.ref(paint)}"`);
|
|
1649
2086
|
}
|
|
1650
2087
|
attrs.push(...strokeWidthAttrs(width));
|
|
1651
2088
|
if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
@@ -1668,7 +2105,8 @@ function coreStrokeAttrs(stroke, registry) {
|
|
|
1668
2105
|
}
|
|
1669
2106
|
return attrs;
|
|
1670
2107
|
}
|
|
1671
|
-
function strokeAttrsFor(stroke, registry) {
|
|
2108
|
+
function strokeAttrsFor(stroke, registry, warn) {
|
|
2109
|
+
warnStrokeAlign(stroke.align, warn);
|
|
1672
2110
|
const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
|
|
1673
2111
|
attrs.push(...strokeWidthAttrs(stroke.width));
|
|
1674
2112
|
const opacity = stroke.opacity ?? (stroke.paint.kind === "solid" ? stroke.paint.opacity : void 0);
|
|
@@ -1735,7 +2173,7 @@ function rotateAboutCenter(ctm, b, angle) {
|
|
|
1735
2173
|
const s = Math.sin(angle);
|
|
1736
2174
|
return multiply(ctm, [c, s, -s, c, cx - cx * c + cy * s, cy - cx * s - cy * c]);
|
|
1737
2175
|
}
|
|
1738
|
-
function textXml(node, registry, namespaces) {
|
|
2176
|
+
function textXml(node, registry, namespaces, warn) {
|
|
1739
2177
|
const attrs = [
|
|
1740
2178
|
`x="${trimNumber(node.x + anchorOffset(node.style, node.width))}"`,
|
|
1741
2179
|
`y="${trimNumber(node.y)}"`,
|
|
@@ -1747,7 +2185,7 @@ function textXml(node, registry, namespaces) {
|
|
|
1747
2185
|
`data-weasel-height="${trimNumber(node.height)}"`
|
|
1748
2186
|
];
|
|
1749
2187
|
const style = node.style;
|
|
1750
|
-
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))}"`);
|
|
1751
2189
|
if (style?.fontFamily) attrs.push(`font-family="${escapeAttr2(style.fontFamily)}"`);
|
|
1752
2190
|
if (style?.fontWeight != null) attrs.push(`font-weight="${String(style.fontWeight)}"`);
|
|
1753
2191
|
if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
|
|
@@ -1759,11 +2197,18 @@ function textXml(node, registry, namespaces) {
|
|
|
1759
2197
|
if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
|
|
1760
2198
|
}
|
|
1761
2199
|
if (style?.letterSpacing != null && style.letterSpacing !== 0) {
|
|
1762
|
-
attrs.push(`letter-spacing="${trimNumber(style.letterSpacing)}"`);
|
|
2200
|
+
attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(style.letterSpacing, 1))}"`);
|
|
1763
2201
|
}
|
|
1764
2202
|
const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
|
|
1765
2203
|
if (decoration) attrs.push(`text-decoration="${decoration}"`);
|
|
1766
|
-
if (style?.wrap)
|
|
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
|
+
}
|
|
1767
2212
|
if (node.fill === null) {
|
|
1768
2213
|
attrs.push('fill="none"');
|
|
1769
2214
|
} else if (node.fill) {
|
|
@@ -1773,11 +2218,10 @@ function textXml(node, registry, namespaces) {
|
|
|
1773
2218
|
attrs.push(`fill-opacity="${trimNumber(node.fill.opacity)}"`);
|
|
1774
2219
|
}
|
|
1775
2220
|
} else {
|
|
1776
|
-
|
|
1777
|
-
attrs.push(`fill="url(#${id})"`);
|
|
2221
|
+
attrs.push(`fill="${registry.ref(node.fill)}"`);
|
|
1778
2222
|
}
|
|
1779
2223
|
}
|
|
1780
|
-
for (const a of coreStrokeAttrs(node.stroke, registry)) attrs.push(a);
|
|
2224
|
+
for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
|
|
1781
2225
|
if (node.opacity != null && node.opacity !== 1) {
|
|
1782
2226
|
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
1783
2227
|
}
|
|
@@ -1787,19 +2231,19 @@ function textXml(node, registry, namespaces) {
|
|
|
1787
2231
|
const deg = node.rotation * 180 / Math.PI;
|
|
1788
2232
|
attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
1789
2233
|
}
|
|
1790
|
-
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);
|
|
1791
2235
|
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
1792
2236
|
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
1793
2237
|
return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
|
|
1794
2238
|
}
|
|
1795
|
-
function runXml(run, registry) {
|
|
2239
|
+
function runXml(run, registry, warn) {
|
|
1796
2240
|
const attrs = [];
|
|
1797
2241
|
if (run.bold) attrs.push(`font-weight="700"`);
|
|
1798
2242
|
if (run.italic) attrs.push(`font-style="italic"`);
|
|
1799
2243
|
if (run.fontFamily) attrs.push(`font-family="${escapeAttr2(run.fontFamily)}"`);
|
|
1800
2244
|
const presetScale = run.script != null && run.baselineShift != null ? SCRIPT_METRICS[run.script].size : void 0;
|
|
1801
2245
|
const scale = run.fontScale ?? presetScale;
|
|
1802
|
-
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))}"`);
|
|
1803
2247
|
else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
|
|
1804
2248
|
if (run.baselineShift != null) {
|
|
1805
2249
|
attrs.push(`baseline-shift="${trimNumber(run.baselineShift * 100)}%"`);
|
|
@@ -1807,7 +2251,7 @@ function runXml(run, registry) {
|
|
|
1807
2251
|
attrs.push(`baseline-shift="${run.script}"`);
|
|
1808
2252
|
}
|
|
1809
2253
|
if (run.letterSpacing != null) {
|
|
1810
|
-
attrs.push(`letter-spacing="${trimNumber(run.letterSpacing)}"`);
|
|
2254
|
+
attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(run.letterSpacing, 1))}"`);
|
|
1811
2255
|
}
|
|
1812
2256
|
const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
|
|
1813
2257
|
if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
|
|
@@ -1815,11 +2259,10 @@ function runXml(run, registry) {
|
|
|
1815
2259
|
if ("color" in run.fill) {
|
|
1816
2260
|
attrs.push(`fill="${run.fill.color}"`);
|
|
1817
2261
|
} else {
|
|
1818
|
-
|
|
1819
|
-
attrs.push(`fill="url(#${id})"`);
|
|
2262
|
+
attrs.push(`fill="${registry.ref(run.fill)}"`);
|
|
1820
2263
|
}
|
|
1821
2264
|
}
|
|
1822
|
-
for (const a of coreStrokeAttrs(run.stroke, registry)) attrs.push(a);
|
|
2265
|
+
for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
|
|
1823
2266
|
const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
|
|
1824
2267
|
return `${head}${escapeText(run.text)}</tspan>`;
|
|
1825
2268
|
}
|
|
@@ -1946,6 +2389,7 @@ function svgNodesToKitDrafts(nodes, nextId) {
|
|
|
1946
2389
|
data: {
|
|
1947
2390
|
text: n.text,
|
|
1948
2391
|
...n.style ? { style: n.style } : {},
|
|
2392
|
+
...n.verticalAlign ? { verticalAlign: n.verticalAlign } : {},
|
|
1949
2393
|
...n.runs ? { runs: n.runs.map((r) => runInBoxFrame(r, box)) } : {},
|
|
1950
2394
|
// `!== undefined`, not a truthiness test: `null` is the document
|
|
1951
2395
|
// saying `fill="none"`, and absent takes the painter's default.
|
|
@@ -1996,13 +2440,14 @@ function svgNodesToKitDrafts(nodes, nextId) {
|
|
|
1996
2440
|
function scaleTextData(data, scale) {
|
|
1997
2441
|
if (scale === 1 || typeof data.text !== "string") return data;
|
|
1998
2442
|
const style = data.style ?? {};
|
|
2443
|
+
const scaleSize = (v) => typeof v === "number" ? v * scale : v;
|
|
1999
2444
|
const runs = data.runs;
|
|
2000
2445
|
return {
|
|
2001
2446
|
...data,
|
|
2002
2447
|
...runs ? {
|
|
2003
|
-
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)
|
|
2004
2449
|
} : {},
|
|
2005
|
-
style: { ...style, fontSize: resolveTextStyle(style).fontSize
|
|
2450
|
+
style: { ...style, fontSize: scaleSize(style.fontSize ?? resolveTextStyle(style).fontSize) }
|
|
2006
2451
|
};
|
|
2007
2452
|
}
|
|
2008
2453
|
function unionRect(a, b) {
|
|
@@ -2020,6 +2465,7 @@ async function unpackSvgFiles(files, ctx) {
|
|
|
2020
2465
|
const text = await readFileText(file);
|
|
2021
2466
|
const parsed = parseSvg(text);
|
|
2022
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);
|
|
2023
2469
|
let drafts = svgNodesToKitDrafts(parsed.nodes, freshSvgNodeId);
|
|
2024
2470
|
if (drafts.length === 0) {
|
|
2025
2471
|
console.warn(`weasel ingest: svg "${file.name}" parsed to no drawable nodes`);
|