@emasoft/svg-matrix 1.0.27 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +325 -0
- package/bin/svg-matrix.js +994 -378
- package/bin/svglinter.cjs +4172 -433
- package/bin/svgm.js +744 -184
- package/package.json +16 -4
- package/src/animation-references.js +71 -52
- package/src/arc-length.js +160 -96
- package/src/bezier-analysis.js +257 -117
- package/src/bezier-intersections.js +411 -148
- package/src/browser-verify.js +240 -100
- package/src/clip-path-resolver.js +350 -142
- package/src/convert-path-data.js +279 -134
- package/src/css-specificity.js +78 -70
- package/src/flatten-pipeline.js +751 -263
- package/src/geometry-to-path.js +511 -182
- package/src/index.js +191 -46
- package/src/inkscape-support.js +404 -0
- package/src/marker-resolver.js +278 -164
- package/src/mask-resolver.js +209 -98
- package/src/matrix.js +147 -67
- package/src/mesh-gradient.js +187 -96
- package/src/off-canvas-detection.js +201 -104
- package/src/path-analysis.js +187 -107
- package/src/path-data-plugins.js +628 -167
- package/src/path-simplification.js +0 -1
- package/src/pattern-resolver.js +125 -88
- package/src/polygon-clip.js +111 -66
- package/src/svg-boolean-ops.js +194 -118
- package/src/svg-collections.js +48 -19
- package/src/svg-flatten.js +282 -164
- package/src/svg-parser.js +427 -200
- package/src/svg-rendering-context.js +147 -104
- package/src/svg-toolbox.js +16411 -3298
- package/src/svg2-polyfills.js +114 -245
- package/src/transform-decomposition.js +46 -41
- package/src/transform-optimization.js +89 -68
- package/src/transforms2d.js +49 -16
- package/src/transforms3d.js +58 -22
- package/src/use-symbol-resolver.js +150 -110
- package/src/vector.js +67 -15
- package/src/vendor/README.md +110 -0
- package/src/vendor/inkscape-hatch-polyfill.js +401 -0
- package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
- package/src/vendor/inkscape-mesh-polyfill.js +843 -0
- package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
- package/src/verification.js +288 -124
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emasoft/svg-matrix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.29",
|
|
4
4
|
"description": "Arbitrary-precision matrix, vector and affine transformation library for JavaScript using decimal.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
"test": "node test/examples.js",
|
|
9
9
|
"test:browser": "node test/browser-verify.mjs",
|
|
10
10
|
"test:playwright": "node test/playwright-diagnose.js",
|
|
11
|
+
"test:embed": "node test/test-embed-real-world.js",
|
|
12
|
+
"test:embed:w3c": "node test/test-embed-w3c.js",
|
|
13
|
+
"test:embed:w3c:all": "node test/test-embed-w3c-all.js",
|
|
14
|
+
"test:toolbox:8bits": "node test/test-toolbox-matrix-8bits.js",
|
|
15
|
+
"test:embed:browser": "node test/test-embed-playwright.js",
|
|
11
16
|
"ci-test": "npm ci && npm test",
|
|
12
17
|
"lint": "node bin/svglinter.cjs",
|
|
13
18
|
"lint:fix": "node bin/svglinter.cjs --fix",
|
|
@@ -69,10 +74,13 @@
|
|
|
69
74
|
"README.md"
|
|
70
75
|
],
|
|
71
76
|
"dependencies": {
|
|
72
|
-
"decimal.js": "^10.6.0"
|
|
77
|
+
"decimal.js": "^10.6.0",
|
|
78
|
+
"js-yaml": "^4.1.0",
|
|
79
|
+
"jsdom": "^27.3.0",
|
|
80
|
+
"toml": "^3.0.0"
|
|
73
81
|
},
|
|
74
82
|
"peerDependencies": {
|
|
75
|
-
"playwright": "^1.
|
|
83
|
+
"playwright": "^1.49.0"
|
|
76
84
|
},
|
|
77
85
|
"peerDependenciesMeta": {
|
|
78
86
|
"playwright": {
|
|
@@ -80,6 +88,10 @@
|
|
|
80
88
|
}
|
|
81
89
|
},
|
|
82
90
|
"devDependencies": {
|
|
83
|
-
"
|
|
91
|
+
"@eslint/js": "^9.39.2",
|
|
92
|
+
"globals": "^16.5.0",
|
|
93
|
+
"playwright": "^1.49.0",
|
|
94
|
+
"svgo": "^4.0.0",
|
|
95
|
+
"typescript": "^5.9.3"
|
|
84
96
|
}
|
|
85
97
|
}
|
|
@@ -28,26 +28,39 @@
|
|
|
28
28
|
* @module animation-references
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { SVGElement } from
|
|
31
|
+
import { SVGElement } from "./svg-parser.js";
|
|
32
32
|
|
|
33
33
|
// Animation elements that can reference other elements
|
|
34
|
-
const ANIMATION_ELEMENTS = [
|
|
34
|
+
const ANIMATION_ELEMENTS = [
|
|
35
|
+
"animate",
|
|
36
|
+
"animateTransform",
|
|
37
|
+
"animateMotion",
|
|
38
|
+
"animateColor",
|
|
39
|
+
"set",
|
|
40
|
+
];
|
|
35
41
|
|
|
36
42
|
// Attributes that can contain ID references
|
|
37
|
-
const HREF_ATTRIBUTES = [
|
|
43
|
+
const HREF_ATTRIBUTES = ["href", "xlink:href"];
|
|
38
44
|
|
|
39
45
|
// Attributes that use url(#id) syntax
|
|
40
46
|
const URL_ATTRIBUTES = [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
"fill",
|
|
48
|
+
"stroke",
|
|
49
|
+
"clip-path",
|
|
50
|
+
"mask",
|
|
51
|
+
"filter",
|
|
52
|
+
"marker-start",
|
|
53
|
+
"marker-mid",
|
|
54
|
+
"marker-end",
|
|
55
|
+
"cursor",
|
|
56
|
+
"color-profile",
|
|
44
57
|
];
|
|
45
58
|
|
|
46
59
|
// Animation timing attributes that can reference other elements by ID
|
|
47
|
-
const TIMING_ATTRIBUTES = [
|
|
60
|
+
const TIMING_ATTRIBUTES = ["begin", "end"];
|
|
48
61
|
|
|
49
62
|
// Animation value attributes that can contain ID references
|
|
50
|
-
const VALUE_ATTRIBUTES = [
|
|
63
|
+
const VALUE_ATTRIBUTES = ["values", "from", "to", "by"];
|
|
51
64
|
|
|
52
65
|
/**
|
|
53
66
|
* Parse ID from url(#id) or url("#id") syntax
|
|
@@ -55,7 +68,7 @@ const VALUE_ATTRIBUTES = ['values', 'from', 'to', 'by'];
|
|
|
55
68
|
* @returns {string|null} Parsed ID or null
|
|
56
69
|
*/
|
|
57
70
|
export function parseUrlId(value) {
|
|
58
|
-
if (!value || typeof value !==
|
|
71
|
+
if (!value || typeof value !== "string") return null;
|
|
59
72
|
|
|
60
73
|
// Match url(#id) or url("#id") or url('#id') with optional whitespace
|
|
61
74
|
const match = value.match(/url\(\s*["']?#([^"')\s]+)\s*["']?\s*\)/);
|
|
@@ -68,8 +81,8 @@ export function parseUrlId(value) {
|
|
|
68
81
|
* @returns {string|null} Parsed ID or null
|
|
69
82
|
*/
|
|
70
83
|
export function parseHrefId(value) {
|
|
71
|
-
if (!value || typeof value !==
|
|
72
|
-
if (value.startsWith(
|
|
84
|
+
if (!value || typeof value !== "string") return null;
|
|
85
|
+
if (value.startsWith("#")) {
|
|
73
86
|
return value.substring(1);
|
|
74
87
|
}
|
|
75
88
|
return null;
|
|
@@ -82,14 +95,14 @@ export function parseHrefId(value) {
|
|
|
82
95
|
* @returns {string[]} Array of parsed IDs
|
|
83
96
|
*/
|
|
84
97
|
export function parseAnimationValueIds(value) {
|
|
85
|
-
if (!value || typeof value !==
|
|
98
|
+
if (!value || typeof value !== "string") return [];
|
|
86
99
|
|
|
87
100
|
const ids = [];
|
|
88
101
|
// Split by semicolon and find #id references
|
|
89
|
-
const parts = value.split(
|
|
102
|
+
const parts = value.split(";");
|
|
90
103
|
for (const part of parts) {
|
|
91
104
|
const trimmed = part.trim();
|
|
92
|
-
if (trimmed.startsWith(
|
|
105
|
+
if (trimmed.startsWith("#")) {
|
|
93
106
|
ids.push(trimmed.substring(1));
|
|
94
107
|
}
|
|
95
108
|
// Also check for url(#id) within values
|
|
@@ -111,17 +124,19 @@ export function parseAnimationValueIds(value) {
|
|
|
111
124
|
* @returns {string[]} Array of parsed IDs
|
|
112
125
|
*/
|
|
113
126
|
export function parseTimingIds(value) {
|
|
114
|
-
if (!value || typeof value !==
|
|
127
|
+
if (!value || typeof value !== "string") return [];
|
|
115
128
|
|
|
116
129
|
const ids = [];
|
|
117
130
|
// Split by semicolon for multiple timing values
|
|
118
|
-
const parts = value.split(
|
|
131
|
+
const parts = value.split(";");
|
|
119
132
|
|
|
120
133
|
for (const part of parts) {
|
|
121
134
|
const trimmed = part.trim();
|
|
122
135
|
// Match patterns like "id.event" or "id.begin" or "id.end"
|
|
123
136
|
// Events: click, mousedown, mouseup, mouseover, mouseout, focusin, focusout, etc.
|
|
124
|
-
const match = trimmed.match(
|
|
137
|
+
const match = trimmed.match(
|
|
138
|
+
/^([a-zA-Z_][a-zA-Z0-9_-]*)\.(begin|end|click|mousedown|mouseup|mouseover|mouseout|mousemove|mouseenter|mouseleave|focusin|focusout|activate|repeat)/,
|
|
139
|
+
);
|
|
125
140
|
if (match) {
|
|
126
141
|
ids.push(match[1]);
|
|
127
142
|
}
|
|
@@ -135,7 +150,7 @@ export function parseTimingIds(value) {
|
|
|
135
150
|
* @returns {string[]} Array of parsed IDs
|
|
136
151
|
*/
|
|
137
152
|
export function parseCSSIds(css) {
|
|
138
|
-
if (!css || typeof css !==
|
|
153
|
+
if (!css || typeof css !== "string") return [];
|
|
139
154
|
|
|
140
155
|
const ids = [];
|
|
141
156
|
|
|
@@ -155,7 +170,7 @@ export function parseCSSIds(css) {
|
|
|
155
170
|
* @returns {string[]} Array of parsed IDs
|
|
156
171
|
*/
|
|
157
172
|
export function parseJavaScriptIds(js) {
|
|
158
|
-
if (!js || typeof js !==
|
|
173
|
+
if (!js || typeof js !== "string") return [];
|
|
159
174
|
|
|
160
175
|
const ids = [];
|
|
161
176
|
|
|
@@ -167,7 +182,8 @@ export function parseJavaScriptIds(js) {
|
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
// querySelector('#id') or querySelector("#id")
|
|
170
|
-
const querySelectorRegex =
|
|
185
|
+
const querySelectorRegex =
|
|
186
|
+
/querySelector(?:All)?\(\s*["']#([^"'#\s]+)["']\s*\)/g;
|
|
171
187
|
while ((match = querySelectorRegex.exec(js)) !== null) {
|
|
172
188
|
ids.push(match[1]);
|
|
173
189
|
}
|
|
@@ -185,10 +201,10 @@ export function collectElementReferences(el) {
|
|
|
185
201
|
static: new Set(),
|
|
186
202
|
animation: new Set(),
|
|
187
203
|
css: new Set(),
|
|
188
|
-
js: new Set()
|
|
204
|
+
js: new Set(),
|
|
189
205
|
};
|
|
190
206
|
|
|
191
|
-
const tagName = el.tagName?.toLowerCase() ||
|
|
207
|
+
const tagName = el.tagName?.toLowerCase() || "";
|
|
192
208
|
const isAnimationElement = ANIMATION_ELEMENTS.includes(tagName);
|
|
193
209
|
|
|
194
210
|
for (const attrName of el.getAttributeNames()) {
|
|
@@ -208,36 +224,36 @@ export function collectElementReferences(el) {
|
|
|
208
224
|
}
|
|
209
225
|
|
|
210
226
|
// Check url() attributes
|
|
211
|
-
if (URL_ATTRIBUTES.includes(attrName) || attrName ===
|
|
227
|
+
if (URL_ATTRIBUTES.includes(attrName) || attrName === "style") {
|
|
212
228
|
const id = parseUrlId(value);
|
|
213
229
|
if (id) refs.static.add(id);
|
|
214
230
|
|
|
215
231
|
// For style attribute, also parse CSS IDs
|
|
216
|
-
if (attrName ===
|
|
217
|
-
parseCSSIds(value).forEach(
|
|
232
|
+
if (attrName === "style") {
|
|
233
|
+
parseCSSIds(value).forEach((cssId) => refs.css.add(cssId));
|
|
218
234
|
}
|
|
219
235
|
}
|
|
220
236
|
|
|
221
237
|
// Check animation timing attributes (begin, end)
|
|
222
238
|
if (TIMING_ATTRIBUTES.includes(attrName)) {
|
|
223
|
-
parseTimingIds(value).forEach(id => refs.animation.add(id));
|
|
239
|
+
parseTimingIds(value).forEach((id) => refs.animation.add(id));
|
|
224
240
|
}
|
|
225
241
|
|
|
226
242
|
// Check animation value attributes (values, from, to, by)
|
|
227
243
|
if (VALUE_ATTRIBUTES.includes(attrName)) {
|
|
228
|
-
parseAnimationValueIds(value).forEach(id => refs.animation.add(id));
|
|
244
|
+
parseAnimationValueIds(value).forEach((id) => refs.animation.add(id));
|
|
229
245
|
}
|
|
230
246
|
|
|
231
247
|
// Also check for url() in any attribute (some custom attributes may use it)
|
|
232
|
-
if (!URL_ATTRIBUTES.includes(attrName) && value.includes(
|
|
248
|
+
if (!URL_ATTRIBUTES.includes(attrName) && value.includes("url(")) {
|
|
233
249
|
const id = parseUrlId(value);
|
|
234
250
|
if (id) refs.static.add(id);
|
|
235
251
|
}
|
|
236
252
|
}
|
|
237
253
|
|
|
238
254
|
// Check <mpath> element inside <animateMotion>
|
|
239
|
-
if (tagName ===
|
|
240
|
-
const mpaths = el.getElementsByTagName(
|
|
255
|
+
if (tagName === "animatemotion") {
|
|
256
|
+
const mpaths = el.getElementsByTagName("mpath");
|
|
241
257
|
for (const mpath of mpaths) {
|
|
242
258
|
for (const attr of HREF_ATTRIBUTES) {
|
|
243
259
|
const value = mpath.getAttribute(attr);
|
|
@@ -271,7 +287,7 @@ export function collectAllReferences(root) {
|
|
|
271
287
|
animation: new Set(),
|
|
272
288
|
css: new Set(),
|
|
273
289
|
js: new Set(),
|
|
274
|
-
details: new Map()
|
|
290
|
+
details: new Map(), // ID -> { sources: [], type: 'static'|'animation'|'css'|'js' }
|
|
275
291
|
};
|
|
276
292
|
|
|
277
293
|
// Type priority: animation > js > css > static
|
|
@@ -294,30 +310,32 @@ export function collectAllReferences(root) {
|
|
|
294
310
|
result.details.get(id).sources.push(source);
|
|
295
311
|
};
|
|
296
312
|
|
|
297
|
-
const processElement = (el, path =
|
|
298
|
-
const tagName = el.tagName?.toLowerCase() ||
|
|
313
|
+
const processElement = (el, path = "") => {
|
|
314
|
+
const tagName = el.tagName?.toLowerCase() || "";
|
|
299
315
|
const currentPath = path ? `${path}>${tagName}` : tagName;
|
|
300
|
-
const elId = el.getAttribute(
|
|
316
|
+
const elId = el.getAttribute("id");
|
|
301
317
|
const elPath = elId ? `${tagName}#${elId}` : currentPath;
|
|
302
318
|
|
|
303
319
|
// Collect element references
|
|
304
320
|
const refs = collectElementReferences(el);
|
|
305
321
|
|
|
306
|
-
refs.static.forEach(id => addRef(id,
|
|
307
|
-
refs.animation.forEach(id => addRef(id,
|
|
308
|
-
refs.css.forEach(id => addRef(id,
|
|
309
|
-
refs.js.forEach(id => addRef(id,
|
|
322
|
+
refs.static.forEach((id) => addRef(id, "static", elPath));
|
|
323
|
+
refs.animation.forEach((id) => addRef(id, "animation", elPath));
|
|
324
|
+
refs.css.forEach((id) => addRef(id, "css", elPath));
|
|
325
|
+
refs.js.forEach((id) => addRef(id, "js", elPath));
|
|
310
326
|
|
|
311
327
|
// Process <style> elements
|
|
312
|
-
if (tagName ===
|
|
313
|
-
const cssContent = el.textContent ||
|
|
314
|
-
parseCSSIds(cssContent).forEach(id => addRef(id,
|
|
328
|
+
if (tagName === "style") {
|
|
329
|
+
const cssContent = el.textContent || "";
|
|
330
|
+
parseCSSIds(cssContent).forEach((id) => addRef(id, "css", `<style>`));
|
|
315
331
|
}
|
|
316
332
|
|
|
317
333
|
// Process <script> elements
|
|
318
|
-
if (tagName ===
|
|
319
|
-
const jsContent = el.textContent ||
|
|
320
|
-
parseJavaScriptIds(jsContent).forEach(id =>
|
|
334
|
+
if (tagName === "script") {
|
|
335
|
+
const jsContent = el.textContent || "";
|
|
336
|
+
parseJavaScriptIds(jsContent).forEach((id) =>
|
|
337
|
+
addRef(id, "js", `<script>`),
|
|
338
|
+
);
|
|
321
339
|
}
|
|
322
340
|
|
|
323
341
|
// Recurse to children
|
|
@@ -357,7 +375,7 @@ export function getIdReferenceInfo(root, id) {
|
|
|
357
375
|
return {
|
|
358
376
|
referenced: refs.all.has(id),
|
|
359
377
|
type: details?.type || null,
|
|
360
|
-
sources: details?.sources || []
|
|
378
|
+
sources: details?.sources || [],
|
|
361
379
|
};
|
|
362
380
|
}
|
|
363
381
|
|
|
@@ -376,11 +394,11 @@ export function findUnreferencedDefs(root) {
|
|
|
376
394
|
const animationReferenced = [];
|
|
377
395
|
|
|
378
396
|
// Scan all defs
|
|
379
|
-
const defsElements = root.getElementsByTagName(
|
|
397
|
+
const defsElements = root.getElementsByTagName("defs");
|
|
380
398
|
for (const defs of defsElements) {
|
|
381
399
|
for (const child of defs.children) {
|
|
382
400
|
if (child instanceof SVGElement) {
|
|
383
|
-
const id = child.getAttribute(
|
|
401
|
+
const id = child.getAttribute("id");
|
|
384
402
|
if (!id) continue;
|
|
385
403
|
|
|
386
404
|
if (refs.animation.has(id)) {
|
|
@@ -405,16 +423,17 @@ export function findUnreferencedDefs(root) {
|
|
|
405
423
|
* @returns {{removed: string[], kept: string[], keptForAnimation: string[]}}
|
|
406
424
|
*/
|
|
407
425
|
export function removeUnreferencedDefsSafe(root) {
|
|
408
|
-
const { safeToRemove, referenced, animationReferenced } =
|
|
426
|
+
const { safeToRemove, referenced, animationReferenced } =
|
|
427
|
+
findUnreferencedDefs(root);
|
|
409
428
|
|
|
410
429
|
const removed = [];
|
|
411
430
|
|
|
412
431
|
// Only remove elements that are truly unreferenced
|
|
413
|
-
const defsElements = root.getElementsByTagName(
|
|
432
|
+
const defsElements = root.getElementsByTagName("defs");
|
|
414
433
|
for (const defs of defsElements) {
|
|
415
434
|
for (const child of [...defs.children]) {
|
|
416
435
|
if (child instanceof SVGElement) {
|
|
417
|
-
const id = child.getAttribute(
|
|
436
|
+
const id = child.getAttribute("id");
|
|
418
437
|
if (id && safeToRemove.includes(id)) {
|
|
419
438
|
defs.removeChild(child);
|
|
420
439
|
removed.push(id);
|
|
@@ -426,7 +445,7 @@ export function removeUnreferencedDefsSafe(root) {
|
|
|
426
445
|
return {
|
|
427
446
|
removed,
|
|
428
447
|
kept: referenced,
|
|
429
|
-
keptForAnimation: animationReferenced
|
|
448
|
+
keptForAnimation: animationReferenced,
|
|
430
449
|
};
|
|
431
450
|
}
|
|
432
451
|
|
|
@@ -436,5 +455,5 @@ export {
|
|
|
436
455
|
HREF_ATTRIBUTES,
|
|
437
456
|
URL_ATTRIBUTES,
|
|
438
457
|
TIMING_ATTRIBUTES,
|
|
439
|
-
VALUE_ATTRIBUTES
|
|
458
|
+
VALUE_ATTRIBUTES,
|
|
440
459
|
};
|