@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.
- package/README.md +3 -3
- package/bin/svg-matrix.js +38 -22
- package/bin/svglinter.cjs +41 -12
- package/bin/svgm.js +133 -44
- package/package.json +1 -1
- package/src/animation-optimization.js +3 -3
- package/src/animation-references.js +201 -8
- package/src/arc-length.js +45 -4
- package/src/bezier-analysis.js +26 -9
- package/src/bezier-intersections.js +84 -13
- package/src/browser-verify.js +89 -33
- package/src/clip-path-resolver.js +91 -20
- package/src/convert-path-data.js +1 -1
- package/src/css-specificity.js +281 -66
- package/src/douglas-peucker.js +71 -1
- package/src/flatten-pipeline.js +18 -14
- package/src/geometry-to-path.js +23 -2
- package/src/gjk-collision.js +81 -19
- package/src/index.js +32 -3
- package/src/inkscape-support.js +63 -19
- package/src/logger.js +85 -57
- package/src/marker-resolver.js +53 -15
- package/src/mask-resolver.js +32 -18
- package/src/matrix.js +10 -8
- package/src/mesh-gradient.js +84 -33
- package/src/off-canvas-detection.js +138 -49
- package/src/path-analysis.js +4 -4
- package/src/path-data-plugins.js +45 -2
- package/src/path-optimization.js +36 -6
- package/src/path-simplification.js +87 -23
- package/src/pattern-resolver.js +26 -4
- package/src/polygon-clip.js +39 -14
- package/src/svg-boolean-ops.js +76 -14
- package/src/svg-collections.js +13 -1
- package/src/svg-flatten.js +101 -62
- package/src/svg-parser.js +44 -4
- package/src/svg-rendering-context.js +6 -6
- package/src/svg-toolbox.js +63 -27
- package/src/svg-validation-data.js +12 -10
- package/src/svg2-polyfills.js +49 -20
- package/src/transform-decomposition.js +48 -16
- package/src/transform-optimization.js +23 -12
- package/src/transforms2d.js +31 -8
- package/src/transforms3d.js +76 -22
- package/src/use-symbol-resolver.js +60 -24
- 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 {
|
|
503
|
+
import { validateSVG, fixInvalidSVG } from '@emasoft/svg-matrix';
|
|
504
504
|
|
|
505
|
-
const result = await
|
|
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
|
|
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
|
-
|
|
2048
|
-
|
|
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 =
|
|
2052
|
+
cfg.output = value.trim();
|
|
2052
2053
|
break;
|
|
2054
|
+
}
|
|
2053
2055
|
case "-l":
|
|
2054
|
-
case "--list":
|
|
2055
|
-
|
|
2056
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
2103
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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)
|
|
405
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1074
|
-
if (
|
|
1075
|
-
|
|
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
|
|
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
|
|
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:
|
|
1417
|
-
if (config.output.length
|
|
1418
|
-
logError(
|
|
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]
|
|
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
|
-
|
|
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
|
@@ -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
|
|
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
|
-
//
|
|
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
|