@optique/core 1.0.0-dev.692 → 1.0.0-dev.701
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 +2 -2
- package/dist/valueparser.cjs +104 -18
- package/dist/valueparser.js +104 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,8 +47,8 @@ const parser = object({
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
const config = runParser(parser, "myapp", process.argv.slice(2), {
|
|
50
|
-
help:
|
|
51
|
-
onError: process.exit,
|
|
50
|
+
help: { command: true, option: true },
|
|
51
|
+
onError: (exitCode) => process.exit(exitCode),
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
console.log(`Hello ${config.name}!`);
|
package/dist/valueparser.cjs
CHANGED
|
@@ -20,31 +20,68 @@ function choice(choices, options = {}) {
|
|
|
20
20
|
if (isNumberChoice) {
|
|
21
21
|
const numberChoices = choices;
|
|
22
22
|
const numberOptions = options;
|
|
23
|
+
const hasNaN = numberChoices.some((v) => Number.isNaN(v));
|
|
24
|
+
const validNumberChoices = hasNaN ? numberChoices.filter((v) => !Number.isNaN(v)) : numberChoices;
|
|
25
|
+
const numberStrings = numberChoices.map((v) => Object.is(v, -0) ? "-0" : String(v));
|
|
23
26
|
return {
|
|
24
27
|
$mode: "sync",
|
|
25
28
|
metavar,
|
|
26
|
-
choices,
|
|
29
|
+
choices: hasNaN ? validNumberChoices : numberChoices,
|
|
27
30
|
parse(input) {
|
|
28
|
-
const
|
|
29
|
-
if (Number.isNaN(
|
|
30
|
-
success: false,
|
|
31
|
-
error: formatNumberChoiceError(input, numberChoices, numberOptions)
|
|
32
|
-
};
|
|
33
|
-
const index = numberChoices.indexOf(parsed);
|
|
34
|
-
if (index < 0) return {
|
|
35
|
-
success: false,
|
|
36
|
-
error: formatNumberChoiceError(input, numberChoices, numberOptions)
|
|
37
|
-
};
|
|
38
|
-
return {
|
|
31
|
+
const index = numberStrings.indexOf(input);
|
|
32
|
+
if (index >= 0 && !Number.isNaN(numberChoices[index])) return {
|
|
39
33
|
success: true,
|
|
40
34
|
value: numberChoices[index]
|
|
41
35
|
};
|
|
36
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)$/.test(input)) {
|
|
37
|
+
const parsed = Number(input);
|
|
38
|
+
if (Number.isFinite(parsed)) {
|
|
39
|
+
const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
|
|
40
|
+
const normalizedInput = normalizeDecimal(input);
|
|
41
|
+
const normalizedCanonical = normalizeDecimal(expandScientific(canonical));
|
|
42
|
+
if (normalizedInput === normalizedCanonical) {
|
|
43
|
+
const fallbackIndex = numberChoices.findIndex((v) => Object.is(v, parsed));
|
|
44
|
+
if (fallbackIndex >= 0) return {
|
|
45
|
+
success: true,
|
|
46
|
+
value: numberChoices[fallbackIndex]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (parsed === 0 && normalizedInput.replace(/^-/, "") === "0" && !numberChoices.some((v) => Object.is(v, -0))) {
|
|
50
|
+
const zeroIndex = numberChoices.indexOf(0);
|
|
51
|
+
if (zeroIndex >= 0) return {
|
|
52
|
+
success: true,
|
|
53
|
+
value: numberChoices[zeroIndex]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)[eE][+-]?\d+$/.test(input)) {
|
|
59
|
+
const parsed = Number(input);
|
|
60
|
+
if (Number.isFinite(parsed)) {
|
|
61
|
+
const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
|
|
62
|
+
if (/[eE]/.test(canonical)) {
|
|
63
|
+
const normalizedInput = normalizeDecimal(expandScientific(input));
|
|
64
|
+
const normalizedCanonical = normalizeDecimal(expandScientific(canonical));
|
|
65
|
+
if (normalizedInput === normalizedCanonical) {
|
|
66
|
+
const fallbackIndex = numberChoices.findIndex((v) => Object.is(v, parsed));
|
|
67
|
+
if (fallbackIndex >= 0) return {
|
|
68
|
+
success: true,
|
|
69
|
+
value: numberChoices[fallbackIndex]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: formatNumberChoiceError(input, validNumberChoices, numberChoices, numberOptions)
|
|
78
|
+
};
|
|
42
79
|
},
|
|
43
80
|
format(value) {
|
|
44
|
-
return String(value);
|
|
81
|
+
return Object.is(value, -0) ? "-0" : String(value);
|
|
45
82
|
},
|
|
46
83
|
suggest(prefix) {
|
|
47
|
-
return
|
|
84
|
+
return numberStrings.filter((valueStr, i) => !Number.isNaN(numberChoices[i]) && valueStr.startsWith(prefix)).map((valueStr) => ({
|
|
48
85
|
kind: "literal",
|
|
49
86
|
text: valueStr
|
|
50
87
|
}));
|
|
@@ -86,6 +123,54 @@ function choice(choices, options = {}) {
|
|
|
86
123
|
};
|
|
87
124
|
}
|
|
88
125
|
/**
|
|
126
|
+
* Expands a numeric string in scientific notation (e.g., `"1e+21"`,
|
|
127
|
+
* `"1.5e-3"`, `".1e-6"`) into plain decimal form for normalization.
|
|
128
|
+
* Used for both canonical `String(number)` output and user input.
|
|
129
|
+
* Returns the input unchanged if it does not contain scientific notation.
|
|
130
|
+
*/
|
|
131
|
+
function expandScientific(s) {
|
|
132
|
+
const match = /^([+-]?)(\d+\.?\d*|\.\d+)[eE]([+-]?\d+)$/.exec(s);
|
|
133
|
+
if (!match) return s;
|
|
134
|
+
const [, rawSign, mantissa, expStr] = match;
|
|
135
|
+
const sign = rawSign === "-" ? "-" : "";
|
|
136
|
+
const exp = parseInt(expStr, 10);
|
|
137
|
+
const dotPos = mantissa.indexOf(".");
|
|
138
|
+
const digits = mantissa.replace(".", "");
|
|
139
|
+
const intLen = dotPos >= 0 ? dotPos : digits.length;
|
|
140
|
+
const newIntLen = intLen + exp;
|
|
141
|
+
let result;
|
|
142
|
+
if (newIntLen >= digits.length) result = digits + "0".repeat(newIntLen - digits.length);
|
|
143
|
+
else if (newIntLen <= 0) result = "0." + "0".repeat(-newIntLen) + digits;
|
|
144
|
+
else result = digits.slice(0, newIntLen) + "." + digits.slice(newIntLen);
|
|
145
|
+
return sign + result;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Normalizes a plain decimal string by stripping leading zeros from the
|
|
149
|
+
* integer part and trailing zeros from the fractional part, so that two
|
|
150
|
+
* strings representing the same mathematical value compare as equal.
|
|
151
|
+
*/
|
|
152
|
+
function normalizeDecimal(s) {
|
|
153
|
+
let sign = "";
|
|
154
|
+
let str = s;
|
|
155
|
+
if (str.startsWith("-") || str.startsWith("+")) {
|
|
156
|
+
if (str[0] === "-") sign = "-";
|
|
157
|
+
str = str.slice(1);
|
|
158
|
+
}
|
|
159
|
+
const dot = str.indexOf(".");
|
|
160
|
+
let int;
|
|
161
|
+
let frac;
|
|
162
|
+
if (dot >= 0) {
|
|
163
|
+
int = str.slice(0, dot);
|
|
164
|
+
frac = str.slice(dot + 1);
|
|
165
|
+
} else {
|
|
166
|
+
int = str;
|
|
167
|
+
frac = "";
|
|
168
|
+
}
|
|
169
|
+
int = int.replace(/^0+/, "") || "0";
|
|
170
|
+
frac = frac.replace(/0+$/, "");
|
|
171
|
+
return sign + (frac ? int + "." + frac : int);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
89
174
|
* Formats error message for string choice parser.
|
|
90
175
|
*/
|
|
91
176
|
function formatStringChoiceError(input, choices, options) {
|
|
@@ -95,15 +180,16 @@ function formatStringChoiceError(input, choices, options) {
|
|
|
95
180
|
/**
|
|
96
181
|
* Formats error message for number choice parser.
|
|
97
182
|
*/
|
|
98
|
-
function formatNumberChoiceError(input,
|
|
99
|
-
if (options.errors?.invalidChoice) return typeof options.errors.invalidChoice === "function" ? options.errors.invalidChoice(input,
|
|
100
|
-
return formatDefaultChoiceError(input,
|
|
183
|
+
function formatNumberChoiceError(input, validChoices, allChoices, options) {
|
|
184
|
+
if (options.errors?.invalidChoice) return typeof options.errors.invalidChoice === "function" ? options.errors.invalidChoice(input, validChoices) : options.errors.invalidChoice;
|
|
185
|
+
return formatDefaultChoiceError(input, allChoices);
|
|
101
186
|
}
|
|
102
187
|
/**
|
|
103
188
|
* Formats default error message for choice parser.
|
|
104
189
|
*/
|
|
105
190
|
function formatDefaultChoiceError(input, choices) {
|
|
106
|
-
const choiceStrings = choices.map((c) => String(c));
|
|
191
|
+
const choiceStrings = choices.filter((c) => typeof c === "string" || !Number.isNaN(c)).map((c) => Object.is(c, -0) ? "-0" : String(c));
|
|
192
|
+
if (choiceStrings.length === 0 && choices.length > 0) return require_message.message`No valid choices are configured, but got ${input}.`;
|
|
107
193
|
return require_message.message`Expected one of ${require_message.valueSet(choiceStrings, { locale: "en-US" })}, but got ${input}.`;
|
|
108
194
|
}
|
|
109
195
|
/**
|
package/dist/valueparser.js
CHANGED
|
@@ -20,31 +20,68 @@ function choice(choices, options = {}) {
|
|
|
20
20
|
if (isNumberChoice) {
|
|
21
21
|
const numberChoices = choices;
|
|
22
22
|
const numberOptions = options;
|
|
23
|
+
const hasNaN = numberChoices.some((v) => Number.isNaN(v));
|
|
24
|
+
const validNumberChoices = hasNaN ? numberChoices.filter((v) => !Number.isNaN(v)) : numberChoices;
|
|
25
|
+
const numberStrings = numberChoices.map((v) => Object.is(v, -0) ? "-0" : String(v));
|
|
23
26
|
return {
|
|
24
27
|
$mode: "sync",
|
|
25
28
|
metavar,
|
|
26
|
-
choices,
|
|
29
|
+
choices: hasNaN ? validNumberChoices : numberChoices,
|
|
27
30
|
parse(input) {
|
|
28
|
-
const
|
|
29
|
-
if (Number.isNaN(
|
|
30
|
-
success: false,
|
|
31
|
-
error: formatNumberChoiceError(input, numberChoices, numberOptions)
|
|
32
|
-
};
|
|
33
|
-
const index = numberChoices.indexOf(parsed);
|
|
34
|
-
if (index < 0) return {
|
|
35
|
-
success: false,
|
|
36
|
-
error: formatNumberChoiceError(input, numberChoices, numberOptions)
|
|
37
|
-
};
|
|
38
|
-
return {
|
|
31
|
+
const index = numberStrings.indexOf(input);
|
|
32
|
+
if (index >= 0 && !Number.isNaN(numberChoices[index])) return {
|
|
39
33
|
success: true,
|
|
40
34
|
value: numberChoices[index]
|
|
41
35
|
};
|
|
36
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)$/.test(input)) {
|
|
37
|
+
const parsed = Number(input);
|
|
38
|
+
if (Number.isFinite(parsed)) {
|
|
39
|
+
const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
|
|
40
|
+
const normalizedInput = normalizeDecimal(input);
|
|
41
|
+
const normalizedCanonical = normalizeDecimal(expandScientific(canonical));
|
|
42
|
+
if (normalizedInput === normalizedCanonical) {
|
|
43
|
+
const fallbackIndex = numberChoices.findIndex((v) => Object.is(v, parsed));
|
|
44
|
+
if (fallbackIndex >= 0) return {
|
|
45
|
+
success: true,
|
|
46
|
+
value: numberChoices[fallbackIndex]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (parsed === 0 && normalizedInput.replace(/^-/, "") === "0" && !numberChoices.some((v) => Object.is(v, -0))) {
|
|
50
|
+
const zeroIndex = numberChoices.indexOf(0);
|
|
51
|
+
if (zeroIndex >= 0) return {
|
|
52
|
+
success: true,
|
|
53
|
+
value: numberChoices[zeroIndex]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)[eE][+-]?\d+$/.test(input)) {
|
|
59
|
+
const parsed = Number(input);
|
|
60
|
+
if (Number.isFinite(parsed)) {
|
|
61
|
+
const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
|
|
62
|
+
if (/[eE]/.test(canonical)) {
|
|
63
|
+
const normalizedInput = normalizeDecimal(expandScientific(input));
|
|
64
|
+
const normalizedCanonical = normalizeDecimal(expandScientific(canonical));
|
|
65
|
+
if (normalizedInput === normalizedCanonical) {
|
|
66
|
+
const fallbackIndex = numberChoices.findIndex((v) => Object.is(v, parsed));
|
|
67
|
+
if (fallbackIndex >= 0) return {
|
|
68
|
+
success: true,
|
|
69
|
+
value: numberChoices[fallbackIndex]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: formatNumberChoiceError(input, validNumberChoices, numberChoices, numberOptions)
|
|
78
|
+
};
|
|
42
79
|
},
|
|
43
80
|
format(value) {
|
|
44
|
-
return String(value);
|
|
81
|
+
return Object.is(value, -0) ? "-0" : String(value);
|
|
45
82
|
},
|
|
46
83
|
suggest(prefix) {
|
|
47
|
-
return
|
|
84
|
+
return numberStrings.filter((valueStr, i) => !Number.isNaN(numberChoices[i]) && valueStr.startsWith(prefix)).map((valueStr) => ({
|
|
48
85
|
kind: "literal",
|
|
49
86
|
text: valueStr
|
|
50
87
|
}));
|
|
@@ -86,6 +123,54 @@ function choice(choices, options = {}) {
|
|
|
86
123
|
};
|
|
87
124
|
}
|
|
88
125
|
/**
|
|
126
|
+
* Expands a numeric string in scientific notation (e.g., `"1e+21"`,
|
|
127
|
+
* `"1.5e-3"`, `".1e-6"`) into plain decimal form for normalization.
|
|
128
|
+
* Used for both canonical `String(number)` output and user input.
|
|
129
|
+
* Returns the input unchanged if it does not contain scientific notation.
|
|
130
|
+
*/
|
|
131
|
+
function expandScientific(s) {
|
|
132
|
+
const match = /^([+-]?)(\d+\.?\d*|\.\d+)[eE]([+-]?\d+)$/.exec(s);
|
|
133
|
+
if (!match) return s;
|
|
134
|
+
const [, rawSign, mantissa, expStr] = match;
|
|
135
|
+
const sign = rawSign === "-" ? "-" : "";
|
|
136
|
+
const exp = parseInt(expStr, 10);
|
|
137
|
+
const dotPos = mantissa.indexOf(".");
|
|
138
|
+
const digits = mantissa.replace(".", "");
|
|
139
|
+
const intLen = dotPos >= 0 ? dotPos : digits.length;
|
|
140
|
+
const newIntLen = intLen + exp;
|
|
141
|
+
let result;
|
|
142
|
+
if (newIntLen >= digits.length) result = digits + "0".repeat(newIntLen - digits.length);
|
|
143
|
+
else if (newIntLen <= 0) result = "0." + "0".repeat(-newIntLen) + digits;
|
|
144
|
+
else result = digits.slice(0, newIntLen) + "." + digits.slice(newIntLen);
|
|
145
|
+
return sign + result;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Normalizes a plain decimal string by stripping leading zeros from the
|
|
149
|
+
* integer part and trailing zeros from the fractional part, so that two
|
|
150
|
+
* strings representing the same mathematical value compare as equal.
|
|
151
|
+
*/
|
|
152
|
+
function normalizeDecimal(s) {
|
|
153
|
+
let sign = "";
|
|
154
|
+
let str = s;
|
|
155
|
+
if (str.startsWith("-") || str.startsWith("+")) {
|
|
156
|
+
if (str[0] === "-") sign = "-";
|
|
157
|
+
str = str.slice(1);
|
|
158
|
+
}
|
|
159
|
+
const dot = str.indexOf(".");
|
|
160
|
+
let int;
|
|
161
|
+
let frac;
|
|
162
|
+
if (dot >= 0) {
|
|
163
|
+
int = str.slice(0, dot);
|
|
164
|
+
frac = str.slice(dot + 1);
|
|
165
|
+
} else {
|
|
166
|
+
int = str;
|
|
167
|
+
frac = "";
|
|
168
|
+
}
|
|
169
|
+
int = int.replace(/^0+/, "") || "0";
|
|
170
|
+
frac = frac.replace(/0+$/, "");
|
|
171
|
+
return sign + (frac ? int + "." + frac : int);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
89
174
|
* Formats error message for string choice parser.
|
|
90
175
|
*/
|
|
91
176
|
function formatStringChoiceError(input, choices, options) {
|
|
@@ -95,15 +180,16 @@ function formatStringChoiceError(input, choices, options) {
|
|
|
95
180
|
/**
|
|
96
181
|
* Formats error message for number choice parser.
|
|
97
182
|
*/
|
|
98
|
-
function formatNumberChoiceError(input,
|
|
99
|
-
if (options.errors?.invalidChoice) return typeof options.errors.invalidChoice === "function" ? options.errors.invalidChoice(input,
|
|
100
|
-
return formatDefaultChoiceError(input,
|
|
183
|
+
function formatNumberChoiceError(input, validChoices, allChoices, options) {
|
|
184
|
+
if (options.errors?.invalidChoice) return typeof options.errors.invalidChoice === "function" ? options.errors.invalidChoice(input, validChoices) : options.errors.invalidChoice;
|
|
185
|
+
return formatDefaultChoiceError(input, allChoices);
|
|
101
186
|
}
|
|
102
187
|
/**
|
|
103
188
|
* Formats default error message for choice parser.
|
|
104
189
|
*/
|
|
105
190
|
function formatDefaultChoiceError(input, choices) {
|
|
106
|
-
const choiceStrings = choices.map((c) => String(c));
|
|
191
|
+
const choiceStrings = choices.filter((c) => typeof c === "string" || !Number.isNaN(c)).map((c) => Object.is(c, -0) ? "-0" : String(c));
|
|
192
|
+
if (choiceStrings.length === 0 && choices.length > 0) return message`No valid choices are configured, but got ${input}.`;
|
|
107
193
|
return message`Expected one of ${valueSet(choiceStrings, { locale: "en-US" })}, but got ${input}.`;
|
|
108
194
|
}
|
|
109
195
|
/**
|