@emasoft/svg-matrix 1.0.31 → 1.0.33

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 (46) hide show
  1. package/README.md +3 -3
  2. package/bin/svg-matrix.js +38 -22
  3. package/bin/svglinter.cjs +41 -12
  4. package/bin/svgm.js +133 -44
  5. package/package.json +1 -1
  6. package/src/animation-optimization.js +3 -3
  7. package/src/animation-references.js +201 -8
  8. package/src/arc-length.js +45 -4
  9. package/src/bezier-analysis.js +26 -9
  10. package/src/bezier-intersections.js +84 -13
  11. package/src/browser-verify.js +89 -33
  12. package/src/clip-path-resolver.js +91 -20
  13. package/src/convert-path-data.js +1 -1
  14. package/src/css-specificity.js +281 -66
  15. package/src/douglas-peucker.js +71 -1
  16. package/src/flatten-pipeline.js +18 -14
  17. package/src/geometry-to-path.js +23 -2
  18. package/src/gjk-collision.js +81 -19
  19. package/src/index.js +32 -3
  20. package/src/inkscape-support.js +63 -19
  21. package/src/logger.js +85 -57
  22. package/src/marker-resolver.js +53 -15
  23. package/src/mask-resolver.js +32 -18
  24. package/src/matrix.js +10 -8
  25. package/src/mesh-gradient.js +84 -33
  26. package/src/off-canvas-detection.js +138 -49
  27. package/src/path-analysis.js +4 -4
  28. package/src/path-data-plugins.js +45 -2
  29. package/src/path-optimization.js +36 -6
  30. package/src/path-simplification.js +87 -23
  31. package/src/pattern-resolver.js +26 -4
  32. package/src/polygon-clip.js +39 -14
  33. package/src/svg-boolean-ops.js +76 -14
  34. package/src/svg-collections.js +13 -1
  35. package/src/svg-flatten.js +101 -62
  36. package/src/svg-parser.js +44 -4
  37. package/src/svg-rendering-context.js +6 -6
  38. package/src/svg-toolbox.js +63 -27
  39. package/src/svg-validation-data.js +12 -10
  40. package/src/svg2-polyfills.js +49 -20
  41. package/src/transform-decomposition.js +48 -16
  42. package/src/transform-optimization.js +23 -12
  43. package/src/transforms2d.js +31 -8
  44. package/src/transforms3d.js +76 -22
  45. package/src/use-symbol-resolver.js +60 -24
  46. package/src/vector.js +1 -1
package/README.md CHANGED
@@ -500,13 +500,13 @@ SVGFlatten.resolveLength('1in', 96); // 96
500
500
  Find and fix problems:
501
501
 
502
502
  ```js
503
- import { validateSvg, fixInvalidSvg } from '@emasoft/svg-matrix';
503
+ import { validateSVG, fixInvalidSVG } from '@emasoft/svg-matrix';
504
504
 
505
- const result = await validateSvg('icon.svg');
505
+ const result = await validateSVG('icon.svg');
506
506
  console.log(result.valid); // true/false
507
507
  console.log(result.issues); // Array of problems
508
508
 
509
- const fixed = await fixInvalidSvg('broken.svg');
509
+ const fixed = await fixInvalidSVG('broken.svg');
510
510
  console.log(fixed.svg); // Fixed SVG string
511
511
  ```
512
512
 
