@emasoft/svg-matrix 1.1.0 → 1.2.0

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.
Files changed (55) hide show
  1. package/bin/svg-matrix.js +7 -6
  2. package/bin/svgm.js +109 -40
  3. package/dist/svg-matrix.min.js +7 -7
  4. package/dist/svg-toolbox.min.js +148 -228
  5. package/dist/svgm.min.js +152 -232
  6. package/dist/version.json +5 -5
  7. package/package.json +1 -1
  8. package/scripts/postinstall.js +72 -41
  9. package/scripts/test-postinstall.js +18 -16
  10. package/scripts/version-sync.js +78 -60
  11. package/src/animation-optimization.js +190 -98
  12. package/src/animation-references.js +11 -3
  13. package/src/arc-length.js +23 -20
  14. package/src/bezier-analysis.js +9 -13
  15. package/src/bezier-intersections.js +18 -4
  16. package/src/browser-verify.js +35 -8
  17. package/src/clip-path-resolver.js +285 -114
  18. package/src/convert-path-data.js +20 -8
  19. package/src/css-specificity.js +33 -9
  20. package/src/douglas-peucker.js +272 -141
  21. package/src/geometry-to-path.js +79 -22
  22. package/src/gjk-collision.js +287 -126
  23. package/src/index.js +56 -21
  24. package/src/inkscape-support.js +122 -101
  25. package/src/logger.js +43 -27
  26. package/src/marker-resolver.js +201 -121
  27. package/src/mask-resolver.js +231 -98
  28. package/src/matrix.js +9 -5
  29. package/src/mesh-gradient.js +22 -14
  30. package/src/off-canvas-detection.js +53 -17
  31. package/src/path-optimization.js +356 -171
  32. package/src/path-simplification.js +671 -256
  33. package/src/pattern-resolver.js +1 -3
  34. package/src/polygon-clip.js +396 -78
  35. package/src/svg-boolean-ops.js +90 -23
  36. package/src/svg-collections.js +1546 -667
  37. package/src/svg-flatten.js +152 -38
  38. package/src/svg-matrix-lib.js +2 -2
  39. package/src/svg-parser.js +5 -1
  40. package/src/svg-rendering-context.js +3 -1
  41. package/src/svg-toolbox-lib.js +2 -2
  42. package/src/svg-toolbox.js +99 -457
  43. package/src/svg-validation-data.js +513 -345
  44. package/src/svg2-polyfills.js +156 -93
  45. package/src/svgm-lib.js +8 -4
  46. package/src/transform-optimization.js +168 -51
  47. package/src/transforms2d.js +73 -40
  48. package/src/transforms3d.js +34 -27
  49. package/src/use-symbol-resolver.js +175 -76
  50. package/src/vector.js +80 -44
  51. package/src/vendor/inkscape-hatch-polyfill.js +143 -108
  52. package/src/vendor/inkscape-hatch-polyfill.min.js +291 -1
  53. package/src/vendor/inkscape-mesh-polyfill.js +953 -766
  54. package/src/vendor/inkscape-mesh-polyfill.min.js +896 -1
  55. package/src/verification.js +3 -4
@@ -648,8 +648,10 @@ const resetLevenshteinCache = () => levenshteinCache.clear();
648
648
  */
