@emasoft/svg-matrix 1.0.30 → 1.0.31
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/bin/svg-matrix.js +310 -61
- package/bin/svglinter.cjs +102 -3
- package/bin/svgm.js +236 -27
- package/package.json +1 -1
- package/src/animation-optimization.js +137 -17
- package/src/animation-references.js +123 -6
- package/src/arc-length.js +213 -4
- package/src/bezier-analysis.js +217 -21
- package/src/bezier-intersections.js +275 -12
- package/src/browser-verify.js +237 -4
- package/src/clip-path-resolver.js +168 -0
- package/src/convert-path-data.js +479 -28
- package/src/css-specificity.js +73 -10
- package/src/douglas-peucker.js +219 -2
- package/src/flatten-pipeline.js +284 -26
- package/src/geometry-to-path.js +250 -25
- package/src/gjk-collision.js +236 -33
- package/src/index.js +261 -3
- package/src/inkscape-support.js +86 -28
- package/src/logger.js +48 -3
- package/src/marker-resolver.js +278 -74
- package/src/mask-resolver.js +265 -66
- package/src/matrix.js +44 -5
- package/src/mesh-gradient.js +352 -102
- package/src/off-canvas-detection.js +382 -13
- package/src/path-analysis.js +192 -18
- package/src/path-data-plugins.js +309 -5
- package/src/path-optimization.js +129 -5
- package/src/path-simplification.js +188 -32
- package/src/pattern-resolver.js +454 -106
- package/src/polygon-clip.js +324 -1
- package/src/svg-boolean-ops.js +226 -9
- package/src/svg-collections.js +7 -5
- package/src/svg-flatten.js +386 -62
- package/src/svg-parser.js +179 -8
- package/src/svg-rendering-context.js +235 -6
- package/src/svg-toolbox.js +45 -8
- package/src/svg2-polyfills.js +40 -10
- package/src/transform-decomposition.js +258 -32
- package/src/transform-optimization.js +259 -13
- package/src/transforms2d.js +82 -9
- package/src/transforms3d.js +62 -10
- package/src/use-symbol-resolver.js +286 -42
- package/src/vector.js +64 -8
- package/src/verification.js +392 -1
package/src/svg-parser.js
CHANGED
|
@@ -18,6 +18,13 @@ Decimal.set({ precision: 80 });
|
|
|
18
18
|
* @returns {void}
|
|
19
19
|
*/
|
|
20
20
|
function setOwnerDocument(el, doc) {
|
|
21
|
+
// Validation: Check for null/undefined parameters
|
|
22
|
+
if (!el || !doc) {
|
|
23
|
+
throw new Error("setOwnerDocument: element and document are required");
|
|
24
|
+
}
|
|
25
|
+
if (!el.children || !Array.isArray(el.children)) {
|
|
26
|
+
throw new Error("setOwnerDocument: element must have a children array");
|
|
27
|
+
}
|
|
21
28
|
el.ownerDocument = doc;
|
|
22
29
|
for (const child of el.children) {
|
|
23
30
|
// eslint-disable-next-line no-use-before-define -- Class hoisting is intentional
|
|
@@ -33,11 +40,11 @@ function setOwnerDocument(el, doc) {
|
|
|
33
40
|
* @returns {SVGElement} Root element with DOM-like interface
|
|
34
41
|
*/
|
|
35
42
|
export function parseSVG(svgString) {
|
|
36
|
-
if (typeof svgString !==
|
|
37
|
-
throw new Error(
|
|
43
|
+
if (typeof svgString !== "string") {
|
|
44
|
+
throw new Error("parseSVG: input must be a string");
|
|
38
45
|
}
|
|
39
46
|
if (svgString.trim().length === 0) {
|
|
40
|
-
throw new Error(
|
|
47
|
+
throw new Error("parseSVG: input cannot be empty");
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
// Normalize whitespace but preserve content
|
|
@@ -79,6 +86,10 @@ class SVGDocument {
|
|
|
79
86
|
* @returns {SVGElement}
|
|
80
87
|
*/
|
|
81
88
|
createElement(tagName) {
|
|
89
|
+
// Validation: Ensure tagName is a valid string
|
|
90
|
+
if (!tagName || typeof tagName !== "string") {
|
|
91
|
+
throw new Error("createElement: tagName must be a non-empty string");
|
|
92
|
+
}
|
|
82
93
|
// eslint-disable-next-line no-use-before-define -- Class hoisting is intentional
|
|
83
94
|
const el = new SVGElement(tagName);
|
|
84
95
|
el.ownerDocument = this;
|
|
@@ -92,6 +103,10 @@ class SVGDocument {
|
|
|
92
103
|
* @returns {SVGElement}
|
|
93
104
|
*/
|
|
94
105
|
createElementNS(namespace, tagName) {
|
|
106
|
+
// Validation: Ensure namespace is a string (though unused, for API compatibility)
|
|
107
|
+
if (namespace !== null && namespace !== undefined && typeof namespace !== "string") {
|
|
108
|
+
throw new Error("createElementNS: namespace must be a string");
|
|
109
|
+
}
|
|
95
110
|
return this.createElement(tagName);
|
|
96
111
|
}
|
|
97
112
|
}
|
|
@@ -118,6 +133,22 @@ export class SVGElement {
|
|
|
118
133
|
* @param {string} textContent - Text content of the element
|
|
119
134
|
*/
|
|
120
135
|
constructor(tagName, attributes = {}, children = [], textContent = "") {
|
|
136
|
+
// Validation: Ensure tagName is a non-empty string
|
|
137
|
+
if (!tagName || typeof tagName !== "string") {
|
|
138
|
+
throw new Error("SVGElement: tagName must be a non-empty string");
|
|
139
|
+
}
|
|
140
|
+
// Validation: Ensure attributes is an object
|
|
141
|
+
if (attributes !== null && typeof attributes !== "object") {
|
|
142
|
+
throw new Error("SVGElement: attributes must be an object");
|
|
143
|
+
}
|
|
144
|
+
// Validation: Ensure children is an array
|
|
145
|
+
if (!Array.isArray(children)) {
|
|
146
|
+
throw new Error("SVGElement: children must be an array");
|
|
147
|
+
}
|
|
148
|
+
// Validation: Ensure textContent is a string
|
|
149
|
+
if (typeof textContent !== "string") {
|
|
150
|
+
throw new Error("SVGElement: textContent must be a string");
|
|
151
|
+
}
|
|
121
152
|
// Preserve original case for W3C SVG compliance (linearGradient, clipPath, etc.)
|
|
122
153
|
this.tagName = tagName;
|
|
123
154
|
this.nodeName = tagName.toUpperCase();
|
|
@@ -142,6 +173,10 @@ export class SVGElement {
|
|
|
142
173
|
* @returns {string|null} Attribute value or null
|
|
143
174
|
*/
|
|
144
175
|
getAttribute(name) {
|
|
176
|
+
// Validation: Ensure name is a string
|
|
177
|
+
if (typeof name !== "string") {
|
|
178
|
+
throw new Error("getAttribute: name must be a string");
|
|
179
|
+
}
|
|
145
180
|
return this._attributes[name] ?? null;
|
|
146
181
|
}
|
|
147
182
|
|
|
@@ -151,7 +186,16 @@ export class SVGElement {
|
|
|
151
186
|
* @param {string} value - Attribute value
|
|
152
187
|
*/
|
|
153
188
|
setAttribute(name, value) {
|
|
154
|
-
|
|
189
|
+
// Validation: Ensure name is a string
|
|
190
|
+
if (typeof name !== "string") {
|
|
191
|
+
throw new Error("setAttribute: name must be a string");
|
|
192
|
+
}
|
|
193
|
+
// Validation: Convert value to string if not already
|
|
194
|
+
if (value !== null && value !== undefined) {
|
|
195
|
+
this._attributes[name] = String(value);
|
|
196
|
+
} else {
|
|
197
|
+
this._attributes[name] = "";
|
|
198
|
+
}
|
|
155
199
|
}
|
|
156
200
|
|
|
157
201
|
/**
|
|
@@ -160,6 +204,10 @@ export class SVGElement {
|
|
|
160
204
|
* @returns {boolean}
|
|
161
205
|
*/
|
|
162
206
|
hasAttribute(name) {
|
|
207
|
+
// Validation: Ensure name is a string
|
|
208
|
+
if (typeof name !== "string") {
|
|
209
|
+
throw new Error("hasAttribute: name must be a string");
|
|
210
|
+
}
|
|
163
211
|
return name in this._attributes;
|
|
164
212
|
}
|
|
165
213
|
|
|
@@ -168,6 +216,10 @@ export class SVGElement {
|
|
|
168
216
|
* @param {string} name - Attribute name
|
|
169
217
|
*/
|
|
170
218
|
removeAttribute(name) {
|
|
219
|
+
// Validation: Ensure name is a string
|
|
220
|
+
if (typeof name !== "string") {
|
|
221
|
+
throw new Error("removeAttribute: name must be a string");
|
|
222
|
+
}
|
|
171
223
|
delete this._attributes[name];
|
|
172
224
|
}
|
|
173
225
|
|
|
@@ -185,6 +237,10 @@ export class SVGElement {
|
|
|
185
237
|
* @returns {SVGElement[]}
|
|
186
238
|
*/
|
|
187
239
|
getElementsByTagName(tagName) {
|
|
240
|
+
// Validation: Ensure tagName is a string
|
|
241
|
+
if (typeof tagName !== "string") {
|
|
242
|
+
throw new Error("getElementsByTagName: tagName must be a string");
|
|
243
|
+
}
|
|
188
244
|
const tag = tagName.toLowerCase();
|
|
189
245
|
const results = [];
|
|
190
246
|
|
|
@@ -210,6 +266,10 @@ export class SVGElement {
|
|
|
210
266
|
* @returns {SVGElement|null}
|
|
211
267
|
*/
|
|
212
268
|
getElementById(id) {
|
|
269
|
+
// Validation: Ensure id is a string
|
|
270
|
+
if (typeof id !== "string") {
|
|
271
|
+
throw new Error("getElementById: id must be a string");
|
|
272
|
+
}
|
|
213
273
|
const search = (el) => {
|
|
214
274
|
if (el.getAttribute("id") === id) {
|
|
215
275
|
return el;
|
|
@@ -233,6 +293,10 @@ export class SVGElement {
|
|
|
233
293
|
* @returns {SVGElement|null}
|
|
234
294
|
*/
|
|
235
295
|
querySelector(selector) {
|
|
296
|
+
// Validation: Ensure selector is a string
|
|
297
|
+
if (typeof selector !== "string") {
|
|
298
|
+
throw new Error("querySelector: selector must be a string");
|
|
299
|
+
}
|
|
236
300
|
const results = this.querySelectorAll(selector);
|
|
237
301
|
return results[0] || null;
|
|
238
302
|
}
|
|
@@ -243,6 +307,10 @@ export class SVGElement {
|
|
|
243
307
|
* @returns {SVGElement[]}
|
|
244
308
|
*/
|
|
245
309
|
querySelectorAll(selector) {
|
|
310
|
+
// Validation: Ensure selector is a string
|
|
311
|
+
if (typeof selector !== "string") {
|
|
312
|
+
throw new Error("querySelectorAll: selector must be a string");
|
|
313
|
+
}
|
|
246
314
|
const results = [];
|
|
247
315
|
const matchers = parseSelector(selector);
|
|
248
316
|
|
|
@@ -267,6 +335,10 @@ export class SVGElement {
|
|
|
267
335
|
* @returns {boolean}
|
|
268
336
|
*/
|
|
269
337
|
matches(selector) {
|
|
338
|
+
// Validation: Ensure selector is a string
|
|
339
|
+
if (typeof selector !== "string") {
|
|
340
|
+
throw new Error("matches: selector must be a string");
|
|
341
|
+
}
|
|
270
342
|
const matchers = parseSelector(selector);
|
|
271
343
|
return matchesAllSelectors(this, matchers);
|
|
272
344
|
}
|
|
@@ -278,6 +350,10 @@ export class SVGElement {
|
|
|
278
350
|
* @returns {SVGElement}
|
|
279
351
|
*/
|
|
280
352
|
cloneNode(deep = true) {
|
|
353
|
+
// Validation: Ensure deep is a boolean
|
|
354
|
+
if (typeof deep !== "boolean") {
|
|
355
|
+
throw new Error("cloneNode: deep must be a boolean");
|
|
356
|
+
}
|
|
281
357
|
const clonedChildren = deep
|
|
282
358
|
? // eslint-disable-next-line no-confusing-arrow -- Prettier multiline format
|
|
283
359
|
this.children.map((c) =>
|
|
@@ -310,6 +386,10 @@ export class SVGElement {
|
|
|
310
386
|
* @returns {SVGElement} The appended child
|
|
311
387
|
*/
|
|
312
388
|
appendChild(child) {
|
|
389
|
+
// Validation: Ensure child is not null/undefined
|
|
390
|
+
if (child === null || child === undefined) {
|
|
391
|
+
throw new Error("appendChild: child cannot be null or undefined");
|
|
392
|
+
}
|
|
313
393
|
// DOM spec: Remove child from its current parent before appending
|
|
314
394
|
if (child instanceof SVGElement && child.parentNode) {
|
|
315
395
|
child.parentNode.removeChild(child);
|
|
@@ -328,6 +408,10 @@ export class SVGElement {
|
|
|
328
408
|
* @returns {SVGElement} The removed child
|
|
329
409
|
*/
|
|
330
410
|
removeChild(child) {
|
|
411
|
+
// Validation: Ensure child is not null/undefined
|
|
412
|
+
if (child === null || child === undefined) {
|
|
413
|
+
throw new Error("removeChild: child cannot be null or undefined");
|
|
414
|
+
}
|
|
331
415
|
const idx = this.children.indexOf(child);
|
|
332
416
|
if (idx >= 0) {
|
|
333
417
|
this.children.splice(idx, 1);
|
|
@@ -347,6 +431,14 @@ export class SVGElement {
|
|
|
347
431
|
* @returns {SVGElement} The inserted child
|
|
348
432
|
*/
|
|
349
433
|
insertBefore(newChild, refChild) {
|
|
434
|
+
// Validation: Ensure newChild is not null/undefined
|
|
435
|
+
if (newChild === null || newChild === undefined) {
|
|
436
|
+
throw new Error("insertBefore: newChild cannot be null or undefined");
|
|
437
|
+
}
|
|
438
|
+
// Validation: Ensure refChild is not null/undefined
|
|
439
|
+
if (refChild === null || refChild === undefined) {
|
|
440
|
+
throw new Error("insertBefore: refChild cannot be null or undefined");
|
|
441
|
+
}
|
|
350
442
|
// DOM spec: Remove newChild from its current parent first
|
|
351
443
|
if (newChild instanceof SVGElement && newChild.parentNode) {
|
|
352
444
|
newChild.parentNode.removeChild(newChild);
|
|
@@ -372,6 +464,14 @@ export class SVGElement {
|
|
|
372
464
|
* @returns {SVGElement} The replaced (old) child
|
|
373
465
|
*/
|
|
374
466
|
replaceChild(newChild, oldChild) {
|
|
467
|
+
// Validation: Ensure newChild is not null/undefined
|
|
468
|
+
if (newChild === null || newChild === undefined) {
|
|
469
|
+
throw new Error("replaceChild: newChild cannot be null or undefined");
|
|
470
|
+
}
|
|
471
|
+
// Validation: Ensure oldChild is not null/undefined
|
|
472
|
+
if (oldChild === null || oldChild === undefined) {
|
|
473
|
+
throw new Error("replaceChild: oldChild cannot be null or undefined");
|
|
474
|
+
}
|
|
375
475
|
// DOM spec: Remove newChild from its current parent first
|
|
376
476
|
if (newChild instanceof SVGElement && newChild.parentNode) {
|
|
377
477
|
newChild.parentNode.removeChild(newChild);
|
|
@@ -398,9 +498,14 @@ export class SVGElement {
|
|
|
398
498
|
const styleAttr = this.getAttribute("style") || "";
|
|
399
499
|
const styles = {};
|
|
400
500
|
styleAttr.split(";").forEach((pair) => {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
501
|
+
// Edge case: Handle malformed styles, multiple colons, and empty pairs
|
|
502
|
+
const colonIdx = pair.indexOf(":");
|
|
503
|
+
if (colonIdx > 0) {
|
|
504
|
+
const key = pair.slice(0, colonIdx).trim();
|
|
505
|
+
const val = pair.slice(colonIdx + 1).trim();
|
|
506
|
+
if (key && val) {
|
|
507
|
+
styles[key] = val;
|
|
508
|
+
}
|
|
404
509
|
}
|
|
405
510
|
});
|
|
406
511
|
return styles;
|
|
@@ -451,6 +556,14 @@ export class SVGElement {
|
|
|
451
556
|
* @returns {string}
|
|
452
557
|
*/
|
|
453
558
|
serialize(indent = 0, minify = false) {
|
|
559
|
+
// Validation: Ensure indent is a valid number
|
|
560
|
+
if (typeof indent !== "number" || !Number.isFinite(indent) || indent < 0) {
|
|
561
|
+
throw new Error("serialize: indent must be a non-negative finite number");
|
|
562
|
+
}
|
|
563
|
+
// Validation: Ensure minify is a boolean
|
|
564
|
+
if (typeof minify !== "boolean") {
|
|
565
|
+
throw new Error("serialize: minify must be a boolean");
|
|
566
|
+
}
|
|
454
567
|
const pad = minify ? "" : " ".repeat(indent);
|
|
455
568
|
|
|
456
569
|
// Check for CDATA pending marker (used by embedExternalDependencies for scripts)
|
|
@@ -545,6 +658,12 @@ export class SVGElement {
|
|
|
545
658
|
* @returns {boolean} True if whitespace should be preserved, false otherwise
|
|
546
659
|
*/
|
|
547
660
|
function _shouldPreserveWhitespace(element) {
|
|
661
|
+
// Validation: Ensure element is not null/undefined
|
|
662
|
+
if (!element) {
|
|
663
|
+
throw new Error(
|
|
664
|
+
"_shouldPreserveWhitespace: element cannot be null or undefined",
|
|
665
|
+
);
|
|
666
|
+
}
|
|
548
667
|
let current = element;
|
|
549
668
|
while (current) {
|
|
550
669
|
const xmlSpace = current.getAttribute("xml:space");
|
|
@@ -564,6 +683,22 @@ function _shouldPreserveWhitespace(element) {
|
|
|
564
683
|
* @returns {{element: SVGElement|null, endPos: number}} Parsed element and final position
|
|
565
684
|
*/
|
|
566
685
|
function parseElement(str, pos, inheritPreserveSpace = false) {
|
|
686
|
+
// Validation: Ensure str is a string
|
|
687
|
+
if (typeof str !== "string") {
|
|
688
|
+
throw new Error("parseElement: str must be a string");
|
|
689
|
+
}
|
|
690
|
+
// Validation: Ensure pos is a valid number
|
|
691
|
+
if (typeof pos !== "number" || !Number.isFinite(pos) || pos < 0) {
|
|
692
|
+
throw new Error("parseElement: pos must be a non-negative finite number");
|
|
693
|
+
}
|
|
694
|
+
// Validation: Ensure str is not empty
|
|
695
|
+
if (str.length === 0) {
|
|
696
|
+
return { element: null, endPos: 0 };
|
|
697
|
+
}
|
|
698
|
+
// Bounds check: Ensure pos is within string bounds
|
|
699
|
+
if (pos >= str.length) {
|
|
700
|
+
return { element: null, endPos: pos };
|
|
701
|
+
}
|
|
567
702
|
// Use local variable to avoid reassigning parameter (ESLint no-param-reassign)
|
|
568
703
|
let position = pos;
|
|
569
704
|
|
|
@@ -787,6 +922,10 @@ function parseElement(str, pos, inheritPreserveSpace = false) {
|
|
|
787
922
|
* @returns {Array<{tag: string|null, id: string|null, classes: Array<string>, attrs: Array<{name: string, value: string|undefined}>}>} Array of matcher objects
|
|
788
923
|
*/
|
|
789
924
|
function parseSelector(selector) {
|
|
925
|
+
// Validation: Ensure selector is a string
|
|
926
|
+
if (typeof selector !== "string") {
|
|
927
|
+
throw new Error("parseSelector: selector must be a string");
|
|
928
|
+
}
|
|
790
929
|
const matchers = [];
|
|
791
930
|
const parts = selector.trim().split(/\s*,\s*/);
|
|
792
931
|
|
|
@@ -838,6 +977,14 @@ function parseSelector(selector) {
|
|
|
838
977
|
* @returns {boolean} True if element matches any matcher
|
|
839
978
|
*/
|
|
840
979
|
function matchesAllSelectors(el, matchers) {
|
|
980
|
+
// Validation: Ensure el is a valid SVGElement
|
|
981
|
+
if (!(el instanceof SVGElement)) {
|
|
982
|
+
throw new Error("matchesAllSelectors: el must be an SVGElement");
|
|
983
|
+
}
|
|
984
|
+
// Validation: Ensure matchers is an array
|
|
985
|
+
if (!Array.isArray(matchers)) {
|
|
986
|
+
throw new Error("matchesAllSelectors: matchers must be an array");
|
|
987
|
+
}
|
|
841
988
|
return matchers.some((matcher) => {
|
|
842
989
|
// Case-insensitive tag matching (tagName preserves original case)
|
|
843
990
|
if (matcher.tag && el.tagName.toLowerCase() !== matcher.tag) return false;
|
|
@@ -878,6 +1025,8 @@ function escapeText(str) {
|
|
|
878
1025
|
* @returns {string} Escaped attribute value
|
|
879
1026
|
*/
|
|
880
1027
|
function escapeAttr(str) {
|
|
1028
|
+
// Edge case: Handle null/undefined by converting to empty string
|
|
1029
|
+
if (str === null || str === undefined) return "";
|
|
881
1030
|
if (typeof str !== "string") return String(str);
|
|
882
1031
|
return str
|
|
883
1032
|
.replace(/&/g, "&")
|
|
@@ -1027,6 +1176,14 @@ function unescapeText(str) {
|
|
|
1027
1176
|
* @returns {Map<string, SVGElement>}
|
|
1028
1177
|
*/
|
|
1029
1178
|
export function buildDefsMap(svgRoot) {
|
|
1179
|
+
// Validation: Ensure svgRoot is not null/undefined
|
|
1180
|
+
if (!svgRoot) {
|
|
1181
|
+
throw new Error("buildDefsMap: svgRoot cannot be null or undefined");
|
|
1182
|
+
}
|
|
1183
|
+
// Validation: Ensure svgRoot is an SVGElement with children
|
|
1184
|
+
if (!(svgRoot instanceof SVGElement)) {
|
|
1185
|
+
throw new Error("buildDefsMap: svgRoot must be an SVGElement");
|
|
1186
|
+
}
|
|
1030
1187
|
const defsMap = new Map();
|
|
1031
1188
|
|
|
1032
1189
|
// Find all elements with IDs
|
|
@@ -1053,6 +1210,20 @@ export function buildDefsMap(svgRoot) {
|
|
|
1053
1210
|
* @returns {SVGElement[]}
|
|
1054
1211
|
*/
|
|
1055
1212
|
export function findElementsWithAttribute(root, attrName) {
|
|
1213
|
+
// Validation: Ensure root is not null/undefined
|
|
1214
|
+
if (!root) {
|
|
1215
|
+
throw new Error(
|
|
1216
|
+
"findElementsWithAttribute: root cannot be null or undefined",
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
// Validation: Ensure root is an SVGElement
|
|
1220
|
+
if (!(root instanceof SVGElement)) {
|
|
1221
|
+
throw new Error("findElementsWithAttribute: root must be an SVGElement");
|
|
1222
|
+
}
|
|
1223
|
+
// Validation: Ensure attrName is a string
|
|
1224
|
+
if (typeof attrName !== "string") {
|
|
1225
|
+
throw new Error("findElementsWithAttribute: attrName must be a string");
|
|
1226
|
+
}
|
|
1056
1227
|
const results = [];
|
|
1057
1228
|
|
|
1058
1229
|
const search = (el) => {
|
|
@@ -1090,7 +1261,7 @@ export function parseUrlReference(urlValue) {
|
|
|
1090
1261
|
*/
|
|
1091
1262
|
export function serializeSVG(root, options = {}) {
|
|
1092
1263
|
if (!root) {
|
|
1093
|
-
throw new Error(
|
|
1264
|
+
throw new Error("serializeSVG: document is required");
|
|
1094
1265
|
}
|
|
1095
1266
|
// Validate that root is a proper SVGElement with serialize method
|
|
1096
1267
|
if (typeof root.serialize !== "function") {
|