@emasoft/svg-matrix 1.0.26 → 1.0.28
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 +57 -0
- package/bin/svg-matrix.js +28 -3
- package/bin/svgm.js +46 -3
- package/package.json +1 -1
- package/src/index.js +7 -2
- package/src/inkscape-support.js +641 -0
- package/src/svg-collections.js +26 -1
- package/src/svg-toolbox.js +155 -41
- package/src/svg2-polyfills.js +444 -0
package/src/svg-toolbox.js
CHANGED
|
@@ -874,6 +874,49 @@ export const removeUnknownsAndDefaults = createOperation(
|
|
|
874
874
|
css.includes(':not(');
|
|
875
875
|
});
|
|
876
876
|
|
|
877
|
+
// Map lowercase SVG tag names to their canonical mixed-case forms
|
|
878
|
+
// SVG is case-sensitive but XML parsers may lowercase tag names
|
|
879
|
+
const tagNameMap = {
|
|
880
|
+
'clippath': 'clipPath',
|
|
881
|
+
'textpath': 'textPath',
|
|
882
|
+
'lineargradient': 'linearGradient',
|
|
883
|
+
'radialgradient': 'radialGradient',
|
|
884
|
+
'meshgradient': 'meshGradient',
|
|
885
|
+
'hatchpath': 'hatchPath',
|
|
886
|
+
'solidcolor': 'solidColor',
|
|
887
|
+
'foreignobject': 'foreignObject',
|
|
888
|
+
'feblend': 'feBlend',
|
|
889
|
+
'fecolormatrix': 'feColorMatrix',
|
|
890
|
+
'fecomponenttransfer': 'feComponentTransfer',
|
|
891
|
+
'fecomposite': 'feComposite',
|
|
892
|
+
'feconvolvematrix': 'feConvolveMatrix',
|
|
893
|
+
'fediffuselighting': 'feDiffuseLighting',
|
|
894
|
+
'fedisplacementmap': 'feDisplacementMap',
|
|
895
|
+
'fedistantlight': 'feDistantLight',
|
|
896
|
+
'fedropshadow': 'feDropShadow',
|
|
897
|
+
'feflood': 'feFlood',
|
|
898
|
+
'fefunca': 'feFuncA',
|
|
899
|
+
'fefuncb': 'feFuncB',
|
|
900
|
+
'fefuncg': 'feFuncG',
|
|
901
|
+
'fefuncr': 'feFuncR',
|
|
902
|
+
'fegaussianblur': 'feGaussianBlur',
|
|
903
|
+
'feimage': 'feImage',
|
|
904
|
+
'femerge': 'feMerge',
|
|
905
|
+
'femergenode': 'feMergeNode',
|
|
906
|
+
'femorphology': 'feMorphology',
|
|
907
|
+
'feoffset': 'feOffset',
|
|
908
|
+
'fepointlight': 'fePointLight',
|
|
909
|
+
'fespecularlighting': 'feSpecularLighting',
|
|
910
|
+
'fespotlight': 'feSpotLight',
|
|
911
|
+
'fetile': 'feTile',
|
|
912
|
+
'feturbulence': 'feTurbulence',
|
|
913
|
+
'animatemotion': 'animateMotion',
|
|
914
|
+
'animatetransform': 'animateTransform',
|
|
915
|
+
};
|
|
916
|
+
const canonicalTagName = (tag) => tagNameMap[tag] || tag;
|
|
917
|
+
|
|
918
|
+
// Known SVG elements - include both mixed-case and lowercase variants
|
|
919
|
+
// because XML parsers may lowercase tag names
|
|
877
920
|
const knownElements = [
|
|
878
921
|
"svg",
|
|
879
922
|
"g",
|
|
@@ -887,13 +930,22 @@ export const removeUnknownsAndDefaults = createOperation(
|
|
|
887
930
|
"text",
|
|
888
931
|
"tspan",
|
|
889
932
|
"tref",
|
|
890
|
-
"textPath",
|
|
933
|
+
"textPath", "textpath",
|
|
891
934
|
"defs",
|
|
892
|
-
"clipPath",
|
|
935
|
+
"clipPath", "clippath",
|
|
893
936
|
"mask",
|
|
894
937
|
"pattern",
|
|
895
|
-
"linearGradient",
|
|
896
|
-
"radialGradient",
|
|
938
|
+
"linearGradient", "lineargradient",
|
|
939
|
+
"radialGradient", "radialgradient",
|
|
940
|
+
// SVG 2.0 gradient elements (mesh gradients)
|
|
941
|
+
"meshGradient", "meshgradient",
|
|
942
|
+
"meshrow",
|
|
943
|
+
"meshpatch",
|
|
944
|
+
// SVG 2.0 hatch elements
|
|
945
|
+
"hatch",
|
|
946
|
+
"hatchpath", "hatchPath",
|
|
947
|
+
// SVG 2.0 solid color
|
|
948
|
+
"solidcolor", "solidColor",
|
|
897
949
|
"stop",
|
|
898
950
|
"image",
|
|
899
951
|
"use",
|
|
@@ -902,38 +954,38 @@ export const removeUnknownsAndDefaults = createOperation(
|
|
|
902
954
|
"title",
|
|
903
955
|
"desc",
|
|
904
956
|
"metadata",
|
|
905
|
-
"foreignObject",
|
|
957
|
+
"foreignObject", "foreignobject",
|
|
906
958
|
"switch",
|
|
907
959
|
"a",
|
|
908
960
|
"filter",
|
|
909
|
-
"feBlend",
|
|
910
|
-
"feColorMatrix",
|
|
911
|
-
"feComponentTransfer",
|
|
912
|
-
"feComposite",
|
|
913
|
-
"feConvolveMatrix",
|
|
914
|
-
"feDiffuseLighting",
|
|
915
|
-
"feDisplacementMap",
|
|
916
|
-
"feDistantLight",
|
|
917
|
-
"feDropShadow",
|
|
918
|
-
"feFlood",
|
|
919
|
-
"feFuncA",
|
|
920
|
-
"feFuncB",
|
|
921
|
-
"feFuncG",
|
|
922
|
-
"feFuncR",
|
|
923
|
-
"feGaussianBlur",
|
|
924
|
-
"feImage",
|
|
925
|
-
"feMerge",
|
|
926
|
-
"feMergeNode",
|
|
927
|
-
"feMorphology",
|
|
928
|
-
"feOffset",
|
|
929
|
-
"fePointLight",
|
|
930
|
-
"feSpecularLighting",
|
|
931
|
-
"feSpotLight",
|
|
932
|
-
"feTile",
|
|
933
|
-
"feTurbulence",
|
|
961
|
+
"feBlend", "feblend",
|
|
962
|
+
"feColorMatrix", "fecolormatrix",
|
|
963
|
+
"feComponentTransfer", "fecomponenttransfer",
|
|
964
|
+
"feComposite", "fecomposite",
|
|
965
|
+
"feConvolveMatrix", "feconvolvematrix",
|
|
966
|
+
"feDiffuseLighting", "fediffuselighting",
|
|
967
|
+
"feDisplacementMap", "fedisplacementmap",
|
|
968
|
+
"feDistantLight", "fedistantlight",
|
|
969
|
+
"feDropShadow", "fedropshadow",
|
|
970
|
+
"feFlood", "feflood",
|
|
971
|
+
"feFuncA", "fefunca",
|
|
972
|
+
"feFuncB", "fefuncb",
|
|
973
|
+
"feFuncG", "fefuncg",
|
|
974
|
+
"feFuncR", "fefuncr",
|
|
975
|
+
"feGaussianBlur", "fegaussianblur",
|
|
976
|
+
"feImage", "feimage",
|
|
977
|
+
"feMerge", "femerge",
|
|
978
|
+
"feMergeNode", "femergenode",
|
|
979
|
+
"feMorphology", "femorphology",
|
|
980
|
+
"feOffset", "feoffset",
|
|
981
|
+
"fePointLight", "fepointlight",
|
|
982
|
+
"feSpecularLighting", "fespecularlighting",
|
|
983
|
+
"feSpotLight", "fespotlight",
|
|
984
|
+
"feTile", "fetile",
|
|
985
|
+
"feTurbulence", "feturbulence",
|
|
934
986
|
"animate",
|
|
935
|
-
"animateMotion",
|
|
936
|
-
"animateTransform",
|
|
987
|
+
"animateMotion", "animatemotion",
|
|
988
|
+
"animateTransform", "animatetransform",
|
|
937
989
|
"set",
|
|
938
990
|
"mpath",
|
|
939
991
|
"view",
|
|
@@ -1123,7 +1175,9 @@ export const removeUnknownsAndDefaults = createOperation(
|
|
|
1123
1175
|
// Remove unknown children (but protect known ones)
|
|
1124
1176
|
// P4-1: Also validate parent-child relationships per SVG spec
|
|
1125
1177
|
const parentTagLower = el.tagName.toLowerCase();
|
|
1126
|
-
|
|
1178
|
+
// Try lowercase key first, then canonical mixed-case (case-insensitive lookup)
|
|
1179
|
+
const allowedChildren = allowedChildrenPerElement[parentTagLower] ||
|
|
1180
|
+
allowedChildrenPerElement[canonicalTagName(parentTagLower)];
|
|
1127
1181
|
|
|
1128
1182
|
for (const child of [...el.children]) {
|
|
1129
1183
|
if (isElement(child)) {
|
|
@@ -1142,7 +1196,10 @@ export const removeUnknownsAndDefaults = createOperation(
|
|
|
1142
1196
|
|
|
1143
1197
|
// P4-1: Check if child is allowed for this parent
|
|
1144
1198
|
// Only validate if we have rules for this parent
|
|
1145
|
-
|
|
1199
|
+
// Use case-insensitive check: try both lowercase and canonical form
|
|
1200
|
+
if (allowedChildren &&
|
|
1201
|
+
!allowedChildren.has(childTagLower) &&
|
|
1202
|
+
!allowedChildren.has(canonicalTagName(childTagLower))) {
|
|
1146
1203
|
// Invalid child for this parent - remove it
|
|
1147
1204
|
el.removeChild(child);
|
|
1148
1205
|
continue;
|
|
@@ -1323,12 +1380,42 @@ export const removeHiddenElements = createOperation((doc, options = {}) => {
|
|
|
1323
1380
|
|
|
1324
1381
|
/**
|
|
1325
1382
|
* Remove empty text elements
|
|
1383
|
+
* Recursively checks for text content in child elements (tspan, etc.)
|
|
1384
|
+
* Preserves text elements with namespace attributes when preserveNamespaces is set
|
|
1326
1385
|
*/
|
|
1327
1386
|
export const removeEmptyText = createOperation((doc, options = {}) => {
|
|
1328
1387
|
const textElements = doc.getElementsByTagName("text");
|
|
1329
1388
|
|
|
1389
|
+
// Helper to get all text content recursively from element and children
|
|
1390
|
+
const getTextContentRecursive = (el) => {
|
|
1391
|
+
let text = el.textContent || '';
|
|
1392
|
+
if (el.children) {
|
|
1393
|
+
for (const child of el.children) {
|
|
1394
|
+
text += getTextContentRecursive(child);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return text;
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
// Helper to check if element has preserved namespace attributes
|
|
1401
|
+
const hasPreservedNsAttrs = (el, preserveNs) => {
|
|
1402
|
+
if (!preserveNs || preserveNs.length === 0) return false;
|
|
1403
|
+
const attrNames = el.getAttributeNames ? el.getAttributeNames() : [];
|
|
1404
|
+
return attrNames.some(attr =>
|
|
1405
|
+
preserveNs.some(ns => attr.startsWith(ns + ':'))
|
|
1406
|
+
);
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
const preserveNamespaces = options.preserveNamespaces || [];
|
|
1410
|
+
|
|
1330
1411
|
for (const text of [...textElements]) {
|
|
1331
|
-
|
|
1412
|
+
const recursiveContent = getTextContentRecursive(text);
|
|
1413
|
+
const isEmpty = !recursiveContent || recursiveContent.trim() === "";
|
|
1414
|
+
|
|
1415
|
+
// Don't remove if it has preserved namespace attributes (e.g., Inkscape tile metadata)
|
|
1416
|
+
const hasNsAttrs = hasPreservedNsAttrs(text, preserveNamespaces);
|
|
1417
|
+
|
|
1418
|
+
if (isEmpty && !hasNsAttrs) {
|
|
1332
1419
|
if (text.parentNode) {
|
|
1333
1420
|
text.parentNode.removeChild(text);
|
|
1334
1421
|
}
|
|
@@ -1340,14 +1427,16 @@ export const removeEmptyText = createOperation((doc, options = {}) => {
|
|
|
1340
1427
|
|
|
1341
1428
|
/**
|
|
1342
1429
|
* Remove empty container elements
|
|
1430
|
+
* Note: Patterns with xlink:href/href inherit content and are NOT empty
|
|
1343
1431
|
*/
|
|
1344
1432
|
export const removeEmptyContainers = createOperation((doc, options = {}) => {
|
|
1433
|
+
// Include both mixed-case and lowercase variants for case-insensitive matching
|
|
1345
1434
|
const containers = [
|
|
1346
1435
|
"g",
|
|
1347
1436
|
"defs",
|
|
1348
1437
|
"symbol",
|
|
1349
1438
|
"marker",
|
|
1350
|
-
"clipPath",
|
|
1439
|
+
"clipPath", "clippath",
|
|
1351
1440
|
"mask",
|
|
1352
1441
|
"pattern",
|
|
1353
1442
|
];
|
|
@@ -1358,8 +1447,21 @@ export const removeEmptyContainers = createOperation((doc, options = {}) => {
|
|
|
1358
1447
|
if (isElement(child)) processElement(child);
|
|
1359
1448
|
}
|
|
1360
1449
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1450
|
+
const tagLower = el.tagName?.toLowerCase();
|
|
1451
|
+
|
|
1452
|
+
// Skip if not a container type
|
|
1453
|
+
if (!containers.includes(el.tagName) && !containers.includes(tagLower)) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// Don't remove if element has href/xlink:href (inherits content from reference)
|
|
1458
|
+
// This is common for patterns that reference other patterns
|
|
1459
|
+
if (el.getAttribute('href') || el.getAttribute('xlink:href')) {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// Remove if container is truly empty (no children)
|
|
1464
|
+
if (el.children.length === 0) {
|
|
1363
1465
|
if (el.parentNode) {
|
|
1364
1466
|
el.parentNode.removeChild(el);
|
|
1365
1467
|
}
|
|
@@ -1456,9 +1558,18 @@ export const removeDesc = createOperation((doc, options = {}) => {
|
|
|
1456
1558
|
* @param {boolean} options.preserveVendor - If true, preserves all editor namespace elements and attributes
|
|
1457
1559
|
*/
|
|
1458
1560
|
export const removeEditorsNSData = createOperation((doc, options = {}) => {
|
|
1459
|
-
//
|
|
1460
|
-
if (options.preserveVendor) return doc;
|
|
1561
|
+
// preserveVendor = true preserves ALL editor namespaces (backward compat)
|
|
1562
|
+
if (options.preserveVendor === true) return doc;
|
|
1563
|
+
|
|
1564
|
+
// preserveNamespaces = array of prefixes to preserve selectively
|
|
1565
|
+
const preserveNamespaces = options.preserveNamespaces || [];
|
|
1566
|
+
|
|
1567
|
+
// Handle namespace aliases: sodipodi and inkscape always go together
|
|
1568
|
+
const normalizedPreserve = new Set(preserveNamespaces);
|
|
1569
|
+
if (normalizedPreserve.has('sodipodi')) normalizedPreserve.add('inkscape');
|
|
1570
|
+
if (normalizedPreserve.has('inkscape')) normalizedPreserve.add('sodipodi');
|
|
1461
1571
|
|
|
1572
|
+
// Filter out preserved namespaces from the removal list
|
|
1462
1573
|
const editorPrefixes = [
|
|
1463
1574
|
"inkscape",
|
|
1464
1575
|
"sodipodi",
|
|
@@ -1468,7 +1579,10 @@ export const removeEditorsNSData = createOperation((doc, options = {}) => {
|
|
|
1468
1579
|
"serif",
|
|
1469
1580
|
"vectornator",
|
|
1470
1581
|
"figma",
|
|
1471
|
-
];
|
|
1582
|
+
].filter(prefix => !normalizedPreserve.has(prefix));
|
|
1583
|
+
|
|
1584
|
+
// If all namespaces are preserved, exit early
|
|
1585
|
+
if (editorPrefixes.length === 0) return doc;
|
|
1472
1586
|
|
|
1473
1587
|
// FIRST: Remove editor-specific elements (sodipodi:namedview, etc.)
|
|
1474
1588
|
// This must happen BEFORE checking remaining prefixes to avoid SVGO bug #1530
|