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