649
649
  const findClosestMatch = (name, validSet, maxDistance = 2) => {
650
650
  if (!name || typeof name !== "string") return null;
651
- if (!validSet || !(validSet instanceof Set) || validSet.size === 0) return null;
652
- const validMaxDistance = (typeof maxDistance !== "number" || maxDistance < 0) ? 2 : maxDistance;
651
+ if (!validSet || !(validSet instanceof Set) || validSet.size === 0)
652
+ return null;
653
+ const validMaxDistance =
654
+ typeof maxDistance !== "number" || maxDistance < 0 ? 2 : maxDistance;
653
655
  const lower = name.toLowerCase();
654
656
  let closest = null;
655
657
  let minDist = validMaxDistance + 1;
@@ -1345,7 +1347,9 @@ export const cleanupIds = createOperation((doc, options = {}) => {
1345
1347
  try {
1346
1348
  return decodeURI(id);
1347
1349
  } catch (e) {
1348
- // Log error for debugging - invalid URI encoding
1350
+ // LEGITIMATE FALLBACK: Malformed percent-encoding (e.g., "%zz") should not crash.
1351
+ // Return original ID which may still be valid as a literal string reference.
1352
+ // Real-world SVGs often contain malformed IDs that work because they're used literally.
1349
1353
  if (process.env.DEBUG) console.warn(`[svg-toolbox] ${e.message}`);
1350
1354
  return id;
1351
1355
  }
@@ -9667,7 +9671,9 @@ export const generateFullCompatibilityMatrix = createOperation(
9667
9671
  const docUrl = new URL(doc.documentURI || doc.baseURI || "");
9668
9672
  isCrossOrigin = imgUrl.origin !== docUrl.origin;
9669
9673
  } catch (e) {
9670
- // If URL parsing fails, assume cross-origin for safety - log error for debugging
9674
+ // LEGITIMATE FALLBACK: URL parsing may fail for relative paths or malformed URLs.
9675
+ // Defaulting to cross-origin=true is the SECURE choice (more restrictive).
9676
+ // This is feature detection behavior, not error-hiding.
9671
9677
  if (process.env.DEBUG) console.warn(`[svg-toolbox] ${e.message}`);
9672
9678
  isCrossOrigin = true;
9673
9679
  }
@@ -11683,374 +11689,6 @@ export function printHierarchicalMatrix(matrixData, options = {}) {
11683
11689
  return matrixData;
11684
11690
  }
11685
11691
 
11686
- /**
11687
- * Generate an SVG visualization of the compatibility matrix (LEGACY - simple version).
11688
- * @deprecated Use generateDetailedCompatibilityMatrixSVG for full hierarchical output
11689
- */
11690
- export function generateCompatibilityMatrixSVG_legacy(
11691
- matrixData,
11692
- options = {},
11693
- ) {
11694
- const cellWidth = options.cellWidth || 60;
11695
- const cellHeight = options.cellHeight || 45;
11696
- const fontFamily =
11697
- options.fontFamily || "system-ui, -apple-system, sans-serif";
11698
- const interactive = options.interactive !== false;
11699
-
11700
- // Axis definitions (same as printHierarchicalMatrix)
11701
- const COL_L1_SYNTAX = [
11702
- { id: "direct", label: "DIRECT", desc: "SVG file" },
11703
- { id: "img", label: "IMG", desc: "<img>" },
11704
- { id: "object", label: "OBJECT", desc: "<object>" },
11705
- { id: "embed", label: "EMBED", desc: "<embed>" },
11706
- { id: "inline", label: "INLINE", desc: "<svg>" },
11707
- { id: "css", label: "CSS", desc: "bg-image" },
11708
- ];
11709
-
11710
- const COL_L2_STORAGE = [
11711
- { id: "embedded", label: "EMB", desc: "Embedded (data-uri)" },
11712
- { id: "local", label: "LOC", desc: "Local path" },
11713
- { id: "remote", label: "REM", desc: "Remote URL" },
11714
- ];
11715
-
11716
- const ROW_L1_DISPLAY = [
11717
- { id: "browser", label: "BROWSER", desc: "Desktop browsers" },
11718
- { id: "mobile", label: "MOBILE", desc: "Mobile WebView" },
11719
- { id: "renderer", label: "RENDER", desc: "Server-side render" },
11720
- ];
11721
-
11722
- const ROW_L2_LOAD = [
11723
- { id: "file", label: "file://", desc: "Local filesystem" },
11724
- { id: "http", label: "http(s)://", desc: "Web server" },
11725
- { id: "data", label: "data:", desc: "Data URI" },
11726
- ];
11727
-
11728
- const ROW_L3_PROCESSING = [
11729
- { id: "script", label: "+JS", desc: "With JavaScript" },
11730
- { id: "noscript", label: "-JS", desc: "No JavaScript" },
11731
- ];
11732
-
11733
- const BROWSERS_LOCAL = [
11734
- { id: "chrome", code: "C", name: "Chrome", color: "#4285F4" },
11735
- { id: "firefox", code: "F", name: "Firefox", color: "#FF7139" },
11736
- { id: "safari", code: "S", name: "Safari", color: "#006CFF" },
11737
- { id: "edge", code: "E", name: "Edge", color: "#0078D7" },
11738
- { id: "ie11", code: "I", name: "IE11", color: "#1EBBEE" },
11739
- { id: "ios", code: "i", name: "iOS", color: "#999" },
11740
- { id: "android", code: "A", name: "Android", color: "#3DDC84" },
11741
- ];
11742
-
11743
- const FUNCS = [
11744
- { id: "display", code: "D", name: "Display", color: "#2196F3" },
11745
- { id: "animation", code: "A", name: "Animation", color: "#9C27B0" },
11746
- { id: "scripting", code: "J", name: "JavaScript", color: "#FF9800" },
11747
- { id: "events", code: "E", name: "Events", color: "#E91E63" },
11748
- { id: "media", code: "M", name: "Media", color: "#00BCD4" },
11749
- { id: "html", code: "H", name: "foreignObject", color: "#795548" },
11750
- { id: "images", code: "I", name: "Images", color: "#8BC34A" },
11751
- { id: "fonts", code: "T", name: "Fonts", color: "#607D8B" },
11752
- { id: "sprites", code: "U", name: "use/sprites", color: "#FF5722" },
11753
- { id: "links", code: "L", name: "Links", color: "#3F51B5" },
11754
- ];
11755
-
11756
- // Calculate dimensions
11757
- const rowHeaderWidth = 140;
11758
- const colHeaderHeight = 80;
11759
- const totalCols = COL_L1_SYNTAX.length * COL_L2_STORAGE.length;
11760
- const totalRows =
11761
- ROW_L1_DISPLAY.length * ROW_L2_LOAD.length * ROW_L3_PROCESSING.length;
11762
- const svgWidth = rowHeaderWidth + totalCols * cellWidth + 40;
11763
- const svgHeight = colHeaderHeight + totalRows * cellHeight + 200; // Extra for legend
11764
-
11765
- // Color helpers
11766
- const statusColors = {
11767
- ok: "#4CAF50",
11768
- warn: "#FF9800",
11769
- block: "#F44336",
11770
- };
11771
-
11772
- /**
11773
- * Evaluate a cell for the legacy SVG visualization (simplified version).
11774
- * @param {string} display - Display environment ID
11775
- * @param {string} load - Load protocol ID
11776
- * @param {string} storage - Storage type ID
11777
- * @param {string} syntax - Embedding syntax ID
11778
- * @param {string} processing - Processing context ID
11779
- * @returns {Object} Simplified cell evaluation result
11780
- */
11781
- const evaluateCell = (display, load, storage, syntax, processing) => {
11782
- const result = { browserStatus: {}, funcStatus: {}, overallStatus: "ok" };
11783
-
11784
- // Initialize
11785
- for (const b of BROWSERS_LOCAL) {
11786
- result.browserStatus[b.id] = {};
11787
- for (const f of FUNCS) result.browserStatus[b.id][f.id] = "ok";
11788
- }
11789
- for (const f of FUNCS) result.funcStatus[f.id] = "ok";
11790
-
11791
- // Apply rules
11792
- if (syntax === "img" || syntax === "css") {
11793
- ["animation", "scripting", "events", "html", "media", "links"].forEach(
11794
- (f) => {
11795
- result.funcStatus[f] = "block";
11796
- BROWSERS_LOCAL.forEach((b) => {
11797
- result.browserStatus[b.id][f] = "block";
11798
- });
11799
- },
11800
- );
11801
- }
11802
-
11803
- if (display === "renderer") {
11804
- ["animation", "scripting", "events", "media", "html"].forEach((f) => {
11805
- result.funcStatus[f] = "block";
11806
- BROWSERS_LOCAL.forEach((b) => {
11807
- result.browserStatus[b.id][f] = "block";
11808
- });
11809
- });
11810
- }
11811
-
11812
- if (processing === "noscript") {
11813
- result.funcStatus["scripting"] = "block";
11814
- result.funcStatus["events"] = "warn";
11815
- BROWSERS_LOCAL.forEach((b) => {
11816
- result.browserStatus[b.id]["scripting"] = "block";
11817
- result.browserStatus[b.id]["events"] = "warn";
11818
- });
11819
- }
11820
-
11821
- // IE11 specific
11822
- result.browserStatus["ie11"]["animation"] = "block";
11823
- result.browserStatus["ie11"]["html"] = "block";
11824
-
11825
- // Media autoplay
11826
- if (matrixData.detectedCapabilities?.some((c) => c.startsWith("AUDIO_"))) {
11827
- BROWSERS_LOCAL.forEach((b) => {
11828
- if (result.browserStatus[b.id]["media"] !== "block") {
11829
- result.browserStatus[b.id]["media"] = "warn";
11830
- }
11831
- });
11832
- result.browserStatus["ios"]["media"] = "block";
11833
- result.funcStatus["media"] = "warn";
11834
- }
11835
-
11836
- // Remote storage
11837
- if (storage === "remote") {
11838
- ["images", "fonts", "sprites"].forEach((f) => {
11839
- if (result.funcStatus[f] !== "block") result.funcStatus[f] = "warn";
11840
- });
11841
- }
11842
-
11843
- // Note: SMIL is fully supported in all modern browsers (except IE11)
11844
- // Google retracted the deprecation notice after community feedback
11845
-
11846
- // Calculate overall
11847
- let hasBlock = false,
11848
- hasWarn = false;
11849
- Object.values(result.funcStatus).forEach((s) => {
11850
- if (s === "block") hasBlock = true;
11851
- else if (s === "warn") hasWarn = true;
11852
- });
11853
- result.overallStatus = hasBlock ? "block" : hasWarn ? "warn" : "ok";
11854
-
11855
- return result;
11856
- };
11857
-
11858
- // Build SVG
11859
- let svg = `<?xml version="1.0" encoding="UTF-8"?>
11860
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
11861
- viewBox="0 0 ${svgWidth} ${svgHeight}" preserveAspectRatio="xMinYMin meet" height="${svgHeight}" width="100%">
11862
- <title>SVG Compatibility Matrix</title>
11863
- <desc>Browser and functionality compatibility for different SVG embedding methods</desc>
11864
-
11865
- <defs>
11866
- <style>
11867
- .title { font: bold 18px ${fontFamily}; fill: #333; }
11868
- .subtitle { font: 12px ${fontFamily}; fill: #666; }
11869
- .col-header { font: bold 11px ${fontFamily}; fill: #333; }
11870
- .col-subheader { font: 10px ${fontFamily}; fill: #555; }
11871
- .row-header { font: bold 10px ${fontFamily}; fill: #333; }
11872
- .row-subheader { font: 9px ${fontFamily}; fill: #555; }
11873
- .cell-text { font: 8px ${fontFamily}; fill: #333; }
11874
- .cell-ok { fill: #E8F5E9; }
11875
- .cell-warn { fill: #FFF3E0; }
11876
- .cell-block { fill: #FFEBEE; }
11877
- .legend-text { font: 10px ${fontFamily}; fill: #333; }
11878
- .grid-line { stroke: #DDD; stroke-width: 1; }
11879
- .grid-line-bold { stroke: #999; stroke-width: 2; }
11880
- ${
11881
- interactive
11882
- ? `
11883
- .cell-group:hover rect { stroke: #333; stroke-width: 2; }
11884
- .tooltip { pointer-events: none; opacity: 0; transition: opacity 0.2s; }
11885
- .cell-group:hover .tooltip { opacity: 1; }
11886
- `
11887
- : ""
11888
- }
11889
- </style>
11890
- </defs>
11891
-
11892
- <!-- Title -->
11893
- <text x="${svgWidth / 2}" y="25" text-anchor="middle" class="title">SVG Compatibility Matrix</text>
11894
- <text x="${svgWidth / 2}" y="42" text-anchor="middle" class="subtitle">
11895
- ${totalCols} columns × ${totalRows} rows = ${totalCols * totalRows} combinations
11896
- </text>
11897
-
11898
- <g transform="translate(20, 55)">
11899
- `;
11900
-
11901
- // Column headers - Level 1 (SYNTAX)
11902
- let colX = rowHeaderWidth;
11903
- for (const syntax of COL_L1_SYNTAX) {
11904
- const groupWidth = COL_L2_STORAGE.length * cellWidth;
11905
- svg += ` <rect x="${colX}" y="0" width="${groupWidth}" height="25" fill="#E3F2FD" stroke="#90CAF9"/>
11906
- <text x="${colX + groupWidth / 2}" y="17" text-anchor="middle" class="col-header">${syntax.label}</text>
11907
- `;
11908
- // Level 2 (STORAGE)
11909
- for (let i = 0; i < COL_L2_STORAGE.length; i++) {
11910
- const storage = COL_L2_STORAGE[i];
11911
- const x = colX + i * cellWidth;
11912
- svg += ` <rect x="${x}" y="25" width="${cellWidth}" height="20" fill="#F5F5F5" stroke="#DDD"/>
11913
- <text x="${x + cellWidth / 2}" y="39" text-anchor="middle" class="col-subheader">${storage.label}</text>
11914
- `;
11915
- }
11916
- colX += groupWidth;
11917
- }
11918
-
11919
- // Row headers and cells
11920
- let rowY = colHeaderHeight - 35;
11921
- for (const displayEnv of ROW_L1_DISPLAY) {
11922
- // Display header
11923
- const displayRows = ROW_L2_LOAD.length * ROW_L3_PROCESSING.length;
11924
- const displayHeight = displayRows * cellHeight;
11925
- svg += ` <rect x="0" y="${rowY}" width="50" height="${displayHeight}" fill="#E8EAF6" stroke="#9FA8DA"/>
11926
- <text x="25" y="${rowY + displayHeight / 2}" text-anchor="middle" dominant-baseline="middle" class="row-header" transform="rotate(-90, 25, ${rowY + displayHeight / 2})">${displayEnv.label}</text>
11927
- `;
11928
-
11929
- for (const loadProto of ROW_L2_LOAD) {
11930
- for (const procCtx of ROW_L3_PROCESSING) {
11931
- // Row label
11932
- svg += ` <rect x="50" y="${rowY}" width="90" height="${cellHeight}" fill="#FAFAFA" stroke="#EEE"/>
11933
- <text x="55" y="${rowY + cellHeight / 2 - 6}" class="row-subheader">${loadProto.label}</text>
11934
- <text x="55" y="${rowY + cellHeight / 2 + 8}" class="row-subheader">${procCtx.label}</text>
11935
- `;
11936
-
11937
- // Cells
11938
- colX = rowHeaderWidth;
11939
- for (const syntax of COL_L1_SYNTAX) {
11940
- for (const storage of COL_L2_STORAGE) {
11941
- const cellResult = evaluateCell(
11942
- displayEnv.id,
11943
- loadProto.id,
11944
- storage.id,
11945
- syntax.id,
11946
- procCtx.id,
11947
- );
11948
- const cellClass = `cell-${cellResult.overallStatus}`;
11949
-
11950
- // Build browser status string
11951
- let browserStr = "";
11952
- for (const b of BROWSERS_LOCAL) {
11953
- let bStatus = "ok";
11954
- for (const f of FUNCS) {
11955
- const s = cellResult.browserStatus[b.id][f.id];
11956
- if (s === "block") {
11957
- bStatus = "block";
11958
- break;
11959
- } else if (s === "warn" && bStatus !== "block")
11960
- bStatus = "warn";
11961
- }
11962
- const symbol =
11963
- bStatus === "ok" ? "✓" : bStatus === "warn" ? "⚠" : "✗";
11964
- browserStr += `${b.code}${symbol}`;
11965
- }
11966
-
11967
- // Build func status string
11968
- let funcStr = "";
11969
- for (const f of FUNCS) {
11970
- const s = cellResult.funcStatus[f.id];
11971
- if (s !== "ok") {
11972
- funcStr += `${f.code}${s === "warn" ? "⚠" : "✗"}`;
11973
- }
11974
- }
11975
- if (!funcStr) funcStr = "OK";
11976
-
11977
- svg += ` <g class="cell-group">
11978
- <rect x="${colX}" y="${rowY}" width="${cellWidth}" height="${cellHeight}" class="${cellClass}" stroke="#DDD"/>
11979
- <text x="${colX + cellWidth / 2}" y="${rowY + 15}" text-anchor="middle" class="cell-text">${browserStr.substring(0, 10)}</text>
11980
- <text x="${colX + cellWidth / 2}" y="${rowY + 28}" text-anchor="middle" class="cell-text">${browserStr.substring(10)}</text>
11981
- <text x="${colX + cellWidth / 2}" y="${rowY + 40}" text-anchor="middle" class="cell-text" fill="${statusColors[cellResult.overallStatus]}">${funcStr.substring(0, 8)}</text>
11982
- `;
11983
- if (interactive) {
11984
- // Tooltip
11985
- svg += ` <g class="tooltip" transform="translate(${colX + cellWidth / 2}, ${rowY - 5})">
11986
- <rect x="-60" y="-45" width="120" height="40" fill="#333" rx="4"/>
11987
- <text x="0" y="-30" text-anchor="middle" fill="#FFF" font-size="9">${syntax.label}/${storage.label}</text>
11988
- <text x="0" y="-17" text-anchor="middle" fill="#FFF" font-size="8">${displayEnv.label} ${loadProto.label} ${procCtx.label}</text>
11989
- </g>
11990
- `;
11991
- }
11992
- svg += ` </g>
11993
- `;
11994
- colX += cellWidth;
11995
- }
11996
- }
11997
- rowY += cellHeight;
11998
- }
11999
- }
12000
- }
12001
-
12002
- // Legend
12003
- const legendY = colHeaderHeight + totalRows * cellHeight + 20;
12004
- svg += `
12005
- <!-- Legend -->
12006
- <g transform="translate(0, ${legendY})">
12007
- <text x="10" y="15" class="col-header">LEGEND</text>
12008
-
12009
- <!-- Status colors -->
12010
- <rect x="10" y="25" width="20" height="15" class="cell-ok" stroke="#4CAF50"/>
12011
- <text x="35" y="37" class="legend-text">OK - All features work</text>
12012
-
12013
- <rect x="160" y="25" width="20" height="15" class="cell-warn" stroke="#FF9800"/>
12014
- <text x="185" y="37" class="legend-text">Warning - Partial support</text>
12015
-
12016
- <rect x="340" y="25" width="20" height="15" class="cell-block" stroke="#F44336"/>
12017
- <text x="365" y="37" class="legend-text">Blocked - Not supported</text>
12018
-
12019
- <!-- Browser codes -->
12020
- <text x="10" y="60" class="col-header">BROWSERS:</text>
12021
- `;
12022
-
12023
- let legendX = 90;
12024
- for (const b of BROWSERS_LOCAL) {
12025
- svg += ` <text x="${legendX}" y="60" class="legend-text"><tspan fill="${b.color}" font-weight="bold">${b.code}</tspan>=${b.name}</text>
12026
- `;
12027
- legendX += 85;
12028
- }
12029
-
12030
- svg += `
12031
- <!-- Functionality codes -->
12032
- <text x="10" y="80" class="col-header">FUNCTIONS:</text>
12033
- `;
12034
-
12035
- legendX = 90;
12036
- let legendRow = 0;
12037
- for (const f of FUNCS) {
12038
- if (legendX > 600) {
12039
- legendX = 90;
12040
- legendRow++;
12041
- }
12042
- svg += ` <text x="${legendX}" y="${80 + legendRow * 18}" class="legend-text"><tspan fill="${f.color}" font-weight="bold">${f.code}</tspan>=${f.name}</text>
12043
- `;
12044
- legendX += 100;
12045
- }
12046
-
12047
- svg += ` </g>
12048
- </g>
12049
- </svg>`;
12050
-
12051
- return svg;
12052
- }
12053
-
12054
11692
  /**
12055
11693
  * Generate a detailed, hierarchical SVG visualization of the compatibility matrix.
12056
11694
  * Features:
@@ -19515,7 +19153,8 @@ export const embedExternalDependencies = createOperation(
19515
19153
  // BUG FIX: Verify svg element exists before manipulating
19516
19154
  if (!svg) {
19517
19155
  warnings.push("Cannot add defs: document has no root SVG element");
19518
- if (onProgress) onProgress("externalSVGs", useArray.length, useArray.length);
19156
+ if (onProgress)
19157
+ onProgress("externalSVGs", useArray.length, useArray.length);
19519
19158
  } else {
19520
19159
  defs = new SVGElement("defs", {}, []);
19521
19160
  // Insert defs as first child - handle null firstChild properly
@@ -19532,104 +19171,105 @@ export const embedExternalDependencies = createOperation(
19532
19171
 
19533
19172
  if (defs) {
19534
19173
  for (let i = 0; i < useArray.length; i++) {
19535
- const useEl = useArray[i];
19536
- const href =
19537
- useEl.getAttribute("href") || useEl.getAttribute("xlink:href");
19538
-
19539
- if (!href || !isExternalHref(href)) continue;
19174
+ const useEl = useArray[i];
19175
+ const href =
19176
+ useEl.getAttribute("href") || useEl.getAttribute("xlink:href");
19540
19177
 
19541
- const { path, fragment } = parseExternalSVGRef(href);
19542
- if (!path) continue;
19178
+ if (!href || !isExternalHref(href)) continue;
19543
19179
 
19544
- // Check circular reference
19545
- const resolvedPath = resolveURL(path, basePath);
19546
- if (visitedURLs.has(resolvedPath)) {
19547
- warnings.push(`Circular reference detected: ${resolvedPath}`);
19548
- continue;
19549
- }
19550
- visitedURLs.add(resolvedPath);
19551
-
19552
- try {
19553
- const { content } = await fetchResource(
19554
- resolvedPath,
19555
- "text",
19556
- timeout,
19557
- );
19558
- const externalDoc = parseSVG(content);
19180
+ const { path, fragment } = parseExternalSVGRef(href);
19181
+ if (!path) continue;
19559
19182
 
19560
- if (!externalDoc) {
19561
- handleMissingResource(path, new Error("Failed to parse SVG"));
19183
+ // Check circular reference
19184
+ const resolvedPath = resolveURL(path, basePath);
19185
+ if (visitedURLs.has(resolvedPath)) {
19186
+ warnings.push(`Circular reference detected: ${resolvedPath}`);
19562
19187
  continue;
19563
19188
  }
19189
+ visitedURLs.add(resolvedPath);
19564
19190
 
19565
- // Extract vendor namespaces from external SVG for later preservation
19566
- const externalSvgRoot = externalDoc.documentElement || externalDoc;
19567
- extractVendorNamespaces(externalSvgRoot);
19568
-
19569
- // Recursively process external SVG if enabled
19570
- if (recursive && maxRecursionDepth > 0) {
19571
- await embedExternalDependencies(externalDoc, {
19572
- ...options,
19573
- basePath: resolvedPath,
19574
- maxRecursionDepth: maxRecursionDepth - 1,
19575
- // BUG FIX: Pass visitedURLs to detect cross-document circular references (A → B → A)
19576
- _visitedURLs: visitedURLs,
19577
- });
19578
- }
19191
+ try {
19192
+ const { content } = await fetchResource(
19193
+ resolvedPath,
19194
+ "text",
19195
+ timeout,
19196
+ );
19197
+ const externalDoc = parseSVG(content);
19579
19198
 
19580
- extCounter++;
19581
- const uniquePrefix = `${idPrefix}${extCounter}_`;
19199
+ if (!externalDoc) {
19200
+ handleMissingResource(path, new Error("Failed to parse SVG"));
19201
+ continue;
19202
+ }
19582
19203
 
19583
- if (externalSVGMode === "extract" && fragment) {
19584
- // Extract just the referenced element
19585
- const targetEl = externalDoc.getElementById(fragment);
19586
- if (targetEl) {
19587
- // Clone and relocate IDs
19588
- const cloned = targetEl.clone
19589
- ? targetEl.clone()
19590
- : JSON.parse(JSON.stringify(targetEl));
19591
- relocateIds(cloned, uniquePrefix);
19204
+ // Extract vendor namespaces from external SVG for later preservation
19205
+ const externalSvgRoot = externalDoc.documentElement || externalDoc;
19206
+ extractVendorNamespaces(externalSvgRoot);
19207
+
19208
+ // Recursively process external SVG if enabled
19209
+ if (recursive && maxRecursionDepth > 0) {
19210
+ await embedExternalDependencies(externalDoc, {
19211
+ ...options,
19212
+ basePath: resolvedPath,
19213
+ maxRecursionDepth: maxRecursionDepth - 1,
19214
+ // BUG FIX: Pass visitedURLs to detect cross-document circular references (A → B → A)
19215
+ _visitedURLs: visitedURLs,
19216
+ });
19217
+ }
19218
+
19219
+ extCounter++;
19220
+ const uniquePrefix = `${idPrefix}${extCounter}_`;
19221
+
19222
+ if (externalSVGMode === "extract" && fragment) {
19223
+ // Extract just the referenced element
19224
+ const targetEl = externalDoc.getElementById(fragment);
19225
+ if (targetEl) {
19226
+ // Clone and relocate IDs
19227
+ const cloned = targetEl.clone
19228
+ ? targetEl.clone()
19229
+ : JSON.parse(JSON.stringify(targetEl));
19230
+ relocateIds(cloned, uniquePrefix);
19231
+
19232
+ // Add to defs
19233
+ defs.children.push(cloned);
19234
+
19235
+ // Update use href to local reference
19236
+ const newId = uniquePrefix + fragment;
19237
+ if (useEl.getAttribute("href"))
19238
+ useEl.setAttribute("href", "#" + newId);
19239
+ if (useEl.getAttribute("xlink:href"))
19240
+ useEl.setAttribute("xlink:href", "#" + newId);
19241
+ } else {
19242
+ handleMissingResource(
19243
+ href,
19244
+ new Error(`Fragment #${fragment} not found in ${path}`),
19245
+ );
19246
+ }
19247
+ } else {
19248
+ // Embed entire external SVG
19249
+ const externalSvg = externalDoc.documentElement || externalDoc;
19250
+ relocateIds(externalSvg, uniquePrefix);
19251
+
19252
+ // Set an ID on the external SVG
19253
+ const svgId = uniquePrefix + "svg";
19254
+ externalSvg.setAttribute("id", svgId);
19592
19255
 
19593
19256
  // Add to defs
19594
- defs.children.push(cloned);
19257
+ defs.children.push(externalSvg);
19595
19258
 
19596
- // Update use href to local reference
19597
- const newId = uniquePrefix + fragment;
19259
+ // Update use href
19260
+ const newHref = fragment
19261
+ ? "#" + uniquePrefix + fragment
19262
+ : "#" + svgId;
19598
19263
  if (useEl.getAttribute("href"))
19599
- useEl.setAttribute("href", "#" + newId);
19264
+ useEl.setAttribute("href", newHref);
19600
19265
  if (useEl.getAttribute("xlink:href"))
19601
- useEl.setAttribute("xlink:href", "#" + newId);
19602
- } else {
19603
- handleMissingResource(
19604
- href,
19605
- new Error(`Fragment #${fragment} not found in ${path}`),
19606
- );
19266
+ useEl.setAttribute("xlink:href", newHref);
19607
19267
  }
19608
- } else {
19609
- // Embed entire external SVG
19610
- const externalSvg = externalDoc.documentElement || externalDoc;
19611
- relocateIds(externalSvg, uniquePrefix);
19612
-
19613
- // Set an ID on the external SVG
19614
- const svgId = uniquePrefix + "svg";
19615
- externalSvg.setAttribute("id", svgId);
19616
-
19617
- // Add to defs
19618
- defs.children.push(externalSvg);
19619
-
19620
- // Update use href
19621
- const newHref = fragment
19622
- ? "#" + uniquePrefix + fragment
19623
- : "#" + svgId;
19624
- if (useEl.getAttribute("href")) useEl.setAttribute("href", newHref);
19625
- if (useEl.getAttribute("xlink:href"))
19626
- useEl.setAttribute("xlink:href", newHref);
19268
+ } catch (e) {
19269
+ handleMissingResource(path, e);
19627
19270
  }
19628
- } catch (e) {
19629
- handleMissingResource(path, e);
19630
- }
19631
19271
 
19632
- if (onProgress) onProgress("externalSVGs", i + 1, useArray.length);
19272
+ if (onProgress) onProgress("externalSVGs", i + 1, useArray.length);
19633
19273
  }
19634
19274
  }
19635
19275
  }
@@ -20062,11 +19702,13 @@ function decodeDataUri(parsed) {
20062
19702
  return Buffer.from(parsed.data, "base64");
20063
19703
  }
20064
19704
  // URL-encoded or plain text
20065
- // BUG FIX: Handle malformed percent-encoded sequences gracefully
19705
+ // LEGITIMATE FALLBACK: Handle malformed percent-encoded sequences.
19706
+ // Real-world data URIs may have invalid percent-encoding but the raw data is still usable.
19707
+ // Returning as-is allows the caller to attempt to use the data rather than crashing.
20066
19708
  try {
20067
19709
  return decodeURIComponent(parsed.data);
20068
19710
  } catch (e) {
20069
- // If decodeURIComponent fails (e.g., malformed %XX), return data as-is - log error for debugging
19711
+ // Malformed %XX sequences - return raw data which may still be valid for the use case
20070
19712
  if (process.env.DEBUG) console.warn(`[svg-toolbox] ${e.message}`);
20071
19713
  return parsed.data;
20072
19714
  }