@optique/core 0.7.0-dev.127 → 0.7.0-dev.128
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 +1 -1
- package/dist/constructs.cjs +8 -3
- package/dist/constructs.js +8 -3
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/message.cjs +39 -6
- package/dist/message.js +39 -6
- package/dist/parser.cjs +6 -3
- package/dist/parser.d.cts +8 -0
- package/dist/parser.d.ts +8 -0
- package/dist/parser.js +6 -3
- package/dist/primitives.cjs +22 -4
- package/dist/primitives.js +22 -4
- package/dist/suggestion.cjs +175 -0
- package/dist/suggestion.js +175 -0
- package/dist/usage.cjs +33 -0
- package/dist/usage.d.cts +22 -1
- package/dist/usage.d.ts +22 -1
- package/dist/usage.js +33 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/constructs.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const require_message = require('./message.cjs');
|
|
2
2
|
const require_usage = require('./usage.cjs');
|
|
3
|
+
const require_suggestion = require('./suggestion.cjs');
|
|
3
4
|
|
|
4
5
|
//#region src/constructs.ts
|
|
5
6
|
/**
|
|
@@ -42,7 +43,8 @@ function or(...args) {
|
|
|
42
43
|
error: context.buffer.length < 1 ? options?.errors?.noMatch ?? require_message.message`No matching option or command found.` : (() => {
|
|
43
44
|
const token = context.buffer[0];
|
|
44
45
|
const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
|
|
45
|
-
|
|
46
|
+
if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
|
|
47
|
+
return require_suggestion.createErrorWithSuggestions(defaultMsg, token, context.usage, "both");
|
|
46
48
|
})()
|
|
47
49
|
};
|
|
48
50
|
const orderedParsers = parsers.map((p, i) => [p, i]);
|
|
@@ -177,7 +179,8 @@ function longestMatch(...args) {
|
|
|
177
179
|
error: context.buffer.length < 1 ? options?.errors?.noMatch ?? require_message.message`No matching option or command found.` : (() => {
|
|
178
180
|
const token = context.buffer[0];
|
|
179
181
|
const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
|
|
180
|
-
|
|
182
|
+
if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
|
|
183
|
+
return require_suggestion.createErrorWithSuggestions(defaultMsg, token, context.usage, "both");
|
|
181
184
|
})()
|
|
182
185
|
};
|
|
183
186
|
for (let i = 0; i < parsers.length; i++) {
|
|
@@ -299,7 +302,9 @@ function object(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
|
|
|
299
302
|
error: context.buffer.length > 0 ? (() => {
|
|
300
303
|
const token = context.buffer[0];
|
|
301
304
|
const customMessage = options.errors?.unexpectedInput;
|
|
302
|
-
|
|
305
|
+
if (customMessage) return typeof customMessage === "function" ? customMessage(token) : customMessage;
|
|
306
|
+
const baseError = require_message.message`Unexpected option or argument: ${token}.`;
|
|
307
|
+
return require_suggestion.createErrorWithSuggestions(baseError, token, context.usage, "both");
|
|
303
308
|
})() : options.errors?.endOfInput ?? require_message.message`Expected an option or argument, but got end of input.`
|
|
304
309
|
};
|
|
305
310
|
let currentContext = context;
|
package/dist/constructs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { message, optionName, values } from "./message.js";
|
|
2
2
|
import { extractOptionNames } from "./usage.js";
|
|
3
|
+
import { createErrorWithSuggestions } from "./suggestion.js";
|
|
3
4
|
|
|
4
5
|
//#region src/constructs.ts
|
|
5
6
|
/**
|
|
@@ -42,7 +43,8 @@ function or(...args) {
|
|
|
42
43
|
error: context.buffer.length < 1 ? options?.errors?.noMatch ?? message`No matching option or command found.` : (() => {
|
|
43
44
|
const token = context.buffer[0];
|
|
44
45
|
const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
|
|
45
|
-
|
|
46
|
+
if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
|
|
47
|
+
return createErrorWithSuggestions(defaultMsg, token, context.usage, "both");
|
|
46
48
|
})()
|
|
47
49
|
};
|
|
48
50
|
const orderedParsers = parsers.map((p, i) => [p, i]);
|
|
@@ -177,7 +179,8 @@ function longestMatch(...args) {
|
|
|
177
179
|
error: context.buffer.length < 1 ? options?.errors?.noMatch ?? message`No matching option or command found.` : (() => {
|
|
178
180
|
const token = context.buffer[0];
|
|
179
181
|
const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
|
|
180
|
-
|
|
182
|
+
if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
|
|
183
|
+
return createErrorWithSuggestions(defaultMsg, token, context.usage, "both");
|
|
181
184
|
})()
|
|
182
185
|
};
|
|
183
186
|
for (let i = 0; i < parsers.length; i++) {
|
|
@@ -299,7 +302,9 @@ function object(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
|
|
|
299
302
|
error: context.buffer.length > 0 ? (() => {
|
|
300
303
|
const token = context.buffer[0];
|
|
301
304
|
const customMessage = options.errors?.unexpectedInput;
|
|
302
|
-
|
|
305
|
+
if (customMessage) return typeof customMessage === "function" ? customMessage(token) : customMessage;
|
|
306
|
+
const baseError = message`Unexpected option or argument: ${token}.`;
|
|
307
|
+
return createErrorWithSuggestions(baseError, token, context.usage, "both");
|
|
303
308
|
})() : options.errors?.endOfInput ?? message`Expected an option or argument, but got end of input.`
|
|
304
309
|
};
|
|
305
310
|
let currentContext = context;
|
package/dist/index.cjs
CHANGED
|
@@ -19,6 +19,7 @@ exports.commandLine = require_message.commandLine;
|
|
|
19
19
|
exports.concat = require_constructs.concat;
|
|
20
20
|
exports.constant = require_primitives.constant;
|
|
21
21
|
exports.envVar = require_message.envVar;
|
|
22
|
+
exports.extractCommandNames = require_usage.extractCommandNames;
|
|
22
23
|
exports.extractOptionNames = require_usage.extractOptionNames;
|
|
23
24
|
exports.fish = require_completion.fish;
|
|
24
25
|
exports.flag = require_primitives.flag;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Message, MessageFormatOptions, MessageTerm, commandLine, envVar, formatMessage, message, metavar, optionName, optionNames, text, value, values } from "./message.cjs";
|
|
2
|
-
import { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.cjs";
|
|
2
|
+
import { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.cjs";
|
|
3
3
|
import { DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, ShowDefaultOptions, formatDocPage } from "./doc.cjs";
|
|
4
4
|
import { ChoiceOptions, FloatOptions, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, StringOptions, UrlOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, choice, float, integer, isValueParser, locale, string, url, uuid } from "./valueparser.cjs";
|
|
5
5
|
import { MultipleErrorOptions, MultipleOptions, WithDefaultError, WithDefaultOptions, map, multiple, optional, withDefault } from "./modifiers.cjs";
|
|
@@ -8,4 +8,4 @@ import { DocState, InferValue, Parser, ParserContext, ParserResult, Result, Sugg
|
|
|
8
8
|
import { LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, ObjectErrorOptions, ObjectOptions, OrErrorOptions, OrOptions, TupleOptions, concat, group, longestMatch, merge, object, or, tuple } from "./constructs.cjs";
|
|
9
9
|
import { ShellCompletion, bash, fish, nu, pwsh, zsh } from "./completion.cjs";
|
|
10
10
|
import { RunError, RunOptions, run } from "./facade.cjs";
|
|
11
|
-
export { ArgumentErrorOptions, ArgumentOptions, ChoiceOptions, CommandErrorOptions, CommandOptions, DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, DocState, FlagErrorOptions, FlagOptions, FloatOptions, InferValue, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, Message, MessageFormatOptions, MessageTerm, MultipleErrorOptions, MultipleOptions, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionName, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, Result, RunError, RunOptions, ShellCompletion, ShowDefaultOptions, StringOptions, Suggestion, TupleOptions, UrlOptions, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, WithDefaultError, WithDefaultOptions, argument, bash, choice, command, commandLine, concat, constant, envVar, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
|
|
11
|
+
export { ArgumentErrorOptions, ArgumentOptions, ChoiceOptions, CommandErrorOptions, CommandOptions, DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, DocState, FlagErrorOptions, FlagOptions, FloatOptions, InferValue, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, Message, MessageFormatOptions, MessageTerm, MultipleErrorOptions, MultipleOptions, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionName, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, Result, RunError, RunOptions, ShellCompletion, ShowDefaultOptions, StringOptions, Suggestion, TupleOptions, UrlOptions, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, WithDefaultError, WithDefaultOptions, argument, bash, choice, command, commandLine, concat, constant, envVar, extractCommandNames, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Message, MessageFormatOptions, MessageTerm, commandLine, envVar, formatMessage, message, metavar, optionName, optionNames, text, value, values } from "./message.js";
|
|
2
|
-
import { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.js";
|
|
2
|
+
import { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.js";
|
|
3
3
|
import { DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, ShowDefaultOptions, formatDocPage } from "./doc.js";
|
|
4
4
|
import { ChoiceOptions, FloatOptions, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, StringOptions, UrlOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, choice, float, integer, isValueParser, locale, string, url, uuid } from "./valueparser.js";
|
|
5
5
|
import { MultipleErrorOptions, MultipleOptions, WithDefaultError, WithDefaultOptions, map, multiple, optional, withDefault } from "./modifiers.js";
|
|
@@ -8,4 +8,4 @@ import { DocState, InferValue, Parser, ParserContext, ParserResult, Result, Sugg
|
|
|
8
8
|
import { LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, ObjectErrorOptions, ObjectOptions, OrErrorOptions, OrOptions, TupleOptions, concat, group, longestMatch, merge, object, or, tuple } from "./constructs.js";
|
|
9
9
|
import { ShellCompletion, bash, fish, nu, pwsh, zsh } from "./completion.js";
|
|
10
10
|
import { RunError, RunOptions, run } from "./facade.js";
|
|
11
|
-
export { ArgumentErrorOptions, ArgumentOptions, ChoiceOptions, CommandErrorOptions, CommandOptions, DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, DocState, FlagErrorOptions, FlagOptions, FloatOptions, InferValue, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, Message, MessageFormatOptions, MessageTerm, MultipleErrorOptions, MultipleOptions, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionName, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, Result, RunError, RunOptions, ShellCompletion, ShowDefaultOptions, StringOptions, Suggestion, TupleOptions, UrlOptions, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, WithDefaultError, WithDefaultOptions, argument, bash, choice, command, commandLine, concat, constant, envVar, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
|
|
11
|
+
export { ArgumentErrorOptions, ArgumentOptions, ChoiceOptions, CommandErrorOptions, CommandOptions, DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, DocState, FlagErrorOptions, FlagOptions, FloatOptions, InferValue, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, Message, MessageFormatOptions, MessageTerm, MultipleErrorOptions, MultipleOptions, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionName, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, Result, RunError, RunOptions, ShellCompletion, ShowDefaultOptions, StringOptions, Suggestion, TupleOptions, UrlOptions, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, WithDefaultError, WithDefaultOptions, argument, bash, choice, command, commandLine, concat, constant, envVar, extractCommandNames, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { commandLine, envVar, formatMessage, message, metavar, optionName, optionNames, text, value, values } from "./message.js";
|
|
2
|
-
import { extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.js";
|
|
2
|
+
import { extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.js";
|
|
3
3
|
import { concat, group, longestMatch, merge, object, or, tuple } from "./constructs.js";
|
|
4
4
|
import { formatDocPage } from "./doc.js";
|
|
5
5
|
import { bash, fish, nu, pwsh, zsh } from "./completion.js";
|
|
@@ -9,4 +9,4 @@ import { argument, command, constant, flag, option } from "./primitives.js";
|
|
|
9
9
|
import { getDocPage, parse, suggest } from "./parser.js";
|
|
10
10
|
import { RunError, run } from "./facade.js";
|
|
11
11
|
|
|
12
|
-
export { RunError, WithDefaultError, argument, bash, choice, command, commandLine, concat, constant, envVar, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
|
|
12
|
+
export { RunError, WithDefaultError, argument, bash, choice, command, commandLine, concat, constant, envVar, extractCommandNames, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
|
package/dist/message.cjs
CHANGED
|
@@ -155,13 +155,41 @@ function formatMessage(msg, options = {}) {
|
|
|
155
155
|
const resetSequence = `\x1b[0m${resetSuffix}`;
|
|
156
156
|
function* stream() {
|
|
157
157
|
const wordPattern = /\s*\S+\s*/g;
|
|
158
|
-
for (const term of msg) if (term.type === "text")
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
158
|
+
for (const term of msg) if (term.type === "text") if (term.text.includes("\n\n")) {
|
|
159
|
+
const paragraphs = term.text.split(/\n\n+/);
|
|
160
|
+
for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
|
|
161
|
+
if (paragraphIndex > 0) yield {
|
|
162
|
+
text: "\n",
|
|
163
|
+
width: -1
|
|
164
|
+
};
|
|
165
|
+
const paragraph = paragraphs[paragraphIndex].replace(/\n/g, " ");
|
|
166
|
+
wordPattern.lastIndex = 0;
|
|
167
|
+
while (true) {
|
|
168
|
+
const match = wordPattern.exec(paragraph);
|
|
169
|
+
if (match == null) break;
|
|
170
|
+
yield {
|
|
171
|
+
text: match[0],
|
|
172
|
+
width: match[0].length
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
const normalizedText = term.text.replace(/\n/g, " ");
|
|
178
|
+
if (normalizedText.trim() === "" && normalizedText.length > 0) yield {
|
|
179
|
+
text: " ",
|
|
180
|
+
width: 1
|
|
164
181
|
};
|
|
182
|
+
else {
|
|
183
|
+
wordPattern.lastIndex = 0;
|
|
184
|
+
while (true) {
|
|
185
|
+
const match = wordPattern.exec(normalizedText);
|
|
186
|
+
if (match == null) break;
|
|
187
|
+
yield {
|
|
188
|
+
text: match[0],
|
|
189
|
+
width: match[0].length
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
165
193
|
}
|
|
166
194
|
else if (term.type === "optionName") {
|
|
167
195
|
const name = useQuotes ? `\`${term.optionName}\`` : term.optionName;
|
|
@@ -223,6 +251,11 @@ function formatMessage(msg, options = {}) {
|
|
|
223
251
|
let output = "";
|
|
224
252
|
let totalWidth = 0;
|
|
225
253
|
for (const { text: text$1, width } of stream()) {
|
|
254
|
+
if (width === -1) {
|
|
255
|
+
output += text$1;
|
|
256
|
+
totalWidth = 0;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
226
259
|
if (options.maxWidth != null && totalWidth + width > options.maxWidth) {
|
|
227
260
|
output += "\n";
|
|
228
261
|
totalWidth = 0;
|
package/dist/message.js
CHANGED
|
@@ -154,13 +154,41 @@ function formatMessage(msg, options = {}) {
|
|
|
154
154
|
const resetSequence = `\x1b[0m${resetSuffix}`;
|
|
155
155
|
function* stream() {
|
|
156
156
|
const wordPattern = /\s*\S+\s*/g;
|
|
157
|
-
for (const term of msg) if (term.type === "text")
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
for (const term of msg) if (term.type === "text") if (term.text.includes("\n\n")) {
|
|
158
|
+
const paragraphs = term.text.split(/\n\n+/);
|
|
159
|
+
for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
|
|
160
|
+
if (paragraphIndex > 0) yield {
|
|
161
|
+
text: "\n",
|
|
162
|
+
width: -1
|
|
163
|
+
};
|
|
164
|
+
const paragraph = paragraphs[paragraphIndex].replace(/\n/g, " ");
|
|
165
|
+
wordPattern.lastIndex = 0;
|
|
166
|
+
while (true) {
|
|
167
|
+
const match = wordPattern.exec(paragraph);
|
|
168
|
+
if (match == null) break;
|
|
169
|
+
yield {
|
|
170
|
+
text: match[0],
|
|
171
|
+
width: match[0].length
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
const normalizedText = term.text.replace(/\n/g, " ");
|
|
177
|
+
if (normalizedText.trim() === "" && normalizedText.length > 0) yield {
|
|
178
|
+
text: " ",
|
|
179
|
+
width: 1
|
|
163
180
|
};
|
|
181
|
+
else {
|
|
182
|
+
wordPattern.lastIndex = 0;
|
|
183
|
+
while (true) {
|
|
184
|
+
const match = wordPattern.exec(normalizedText);
|
|
185
|
+
if (match == null) break;
|
|
186
|
+
yield {
|
|
187
|
+
text: match[0],
|
|
188
|
+
width: match[0].length
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
164
192
|
}
|
|
165
193
|
else if (term.type === "optionName") {
|
|
166
194
|
const name = useQuotes ? `\`${term.optionName}\`` : term.optionName;
|
|
@@ -222,6 +250,11 @@ function formatMessage(msg, options = {}) {
|
|
|
222
250
|
let output = "";
|
|
223
251
|
let totalWidth = 0;
|
|
224
252
|
for (const { text: text$1, width } of stream()) {
|
|
253
|
+
if (width === -1) {
|
|
254
|
+
output += text$1;
|
|
255
|
+
totalWidth = 0;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
225
258
|
if (options.maxWidth != null && totalWidth + width > options.maxWidth) {
|
|
226
259
|
output += "\n";
|
|
227
260
|
totalWidth = 0;
|
package/dist/parser.cjs
CHANGED
|
@@ -23,7 +23,8 @@ function parse(parser, args) {
|
|
|
23
23
|
let context = {
|
|
24
24
|
buffer: args,
|
|
25
25
|
optionsTerminated: false,
|
|
26
|
-
state: parser.initialState
|
|
26
|
+
state: parser.initialState,
|
|
27
|
+
usage: parser.usage
|
|
27
28
|
};
|
|
28
29
|
do {
|
|
29
30
|
const result = parser.parse(context);
|
|
@@ -81,7 +82,8 @@ function suggest(parser, args) {
|
|
|
81
82
|
let context = {
|
|
82
83
|
buffer: allButLast,
|
|
83
84
|
optionsTerminated: false,
|
|
84
|
-
state: parser.initialState
|
|
85
|
+
state: parser.initialState,
|
|
86
|
+
usage: parser.usage
|
|
85
87
|
};
|
|
86
88
|
while (context.buffer.length > 0) {
|
|
87
89
|
const result = parser.parse(context);
|
|
@@ -129,7 +131,8 @@ function getDocPage(parser, args = []) {
|
|
|
129
131
|
let context = {
|
|
130
132
|
buffer: args,
|
|
131
133
|
optionsTerminated: false,
|
|
132
|
-
state: parser.initialState
|
|
134
|
+
state: parser.initialState,
|
|
135
|
+
usage: parser.usage
|
|
133
136
|
};
|
|
134
137
|
do {
|
|
135
138
|
const result = parser.parse(context);
|
package/dist/parser.d.cts
CHANGED
|
@@ -125,6 +125,14 @@ interface ParserContext<TState> {
|
|
|
125
125
|
* that no further options should be processed.
|
|
126
126
|
*/
|
|
127
127
|
readonly optionsTerminated: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Usage information for the entire parser tree.
|
|
130
|
+
* Used to provide better error messages with suggestions for typos.
|
|
131
|
+
* When a parser encounters an invalid option or command, it can use
|
|
132
|
+
* this information to suggest similar valid options.
|
|
133
|
+
* @since 0.7.0
|
|
134
|
+
*/
|
|
135
|
+
readonly usage: Usage;
|
|
128
136
|
}
|
|
129
137
|
/**
|
|
130
138
|
* Represents a suggestion for command-line completion or guidance.
|
package/dist/parser.d.ts
CHANGED
|
@@ -125,6 +125,14 @@ interface ParserContext<TState> {
|
|
|
125
125
|
* that no further options should be processed.
|
|
126
126
|
*/
|
|
127
127
|
readonly optionsTerminated: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Usage information for the entire parser tree.
|
|
130
|
+
* Used to provide better error messages with suggestions for typos.
|
|
131
|
+
* When a parser encounters an invalid option or command, it can use
|
|
132
|
+
* this information to suggest similar valid options.
|
|
133
|
+
* @since 0.7.0
|
|
134
|
+
*/
|
|
135
|
+
readonly usage: Usage;
|
|
128
136
|
}
|
|
129
137
|
/**
|
|
130
138
|
* Represents a suggestion for command-line completion or guidance.
|
package/dist/parser.js
CHANGED
|
@@ -23,7 +23,8 @@ function parse(parser, args) {
|
|
|
23
23
|
let context = {
|
|
24
24
|
buffer: args,
|
|
25
25
|
optionsTerminated: false,
|
|
26
|
-
state: parser.initialState
|
|
26
|
+
state: parser.initialState,
|
|
27
|
+
usage: parser.usage
|
|
27
28
|
};
|
|
28
29
|
do {
|
|
29
30
|
const result = parser.parse(context);
|
|
@@ -81,7 +82,8 @@ function suggest(parser, args) {
|
|
|
81
82
|
let context = {
|
|
82
83
|
buffer: allButLast,
|
|
83
84
|
optionsTerminated: false,
|
|
84
|
-
state: parser.initialState
|
|
85
|
+
state: parser.initialState,
|
|
86
|
+
usage: parser.usage
|
|
85
87
|
};
|
|
86
88
|
while (context.buffer.length > 0) {
|
|
87
89
|
const result = parser.parse(context);
|
|
@@ -129,7 +131,8 @@ function getDocPage(parser, args = []) {
|
|
|
129
131
|
let context = {
|
|
130
132
|
buffer: args,
|
|
131
133
|
optionsTerminated: false,
|
|
132
|
-
state: parser.initialState
|
|
134
|
+
state: parser.initialState,
|
|
135
|
+
usage: parser.usage
|
|
133
136
|
};
|
|
134
137
|
do {
|
|
135
138
|
const result = parser.parse(context);
|
package/dist/primitives.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_message = require('./message.cjs');
|
|
2
|
+
const require_suggestion = require('./suggestion.cjs');
|
|
2
3
|
const require_valueparser = require('./valueparser.cjs');
|
|
3
4
|
|
|
4
5
|
//#region src/primitives.ts
|
|
@@ -182,10 +183,12 @@ function option(...args) {
|
|
|
182
183
|
};
|
|
183
184
|
}
|
|
184
185
|
}
|
|
186
|
+
const invalidOption = context.buffer[0];
|
|
187
|
+
const baseError = require_message.message`No matched option for ${require_message.optionName(invalidOption)}.`;
|
|
185
188
|
return {
|
|
186
189
|
success: false,
|
|
187
190
|
consumed: 0,
|
|
188
|
-
error:
|
|
191
|
+
error: require_suggestion.createErrorWithSuggestions(baseError, invalidOption, context.usage, "option")
|
|
189
192
|
};
|
|
190
193
|
},
|
|
191
194
|
complete(state) {
|
|
@@ -386,10 +389,12 @@ function flag(...args) {
|
|
|
386
389
|
consumed: [context.buffer[0].slice(0, 2)]
|
|
387
390
|
};
|
|
388
391
|
}
|
|
392
|
+
const invalidOption = context.buffer[0];
|
|
393
|
+
const baseError = require_message.message`No matched option for ${require_message.optionName(invalidOption)}.`;
|
|
389
394
|
return {
|
|
390
395
|
success: false,
|
|
391
396
|
consumed: 0,
|
|
392
|
-
error:
|
|
397
|
+
error: require_suggestion.createErrorWithSuggestions(baseError, invalidOption, context.usage, "option")
|
|
393
398
|
};
|
|
394
399
|
},
|
|
395
400
|
complete(state) {
|
|
@@ -565,11 +570,24 @@ function command(name, parser, options = {}) {
|
|
|
565
570
|
if (context.state === void 0) {
|
|
566
571
|
if (context.buffer.length < 1 || context.buffer[0] !== name) {
|
|
567
572
|
const actual = context.buffer.length > 0 ? context.buffer[0] : null;
|
|
568
|
-
|
|
573
|
+
if (options.errors?.notMatched) {
|
|
574
|
+
const errorMessage = options.errors.notMatched;
|
|
575
|
+
return {
|
|
576
|
+
success: false,
|
|
577
|
+
consumed: 0,
|
|
578
|
+
error: typeof errorMessage === "function" ? errorMessage(name, actual) : errorMessage
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (actual == null) return {
|
|
582
|
+
success: false,
|
|
583
|
+
consumed: 0,
|
|
584
|
+
error: require_message.message`Expected command ${require_message.optionName(name)}, but got end of input.`
|
|
585
|
+
};
|
|
586
|
+
const baseError = require_message.message`Expected command ${require_message.optionName(name)}, but got ${actual}.`;
|
|
569
587
|
return {
|
|
570
588
|
success: false,
|
|
571
589
|
consumed: 0,
|
|
572
|
-
error:
|
|
590
|
+
error: require_suggestion.createErrorWithSuggestions(baseError, actual, context.usage, "command")
|
|
573
591
|
};
|
|
574
592
|
}
|
|
575
593
|
return {
|
package/dist/primitives.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { message, metavar, optionName, optionNames } from "./message.js";
|
|
2
|
+
import { createErrorWithSuggestions } from "./suggestion.js";
|
|
2
3
|
import { isValueParser } from "./valueparser.js";
|
|
3
4
|
|
|
4
5
|
//#region src/primitives.ts
|
|
@@ -182,10 +183,12 @@ function option(...args) {
|
|
|
182
183
|
};
|
|
183
184
|
}
|
|
184
185
|
}
|
|
186
|
+
const invalidOption = context.buffer[0];
|
|
187
|
+
const baseError = message`No matched option for ${optionName(invalidOption)}.`;
|
|
185
188
|
return {
|
|
186
189
|
success: false,
|
|
187
190
|
consumed: 0,
|
|
188
|
-
error:
|
|
191
|
+
error: createErrorWithSuggestions(baseError, invalidOption, context.usage, "option")
|
|
189
192
|
};
|
|
190
193
|
},
|
|
191
194
|
complete(state) {
|
|
@@ -386,10 +389,12 @@ function flag(...args) {
|
|
|
386
389
|
consumed: [context.buffer[0].slice(0, 2)]
|
|
387
390
|
};
|
|
388
391
|
}
|
|
392
|
+
const invalidOption = context.buffer[0];
|
|
393
|
+
const baseError = message`No matched option for ${optionName(invalidOption)}.`;
|
|
389
394
|
return {
|
|
390
395
|
success: false,
|
|
391
396
|
consumed: 0,
|
|
392
|
-
error:
|
|
397
|
+
error: createErrorWithSuggestions(baseError, invalidOption, context.usage, "option")
|
|
393
398
|
};
|
|
394
399
|
},
|
|
395
400
|
complete(state) {
|
|
@@ -565,11 +570,24 @@ function command(name, parser, options = {}) {
|
|
|
565
570
|
if (context.state === void 0) {
|
|
566
571
|
if (context.buffer.length < 1 || context.buffer[0] !== name) {
|
|
567
572
|
const actual = context.buffer.length > 0 ? context.buffer[0] : null;
|
|
568
|
-
|
|
573
|
+
if (options.errors?.notMatched) {
|
|
574
|
+
const errorMessage = options.errors.notMatched;
|
|
575
|
+
return {
|
|
576
|
+
success: false,
|
|
577
|
+
consumed: 0,
|
|
578
|
+
error: typeof errorMessage === "function" ? errorMessage(name, actual) : errorMessage
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (actual == null) return {
|
|
582
|
+
success: false,
|
|
583
|
+
consumed: 0,
|
|
584
|
+
error: message`Expected command ${optionName(name)}, but got end of input.`
|
|
585
|
+
};
|
|
586
|
+
const baseError = message`Expected command ${optionName(name)}, but got ${actual}.`;
|
|
569
587
|
return {
|
|
570
588
|
success: false,
|
|
571
589
|
consumed: 0,
|
|
572
|
-
error:
|
|
590
|
+
error: createErrorWithSuggestions(baseError, actual, context.usage, "command")
|
|
573
591
|
};
|
|
574
592
|
}
|
|
575
593
|
return {
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const require_message = require('./message.cjs');
|
|
2
|
+
const require_usage = require('./usage.cjs');
|
|
3
|
+
|
|
4
|
+
//#region src/suggestion.ts
|
|
5
|
+
/**
|
|
6
|
+
* Calculates the Levenshtein distance between two strings.
|
|
7
|
+
*
|
|
8
|
+
* The Levenshtein distance is the minimum number of single-character edits
|
|
9
|
+
* (insertions, deletions, or substitutions) required to transform one string
|
|
10
|
+
* into another.
|
|
11
|
+
*
|
|
12
|
+
* @param source The source string
|
|
13
|
+
* @param target The target string
|
|
14
|
+
* @returns The edit distance (number of insertions, deletions, substitutions)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* levenshteinDistance("kitten", "sitting"); // returns 3
|
|
19
|
+
* levenshteinDistance("--verbos", "--verbose"); // returns 1
|
|
20
|
+
* levenshteinDistance("hello", "hello"); // returns 0
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
function levenshteinDistance(source, target) {
|
|
24
|
+
if (source.length === 0) return target.length;
|
|
25
|
+
if (target.length === 0) return source.length;
|
|
26
|
+
if (source.length > target.length) [source, target] = [target, source];
|
|
27
|
+
let previousRow = new Array(source.length + 1);
|
|
28
|
+
let currentRow = new Array(source.length + 1);
|
|
29
|
+
for (let i = 0; i <= source.length; i++) previousRow[i] = i;
|
|
30
|
+
for (let j = 1; j <= target.length; j++) {
|
|
31
|
+
currentRow[0] = j;
|
|
32
|
+
for (let i = 1; i <= source.length; i++) {
|
|
33
|
+
const cost = source[i - 1] === target[j - 1] ? 0 : 1;
|
|
34
|
+
currentRow[i] = Math.min(currentRow[i - 1] + 1, previousRow[i] + 1, previousRow[i - 1] + cost);
|
|
35
|
+
}
|
|
36
|
+
[previousRow, currentRow] = [currentRow, previousRow];
|
|
37
|
+
}
|
|
38
|
+
return previousRow[source.length];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Finds similar strings from a list of candidates.
|
|
42
|
+
*
|
|
43
|
+
* This function uses Levenshtein distance to find strings that are similar
|
|
44
|
+
* to the input string. Results are sorted by similarity (most similar first).
|
|
45
|
+
*
|
|
46
|
+
* @param input The input string to find matches for
|
|
47
|
+
* @param candidates List of candidate strings to compare against
|
|
48
|
+
* @param options Configuration options
|
|
49
|
+
* @returns Array of similar strings, sorted by similarity (most similar first)
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const candidates = ["--verbose", "--version", "--verify", "--help"];
|
|
54
|
+
* findSimilar("--verbos", candidates);
|
|
55
|
+
* // returns ["--verbose"]
|
|
56
|
+
*
|
|
57
|
+
* findSimilar("--ver", candidates, { maxDistance: 5 });
|
|
58
|
+
* // returns ["--verify", "--version", "--verbose"]
|
|
59
|
+
*
|
|
60
|
+
* findSimilar("--xyz", candidates);
|
|
61
|
+
* // returns [] (no similar matches)
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
function findSimilar(input, candidates, options = {}) {
|
|
65
|
+
const maxDistance = options.maxDistance ?? 3;
|
|
66
|
+
const maxDistanceRatio = options.maxDistanceRatio ?? .5;
|
|
67
|
+
const maxSuggestions = options.maxSuggestions ?? 3;
|
|
68
|
+
const caseSensitive = options.caseSensitive ?? false;
|
|
69
|
+
if (input.length === 0) return [];
|
|
70
|
+
const normalizedInput = caseSensitive ? input : input.toLowerCase();
|
|
71
|
+
const matches = [];
|
|
72
|
+
for (const candidate of candidates) {
|
|
73
|
+
const normalizedCandidate = caseSensitive ? candidate : candidate.toLowerCase();
|
|
74
|
+
const distance = levenshteinDistance(normalizedInput, normalizedCandidate);
|
|
75
|
+
if (distance === 0) return [candidate];
|
|
76
|
+
const distanceRatio = distance / input.length;
|
|
77
|
+
if (distance <= maxDistance && distanceRatio <= maxDistanceRatio) matches.push({
|
|
78
|
+
candidate,
|
|
79
|
+
distance
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
matches.sort((a, b) => {
|
|
83
|
+
if (a.distance !== b.distance) return a.distance - b.distance;
|
|
84
|
+
const lengthDiffA = Math.abs(a.candidate.length - input.length);
|
|
85
|
+
const lengthDiffB = Math.abs(b.candidate.length - input.length);
|
|
86
|
+
if (lengthDiffA !== lengthDiffB) return lengthDiffA - lengthDiffB;
|
|
87
|
+
return a.candidate.localeCompare(b.candidate);
|
|
88
|
+
});
|
|
89
|
+
return matches.slice(0, maxSuggestions).map((m) => m.candidate);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Creates a suggestion message for a mismatched option/command.
|
|
93
|
+
*
|
|
94
|
+
* This function formats suggestions in a user-friendly way:
|
|
95
|
+
* - No suggestions: returns empty message
|
|
96
|
+
* - One suggestion: "Did you mean `option`?"
|
|
97
|
+
* - Multiple suggestions: "Did you mean one of these?\n option1\n option2"
|
|
98
|
+
*
|
|
99
|
+
* @param suggestions List of similar valid options/commands
|
|
100
|
+
* @returns A Message array with suggestion text
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* createSuggestionMessage(["--verbose", "--version"]);
|
|
105
|
+
* // returns message parts for:
|
|
106
|
+
* // "Did you mean one of these?
|
|
107
|
+
* // --verbose
|
|
108
|
+
* // --version"
|
|
109
|
+
*
|
|
110
|
+
* createSuggestionMessage(["--verbose"]);
|
|
111
|
+
* // returns message parts for:
|
|
112
|
+
* // "Did you mean `--verbose`?"
|
|
113
|
+
*
|
|
114
|
+
* createSuggestionMessage([]);
|
|
115
|
+
* // returns []
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
function createSuggestionMessage(suggestions) {
|
|
119
|
+
if (suggestions.length === 0) return [];
|
|
120
|
+
if (suggestions.length === 1) return require_message.message`Did you mean ${require_message.optionName(suggestions[0])}?`;
|
|
121
|
+
const messageParts = [require_message.text("Did you mean one of these?")];
|
|
122
|
+
for (const suggestion of suggestions) {
|
|
123
|
+
messageParts.push(require_message.text("\n "));
|
|
124
|
+
messageParts.push(require_message.optionName(suggestion));
|
|
125
|
+
}
|
|
126
|
+
return messageParts;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Creates an error message with suggestions for similar options or commands.
|
|
130
|
+
*
|
|
131
|
+
* This is a convenience function that combines the functionality of
|
|
132
|
+
* `findSimilar()` and `createSuggestionMessage()` to generate user-friendly
|
|
133
|
+
* error messages with "Did you mean?" suggestions.
|
|
134
|
+
*
|
|
135
|
+
* @param baseError The base error message to display
|
|
136
|
+
* @param invalidInput The invalid option or command name that the user typed
|
|
137
|
+
* @param usage The usage information to extract available options/commands from
|
|
138
|
+
* @param type What type of names to suggest ("option", "command", or "both")
|
|
139
|
+
* @returns A message combining the base error with suggestions, or just the
|
|
140
|
+
* base error if no similar names are found
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const baseError = message`No matched option for ${optionName("--verbos")}.`;
|
|
145
|
+
* const error = createErrorWithSuggestions(
|
|
146
|
+
* baseError,
|
|
147
|
+
* "--verbos",
|
|
148
|
+
* context.usage,
|
|
149
|
+
* "option"
|
|
150
|
+
* );
|
|
151
|
+
* // Returns: "No matched option for `--verbos`.\nDid you mean `--verbose`?"
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* @since 0.7.0
|
|
155
|
+
*/
|
|
156
|
+
function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both") {
|
|
157
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
158
|
+
if (type === "option" || type === "both") for (const name of require_usage.extractOptionNames(usage)) candidates.add(name);
|
|
159
|
+
if (type === "command" || type === "both") for (const name of require_usage.extractCommandNames(usage)) candidates.add(name);
|
|
160
|
+
const suggestions = findSimilar(invalidInput, candidates, {
|
|
161
|
+
maxDistance: 3,
|
|
162
|
+
maxDistanceRatio: .5,
|
|
163
|
+
maxSuggestions: 3,
|
|
164
|
+
caseSensitive: false
|
|
165
|
+
});
|
|
166
|
+
const suggestionMsg = createSuggestionMessage(suggestions);
|
|
167
|
+
return suggestionMsg.length > 0 ? [
|
|
168
|
+
...baseError,
|
|
169
|
+
require_message.text("\n\n"),
|
|
170
|
+
...suggestionMsg
|
|
171
|
+
] : baseError;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
exports.createErrorWithSuggestions = createErrorWithSuggestions;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { message, optionName, text } from "./message.js";
|
|
2
|
+
import { extractCommandNames, extractOptionNames } from "./usage.js";
|
|
3
|
+
|
|
4
|
+
//#region src/suggestion.ts
|
|
5
|
+
/**
|
|
6
|
+
* Calculates the Levenshtein distance between two strings.
|
|
7
|
+
*
|
|
8
|
+
* The Levenshtein distance is the minimum number of single-character edits
|
|
9
|
+
* (insertions, deletions, or substitutions) required to transform one string
|
|
10
|
+
* into another.
|
|
11
|
+
*
|
|
12
|
+
* @param source The source string
|
|
13
|
+
* @param target The target string
|
|
14
|
+
* @returns The edit distance (number of insertions, deletions, substitutions)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* levenshteinDistance("kitten", "sitting"); // returns 3
|
|
19
|
+
* levenshteinDistance("--verbos", "--verbose"); // returns 1
|
|
20
|
+
* levenshteinDistance("hello", "hello"); // returns 0
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
function levenshteinDistance(source, target) {
|
|
24
|
+
if (source.length === 0) return target.length;
|
|
25
|
+
if (target.length === 0) return source.length;
|
|
26
|
+
if (source.length > target.length) [source, target] = [target, source];
|
|
27
|
+
let previousRow = new Array(source.length + 1);
|
|
28
|
+
let currentRow = new Array(source.length + 1);
|
|
29
|
+
for (let i = 0; i <= source.length; i++) previousRow[i] = i;
|
|
30
|
+
for (let j = 1; j <= target.length; j++) {
|
|
31
|
+
currentRow[0] = j;
|
|
32
|
+
for (let i = 1; i <= source.length; i++) {
|
|
33
|
+
const cost = source[i - 1] === target[j - 1] ? 0 : 1;
|
|
34
|
+
currentRow[i] = Math.min(currentRow[i - 1] + 1, previousRow[i] + 1, previousRow[i - 1] + cost);
|
|
35
|
+
}
|
|
36
|
+
[previousRow, currentRow] = [currentRow, previousRow];
|
|
37
|
+
}
|
|
38
|
+
return previousRow[source.length];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Finds similar strings from a list of candidates.
|
|
42
|
+
*
|
|
43
|
+
* This function uses Levenshtein distance to find strings that are similar
|
|
44
|
+
* to the input string. Results are sorted by similarity (most similar first).
|
|
45
|
+
*
|
|
46
|
+
* @param input The input string to find matches for
|
|
47
|
+
* @param candidates List of candidate strings to compare against
|
|
48
|
+
* @param options Configuration options
|
|
49
|
+
* @returns Array of similar strings, sorted by similarity (most similar first)
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const candidates = ["--verbose", "--version", "--verify", "--help"];
|
|
54
|
+
* findSimilar("--verbos", candidates);
|
|
55
|
+
* // returns ["--verbose"]
|
|
56
|
+
*
|
|
57
|
+
* findSimilar("--ver", candidates, { maxDistance: 5 });
|
|
58
|
+
* // returns ["--verify", "--version", "--verbose"]
|
|
59
|
+
*
|
|
60
|
+
* findSimilar("--xyz", candidates);
|
|
61
|
+
* // returns [] (no similar matches)
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
function findSimilar(input, candidates, options = {}) {
|
|
65
|
+
const maxDistance = options.maxDistance ?? 3;
|
|
66
|
+
const maxDistanceRatio = options.maxDistanceRatio ?? .5;
|
|
67
|
+
const maxSuggestions = options.maxSuggestions ?? 3;
|
|
68
|
+
const caseSensitive = options.caseSensitive ?? false;
|
|
69
|
+
if (input.length === 0) return [];
|
|
70
|
+
const normalizedInput = caseSensitive ? input : input.toLowerCase();
|
|
71
|
+
const matches = [];
|
|
72
|
+
for (const candidate of candidates) {
|
|
73
|
+
const normalizedCandidate = caseSensitive ? candidate : candidate.toLowerCase();
|
|
74
|
+
const distance = levenshteinDistance(normalizedInput, normalizedCandidate);
|
|
75
|
+
if (distance === 0) return [candidate];
|
|
76
|
+
const distanceRatio = distance / input.length;
|
|
77
|
+
if (distance <= maxDistance && distanceRatio <= maxDistanceRatio) matches.push({
|
|
78
|
+
candidate,
|
|
79
|
+
distance
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
matches.sort((a, b) => {
|
|
83
|
+
if (a.distance !== b.distance) return a.distance - b.distance;
|
|
84
|
+
const lengthDiffA = Math.abs(a.candidate.length - input.length);
|
|
85
|
+
const lengthDiffB = Math.abs(b.candidate.length - input.length);
|
|
86
|
+
if (lengthDiffA !== lengthDiffB) return lengthDiffA - lengthDiffB;
|
|
87
|
+
return a.candidate.localeCompare(b.candidate);
|
|
88
|
+
});
|
|
89
|
+
return matches.slice(0, maxSuggestions).map((m) => m.candidate);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Creates a suggestion message for a mismatched option/command.
|
|
93
|
+
*
|
|
94
|
+
* This function formats suggestions in a user-friendly way:
|
|
95
|
+
* - No suggestions: returns empty message
|
|
96
|
+
* - One suggestion: "Did you mean `option`?"
|
|
97
|
+
* - Multiple suggestions: "Did you mean one of these?\n option1\n option2"
|
|
98
|
+
*
|
|
99
|
+
* @param suggestions List of similar valid options/commands
|
|
100
|
+
* @returns A Message array with suggestion text
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* createSuggestionMessage(["--verbose", "--version"]);
|
|
105
|
+
* // returns message parts for:
|
|
106
|
+
* // "Did you mean one of these?
|
|
107
|
+
* // --verbose
|
|
108
|
+
* // --version"
|
|
109
|
+
*
|
|
110
|
+
* createSuggestionMessage(["--verbose"]);
|
|
111
|
+
* // returns message parts for:
|
|
112
|
+
* // "Did you mean `--verbose`?"
|
|
113
|
+
*
|
|
114
|
+
* createSuggestionMessage([]);
|
|
115
|
+
* // returns []
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
function createSuggestionMessage(suggestions) {
|
|
119
|
+
if (suggestions.length === 0) return [];
|
|
120
|
+
if (suggestions.length === 1) return message`Did you mean ${optionName(suggestions[0])}?`;
|
|
121
|
+
const messageParts = [text("Did you mean one of these?")];
|
|
122
|
+
for (const suggestion of suggestions) {
|
|
123
|
+
messageParts.push(text("\n "));
|
|
124
|
+
messageParts.push(optionName(suggestion));
|
|
125
|
+
}
|
|
126
|
+
return messageParts;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Creates an error message with suggestions for similar options or commands.
|
|
130
|
+
*
|
|
131
|
+
* This is a convenience function that combines the functionality of
|
|
132
|
+
* `findSimilar()` and `createSuggestionMessage()` to generate user-friendly
|
|
133
|
+
* error messages with "Did you mean?" suggestions.
|
|
134
|
+
*
|
|
135
|
+
* @param baseError The base error message to display
|
|
136
|
+
* @param invalidInput The invalid option or command name that the user typed
|
|
137
|
+
* @param usage The usage information to extract available options/commands from
|
|
138
|
+
* @param type What type of names to suggest ("option", "command", or "both")
|
|
139
|
+
* @returns A message combining the base error with suggestions, or just the
|
|
140
|
+
* base error if no similar names are found
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const baseError = message`No matched option for ${optionName("--verbos")}.`;
|
|
145
|
+
* const error = createErrorWithSuggestions(
|
|
146
|
+
* baseError,
|
|
147
|
+
* "--verbos",
|
|
148
|
+
* context.usage,
|
|
149
|
+
* "option"
|
|
150
|
+
* );
|
|
151
|
+
* // Returns: "No matched option for `--verbos`.\nDid you mean `--verbose`?"
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* @since 0.7.0
|
|
155
|
+
*/
|
|
156
|
+
function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both") {
|
|
157
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
158
|
+
if (type === "option" || type === "both") for (const name of extractOptionNames(usage)) candidates.add(name);
|
|
159
|
+
if (type === "command" || type === "both") for (const name of extractCommandNames(usage)) candidates.add(name);
|
|
160
|
+
const suggestions = findSimilar(invalidInput, candidates, {
|
|
161
|
+
maxDistance: 3,
|
|
162
|
+
maxDistanceRatio: .5,
|
|
163
|
+
maxSuggestions: 3,
|
|
164
|
+
caseSensitive: false
|
|
165
|
+
});
|
|
166
|
+
const suggestionMsg = createSuggestionMessage(suggestions);
|
|
167
|
+
return suggestionMsg.length > 0 ? [
|
|
168
|
+
...baseError,
|
|
169
|
+
text("\n\n"),
|
|
170
|
+
...suggestionMsg
|
|
171
|
+
] : baseError;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
export { createErrorWithSuggestions };
|
package/dist/usage.cjs
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
function extractOptionNames(usage) {
|
|
24
24
|
const names = /* @__PURE__ */ new Set();
|
|
25
25
|
function traverseUsage(terms) {
|
|
26
|
+
if (!terms || !Array.isArray(terms)) return;
|
|
26
27
|
for (const term of terms) if (term.type === "option") for (const name of term.names) names.add(name);
|
|
27
28
|
else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
|
|
28
29
|
else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
|
|
@@ -31,6 +32,37 @@ function extractOptionNames(usage) {
|
|
|
31
32
|
return names;
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
35
|
+
* Extracts all command names from a Usage array.
|
|
36
|
+
*
|
|
37
|
+
* This function recursively traverses the usage structure and collects
|
|
38
|
+
* all command names, similar to {@link extractOptionNames}.
|
|
39
|
+
*
|
|
40
|
+
* @param usage The usage structure to extract command names from
|
|
41
|
+
* @returns A Set of all command names found in the usage structure
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const usage: Usage = [
|
|
46
|
+
* { type: "command", name: "build" },
|
|
47
|
+
* { type: "command", name: "test" },
|
|
48
|
+
* ];
|
|
49
|
+
* const names = extractCommandNames(usage);
|
|
50
|
+
* // names = Set(["build", "test"])
|
|
51
|
+
* ```
|
|
52
|
+
* @since 0.7.0
|
|
53
|
+
*/
|
|
54
|
+
function extractCommandNames(usage) {
|
|
55
|
+
const names = /* @__PURE__ */ new Set();
|
|
56
|
+
function traverseUsage(terms) {
|
|
57
|
+
if (!terms || !Array.isArray(terms)) return;
|
|
58
|
+
for (const term of terms) if (term.type === "command") names.add(term.name);
|
|
59
|
+
else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
|
|
60
|
+
else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
|
|
61
|
+
}
|
|
62
|
+
traverseUsage(usage);
|
|
63
|
+
return names;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
34
66
|
* Formats a usage description into a human-readable string representation
|
|
35
67
|
* suitable for command-line help text.
|
|
36
68
|
*
|
|
@@ -267,6 +299,7 @@ function* formatUsageTermInternal(term, options) {
|
|
|
267
299
|
}
|
|
268
300
|
|
|
269
301
|
//#endregion
|
|
302
|
+
exports.extractCommandNames = extractCommandNames;
|
|
270
303
|
exports.extractOptionNames = extractOptionNames;
|
|
271
304
|
exports.formatUsage = formatUsage;
|
|
272
305
|
exports.formatUsageTerm = formatUsageTerm;
|
package/dist/usage.d.cts
CHANGED
|
@@ -137,6 +137,27 @@ type Usage = readonly UsageTerm[];
|
|
|
137
137
|
* ```
|
|
138
138
|
*/
|
|
139
139
|
declare function extractOptionNames(usage: Usage): Set<string>;
|
|
140
|
+
/**
|
|
141
|
+
* Extracts all command names from a Usage array.
|
|
142
|
+
*
|
|
143
|
+
* This function recursively traverses the usage structure and collects
|
|
144
|
+
* all command names, similar to {@link extractOptionNames}.
|
|
145
|
+
*
|
|
146
|
+
* @param usage The usage structure to extract command names from
|
|
147
|
+
* @returns A Set of all command names found in the usage structure
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* const usage: Usage = [
|
|
152
|
+
* { type: "command", name: "build" },
|
|
153
|
+
* { type: "command", name: "test" },
|
|
154
|
+
* ];
|
|
155
|
+
* const names = extractCommandNames(usage);
|
|
156
|
+
* // names = Set(["build", "test"])
|
|
157
|
+
* ```
|
|
158
|
+
* @since 0.7.0
|
|
159
|
+
*/
|
|
160
|
+
declare function extractCommandNames(usage: Usage): Set<string>;
|
|
140
161
|
/**
|
|
141
162
|
* Options for formatting usage descriptions.
|
|
142
163
|
*/
|
|
@@ -235,4 +256,4 @@ interface UsageTermFormatOptions extends UsageFormatOptions {
|
|
|
235
256
|
*/
|
|
236
257
|
declare function formatUsageTerm(term: UsageTerm, options?: UsageTermFormatOptions): string;
|
|
237
258
|
//#endregion
|
|
238
|
-
export { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage };
|
|
259
|
+
export { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage };
|
package/dist/usage.d.ts
CHANGED
|
@@ -137,6 +137,27 @@ type Usage = readonly UsageTerm[];
|
|
|
137
137
|
* ```
|
|
138
138
|
*/
|
|
139
139
|
declare function extractOptionNames(usage: Usage): Set<string>;
|
|
140
|
+
/**
|
|
141
|
+
* Extracts all command names from a Usage array.
|
|
142
|
+
*
|
|
143
|
+
* This function recursively traverses the usage structure and collects
|
|
144
|
+
* all command names, similar to {@link extractOptionNames}.
|
|
145
|
+
*
|
|
146
|
+
* @param usage The usage structure to extract command names from
|
|
147
|
+
* @returns A Set of all command names found in the usage structure
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* const usage: Usage = [
|
|
152
|
+
* { type: "command", name: "build" },
|
|
153
|
+
* { type: "command", name: "test" },
|
|
154
|
+
* ];
|
|
155
|
+
* const names = extractCommandNames(usage);
|
|
156
|
+
* // names = Set(["build", "test"])
|
|
157
|
+
* ```
|
|
158
|
+
* @since 0.7.0
|
|
159
|
+
*/
|
|
160
|
+
declare function extractCommandNames(usage: Usage): Set<string>;
|
|
140
161
|
/**
|
|
141
162
|
* Options for formatting usage descriptions.
|
|
142
163
|
*/
|
|
@@ -235,4 +256,4 @@ interface UsageTermFormatOptions extends UsageFormatOptions {
|
|
|
235
256
|
*/
|
|
236
257
|
declare function formatUsageTerm(term: UsageTerm, options?: UsageTermFormatOptions): string;
|
|
237
258
|
//#endregion
|
|
238
|
-
export { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage };
|
|
259
|
+
export { OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage };
|
package/dist/usage.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
function extractOptionNames(usage) {
|
|
23
23
|
const names = /* @__PURE__ */ new Set();
|
|
24
24
|
function traverseUsage(terms) {
|
|
25
|
+
if (!terms || !Array.isArray(terms)) return;
|
|
25
26
|
for (const term of terms) if (term.type === "option") for (const name of term.names) names.add(name);
|
|
26
27
|
else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
|
|
27
28
|
else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
|
|
@@ -30,6 +31,37 @@ function extractOptionNames(usage) {
|
|
|
30
31
|
return names;
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
34
|
+
* Extracts all command names from a Usage array.
|
|
35
|
+
*
|
|
36
|
+
* This function recursively traverses the usage structure and collects
|
|
37
|
+
* all command names, similar to {@link extractOptionNames}.
|
|
38
|
+
*
|
|
39
|
+
* @param usage The usage structure to extract command names from
|
|
40
|
+
* @returns A Set of all command names found in the usage structure
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const usage: Usage = [
|
|
45
|
+
* { type: "command", name: "build" },
|
|
46
|
+
* { type: "command", name: "test" },
|
|
47
|
+
* ];
|
|
48
|
+
* const names = extractCommandNames(usage);
|
|
49
|
+
* // names = Set(["build", "test"])
|
|
50
|
+
* ```
|
|
51
|
+
* @since 0.7.0
|
|
52
|
+
*/
|
|
53
|
+
function extractCommandNames(usage) {
|
|
54
|
+
const names = /* @__PURE__ */ new Set();
|
|
55
|
+
function traverseUsage(terms) {
|
|
56
|
+
if (!terms || !Array.isArray(terms)) return;
|
|
57
|
+
for (const term of terms) if (term.type === "command") names.add(term.name);
|
|
58
|
+
else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
|
|
59
|
+
else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
|
|
60
|
+
}
|
|
61
|
+
traverseUsage(usage);
|
|
62
|
+
return names;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
33
65
|
* Formats a usage description into a human-readable string representation
|
|
34
66
|
* suitable for command-line help text.
|
|
35
67
|
*
|
|
@@ -266,4 +298,4 @@ function* formatUsageTermInternal(term, options) {
|
|
|
266
298
|
}
|
|
267
299
|
|
|
268
300
|
//#endregion
|
|
269
|
-
export { extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage };
|
|
301
|
+
export { extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage };
|