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