package/bin/svg-matrix.js CHANGED
@@ -2043,32 +2043,37 @@ function parseArgs(args) {
2043
2043
 
2044
2044
  switch (arg) {
2045
2045
  case "-o":
2046
- case "--output":
2047
- if (i + 1 >= args.length) {
2048
- logError("-o/--output requires a value");
2046
+ case "--output": {
2047
+ const value = argValue || (i + 1 < args.length ? args[++i] : null);
2048
+ if (!value || !value.trim()) {
2049
+ logError("-o/--output requires a non-empty value");
2049
2050
  process.exit(CONSTANTS.EXIT_ERROR);
2050
2051
  }
2051
- cfg.output = args[++i];
2052
+ cfg.output = value.trim();
2052
2053
  break;
2054
+ }
2053
2055
  case "-l":
2054
- case "--list":
2055
- if (i + 1 >= args.length) {
2056
- logError("-l/--list requires a value");
2056
+ case "--list": {
2057
+ const value = argValue || (i + 1 < args.length ? args[++i] : null);
2058
+ if (!value || !value.trim()) {
2059
+ logError("-l/--list requires a non-empty value");
2057
2060
  process.exit(CONSTANTS.EXIT_ERROR);
2058
2061
  }
2059
- cfg.listFile = args[++i];
2062
+ cfg.listFile = value.trim();
2060
2063
  break;
2064
+ }
2061
2065
  case "-r":
2062
2066
  case "--recursive":
2063
2067
  cfg.recursive = true;
2064
2068
  break;
2065
2069
  case "-p":
2066
2070
  case "--precision": {
2067
- if (i + 1 >= args.length) {
2071
+ const value = argValue || (i + 1 < args.length ? args[++i] : null);
2072
+ if (!value) {
2068
2073
  logError("-p/--precision requires a value");
2069
2074
  process.exit(CONSTANTS.EXIT_ERROR);
2070
2075
  }
2071
- const precision = parseInt(args[++i], 10);
2076
+ const precision = parseInt(value, 10);
2072
2077
  if (
2073
2078
  isNaN(precision) ||
2074
2079
  precision < CONSTANTS.MIN_PRECISION ||
@@ -2098,13 +2103,15 @@ function parseArgs(args) {
2098
2103
  case "--verbose":
2099
2104
  cfg.verbose = true;
2100
2105
  break;
2101
- case "--log-file":
2102
- if (i + 1 >= args.length) {
2103
- logError("--log-file requires a value");
2106
+ case "--log-file": {
2107
+ const value = argValue || (i + 1 < args.length ? args[++i] : null);
2108
+ if (!value || !value.trim()) {
2109
+ logError("--log-file requires a non-empty value");
2104
2110
  process.exit(CONSTANTS.EXIT_ERROR);
2105
2111
  }
2106
- cfg.logFile = args[++i];
2112
+ cfg.logFile = value.trim();
2107
2113
  break;
2114
+ }
2108
2115
  case "-h":
2109
2116
  case "--help":
2110
2117
  // If a command is already set (not 'help'), show command-specific help
@@ -2185,11 +2192,12 @@ function parseArgs(args) {
2185
2192
  break;
2186
2193
  // E2E verification precision options
2187
2194
  case "--clip-segments": {
2188
- if (i + 1 >= args.length) {
2195
+ const value = argValue || (i + 1 < args.length ? args[++i] : null);
2196
+ if (!value) {
2189
2197
  logError("--clip-segments requires a value");
2190
2198
  process.exit(CONSTANTS.EXIT_ERROR);
2191
2199
  }
2192
- const segs = parseInt(args[++i], 10);
2200
+ const segs = parseInt(value, 10);
2193
2201
  if (isNaN(segs) || segs < 8 || segs > 512) {
2194
2202
  logError("clip-segments must be between 8 and 512");
2195
2203
  process.exit(CONSTANTS.EXIT_ERROR);
@@ -2198,11 +2206,12 @@ function parseArgs(args) {
2198
2206
  break;
2199
2207
  }
2200
2208
  case "--bezier-arcs": {
2201
- if (i + 1 >= args.length) {
2209
+ const value = argValue || (i + 1 < args.length ? args[++i] : null);
2210
+ if (!value) {
2202
2211
  logError("--bezier-arcs requires a value");
2203
2212
  process.exit(CONSTANTS.EXIT_ERROR);
2204
2213
  }
2205
- const arcs = parseInt(args[++i], 10);
2214
+ const arcs = parseInt(value, 10);
2206
2215
  if (isNaN(arcs) || arcs < 4 || arcs > 128) {
2207
2216
  logError("bezier-arcs must be between 4 and 128");
2208
2217
  process.exit(CONSTANTS.EXIT_ERROR);
@@ -2216,16 +2225,16 @@ function parseArgs(args) {
2216
2225
  break;
2217
2226
  }
2218
2227
  case "--e2e-tolerance": {
2219
- if (i + 1 >= args.length) {
2228
+ const value = argValue || (i + 1 < args.length ? args[++i] : null);
2229
+ if (!value) {
2220
2230
  logError("--e2e-tolerance requires a value");
2221
2231
  process.exit(CONSTANTS.EXIT_ERROR);
2222
2232
  }
2223
- const tol = args[++i];
2224
- if (!/^1e-\d+$/.test(tol)) {
2233
+ if (!/^1e-\d+$/.test(value)) {
2225
2234
  logError("e2e-tolerance must be in format 1e-N (e.g., 1e-10, 1e-12)");
2226
2235
  process.exit(CONSTANTS.EXIT_ERROR);
2227
2236
  }
2228
- cfg.e2eTolerance = tol;
2237
+ cfg.e2eTolerance = value;
2229
2238
  break;
2230
2239
  }
2231
2240
  // NOTE: --verify removed - verification is ALWAYS enabled
@@ -2254,6 +2263,13 @@ function parseArgs(args) {
2254
2263
  i++;
2255
2264
  }
2256
2265
  cfg.inputs = inputs;
2266
+
2267
+ // Validate conflicting options
2268
+ if (cfg.quiet && cfg.verbose) {
2269
+ logError("Cannot specify both --quiet and --verbose");
2270
+ process.exit(CONSTANTS.EXIT_ERROR);
2271
+ }
2272
+
2257
2273
  return cfg;
2258
2274
  }
2259
2275
 
package/bin/svglinter.cjs CHANGED
@@ -1192,6 +1192,12 @@ function loadConfig(configPath) {
1192
1192
  try {
1193
1193
  const files = fs.readdirSync(cwd).filter((f) => f.endsWith(pattern));
1194
1194
  if (files.length === 0) continue;
1195
+ // BUG FIX: Warn if multiple files match the pattern
1196
+ if (files.length > 1) {
1197
+ verbose(
1198
+ `Multiple files match pattern '${file}': ${files.join(", ")} - using first: ${files[0]}`,
1199
+ );
1200
+ }
1195
1201
  filepath = path.join(cwd, files[0]);
1196
1202
  } catch (err) {
1197
1203
  verbose(
@@ -2929,9 +2935,11 @@ async function globPromise(pattern) {
2929
2935
  const filePattern = path.basename(pattern);
2930
2936
  // Escape special regex chars, then convert glob * to [^/\\]* instead of .* to prevent
2931
2937
  // catastrophic backtracking and avoid matching across path separators
2938
+ // BUG FIX: Use path.sep instead of hardcoded separators for cross-platform compatibility
2939
+ const pathSepPattern = path.sep === "\\" ? "\\\\" : "/";
2932
2940
  const escapedPattern = escapeRegexCharsForGlob(filePattern).replace(
2933
2941
  /\*/g,
2934
- "[^/\\\\]*",
2942
+ `[^${pathSepPattern}]*`,
2935
2943
  );
2936
2944
  const regex = new RegExp("^" + escapedPattern + "$");
2937
2945
 
@@ -3152,7 +3160,7 @@ function parseInlineDisables(content) {
3152
3160
  if (currentDisabled !== null) {
3153
3161
  disabledRanges.push({
3154
3162
  startLine: currentDisabled,
3155
- endLine: lines.length + 1,
3163
+ endLine: lines.length, // BUG FIX: Use lines.length not lines.length + 1 to avoid off-by-one
3156
3164
  rules: disabledRules,
3157
3165
  });
3158
3166
  }
@@ -3615,6 +3623,17 @@ function truncateLineAroundColumn(line, column, maxWidth, _isErrorLine) {
3615
3623
  const safeColumn = Number.isFinite(column) && column >= 1 ? column : 1;
3616
3624
  const col = safeColumn - 1; // Convert to 0-based
3617
3625
 
3626
+ // BUG FIX: Handle column beyond line length edge case
3627
+ if (col >= expandedLine.length) {
3628
+ // Column points beyond the line - clamp to end of line
3629
+ const clampedCol = Math.max(0, expandedLine.length - 1);
3630
+ return {
3631
+ text: expandedLine.substring(0, maxWidth),
3632
+ visibleColumn: clampedCol,
3633
+ errorLength: 1,
3634
+ };
3635
+ }
3636
+
3618
3637
  if (expandedLine.length <= maxWidth) {
3619
3638
  // No truncation needed
3620
3639
  return {
@@ -4103,13 +4122,16 @@ async function main() {
4103
4122
  verbose(`Color mode: ${useColors ? "enabled" : "disabled"}`);
4104
4123
  }
4105
4124
 
4106
- // Validate output format
4107
- if (!VALID_FORMATS.has(args.format)) {
4125
+ // Validate output format (case-insensitive for better UX)
4126
+ // BUG FIX: Normalize format to lowercase for case-insensitive matching
4127
+ const normalizedFormat = args.format.toLowerCase();
4128
+ if (!VALID_FORMATS.has(normalizedFormat)) {
4108
4129
  const validList = Array.from(VALID_FORMATS).join(", ");
4109
4130
  console.error(c("red", `Error: Invalid format '${args.format}'`));
4110
- console.error(`Valid formats: ${validList}`);
4131
+ console.error(`Valid formats: ${validList} (case-insensitive)`);
4111
4132
  process.exit(2);
4112
4133
  }
4134
+ args.format = normalizedFormat; // Use normalized format
4113
4135
  verbose(`Output format: ${args.format}`);
4114
4136
 
4115
4137
  // Validate maxLineWidth bounds
@@ -4573,13 +4595,20 @@ async function main() {
4573
4595
  // Validate output file path for safety
4574
4596
  const outputPath = path.resolve(args.outputFile);
4575
4597
  const normalizedOutput = path.normalize(outputPath);
4576
- const dangerousPaths = [
4577
- "/dev/",
4578
- "/proc/",
4579
- "/sys/",
4580
- "/etc/passwd",
4581
- "/etc/shadow",
4582
- ];
4598
+ // BUG FIX: Only apply Unix dangerous paths on Unix systems (not Windows with drive letters)
4599
+ const isWindows = process.platform === "win32";
4600
+ const dangerousPaths = isWindows
4601
+ ? [
4602
+ // Windows-specific dangerous paths (don't block C:\dev\ which is a common dev folder)
4603
+ ]
4604
+ : [
4605
+ // Unix-specific dangerous paths
4606
+ "/dev/",
4607
+ "/proc/",
4608
+ "/sys/",
4609
+ "/etc/passwd",
4610
+ "/etc/shadow",
4611
+ ];
4583
4612
  // BUG FIX: Use normalized paths and proper segment matching to avoid false positives
4584
4613
  // (e.g., /developer/ should NOT match /dev/)
4585
4614
  const isDangerous = dangerousPaths.some((p) => {
package/bin/svgm.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  mkdirSync,
21
21
  readdirSync,
22
22
  statSync,
23
+ realpathSync,
23
24
  } from "fs";
24
25
  import { join, dirname, basename, extname, resolve, isAbsolute } from "path";
25
26
  import yaml from "js-yaml";
@@ -383,26 +384,48 @@ function getSvgFiles(dir, recursive = false, exclude = []) {
383
384
  throw new TypeError(`getSvgFiles: expected array exclude, got ${typeof exclude}`);
384
385
  }
385
386
 
387
+ // Why: Validate and compile regex patterns once before scanning to fail fast
388
+ const excludeRegexes = [];
389
+ for (const pattern of exclude) {
390
+ try {
391
+ excludeRegexes.push(new RegExp(pattern));
392
+ } catch (e) {
393
+ logError(`Invalid exclusion pattern "${pattern}": ${e.message}`);
394
+ process.exit(CONSTANTS.EXIT_ERROR);
395
+ }
396
+ }
397
+
386
398
  const files = [];
399
+ // Why: Track visited directories to prevent infinite loops from symlinks
400
+ const visitedDirs = new Set();
401
+
387
402
  function scan(d) {
403
+ // Why: Resolve real path to detect symlink loops
404
+ let realPath;
405
+ try {
406
+ realPath = normalizePath(realpathSync(d));
407
+ } catch (e) {
408
+ // Why: Skip directories that can't be resolved (broken symlinks, permission errors)
409
+ logError(`Cannot access directory "${d}": ${e.message}`);
410
+ return;
411
+ }
412
+ if (visitedDirs.has(realPath)) {
413
+ return; // Skip already visited directories (symlink loop detected)
414
+ }
415
+ visitedDirs.add(realPath);
416
+
388
417
  for (const entry of readdirSync(d, { withFileTypes: true })) {
389
418
  const fullPath = join(d, entry.name);
390
- // Check exclusion patterns
391
- const shouldExclude = exclude.some((pattern) => {
392
- try {
393
- // Why: Wrap RegExp constructor in try-catch to handle invalid patterns
394
- const regex = new RegExp(pattern);
395
- return regex.test(fullPath) || regex.test(entry.name);
396
- } catch (_e) {
397
- // Why: Catch invalid regex patterns (unused error marked with underscore per ESLint)
398
- logError(`Invalid exclusion pattern: ${pattern}`);
399
- return false;
400
- }
401
- });
419
+
420
+ // Check exclusion patterns using pre-compiled regexes
421
+ const shouldExclude = excludeRegexes.some((regex) =>
422
+ regex.test(fullPath) || regex.test(entry.name)
423
+ );
402
424
  if (shouldExclude) continue;
403
425
 
404
- if (entry.isDirectory() && recursive) scan(fullPath);
405
- else if (
426
+ if (entry.isDirectory() && recursive) {
427
+ scan(fullPath);
428
+ } else if (
406
429
  entry.isFile() &&
407
430
  CONSTANTS.SVG_EXTENSIONS.includes(extname(entry.name).toLowerCase())
408
431
  ) {
@@ -411,7 +434,9 @@ function getSvgFiles(dir, recursive = false, exclude = []) {
411
434
  }
412
435
  }
413
436
  scan(dir);
414
- return files;
437
+
438
+ // Why: Deduplicate file paths in case same file reached via different routes
439
+ return [...new Set(files)];
415
440
  }
416
441
 
417
442
  // ============================================================================
@@ -681,7 +706,11 @@ async function processFile(inputPath, outputPath, options) {
681
706
  if (outputPath === "-") {
682
707
  process.stdout.write(output);
683
708
  } else {
684
- ensureDir(dirname(outputPath));
709
+ // Why: Validate output path and ensure parent directory exists
710
+ const outputDir = dirname(outputPath);
711
+ if (outputDir && outputDir !== ".") {
712
+ ensureDir(outputDir);
713
+ }
685
714
  writeFileSync(outputPath, output, "utf8");
686
715
  }
687
716
 
@@ -932,10 +961,16 @@ function parseArgs(args) {
932
961
  let argValue = null;
933
962
 
934
963
  // Handle --arg=value format
964
+ // Why: Validate that value after = is not empty to prevent silent failures
935
965
  if (arg.includes("=") && arg.startsWith("--")) {
936
966
  const eqIdx = arg.indexOf("=");
937
967
  argValue = arg.substring(eqIdx + 1);
938
968
  arg = arg.substring(0, eqIdx);
969
+ // Why: Empty value after = is invalid (e.g., --precision=)
970
+ if (argValue === "") {
971
+ logError(`${arg} requires a non-empty value`);
972
+ process.exit(CONSTANTS.EXIT_ERROR);
973
+ }
939
974
  }
940
975
 
941
976
  switch (arg) {
@@ -953,12 +988,20 @@ function parseArgs(args) {
953
988
 
954
989
  case "-i":
955
990
  case "--input":
956
- i++;
957
- while (i < args.length && !args[i].startsWith("-")) {
958
- inputs.push(args[i]);
991
+ {
959
992
  i++;
993
+ const startIdx = i;
994
+ while (i < args.length && !args[i].startsWith("-")) {
995
+ inputs.push(args[i]);
996
+ i++;
997
+ }
998
+ // Why: Validate at least one input was provided after -i flag
999
+ if (i === startIdx) {
1000
+ logError(`${arg} requires at least one input file`);
1001
+ process.exit(CONSTANTS.EXIT_ERROR);
1002
+ }
1003
+ i--; // Back up one since the while loop went past
960
1004
  }
961
- i--; // Back up one since the while loop went past
962
1005
  break;
963
1006
 
964
1007
  case "-s":
@@ -984,6 +1027,11 @@ function parseArgs(args) {
984
1027
  outputs.push(args[i]);
985
1028
  i++;
986
1029
  }
1030
+ // Why: Validate at least one output was provided after -o flag
1031
+ if (outputs.length === 0) {
1032
+ logError(`${arg} requires at least one output path`);
1033
+ process.exit(CONSTANTS.EXIT_ERROR);
1034
+ }
987
1035
  i--;
988
1036
  cfg.output = outputs.length === 1 ? outputs[0] : outputs;
989
1037
  break;
@@ -995,9 +1043,9 @@ function parseArgs(args) {
995
1043
  {
996
1044
  const precisionArg = getNextArg(args, i, arg);
997
1045
  const parsed = parseInt(precisionArg, 10);
998
- // Why: Validate parseInt result to prevent NaN values
999
- if (isNaN(parsed)) {
1000
- logError(`--precision requires a valid number, got: ${precisionArg}`);
1046
+ // Why: Validate parseInt result to prevent NaN and negative values
1047
+ if (isNaN(parsed) || parsed < 0) {
1048
+ logError(`--precision requires a non-negative number, got: ${precisionArg}`);
1001
1049
  process.exit(CONSTANTS.EXIT_ERROR);
1002
1050
  }
1003
1051
  cfg.precision = parsed;
@@ -1024,9 +1072,9 @@ function parseArgs(args) {
1024
1072
  {
1025
1073
  const indentArg = getNextArg(args, i, arg);
1026
1074
  const parsed = parseInt(indentArg, 10);
1027
- // Why: Validate parseInt result to prevent NaN values
1028
- if (isNaN(parsed)) {
1029
- logError(`--indent requires a valid number, got: ${indentArg}`);
1075
+ // Why: Validate parseInt result to prevent NaN and negative values
1076
+ if (isNaN(parsed) || parsed < 0) {
1077
+ logError(`--indent requires a non-negative number, got: ${indentArg}`);
1030
1078
  process.exit(CONSTANTS.EXIT_ERROR);
1031
1079
  }
1032
1080
  cfg.indent = parsed;
@@ -1050,12 +1098,20 @@ function parseArgs(args) {
1050
1098
  break;
1051
1099
 
1052
1100
  case "--exclude":
1053
- i++;
1054
- while (i < args.length && !args[i].startsWith("-")) {
1055
- cfg.exclude.push(args[i]);
1101
+ {
1056
1102
  i++;
1103
+ const startIdx = i;
1104
+ while (i < args.length && !args[i].startsWith("-")) {
1105
+ cfg.exclude.push(args[i]);
1106
+ i++;
1107
+ }
1108
+ // Why: Validate at least one exclusion pattern was provided
1109
+ if (i === startIdx) {
1110
+ logError(`${arg} requires at least one exclusion pattern`);
1111
+ process.exit(CONSTANTS.EXIT_ERROR);
1112
+ }
1113
+ i--;
1057
1114
  }
1058
- i--;
1059
1115
  break;
1060
1116
 
1061
1117
  case "-q":
@@ -1070,9 +1126,10 @@ function parseArgs(args) {
1070
1126
  case "--preserve-ns":
1071
1127
  {
1072
1128
  // Why: Use helper to safely get next argument with bounds checking
1073
- const val = argValue || getNextArg(args, i, arg);
1074
- if (!argValue) i++; // Only increment if we used getNextArg
1075
- if (!val) {
1129
+ const val = argValue !== null ? argValue : getNextArg(args, i, arg);
1130
+ if (argValue === null) i++; // Only increment if we used getNextArg
1131
+ // Why: Validate value is not empty (catches --preserve-ns= with no value)
1132
+ if (!val || val.trim() === "") {
1076
1133
  logError(
1077
1134
  "--preserve-ns requires a comma-separated list of namespaces",
1078
1135
  );
@@ -1082,6 +1139,11 @@ function parseArgs(args) {
1082
1139
  .split(",")
1083
1140
  .map((s) => s.trim().toLowerCase())
1084
1141
  .filter((s) => s.length > 0);
1142
+ // Why: Validate that after filtering empty strings, we have at least one namespace
1143
+ if (cfg.preserveNamespaces.length === 0) {
1144
+ logError("--preserve-ns requires at least one valid namespace");
1145
+ process.exit(CONSTANTS.EXIT_ERROR);
1146
+ }
1085
1147
  }
1086
1148
  break;
1087
1149
 
@@ -1172,9 +1234,9 @@ function parseArgs(args) {
1172
1234
  {
1173
1235
  const depthArg = getNextArg(args, i, arg);
1174
1236
  const parsed = parseInt(depthArg, 10);
1175
- // Why: Validate parseInt result to prevent NaN values
1176
- if (isNaN(parsed)) {
1177
- logError(`--embed-max-depth requires a valid number, got: ${depthArg}`);
1237
+ // Why: Validate parseInt result to prevent NaN and negative/zero values
1238
+ if (isNaN(parsed) || parsed < 1) {
1239
+ logError(`--embed-max-depth requires a positive number, got: ${depthArg}`);
1178
1240
  process.exit(CONSTANTS.EXIT_ERROR);
1179
1241
  }
1180
1242
  cfg.embedMaxRecursionDepth = parsed;
@@ -1187,9 +1249,9 @@ function parseArgs(args) {
1187
1249
  {
1188
1250
  const timeoutArg = getNextArg(args, i, arg);
1189
1251
  const parsed = parseInt(timeoutArg, 10);
1190
- // Why: Validate parseInt result to prevent NaN values
1191
- if (isNaN(parsed)) {
1192
- logError(`--embed-timeout requires a valid number, got: ${timeoutArg}`);
1252
+ // Why: Validate parseInt result to prevent NaN and negative/zero values
1253
+ if (isNaN(parsed) || parsed < 1000) {
1254
+ logError(`--embed-timeout requires a number >= 1000 (ms), got: ${timeoutArg}`);
1193
1255
  process.exit(CONSTANTS.EXIT_ERROR);
1194
1256
  }
1195
1257
  cfg.embedTimeout = parsed;
@@ -1352,6 +1414,16 @@ async function main() {
1352
1414
  ? toDataUri(result, config.datauri)
1353
1415
  : result;
1354
1416
  if (config.output && config.output !== "-") {
1417
+ // Why: Validate output is not an array for string input mode
1418
+ if (Array.isArray(config.output)) {
1419
+ logError("Cannot specify multiple outputs with --string input");
1420
+ process.exit(CONSTANTS.EXIT_ERROR);
1421
+ }
1422
+ // Why: Ensure parent directory exists before writing
1423
+ const outputDir = dirname(config.output);
1424
+ if (outputDir && outputDir !== ".") {
1425
+ ensureDir(outputDir);
1426
+ }
1355
1427
  writeFileSync(config.output, output, "utf8");
1356
1428
  log(`${colors.green}Done!${colors.reset}`);
1357
1429
  } else {
@@ -1411,17 +1483,34 @@ async function main() {
1411
1483
 
1412
1484
  if (config.output) {
1413
1485
  if (config.output === "-") {
1486
+ // Why: Validate stdout output only works with single file
1487
+ if (files.length > 1) {
1488
+ logError("Cannot output multiple files to stdout (use -o <dir> instead)");
1489
+ process.exit(CONSTANTS.EXIT_ERROR);
1490
+ }
1414
1491
  outputPath = "-";
1415
1492
  } else if (Array.isArray(config.output)) {
1416
- // Why: Bounds check when accessing array to prevent undefined values
1417
- if (config.output.length === 0) {
1418
- logError("Output array is empty");
1493
+ // Why: Validate array length matches input file count or error clearly
1494
+ if (config.output.length !== files.length) {
1495
+ logError(
1496
+ `Output count mismatch: ${files.length} input file(s) but ${config.output.length} output path(s). ` +
1497
+ `Provide either one output directory or exactly ${files.length} output file(s).`
1498
+ );
1419
1499
  process.exit(CONSTANTS.EXIT_ERROR);
1420
1500
  }
1421
- outputPath = config.output[i] || config.output[config.output.length - 1];
1501
+ outputPath = resolvePath(config.output[i]);
1422
1502
  } else if (files.length > 1 || isDir(resolvePath(config.output))) {
1423
1503
  // Multiple files or output is a directory
1424
- outputPath = join(resolvePath(config.output), basename(inputPath));
1504
+ const outputDir = resolvePath(config.output);
1505
+ // Why: Validate output is actually a directory when processing multiple files
1506
+ if (files.length > 1 && !isDir(outputDir)) {
1507
+ logError(
1508
+ `Processing ${files.length} files but output "${config.output}" is not a directory. ` +
1509
+ `Create the directory first or provide ${files.length} output paths.`
1510
+ );
1511
+ process.exit(CONSTANTS.EXIT_ERROR);
1512
+ }
1513
+ outputPath = join(outputDir, basename(inputPath));
1425
1514
  } else {
1426
1515
  outputPath = resolvePath(config.output);
1427
1516
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emasoft/svg-matrix",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
4
4
  "description": "Arbitrary-precision matrix, vector and affine transformation library for JavaScript using decimal.js",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -80,8 +80,8 @@ export function formatSplineValue(value, precision = 3) {
80
80
  str = '-' + str.substring(2); // "-0.5" -> "-.5"
81
81
  }
82
82
 
83
- // Handle edge case: ".0" should be "0"
84
- if (str === '' || str === '.') str = '0';
83
+ // Handle edge cases: ".0" should be "0", "-0" should be "0"
84
+ if (str === '' || str === '.' || str === '-0') str = '0';
85
85
 
86
86
  return str;
87
87
  }
@@ -229,7 +229,7 @@ export function optimizeKeySplines(keySplines, options = {}) {
229
229
  return { value: null, allLinear: false, standardEasings: [] };
230
230
  }
231
231
 
232
- // Check for all linear splines (wrap to avoid .every() passing index as tolerance)
232
+ // Must wrap in arrow function to avoid .every() passing index as tolerance
233
233
  const allLinear = splines.every(s => isLinearSpline(s));
234
234
 
235
235
  // Identify standard easings