@emasoft/svg-matrix 1.0.19 → 1.0.20
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 +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +8 -2
- package/src/animation-optimization.js +394 -0
- package/src/animation-references.js +440 -0
- package/src/arc-length.js +940 -0
- package/src/bezier-analysis.js +1626 -0
- package/src/bezier-intersections.js +1369 -0
- package/src/clip-path-resolver.js +110 -2
- package/src/convert-path-data.js +583 -0
- package/src/css-specificity.js +443 -0
- package/src/douglas-peucker.js +356 -0
- package/src/flatten-pipeline.js +109 -4
- package/src/geometry-to-path.js +126 -16
- package/src/gjk-collision.js +840 -0
- package/src/index.js +175 -2
- package/src/off-canvas-detection.js +1222 -0
- package/src/path-analysis.js +1241 -0
- package/src/path-data-plugins.js +928 -0
- package/src/path-optimization.js +825 -0
- package/src/path-simplification.js +1140 -0
- package/src/polygon-clip.js +376 -99
- package/src/svg-boolean-ops.js +898 -0
- package/src/svg-collections.js +910 -0
- package/src/svg-parser.js +175 -16
- package/src/svg-rendering-context.js +627 -0
- package/src/svg-toolbox.js +7495 -0
- package/src/svg-validation-data.js +944 -0
- package/src/transform-decomposition.js +810 -0
- package/src/transform-optimization.js +936 -0
- package/src/use-symbol-resolver.js +75 -7
package/src/svg-parser.js
CHANGED
|
@@ -319,31 +319,49 @@ export class SVGElement {
|
|
|
319
319
|
/**
|
|
320
320
|
* Serialize to SVG string.
|
|
321
321
|
* @param {number} indent - Indentation level
|
|
322
|
+
* @param {boolean} minify - Whether to minify output (no whitespace/newlines)
|
|
322
323
|
* @returns {string}
|
|
323
324
|
*/
|
|
324
|
-
serialize(indent = 0) {
|
|
325
|
-
const pad = ' '.repeat(indent);
|
|
325
|
+
serialize(indent = 0, minify = false) {
|
|
326
|
+
const pad = minify ? '' : ' '.repeat(indent);
|
|
326
327
|
const attrs = Object.entries(this._attributes)
|
|
327
328
|
.map(([k, v]) => `${k}="${escapeAttr(v)}"`)
|
|
328
329
|
.join(' ');
|
|
329
330
|
|
|
330
331
|
const attrStr = attrs ? ' ' + attrs : '';
|
|
331
332
|
|
|
333
|
+
// Self-closing tag for empty elements
|
|
332
334
|
if (this.children.length === 0 && !this.textContent) {
|
|
333
335
|
return `${pad}<${this.tagName}${attrStr}/>`;
|
|
334
336
|
}
|
|
335
337
|
|
|
336
|
-
const
|
|
337
|
-
.map(c => c instanceof SVGElement ? c.serialize(indent + 1) : escapeText(c))
|
|
338
|
-
.join('\n');
|
|
338
|
+
const separator = minify ? '' : '\n';
|
|
339
339
|
|
|
340
|
-
|
|
340
|
+
// Serialize children (including animation elements inside text elements)
|
|
341
|
+
const childStr = this.children
|
|
342
|
+
.map(c => c instanceof SVGElement ? c.serialize(indent + 1, minify) : escapeText(c))
|
|
343
|
+
.join(separator);
|
|
344
|
+
|
|
345
|
+
// CRITICAL FIX: Elements can have BOTH textContent AND children
|
|
346
|
+
// Example: <text>Some text<set attributeName="display" .../></text>
|
|
347
|
+
// We must include both, not choose one or the other
|
|
348
|
+
let content = '';
|
|
349
|
+
if (this.textContent && this.children.length > 0) {
|
|
350
|
+
// Both text and children - combine them
|
|
351
|
+
content = escapeText(this.textContent) + separator + childStr;
|
|
352
|
+
} else if (this.textContent) {
|
|
353
|
+
// Only text content
|
|
354
|
+
content = escapeText(this.textContent);
|
|
355
|
+
} else {
|
|
356
|
+
// Only children
|
|
357
|
+
content = childStr;
|
|
358
|
+
}
|
|
341
359
|
|
|
342
360
|
if (this.children.length === 0) {
|
|
343
361
|
return `${pad}<${this.tagName}${attrStr}>${content}</${this.tagName}>`;
|
|
344
362
|
}
|
|
345
363
|
|
|
346
|
-
return `${pad}<${this.tagName}${attrStr}
|
|
364
|
+
return `${pad}<${this.tagName}${attrStr}>${separator}${content}${separator}${pad}</${this.tagName}>`;
|
|
347
365
|
}
|
|
348
366
|
|
|
349
367
|
/**
|
|
@@ -369,11 +387,30 @@ export class SVGElement {
|
|
|
369
387
|
// INTERNAL PARSING FUNCTIONS
|
|
370
388
|
// ============================================================================
|
|
371
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Check if whitespace should be preserved based on xml:space attribute.
|
|
392
|
+
* BUG FIX 2: Helper function to check xml:space="preserve" on element or ancestors
|
|
393
|
+
* @private
|
|
394
|
+
*/
|
|
395
|
+
function shouldPreserveWhitespace(element) {
|
|
396
|
+
let current = element;
|
|
397
|
+
while (current) {
|
|
398
|
+
const xmlSpace = current.getAttribute('xml:space');
|
|
399
|
+
if (xmlSpace === 'preserve') return true;
|
|
400
|
+
if (xmlSpace === 'default') return false;
|
|
401
|
+
current = current.parentNode;
|
|
402
|
+
}
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
372
406
|
/**
|
|
373
407
|
* Parse a single element from SVG string.
|
|
374
408
|
* @private
|
|
409
|
+
* @param {string} str - SVG string to parse
|
|
410
|
+
* @param {number} pos - Current position in string
|
|
411
|
+
* @param {boolean} inheritPreserveSpace - Whether xml:space="preserve" is inherited from ancestor
|
|
375
412
|
*/
|
|
376
|
-
function parseElement(str, pos) {
|
|
413
|
+
function parseElement(str, pos, inheritPreserveSpace = false) {
|
|
377
414
|
// Skip whitespace and comments
|
|
378
415
|
while (pos < str.length) {
|
|
379
416
|
const ws = str.slice(pos).match(/^(\s+)/);
|
|
@@ -406,11 +443,23 @@ function parseElement(str, pos) {
|
|
|
406
443
|
continue;
|
|
407
444
|
}
|
|
408
445
|
|
|
409
|
-
// Skip DOCTYPE
|
|
446
|
+
// Skip DOCTYPE (can contain internal subset with brackets)
|
|
410
447
|
if (str.slice(pos, pos + 9).toUpperCase() === '<!DOCTYPE') {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
448
|
+
let depth = 0;
|
|
449
|
+
let i = pos + 9;
|
|
450
|
+
while (i < str.length) {
|
|
451
|
+
if (str[i] === '[') depth++;
|
|
452
|
+
else if (str[i] === ']') depth--;
|
|
453
|
+
else if (str[i] === '>' && depth === 0) {
|
|
454
|
+
pos = i + 1;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
i++;
|
|
458
|
+
}
|
|
459
|
+
if (i >= str.length) {
|
|
460
|
+
// Malformed DOCTYPE, skip to end
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
414
463
|
continue;
|
|
415
464
|
}
|
|
416
465
|
|
|
@@ -506,7 +555,14 @@ function parseElement(str, pos) {
|
|
|
506
555
|
}
|
|
507
556
|
}
|
|
508
557
|
|
|
509
|
-
|
|
558
|
+
// BUG FIX 1: Pass down xml:space inheritance to children
|
|
559
|
+
// Determine what to pass: if current element has xml:space, use it; otherwise inherit
|
|
560
|
+
const currentXmlSpace = attributes['xml:space'];
|
|
561
|
+
const childInheritPreserve = currentXmlSpace === 'preserve' ? true :
|
|
562
|
+
currentXmlSpace === 'default' ? false :
|
|
563
|
+
inheritPreserveSpace;
|
|
564
|
+
|
|
565
|
+
const child = parseElement(str, pos, childInheritPreserve);
|
|
510
566
|
if (child.element) {
|
|
511
567
|
children.push(child.element);
|
|
512
568
|
pos = child.endPos;
|
|
@@ -534,7 +590,21 @@ function parseElement(str, pos) {
|
|
|
534
590
|
}
|
|
535
591
|
}
|
|
536
592
|
|
|
537
|
-
|
|
593
|
+
// BUG FIX 1: Create element first to set up parent references
|
|
594
|
+
const element = new SVGElement(tagName, attributes, children, '');
|
|
595
|
+
|
|
596
|
+
// BUG FIX 1: Check xml:space on this element, otherwise use inherited value
|
|
597
|
+
// xml:space="preserve" means preserve whitespace
|
|
598
|
+
// xml:space="default" means collapse/trim whitespace
|
|
599
|
+
// If not specified, inherit from ancestor
|
|
600
|
+
const currentXmlSpace = attributes['xml:space'];
|
|
601
|
+
const preserveWhitespace = currentXmlSpace === 'preserve' ? true :
|
|
602
|
+
currentXmlSpace === 'default' ? false :
|
|
603
|
+
inheritPreserveSpace;
|
|
604
|
+
|
|
605
|
+
const processedText = preserveWhitespace ? unescapeText(textContent) : unescapeText(textContent.trim());
|
|
606
|
+
element.textContent = processedText;
|
|
607
|
+
|
|
538
608
|
return { element, endPos: pos };
|
|
539
609
|
}
|
|
540
610
|
|
|
@@ -635,12 +705,96 @@ function escapeAttr(str) {
|
|
|
635
705
|
|
|
636
706
|
/**
|
|
637
707
|
* Unescape attribute value from XML.
|
|
708
|
+
* Handles numeric entities (decimal and hex) and named entities.
|
|
638
709
|
* @private
|
|
639
710
|
*/
|
|
640
711
|
function unescapeAttr(str) {
|
|
712
|
+
if (!str) return str;
|
|
641
713
|
return str
|
|
714
|
+
// Decode hex entities first: A or A -> A (case-insensitive)
|
|
715
|
+
// BUG FIX 1 & 4: Use fromCodePoint for surrogate pairs, validate code point range
|
|
716
|
+
.replace(/&#[xX]([0-9A-Fa-f]+);/g, (match, hex) => {
|
|
717
|
+
const codePoint = parseInt(hex, 16);
|
|
718
|
+
// BUG FIX 2: Validate XML-invalid characters (NULL and control characters)
|
|
719
|
+
const isXMLInvalid = codePoint === 0 ||
|
|
720
|
+
(codePoint >= 0x1 && codePoint <= 0x8) ||
|
|
721
|
+
(codePoint >= 0xB && codePoint <= 0xC) ||
|
|
722
|
+
(codePoint >= 0xE && codePoint <= 0x1F) ||
|
|
723
|
+
codePoint === 0xFFFE || codePoint === 0xFFFF;
|
|
724
|
+
// BUG FIX 4: Validate code point range (0x0 to 0x10FFFF, excluding surrogates)
|
|
725
|
+
if (isXMLInvalid || codePoint > 0x10FFFF || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
|
|
726
|
+
return '\uFFFD'; // Replacement character for invalid code points
|
|
727
|
+
}
|
|
728
|
+
return String.fromCodePoint(codePoint);
|
|
729
|
+
})
|
|
730
|
+
// Decode decimal entities: A -> A
|
|
731
|
+
// BUG FIX 1 & 4: Use fromCodePoint for surrogate pairs, validate code point range
|
|
732
|
+
.replace(/&#(\d+);/g, (match, dec) => {
|
|
733
|
+
const codePoint = parseInt(dec, 10);
|
|
734
|
+
// BUG FIX 2: Validate XML-invalid characters (NULL and control characters)
|
|
735
|
+
const isXMLInvalid = codePoint === 0 ||
|
|
736
|
+
(codePoint >= 0x1 && codePoint <= 0x8) ||
|
|
737
|
+
(codePoint >= 0xB && codePoint <= 0xC) ||
|
|
738
|
+
(codePoint >= 0xE && codePoint <= 0x1F) ||
|
|
739
|
+
codePoint === 0xFFFE || codePoint === 0xFFFF;
|
|
740
|
+
// BUG FIX 4: Validate code point range (0x0 to 0x10FFFF, excluding surrogates)
|
|
741
|
+
if (isXMLInvalid || codePoint > 0x10FFFF || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
|
|
742
|
+
return '\uFFFD'; // Replacement character for invalid code points
|
|
743
|
+
}
|
|
744
|
+
return String.fromCodePoint(codePoint);
|
|
745
|
+
})
|
|
746
|
+
// Then named entities (order matters - & last to avoid double-decoding)
|
|
642
747
|
.replace(/"/g, '"')
|
|
643
748
|
.replace(/'/g, "'")
|
|
749
|
+
.replace(/'/g, "'")
|
|
750
|
+
.replace(/ /g, '\u00A0') // BUG FIX 3: Add support for non-breaking space entity
|
|
751
|
+
.replace(/</g, '<')
|
|
752
|
+
.replace(/>/g, '>')
|
|
753
|
+
.replace(/&/g, '&');
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Unescape text content from XML.
|
|
758
|
+
* Handles numeric entities (decimal and hex) and named entities.
|
|
759
|
+
* @private
|
|
760
|
+
*/
|
|
761
|
+
function unescapeText(str) {
|
|
762
|
+
if (!str) return str;
|
|
763
|
+
return str
|
|
764
|
+
// Decode hex entities: A or A -> A (case-insensitive)
|
|
765
|
+
// BUG FIX 1 & 4: Use fromCodePoint for surrogate pairs, validate code point range
|
|
766
|
+
.replace(/&#[xX]([0-9A-Fa-f]+);/g, (match, hex) => {
|
|
767
|
+
const codePoint = parseInt(hex, 16);
|
|
768
|
+
// BUG FIX 2: Validate XML-invalid characters (NULL and control characters)
|
|
769
|
+
const isXMLInvalid = codePoint === 0 ||
|
|
770
|
+
(codePoint >= 0x1 && codePoint <= 0x8) ||
|
|
771
|
+
(codePoint >= 0xB && codePoint <= 0xC) ||
|
|
772
|
+
(codePoint >= 0xE && codePoint <= 0x1F) ||
|
|
773
|
+
codePoint === 0xFFFE || codePoint === 0xFFFF;
|
|
774
|
+
// BUG FIX 4: Validate code point range (0x0 to 0x10FFFF, excluding surrogates)
|
|
775
|
+
if (isXMLInvalid || codePoint > 0x10FFFF || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
|
|
776
|
+
return '\uFFFD'; // Replacement character for invalid code points
|
|
777
|
+
}
|
|
778
|
+
return String.fromCodePoint(codePoint);
|
|
779
|
+
})
|
|
780
|
+
// Decode decimal entities: A -> A
|
|
781
|
+
// BUG FIX 1 & 4: Use fromCodePoint for surrogate pairs, validate code point range
|
|
782
|
+
.replace(/&#(\d+);/g, (match, dec) => {
|
|
783
|
+
const codePoint = parseInt(dec, 10);
|
|
784
|
+
// BUG FIX 2: Validate XML-invalid characters (NULL and control characters)
|
|
785
|
+
const isXMLInvalid = codePoint === 0 ||
|
|
786
|
+
(codePoint >= 0x1 && codePoint <= 0x8) ||
|
|
787
|
+
(codePoint >= 0xB && codePoint <= 0xC) ||
|
|
788
|
+
(codePoint >= 0xE && codePoint <= 0x1F) ||
|
|
789
|
+
codePoint === 0xFFFE || codePoint === 0xFFFF;
|
|
790
|
+
// BUG FIX 4: Validate code point range (0x0 to 0x10FFFF, excluding surrogates)
|
|
791
|
+
if (isXMLInvalid || codePoint > 0x10FFFF || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
|
|
792
|
+
return '\uFFFD'; // Replacement character for invalid code points
|
|
793
|
+
}
|
|
794
|
+
return String.fromCodePoint(codePoint);
|
|
795
|
+
})
|
|
796
|
+
// Named entities (& last)
|
|
797
|
+
.replace(/ /g, '\u00A0') // BUG FIX 3: Add support for non-breaking space entity
|
|
644
798
|
.replace(/</g, '<')
|
|
645
799
|
.replace(/>/g, '>')
|
|
646
800
|
.replace(/&/g, '&');
|
|
@@ -714,10 +868,15 @@ export function parseUrlReference(urlValue) {
|
|
|
714
868
|
/**
|
|
715
869
|
* Serialize SVG element tree back to string.
|
|
716
870
|
* @param {SVGElement} root - Root element
|
|
871
|
+
* @param {Object} options - Serialization options
|
|
872
|
+
* @param {boolean} options.minify - Whether to minify output (no whitespace/newlines)
|
|
717
873
|
* @returns {string}
|
|
718
874
|
*/
|
|
719
|
-
export function serializeSVG(root) {
|
|
720
|
-
|
|
875
|
+
export function serializeSVG(root, options = {}) {
|
|
876
|
+
const minify = options.minify || false;
|
|
877
|
+
const xmlDecl = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
878
|
+
const separator = minify ? '' : '\n';
|
|
879
|
+
return xmlDecl + separator + root.serialize(0, minify);
|
|
721
880
|
}
|
|
722
881
|
|
|
723
882
|
export default {
|