@emasoft/svg-matrix 1.3.4 → 1.3.6
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 +56 -0
- package/bin/svg-matrix.js +67 -3
- package/bin/svglinter.cjs +84 -0
- package/bin/svgm.js +37 -0
- package/dist/svg-matrix.global.min.js +2 -2
- package/dist/svg-matrix.min.js +2 -2
- package/dist/svg-toolbox.global.min.js +110 -110
- package/dist/svg-toolbox.min.js +110 -110
- package/dist/svgm.min.js +112 -112
- package/dist/version.json +8 -8
- package/package.json +1 -1
- package/src/animation-optimization.js +29 -0
- package/src/index.js +2 -2
- package/src/inkscape-support.js +768 -0
- package/src/svg-matrix-lib.js +2 -2
- package/src/svg-toolbox-lib.js +2 -2
- package/src/svg-toolbox.js +290 -3
- package/src/svg-validation-data.js +1 -0
- package/src/svgm-lib.js +2 -2
package/src/svg-matrix-lib.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Works in both Node.js and browser environments.
|
|
6
6
|
*
|
|
7
7
|
* @module svg-matrix-lib
|
|
8
|
-
* @version 1.3.
|
|
8
|
+
* @version 1.3.6
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example Browser usage:
|
|
@@ -32,7 +32,7 @@ Decimal.set({ precision: 80 });
|
|
|
32
32
|
/**
|
|
33
33
|
* Library version
|
|
34
34
|
*/
|
|
35
|
-
export const VERSION = "1.3.
|
|
35
|
+
export const VERSION = "1.3.6";
|
|
36
36
|
|
|
37
37
|
// Export core classes
|
|
38
38
|
export { Decimal, Matrix, Vector };
|
package/src/svg-toolbox-lib.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Provides 69+ operations for cleaning, optimizing, and transforming SVG files.
|
|
6
6
|
*
|
|
7
7
|
* @module svg-toolbox-lib
|
|
8
|
-
* @version 1.3.
|
|
8
|
+
* @version 1.3.6
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example Browser usage:
|
|
@@ -34,7 +34,7 @@ import * as SVGToolboxModule from "./svg-toolbox.js";
|
|
|
34
34
|
/**
|
|
35
35
|
* Library version
|
|
36
36
|
*/
|
|
37
|
-
export const VERSION = "1.3.
|
|
37
|
+
export const VERSION = "1.3.6";
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Default export for browser global (window.SVGToolbox)
|
package/src/svg-toolbox.js
CHANGED
|
@@ -56,6 +56,14 @@ import {
|
|
|
56
56
|
findElementsWithAttribute,
|
|
57
57
|
} from "./svg-parser.js";
|
|
58
58
|
import { flattenSVG } from "./flatten-pipeline.js";
|
|
59
|
+
import {
|
|
60
|
+
validateInkscapeDocument,
|
|
61
|
+
detectInkscapeDocument,
|
|
62
|
+
getPolyfillRequirements,
|
|
63
|
+
InkscapeValidationSeverity,
|
|
64
|
+
INKSCAPE_ATTRIBUTES,
|
|
65
|
+
SODIPODI_ATTRIBUTES,
|
|
66
|
+
} from "./inkscape-support.js";
|
|
59
67
|
import {
|
|
60
68
|
referencesProps as _referencesProps,
|
|
61
69
|
inheritableAttrs,
|
|
@@ -266,6 +274,7 @@ const SVG11_ATTRIBUTES = new Set([
|
|
|
266
274
|
"filter",
|
|
267
275
|
"filterRes",
|
|
268
276
|
"filterUnits",
|
|
277
|
+
"filterPrimitiveUnits",
|
|
269
278
|
"flood-color",
|
|
270
279
|
"flood-opacity",
|
|
271
280
|
"font-family",
|
|
@@ -2845,6 +2854,175 @@ export const removeEmptyAttrs = createOperation((doc, _options = {}) => {
|
|
|
2845
2854
|
return doc;
|
|
2846
2855
|
});
|
|
2847
2856
|
|
|
2857
|
+
/**
|
|
2858
|
+
* Convert Inkscape SVG to plain/standard SVG.
|
|
2859
|
+
*
|
|
2860
|
+
* This function removes all Inkscape-specific and Sodipodi-specific content
|
|
2861
|
+
* from an SVG file, producing a clean SVG that conforms to SVG 1.1 or 2.0 specs.
|
|
2862
|
+
*
|
|
2863
|
+
* What gets removed:
|
|
2864
|
+
* - All sodipodi:* elements (namedview, guide)
|
|
2865
|
+
* - All inkscape:* elements (path-effect, perspective, page, grid, clipboard, box3dside)
|
|
2866
|
+
* - All sodipodi:* attributes (nodetypes, type, cx, cy, rx, ry, start, end, etc.)
|
|
2867
|
+
* - All inkscape:* attributes (groupmode, label, version, collect, etc.)
|
|
2868
|
+
* - Inkscape/Sodipodi namespace declarations (xmlns:inkscape, xmlns:sodipodi)
|
|
2869
|
+
* - SVG 1.2 flowText elements (flowRoot, flowPara, flowRegion, etc.) - Inkscape-specific
|
|
2870
|
+
*
|
|
2871
|
+
* What is preserved:
|
|
2872
|
+
* - All standard SVG elements and attributes
|
|
2873
|
+
* - Visual appearance (the SVG renders identically)
|
|
2874
|
+
* - IDs, classes, and CSS styles
|
|
2875
|
+
* - Standard namespaces (svg, xlink, xml)
|
|
2876
|
+
*
|
|
2877
|
+
* @param {Object} options - Conversion options
|
|
2878
|
+
* @param {boolean} [options.removeFlowText=true] - Remove SVG 1.2 flowText elements (not browser-supported)
|
|
2879
|
+
* @param {boolean} [options.removeEmptyDefs=true] - Remove empty <defs> elements after cleanup
|
|
2880
|
+
* @param {boolean} [options.removeEmptyGroups=false] - Remove groups that become empty after stripping
|
|
2881
|
+
* @returns {Object} The cleaned SVG document
|
|
2882
|
+
*
|
|
2883
|
+
* @example
|
|
2884
|
+
* // Convert Inkscape SVG to plain SVG
|
|
2885
|
+
* const plainSVG = convertToPlainSVG(doc);
|
|
2886
|
+
*
|
|
2887
|
+
* @example
|
|
2888
|
+
* // Keep flowText elements (for manual conversion later)
|
|
2889
|
+
* const plainSVG = convertToPlainSVG(doc, { removeFlowText: false });
|
|
2890
|
+
*/
|
|
2891
|
+
export const convertToPlainSVG = createOperation((doc, options = {}) => {
|
|
2892
|
+
const {
|
|
2893
|
+
removeFlowText = true,
|
|
2894
|
+
removeEmptyDefs = true,
|
|
2895
|
+
removeEmptyGroups = false,
|
|
2896
|
+
} = options;
|
|
2897
|
+
|
|
2898
|
+
// Inkscape/Sodipodi namespace prefixes to remove
|
|
2899
|
+
const inkscapePrefixes = ["inkscape", "sodipodi"];
|
|
2900
|
+
|
|
2901
|
+
// SVG 1.2 flowText elements (Inkscape-specific, not browser-supported)
|
|
2902
|
+
const flowTextElements = [
|
|
2903
|
+
"flowRoot",
|
|
2904
|
+
"flowPara",
|
|
2905
|
+
"flowRegion",
|
|
2906
|
+
"flowSpan",
|
|
2907
|
+
"flowDiv",
|
|
2908
|
+
"flowLine",
|
|
2909
|
+
];
|
|
2910
|
+
|
|
2911
|
+
// STEP 1: Remove all Inkscape/Sodipodi namespaced elements
|
|
2912
|
+
// Why: These elements are Inkscape UI data and not part of standard SVG
|
|
2913
|
+
// Use [...el.children] to create a copy of the array before iterating (avoids live collection issues)
|
|
2914
|
+
const removeInkscapeElements = (el) => {
|
|
2915
|
+
for (const child of [...el.children]) {
|
|
2916
|
+
if (isElement(child)) {
|
|
2917
|
+
const tagColonIdx = child.tagName.indexOf(":");
|
|
2918
|
+
if (tagColonIdx > 0) {
|
|
2919
|
+
const prefix = child.tagName.substring(0, tagColonIdx);
|
|
2920
|
+
if (inkscapePrefixes.includes(prefix)) {
|
|
2921
|
+
el.removeChild(child);
|
|
2922
|
+
continue;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
// Recurse into non-inkscape children
|
|
2926
|
+
removeInkscapeElements(child);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
};
|
|
2930
|
+
removeInkscapeElements(doc);
|
|
2931
|
+
|
|
2932
|
+
// STEP 2: Remove SVG 1.2 flowText elements if requested
|
|
2933
|
+
// Why: flowText is from SVG 1.2 draft, only Inkscape supports it, no browsers do
|
|
2934
|
+
if (removeFlowText) {
|
|
2935
|
+
const removeFlowTextElements = (el) => {
|
|
2936
|
+
for (const child of [...el.children]) {
|
|
2937
|
+
if (isElement(child)) {
|
|
2938
|
+
if (flowTextElements.includes(child.tagName)) {
|
|
2939
|
+
el.removeChild(child);
|
|
2940
|
+
continue;
|
|
2941
|
+
}
|
|
2942
|
+
removeFlowTextElements(child);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
};
|
|
2946
|
+
removeFlowTextElements(doc);
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
// STEP 3: Remove all Inkscape/Sodipodi namespaced attributes
|
|
2950
|
+
// Why: These attributes store Inkscape editor state, not visual data
|
|
2951
|
+
const removeInkscapeAttributes = (el) => {
|
|
2952
|
+
for (const attrName of [...el.getAttributeNames()]) {
|
|
2953
|
+
// Skip namespace declarations (handled in step 4)
|
|
2954
|
+
if (attrName.startsWith("xmlns:")) continue;
|
|
2955
|
+
|
|
2956
|
+
const colonIdx = attrName.indexOf(":");
|
|
2957
|
+
if (colonIdx > 0) {
|
|
2958
|
+
const prefix = attrName.substring(0, colonIdx);
|
|
2959
|
+
if (inkscapePrefixes.includes(prefix)) {
|
|
2960
|
+
el.removeAttribute(attrName);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
// Recurse into children
|
|
2966
|
+
for (const child of el.children) {
|
|
2967
|
+
if (isElement(child)) removeInkscapeAttributes(child);
|
|
2968
|
+
}
|
|
2969
|
+
};
|
|
2970
|
+
removeInkscapeAttributes(doc);
|
|
2971
|
+
|
|
2972
|
+
// STEP 4: Remove Inkscape/Sodipodi namespace declarations from root SVG
|
|
2973
|
+
// Why: After removing all prefixed elements/attributes, declarations are orphaned
|
|
2974
|
+
for (const prefix of inkscapePrefixes) {
|
|
2975
|
+
doc.removeAttribute(`xmlns:${prefix}`);
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
// STEP 5: Remove empty <defs> elements if requested
|
|
2979
|
+
// Why: Inkscape often puts items in defs that are now removed
|
|
2980
|
+
if (removeEmptyDefs) {
|
|
2981
|
+
const removeEmptyDefsElements = (el) => {
|
|
2982
|
+
for (const child of [...el.children]) {
|
|
2983
|
+
if (isElement(child)) {
|
|
2984
|
+
if (child.tagName === "defs" && child.children.length === 0) {
|
|
2985
|
+
el.removeChild(child);
|
|
2986
|
+
} else {
|
|
2987
|
+
removeEmptyDefsElements(child);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
};
|
|
2992
|
+
removeEmptyDefsElements(doc);
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
// STEP 6: Remove empty groups if requested
|
|
2996
|
+
// Why: Groups that only contained Inkscape elements may now be empty
|
|
2997
|
+
if (removeEmptyGroups) {
|
|
2998
|
+
const removeEmptyGroupElements = (el) => {
|
|
2999
|
+
let changed = true;
|
|
3000
|
+
// Iterate until no more changes (handles nested empty groups)
|
|
3001
|
+
while (changed) {
|
|
3002
|
+
changed = false;
|
|
3003
|
+
for (const child of [...el.children]) {
|
|
3004
|
+
if (isElement(child)) {
|
|
3005
|
+
if (child.tagName === "g" && child.children.length === 0) {
|
|
3006
|
+
// Only remove if no attributes that might matter (like id for references)
|
|
3007
|
+
const hasId = child.hasAttribute("id");
|
|
3008
|
+
const hasClass = child.hasAttribute("class");
|
|
3009
|
+
if (!hasId && !hasClass) {
|
|
3010
|
+
el.removeChild(child);
|
|
3011
|
+
changed = true;
|
|
3012
|
+
}
|
|
3013
|
+
} else {
|
|
3014
|
+
removeEmptyGroupElements(child);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
removeEmptyGroupElements(doc);
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
return doc;
|
|
3024
|
+
});
|
|
3025
|
+
|
|
2848
3026
|
/**
|
|
2849
3027
|
* Remove viewBox if matches dimensions
|
|
2850
3028
|
*/
|
|
@@ -13805,6 +13983,28 @@ export const fixInvalidSVG = createOperation((doc, options = {}) => {
|
|
|
13805
13983
|
reason: `Element <${tagName}> is missing required attribute '${attr}' (or 'xlink:href')`,
|
|
13806
13984
|
});
|
|
13807
13985
|
}
|
|
13986
|
+
} else if (tagName === "stop" && attr === "offset") {
|
|
13987
|
+
// SVG 2 CR 2016 §13.8: Mesh gradient stops use 'path' attribute, NOT 'offset'
|
|
13988
|
+
// Reference: https://www.w3.org/TR/2016/CR-SVG2-20160915/pservers.html
|
|
13989
|
+
// Linear/radial gradient stops require 'offset', mesh stops use 'path' instead
|
|
13990
|
+
let parent = el.parentNode;
|
|
13991
|
+
let isMeshStop = false;
|
|
13992
|
+
while (parent && parent.tagName) {
|
|
13993
|
+
const parentTag = parent.tagName.toLowerCase();
|
|
13994
|
+
if (parentTag === "meshpatch" || parentTag === "meshgradient") {
|
|
13995
|
+
isMeshStop = true;
|
|
13996
|
+
break;
|
|
13997
|
+
}
|
|
13998
|
+
parent = parent.parentNode;
|
|
13999
|
+
}
|
|
14000
|
+
if (!isMeshStop && !el.hasAttribute(attr)) {
|
|
14001
|
+
fixes.push({
|
|
14002
|
+
type: "missing_required_attribute",
|
|
14003
|
+
element: tagName,
|
|
14004
|
+
attr: attr,
|
|
14005
|
+
reason: `Element <${tagName}> is missing required attribute '${attr}'`,
|
|
14006
|
+
});
|
|
14007
|
+
}
|
|
13808
14008
|
} else {
|
|
13809
14009
|
if (!el.hasAttribute(attr)) {
|
|
13810
14010
|
fixes.push({
|
|
@@ -14171,6 +14371,7 @@ export const fixInvalidSVG = createOperation((doc, options = {}) => {
|
|
|
14171
14371
|
maskUnits: ["userSpaceOnUse", "objectBoundingBox"],
|
|
14172
14372
|
maskContentUnits: ["userSpaceOnUse", "objectBoundingBox"],
|
|
14173
14373
|
filterUnits: ["userSpaceOnUse", "objectBoundingBox"],
|
|
14374
|
+
filterPrimitiveUnits: ["userSpaceOnUse", "objectBoundingBox"],
|
|
14174
14375
|
primitiveUnits: ["userSpaceOnUse", "objectBoundingBox"],
|
|
14175
14376
|
markerUnits: ["strokeWidth", "userSpaceOnUse"],
|
|
14176
14377
|
// Other
|
|
@@ -15392,6 +15593,8 @@ export async function validateSVGAsync(input, options = {}) {
|
|
|
15392
15593
|
const outputFile = options.outputFile || null;
|
|
15393
15594
|
const outputFormat = (options.outputFormat || "json").toLowerCase();
|
|
15394
15595
|
const includeSource = options.includeSource === true;
|
|
15596
|
+
const validateInkscape = options.validateInkscape === true;
|
|
15597
|
+
const inkscapeStrict = options.inkscapeStrict === true;
|
|
15395
15598
|
|
|
15396
15599
|
// Validate outputFormat
|
|
15397
15600
|
const validFormats = ["text", "json", "xml", "yaml"];
|
|
@@ -16143,11 +16346,20 @@ export async function validateSVGAsync(input, options = {}) {
|
|
|
16143
16346
|
// - title/desc (text content for accessibility - not SVG structure)
|
|
16144
16347
|
// - metadata (can contain arbitrary XML metadata like Dublin Core, RDF)
|
|
16145
16348
|
// - Namespaced elements (e.g., d:testDescription, rdf:RDF)
|
|
16349
|
+
// - Elements in non-SVG namespaces (e.g., <foo xmlns="http://example.org/foo">)
|
|
16146
16350
|
const isNamespacedElement = tagName.includes(":");
|
|
16351
|
+
const isNonSvgNamespace =
|
|
16352
|
+
el.namespaceURI && el.namespaceURI !== "http://www.w3.org/2000/svg";
|
|
16353
|
+
// Check for explicit xmlns attribute declaring a non-SVG namespace
|
|
16354
|
+
const xmlns = el.getAttribute ? el.getAttribute("xmlns") : null;
|
|
16355
|
+
const hasNonSvgXmlns =
|
|
16356
|
+
xmlns && xmlns !== "http://www.w3.org/2000/svg";
|
|
16147
16357
|
const inNonSvgContext =
|
|
16148
16358
|
insideNonSvgContext ||
|
|
16149
16359
|
NON_SVG_CONTEXT_ELEMENTS.has(tagLower) ||
|
|
16150
|
-
isNamespacedElement
|
|
16360
|
+
isNamespacedElement ||
|
|
16361
|
+
isNonSvgNamespace ||
|
|
16362
|
+
hasNonSvgXmlns;
|
|
16151
16363
|
|
|
16152
16364
|
// Skip validation entirely if we're in non-SVG context
|
|
16153
16365
|
if (!inNonSvgContext) {
|
|
@@ -16206,11 +16418,20 @@ export async function validateSVGAsync(input, options = {}) {
|
|
|
16206
16418
|
// - title/desc (text content elements)
|
|
16207
16419
|
// - metadata (arbitrary XML metadata content)
|
|
16208
16420
|
// - Namespaced elements (e.g., d:testDescription, rdf:RDF)
|
|
16421
|
+
// - Elements in non-SVG namespaces (e.g., <foo xmlns="http://example.org/foo">)
|
|
16209
16422
|
const isNamespacedElement = el.tagName.includes(":");
|
|
16423
|
+
const isNonSvgNamespace =
|
|
16424
|
+
el.namespaceURI && el.namespaceURI !== "http://www.w3.org/2000/svg";
|
|
16425
|
+
// Check for explicit xmlns attribute declaring a non-SVG namespace
|
|
16426
|
+
const xmlns = el.getAttribute ? el.getAttribute("xmlns") : null;
|
|
16427
|
+
const hasNonSvgXmlns =
|
|
16428
|
+
xmlns && xmlns !== "http://www.w3.org/2000/svg";
|
|
16210
16429
|
const inNonSvgContext =
|
|
16211
16430
|
insideNonSvgContext ||
|
|
16212
16431
|
NON_SVG_CONTEXT_ELEMENTS.has(tagName) ||
|
|
16213
|
-
isNamespacedElement
|
|
16432
|
+
isNamespacedElement ||
|
|
16433
|
+
isNonSvgNamespace ||
|
|
16434
|
+
hasNonSvgXmlns;
|
|
16214
16435
|
|
|
16215
16436
|
// Skip attribute validation entirely if we're in non-SVG context
|
|
16216
16437
|
if (!inNonSvgContext) {
|
|
@@ -16336,6 +16557,26 @@ export async function validateSVGAsync(input, options = {}) {
|
|
|
16336
16557
|
}
|
|
16337
16558
|
} else {
|
|
16338
16559
|
for (const attr of required) {
|
|
16560
|
+
// SVG 2 CR 2016 §13.8: Mesh gradient stops use 'path' attribute, NOT 'offset'
|
|
16561
|
+
// Reference: https://www.w3.org/TR/2016/CR-SVG2-20160915/pservers.html
|
|
16562
|
+
// Quote: "offset - does not apply to mesh gradients"
|
|
16563
|
+
// "path - applies only to mesh gradients"
|
|
16564
|
+
// The 'path' attribute contains a single c/C/l/L bezier command defining one edge
|
|
16565
|
+
// of the Coons patch. Linear/radial gradient stops still require 'offset'.
|
|
16566
|
+
if (tagName === "stop" && attr === "offset") {
|
|
16567
|
+
// Check if this stop is inside a mesh gradient structure by walking up the tree
|
|
16568
|
+
let parent = el.parentNode;
|
|
16569
|
+
let isMeshStop = false;
|
|
16570
|
+
while (parent && parent.tagName) {
|
|
16571
|
+
const parentTag = parent.tagName.toLowerCase();
|
|
16572
|
+
if (parentTag === "meshpatch" || parentTag === "meshgradient") {
|
|
16573
|
+
isMeshStop = true;
|
|
16574
|
+
break;
|
|
16575
|
+
}
|
|
16576
|
+
parent = parent.parentNode;
|
|
16577
|
+
}
|
|
16578
|
+
if (isMeshStop) continue; // Skip offset requirement - mesh stops use 'path' instead
|
|
16579
|
+
}
|
|
16339
16580
|
if (!el.hasAttribute(attr)) {
|
|
16340
16581
|
issues.push(
|
|
16341
16582
|
createIssue(
|
|
@@ -16446,6 +16687,7 @@ export async function validateSVGAsync(input, options = {}) {
|
|
|
16446
16687
|
maskUnits: new Set(["userSpaceOnUse", "objectBoundingBox"]),
|
|
16447
16688
|
maskContentUnits: new Set(["userSpaceOnUse", "objectBoundingBox"]),
|
|
16448
16689
|
filterUnits: new Set(["userSpaceOnUse", "objectBoundingBox"]),
|
|
16690
|
+
filterPrimitiveUnits: new Set(["userSpaceOnUse", "objectBoundingBox"]),
|
|
16449
16691
|
primitiveUnits: new Set(["userSpaceOnUse", "objectBoundingBox"]),
|
|
16450
16692
|
patternUnits: new Set(["userSpaceOnUse", "objectBoundingBox"]),
|
|
16451
16693
|
patternContentUnits: new Set(["userSpaceOnUse", "objectBoundingBox"]),
|
|
@@ -17617,6 +17859,37 @@ export async function validateSVGAsync(input, options = {}) {
|
|
|
17617
17859
|
detectEventHandlers(doc);
|
|
17618
17860
|
detectAccessibilityIssues();
|
|
17619
17861
|
|
|
17862
|
+
// Inkscape namespace validation (if enabled)
|
|
17863
|
+
// Always use strict mode - a file either conforms to the spec or it doesn't
|
|
17864
|
+
let inkscapeValidation = null;
|
|
17865
|
+
if (validateInkscape && doc) {
|
|
17866
|
+
inkscapeValidation = validateInkscapeDocument(doc, {
|
|
17867
|
+
strict: true, // Always strict - unknown attributes are errors
|
|
17868
|
+
warnFlowText: true,
|
|
17869
|
+
checkPolyfillNeeds: true,
|
|
17870
|
+
});
|
|
17871
|
+
// Add Inkscape validation issues to the main issues array
|
|
17872
|
+
for (const inkIssue of inkscapeValidation.issues) {
|
|
17873
|
+
// Map Inkscape severity to ValidationSeverity
|
|
17874
|
+
let severity = ValidationSeverity.INFO;
|
|
17875
|
+
if (inkIssue.severity === InkscapeValidationSeverity.ERROR) {
|
|
17876
|
+
severity = ValidationSeverity.ERROR;
|
|
17877
|
+
} else if (inkIssue.severity === InkscapeValidationSeverity.WARNING) {
|
|
17878
|
+
severity = ValidationSeverity.WARNING;
|
|
17879
|
+
}
|
|
17880
|
+
issues.push({
|
|
17881
|
+
type: `inkscape-${inkIssue.type}`,
|
|
17882
|
+
reason: inkIssue.message,
|
|
17883
|
+
severity,
|
|
17884
|
+
line: inkIssue.line || 1,
|
|
17885
|
+
column: inkIssue.column || 1,
|
|
17886
|
+
element: inkIssue.element || null,
|
|
17887
|
+
attribute: inkIssue.attribute || null,
|
|
17888
|
+
context: inkIssue.context || null,
|
|
17889
|
+
});
|
|
17890
|
+
}
|
|
17891
|
+
}
|
|
17892
|
+
|
|
17620
17893
|
// Sort issues by line, then by column (for consistent, predictable output)
|
|
17621
17894
|
issues.sort((a, b) => {
|
|
17622
17895
|
if (a.line !== b.line) return a.line - b.line;
|
|
@@ -17657,6 +17930,19 @@ export async function validateSVGAsync(input, options = {}) {
|
|
|
17657
17930
|
summary,
|
|
17658
17931
|
};
|
|
17659
17932
|
|
|
17933
|
+
// Add Inkscape-specific info if validation was performed
|
|
17934
|
+
if (inkscapeValidation) {
|
|
17935
|
+
result.inkscape = {
|
|
17936
|
+
isInkscapeDocument: inkscapeValidation.isInkscape,
|
|
17937
|
+
inkscapeVersion: inkscapeValidation.version || null,
|
|
17938
|
+
polyfillsNeeded: inkscapeValidation.polyfillsNeeded || [],
|
|
17939
|
+
hasFlowText: inkscapeValidation.hasFlowText || false,
|
|
17940
|
+
inkscapeIssueCount: inkscapeValidation.issues.length,
|
|
17941
|
+
inkscapeErrorCount: inkscapeValidation.summary.errors,
|
|
17942
|
+
inkscapeWarningCount: inkscapeValidation.summary.warnings,
|
|
17943
|
+
};
|
|
17944
|
+
}
|
|
17945
|
+
|
|
17660
17946
|
// Export to file if requested
|
|
17661
17947
|
if (outputFile) {
|
|
17662
17948
|
try {
|
|
@@ -20349,7 +20635,7 @@ export default {
|
|
|
20349
20635
|
removeEmptyText,
|
|
20350
20636
|
removeEmptyContainers,
|
|
20351
20637
|
|
|
20352
|
-
// Category 2: Removal (
|
|
20638
|
+
// Category 2: Removal (13)
|
|
20353
20639
|
removeDoctype,
|
|
20354
20640
|
removeXMLProcInst,
|
|
20355
20641
|
removeComments,
|
|
@@ -20362,6 +20648,7 @@ export default {
|
|
|
20362
20648
|
removeXMLNS,
|
|
20363
20649
|
removeRasterImages,
|
|
20364
20650
|
removeScriptElement,
|
|
20651
|
+
convertToPlainSVG, // Converts Inkscape SVG to plain/standard SVG
|
|
20365
20652
|
|
|
20366
20653
|
// Category 3: Conversion (10)
|
|
20367
20654
|
convertShapesToPath,
|
package/src/svgm-lib.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* comprehensive SVG manipulation (SVGToolbox). Works in Node.js and browser.
|
|
6
6
|
*
|
|
7
7
|
* @module svgm-lib
|
|
8
|
-
* @version 1.3.
|
|
8
|
+
* @version 1.3.6
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example Browser usage:
|
|
@@ -49,7 +49,7 @@ Decimal.set({ precision: 80 });
|
|
|
49
49
|
/**
|
|
50
50
|
* Library version
|
|
51
51
|
*/
|
|
52
|
-
export const VERSION = "1.3.
|
|
52
|
+
export const VERSION = "1.3.6";
|
|
53
53
|
|
|
54
54
|
// Export math classes
|
|
55
55
|
export { Decimal, Matrix, Vector };
|