@optique/core 1.0.0-dev.908 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/annotation-state.cjs +425 -0
  2. package/dist/annotation-state.d.cts +24 -0
  3. package/dist/annotation-state.d.ts +24 -0
  4. package/dist/annotation-state.js +414 -0
  5. package/dist/annotations.cjs +2 -248
  6. package/dist/annotations.d.cts +2 -137
  7. package/dist/annotations.d.ts +2 -137
  8. package/dist/annotations.js +2 -238
  9. package/dist/completion.cjs +611 -100
  10. package/dist/completion.d.cts +1 -1
  11. package/dist/completion.d.ts +1 -1
  12. package/dist/completion.js +611 -100
  13. package/dist/constructs.cjs +3338 -827
  14. package/dist/constructs.d.cts +48 -7
  15. package/dist/constructs.d.ts +48 -7
  16. package/dist/constructs.js +3338 -827
  17. package/dist/context.cjs +0 -23
  18. package/dist/context.d.cts +119 -53
  19. package/dist/context.d.ts +119 -53
  20. package/dist/context.js +0 -22
  21. package/dist/dependency-metadata.cjs +139 -0
  22. package/dist/dependency-metadata.d.cts +112 -0
  23. package/dist/dependency-metadata.d.ts +112 -0
  24. package/dist/dependency-metadata.js +138 -0
  25. package/dist/dependency-runtime.cjs +698 -0
  26. package/dist/dependency-runtime.d.cts +149 -0
  27. package/dist/dependency-runtime.d.ts +149 -0
  28. package/dist/dependency-runtime.js +687 -0
  29. package/dist/dependency.cjs +7 -928
  30. package/dist/dependency.d.cts +2 -794
  31. package/dist/dependency.d.ts +2 -794
  32. package/dist/dependency.js +2 -899
  33. package/dist/displaywidth.cjs +44 -0
  34. package/dist/displaywidth.js +43 -0
  35. package/dist/doc.cjs +285 -23
  36. package/dist/doc.d.cts +57 -2
  37. package/dist/doc.d.ts +57 -2
  38. package/dist/doc.js +283 -25
  39. package/dist/execution-context.cjs +56 -0
  40. package/dist/execution-context.js +53 -0
  41. package/dist/extension.cjs +87 -0
  42. package/dist/extension.d.cts +97 -0
  43. package/dist/extension.d.ts +97 -0
  44. package/dist/extension.js +76 -0
  45. package/dist/facade.cjs +718 -523
  46. package/dist/facade.d.cts +87 -18
  47. package/dist/facade.d.ts +87 -18
  48. package/dist/facade.js +718 -523
  49. package/dist/index.cjs +14 -29
  50. package/dist/index.d.cts +10 -10
  51. package/dist/index.d.ts +10 -10
  52. package/dist/index.js +7 -7
  53. package/dist/input-trace.cjs +56 -0
  54. package/dist/input-trace.d.cts +77 -0
  55. package/dist/input-trace.d.ts +77 -0
  56. package/dist/input-trace.js +55 -0
  57. package/dist/internal/annotations.cjs +316 -0
  58. package/dist/internal/annotations.d.cts +140 -0
  59. package/dist/internal/annotations.d.ts +140 -0
  60. package/dist/internal/annotations.js +306 -0
  61. package/dist/internal/dependency.cjs +984 -0
  62. package/dist/internal/dependency.d.cts +539 -0
  63. package/dist/internal/dependency.d.ts +539 -0
  64. package/dist/internal/dependency.js +964 -0
  65. package/dist/{mode-dispatch.cjs → internal/mode-dispatch.cjs} +1 -3
  66. package/dist/{mode-dispatch.d.cts → internal/mode-dispatch.d.cts} +3 -7
  67. package/dist/{mode-dispatch.d.ts → internal/mode-dispatch.d.ts} +3 -7
  68. package/dist/{mode-dispatch.js → internal/mode-dispatch.js} +1 -3
  69. package/dist/internal/parser.cjs +728 -0
  70. package/dist/internal/parser.d.cts +947 -0
  71. package/dist/internal/parser.d.ts +947 -0
  72. package/dist/internal/parser.js +711 -0
  73. package/dist/message.cjs +84 -26
  74. package/dist/message.d.cts +49 -9
  75. package/dist/message.d.ts +49 -9
  76. package/dist/message.js +84 -27
  77. package/dist/modifiers.cjs +1023 -240
  78. package/dist/modifiers.d.cts +42 -1
  79. package/dist/modifiers.d.ts +42 -1
  80. package/dist/modifiers.js +1023 -240
  81. package/dist/parser.cjs +11 -463
  82. package/dist/parser.d.cts +3 -537
  83. package/dist/parser.d.ts +3 -537
  84. package/dist/parser.js +2 -433
  85. package/dist/phase2-seed.cjs +59 -0
  86. package/dist/phase2-seed.js +56 -0
  87. package/dist/primitives.cjs +557 -208
  88. package/dist/primitives.d.cts +10 -14
  89. package/dist/primitives.d.ts +10 -14
  90. package/dist/primitives.js +557 -208
  91. package/dist/program.cjs +5 -1
  92. package/dist/program.d.cts +5 -3
  93. package/dist/program.d.ts +5 -3
  94. package/dist/program.js +6 -1
  95. package/dist/suggestion.cjs +22 -8
  96. package/dist/suggestion.js +22 -8
  97. package/dist/usage-internals.cjs +3 -2
  98. package/dist/usage-internals.js +4 -2
  99. package/dist/usage.cjs +195 -40
  100. package/dist/usage.d.cts +92 -11
  101. package/dist/usage.d.ts +92 -11
  102. package/dist/usage.js +194 -41
  103. package/dist/validate.cjs +170 -0
  104. package/dist/validate.js +164 -0
  105. package/dist/valueparser.cjs +1278 -191
  106. package/dist/valueparser.d.cts +330 -20
  107. package/dist/valueparser.d.ts +330 -20
  108. package/dist/valueparser.js +1277 -192
  109. package/package.json +9 -9
package/dist/usage.d.ts CHANGED
@@ -10,8 +10,14 @@ import { NonEmptyString } from "./nonempty.js";
10
10
  * - POSIX-style short options (`-o`) or Java-style options (`-option`)
11
11
  * - MS-DOS-style options (`/o`, `/option`)
12
12
  * - Plus-prefixed options (`+o`)
13
+ *
14
+ * Each prefix must be followed by at least one character, so bare prefixes
15
+ * like `"-"`, `"/"`, or `"+"` are rejected at compile time. Due to
16
+ * TypeScript template literal limitations, `"--"` still matches the
17
+ * `-${NonEmptyString}` branch and is only rejected at runtime by the
18
+ * `option()` and `flag()` validators.
13
19
  */
14
- type OptionName = `--${string}` | `-${string}` | `/${string}` | `+${string}`;
20
+ type OptionName = `--${NonEmptyString}` | `-${NonEmptyString}` | `/${NonEmptyString}` | `+${NonEmptyString}`;
15
21
  /**
16
22
  * Visibility control for parser terms.
17
23
  *
@@ -172,6 +178,18 @@ type UsageTerm =
172
178
  * The literal value that must be provided exactly as written.
173
179
  */
174
180
  readonly value: string;
181
+ /**
182
+ * When `true`, this literal was derived from an option's metavar by
183
+ * `appendLiteralToUsage()` in `conditional()` and represents an option
184
+ * value, not a standalone positional token.
185
+ * {@link extractLeadingLiteralValues} and the `skipOptionValueLiterals`
186
+ * mode of `branchConsumesToken()` use this to distinguish option values
187
+ * from real positional literals. {@link extractLeadingOptionNames} and
188
+ * {@link extractLeadingCommandNames} intentionally still treat these
189
+ * literals as positional gates.
190
+ * @since 1.0.0
191
+ */
192
+ readonly optionValue?: boolean;
175
193
  }
176
194
  /**
177
195
  * A pass-through term, which represents unrecognized options that are
@@ -214,6 +232,8 @@ type Usage = readonly UsageTerm[];
214
232
  * multiple, and exclusive terms.
215
233
  *
216
234
  * @param usage The usage description to extract option names from.
235
+ * @param includeHidden Whether to include fully hidden options (`hidden: true`)
236
+ * in the result. Defaults to `false`.
217
237
  * @returns A set containing all option names found in the usage description.
218
238
  *
219
239
  * @example
@@ -226,15 +246,17 @@ type Usage = readonly UsageTerm[];
226
246
  * // names = Set(["--verbose", "-v", "--quiet", "-q"])
227
247
  * ```
228
248
  */
229
- declare function extractOptionNames(usage: Usage): Set<string>;
249
+ declare function extractOptionNames(usage: Usage, includeHidden?: boolean): Set<string>;
230
250
  /**
231
251
  * Extracts all command names from a Usage array.
232
252
  *
233
253
  * This function recursively traverses the usage structure and collects
234
254
  * all command names, similar to {@link extractOptionNames}.
235
255
  *
236
- * @param usage The usage structure to extract command names from
237
- * @returns A Set of all command names found in the usage structure
256
+ * @param usage The usage structure to extract command names from.
257
+ * @param includeHidden Whether to include fully hidden commands
258
+ * (`hidden: true`) in the result. Defaults to `false`.
259
+ * @returns A set of all command names found in the usage structure.
238
260
  *
239
261
  * @example
240
262
  * ```typescript
@@ -247,7 +269,20 @@ declare function extractOptionNames(usage: Usage): Set<string>;
247
269
  * ```
248
270
  * @since 0.7.0
249
271
  */
250
- declare function extractCommandNames(usage: Usage): Set<string>;
272
+ declare function extractCommandNames(usage: Usage, includeHidden?: boolean): Set<string>;
273
+ /**
274
+ * Extracts all literal values from a usage description.
275
+ *
276
+ * This function recursively traverses the usage tree and collects all
277
+ * `literal` term values. Literal values represent fixed strings that
278
+ * the user must type (e.g., conditional discriminator values like
279
+ * `"server"` in `conditional(option("--mode", string()), { server: ... })`).
280
+ *
281
+ * @param usage The usage description to extract literal values from.
282
+ * @returns A set of all literal values found in the usage description.
283
+ * @since 1.0.0
284
+ */
285
+ declare function extractLiteralValues(usage: Usage): Set<string>;
251
286
  /**
252
287
  * Extracts all argument metavars from a Usage array.
253
288
  *
@@ -319,6 +354,8 @@ interface UsageFormatOptions {
319
354
  * @param options Optional formatting options to customize the output.
320
355
  * See {@link UsageFormatOptions} for available options.
321
356
  * @returns A formatted string representation of the usage description.
357
+ * @throws {TypeError} If `programName` is not a string, is empty,
358
+ * whitespace-only, or contains control characters.
322
359
  */
323
360
  declare function formatUsage(programName: string, usage: Usage, options?: UsageFormatOptions): string;
324
361
  /**
@@ -326,14 +363,27 @@ declare function formatUsage(programName: string, usage: Usage, options?: UsageF
326
363
  * sorting terms for better readability, and ensuring consistent structure
327
364
  * throughout the usage tree.
328
365
  *
329
- * This function performs two main operations:
366
+ * This function performs three main operations:
330
367
  *
331
- * 1. *Flattening*: Recursively processes all usage terms and merges any
368
+ * 1. *Stripping*: Removes degenerate terms that would render as empty or
369
+ * malformed output, such as options with no names, commands with empty
370
+ * names, arguments with empty metavars, and container terms (`optional`,
371
+ * `multiple`, `exclusive`) whose top-level terms array is empty after
372
+ * recursive normalization. Exclusive branches representing valid
373
+ * zero-token alternatives (e.g., `conditional()` default branches or
374
+ * `optional(constant(...))`) and empty-value literals are preserved.
375
+ * Only branches that become empty because all their content was
376
+ * malformed are removed.
377
+ *
378
+ * 2. *Flattening*: Recursively processes all usage terms and merges any
332
379
  * nested exclusive terms into their parent exclusive term to avoid
333
380
  * redundant nesting. For example, an exclusive term containing another
334
381
  * exclusive term will have its nested terms flattened into the parent.
382
+ * Similarly, nested optional terms are collapsed:
383
+ * `optional(optional(X))` becomes `optional(X)` when the outer optional
384
+ * contains only a single inner optional term.
335
385
  *
336
- * 2. *Sorting*: Reorders terms to improve readability by placing:
386
+ * 3. *Sorting*: Reorders terms to improve readability by placing:
337
387
  * - Commands (subcommands) first
338
388
  * - Options and other terms in the middle
339
389
  * - Positional arguments last (including optional/multiple wrappers around
@@ -343,10 +393,31 @@ declare function formatUsage(programName: string, usage: Usage, options?: UsageF
343
393
  * positional arguments and treats them as arguments for sorting purposes.
344
394
  *
345
395
  * @param usage The usage description to normalize.
346
- * @returns A normalized usage description with flattened exclusive terms
347
- * and terms sorted for optimal readability.
396
+ * @returns A normalized usage description with degenerate terms removed,
397
+ * nested exclusive and optional terms flattened, and remaining
398
+ * terms sorted for optimal readability.
348
399
  */
349
400
  declare function normalizeUsage(usage: Usage): Usage;
401
+ /**
402
+ * Creates a deep clone of a single {@link UsageTerm}. Recursive term
403
+ * variants (`optional`, `multiple`, `exclusive`) are cloned recursively.
404
+ * For `command` terms, a function-valued `usageLine` is preserved by
405
+ * reference (functions are stateless callbacks), while an array-valued
406
+ * `usageLine` is deep-cloned.
407
+ *
408
+ * @param term The usage term to clone.
409
+ * @returns A structurally equal but referentially distinct copy.
410
+ * @since 1.0.0
411
+ */
412
+ declare function cloneUsageTerm(term: UsageTerm): UsageTerm;
413
+ /**
414
+ * Creates a deep clone of a {@link Usage} array and all of its terms.
415
+ *
416
+ * @param usage The usage array to clone.
417
+ * @returns A mutable array of deeply cloned usage terms.
418
+ * @since 1.0.0
419
+ */
420
+ declare function cloneUsage(usage: Usage): UsageTerm[];
350
421
  /**
351
422
  * Options for formatting a single {@link UsageTerm}.
352
423
  */
@@ -356,6 +427,16 @@ interface UsageTermFormatOptions extends UsageFormatOptions {
356
427
  * @default `"/"`
357
428
  */
358
429
  readonly optionsSeparator?: string;
430
+ /**
431
+ * The rendering context, which determines which hidden visibility values
432
+ * cause terms to be filtered out.
433
+ *
434
+ * - `"usage"` (default): filters terms hidden from usage output
435
+ * - `"doc"`: filters terms hidden from documentation output
436
+ * @default `"usage"`
437
+ * @since 1.0.0
438
+ */
439
+ readonly context?: "usage" | "doc";
359
440
  }
360
441
  /**
361
442
  * Formats a single {@link UsageTerm} into a string representation
@@ -368,4 +449,4 @@ interface UsageTermFormatOptions extends UsageFormatOptions {
368
449
  */
369
450
  declare function formatUsageTerm(term: UsageTerm, options?: UsageTermFormatOptions): string;
370
451
  //#endregion
371
- export { HiddenVisibility, OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, extractArgumentMetavars, extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, isDocHidden, isSuggestionHidden, isUsageHidden, mergeHidden, normalizeUsage };
452
+ export { HiddenVisibility, OptionName, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, cloneUsage, cloneUsageTerm, extractArgumentMetavars, extractCommandNames, extractLiteralValues, extractOptionNames, formatUsage, formatUsageTerm, isDocHidden, isSuggestionHidden, isUsageHidden, mergeHidden, normalizeUsage };
package/dist/usage.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { getDisplayWidth } from "./displaywidth.js";
2
+ import { validateProgramName } from "./validate.js";
3
+
1
4
  //#region src/usage.ts
2
5
  /**
3
6
  * Returns whether the term should be hidden from usage output.
@@ -39,6 +42,8 @@ function mergeHidden(a, b) {
39
42
  * multiple, and exclusive terms.
40
43
  *
41
44
  * @param usage The usage description to extract option names from.
45
+ * @param includeHidden Whether to include fully hidden options (`hidden: true`)
46
+ * in the result. Defaults to `false`.
42
47
  * @returns A set containing all option names found in the usage description.
43
48
  *
44
49
  * @example
@@ -51,12 +56,12 @@ function mergeHidden(a, b) {
51
56
  * // names = Set(["--verbose", "-v", "--quiet", "-q"])
52
57
  * ```
53
58
  */
54
- function extractOptionNames(usage) {
59
+ function extractOptionNames(usage, includeHidden) {
55
60
  const names = /* @__PURE__ */ new Set();
56
61
  function traverseUsage(terms) {
57
62
  if (!terms || !Array.isArray(terms)) return;
58
63
  for (const term of terms) if (term.type === "option") {
59
- if (isSuggestionHidden(term.hidden)) continue;
64
+ if (!includeHidden && isSuggestionHidden(term.hidden)) continue;
60
65
  for (const name of term.names) names.add(name);
61
66
  } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
62
67
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
@@ -70,8 +75,10 @@ function extractOptionNames(usage) {
70
75
  * This function recursively traverses the usage structure and collects
71
76
  * all command names, similar to {@link extractOptionNames}.
72
77
  *
73
- * @param usage The usage structure to extract command names from
74
- * @returns A Set of all command names found in the usage structure
78
+ * @param usage The usage structure to extract command names from.
79
+ * @param includeHidden Whether to include fully hidden commands
80
+ * (`hidden: true`) in the result. Defaults to `false`.
81
+ * @returns A set of all command names found in the usage structure.
75
82
  *
76
83
  * @example
77
84
  * ```typescript
@@ -84,12 +91,12 @@ function extractOptionNames(usage) {
84
91
  * ```
85
92
  * @since 0.7.0
86
93
  */
87
- function extractCommandNames(usage) {
94
+ function extractCommandNames(usage, includeHidden) {
88
95
  const names = /* @__PURE__ */ new Set();
89
96
  function traverseUsage(terms) {
90
97
  if (!terms || !Array.isArray(terms)) return;
91
98
  for (const term of terms) if (term.type === "command") {
92
- if (isSuggestionHidden(term.hidden)) continue;
99
+ if (!includeHidden && isSuggestionHidden(term.hidden)) continue;
93
100
  names.add(term.name);
94
101
  } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
95
102
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
@@ -98,6 +105,29 @@ function extractCommandNames(usage) {
98
105
  return names;
99
106
  }
100
107
  /**
108
+ * Extracts all literal values from a usage description.
109
+ *
110
+ * This function recursively traverses the usage tree and collects all
111
+ * `literal` term values. Literal values represent fixed strings that
112
+ * the user must type (e.g., conditional discriminator values like
113
+ * `"server"` in `conditional(option("--mode", string()), { server: ... })`).
114
+ *
115
+ * @param usage The usage description to extract literal values from.
116
+ * @returns A set of all literal values found in the usage description.
117
+ * @since 1.0.0
118
+ */
119
+ function extractLiteralValues(usage) {
120
+ const values = /* @__PURE__ */ new Set();
121
+ function traverseUsage(terms) {
122
+ if (!terms || !Array.isArray(terms)) return;
123
+ for (const term of terms) if (term.type === "literal") values.add(term.value);
124
+ else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
125
+ else if (term.type === "exclusive") for (const branch of term.terms) traverseUsage(branch);
126
+ }
127
+ traverseUsage(usage);
128
+ return values;
129
+ }
130
+ /**
101
131
  * Extracts all argument metavars from a Usage array.
102
132
  *
103
133
  * This function recursively traverses the usage structure and collects
@@ -146,8 +176,11 @@ function extractArgumentMetavars(usage) {
146
176
  * @param options Optional formatting options to customize the output.
147
177
  * See {@link UsageFormatOptions} for available options.
148
178
  * @returns A formatted string representation of the usage description.
179
+ * @throws {TypeError} If `programName` is not a string, is empty,
180
+ * whitespace-only, or contains control characters.
149
181
  */
150
182
  function formatUsage(programName, usage, options = {}) {
183
+ validateProgramName(programName);
151
184
  usage = normalizeUsage(filterUsageForDisplay(usage));
152
185
  if (options.expandCommands) {
153
186
  const lastTerm = usage.at(-1);
@@ -159,13 +192,24 @@ function formatUsage(programName, usage, options = {}) {
159
192
  if (usage.length > 1) command = [...usage.slice(0, -1), ...command];
160
193
  lines.push(formatUsage(programName, command, options));
161
194
  }
162
- return lines.join("\n");
195
+ if (lines.length > 0) return lines.join("\n");
163
196
  }
164
197
  }
165
- let output = options.colors ? `\x1b[1m${programName}\x1b[0m ` : `${programName} `;
166
- let lineWidth = programName.length + 1;
198
+ let output = options.colors ? `\x1b[1m${programName}\x1b[0m` : programName;
199
+ let lineWidth = getDisplayWidth(programName);
200
+ let first = true;
167
201
  for (const { text, width } of formatUsageTerms(usage, options)) {
168
- if (options.maxWidth != null && lineWidth + width > options.maxWidth) {
202
+ if (first) {
203
+ first = false;
204
+ if (options.maxWidth != null && lineWidth + 1 + width > options.maxWidth) {
205
+ output += "\n";
206
+ lineWidth = 0;
207
+ } else {
208
+ output += " ";
209
+ lineWidth += 1;
210
+ }
211
+ } else if (options.maxWidth != null && lineWidth > 0 && lineWidth + width > options.maxWidth) {
212
+ if (output.endsWith(" ")) output = output.slice(0, -1);
169
213
  output += "\n";
170
214
  lineWidth = 0;
171
215
  if (text === " ") continue;
@@ -180,14 +224,27 @@ function formatUsage(programName, usage, options = {}) {
180
224
  * sorting terms for better readability, and ensuring consistent structure
181
225
  * throughout the usage tree.
182
226
  *
183
- * This function performs two main operations:
227
+ * This function performs three main operations:
228
+ *
229
+ * 1. *Stripping*: Removes degenerate terms that would render as empty or
230
+ * malformed output, such as options with no names, commands with empty
231
+ * names, arguments with empty metavars, and container terms (`optional`,
232
+ * `multiple`, `exclusive`) whose top-level terms array is empty after
233
+ * recursive normalization. Exclusive branches representing valid
234
+ * zero-token alternatives (e.g., `conditional()` default branches or
235
+ * `optional(constant(...))`) and empty-value literals are preserved.
236
+ * Only branches that become empty because all their content was
237
+ * malformed are removed.
184
238
  *
185
- * 1. *Flattening*: Recursively processes all usage terms and merges any
239
+ * 2. *Flattening*: Recursively processes all usage terms and merges any
186
240
  * nested exclusive terms into their parent exclusive term to avoid
187
241
  * redundant nesting. For example, an exclusive term containing another
188
242
  * exclusive term will have its nested terms flattened into the parent.
243
+ * Similarly, nested optional terms are collapsed:
244
+ * `optional(optional(X))` becomes `optional(X)` when the outer optional
245
+ * contains only a single inner optional term.
189
246
  *
190
- * 2. *Sorting*: Reorders terms to improve readability by placing:
247
+ * 3. *Sorting*: Reorders terms to improve readability by placing:
191
248
  * - Commands (subcommands) first
192
249
  * - Options and other terms in the middle
193
250
  * - Positional arguments last (including optional/multiple wrappers around
@@ -197,11 +254,12 @@ function formatUsage(programName, usage, options = {}) {
197
254
  * positional arguments and treats them as arguments for sorting purposes.
198
255
  *
199
256
  * @param usage The usage description to normalize.
200
- * @returns A normalized usage description with flattened exclusive terms
201
- * and terms sorted for optimal readability.
257
+ * @returns A normalized usage description with degenerate terms removed,
258
+ * nested exclusive and optional terms flattened, and remaining
259
+ * terms sorted for optimal readability.
202
260
  */
203
261
  function normalizeUsage(usage) {
204
- const terms = usage.map(normalizeUsageTerm);
262
+ const terms = usage.map(normalizeUsageTerm).filter(isNonDegenerateTerm);
205
263
  terms.sort((a, b) => {
206
264
  const aCmd = a.type === "command";
207
265
  const bCmd = b.type === "command";
@@ -212,11 +270,14 @@ function normalizeUsage(usage) {
212
270
  return terms;
213
271
  }
214
272
  function normalizeUsageTerm(term) {
215
- if (term.type === "optional") return {
216
- type: "optional",
217
- terms: normalizeUsage(term.terms)
218
- };
219
- else if (term.type === "multiple") return {
273
+ if (term.type === "optional") {
274
+ const normalized = normalizeUsage(term.terms);
275
+ if (normalized.length === 1 && normalized[0].type === "optional") return normalized[0];
276
+ return {
277
+ type: "optional",
278
+ terms: normalized
279
+ };
280
+ } else if (term.type === "multiple") return {
220
281
  type: "multiple",
221
282
  terms: normalizeUsage(term.terms),
222
283
  min: term.min
@@ -228,20 +289,111 @@ function normalizeUsageTerm(term) {
228
289
  if (normalized.length >= 1 && normalized[0].type === "exclusive") {
229
290
  const rest = normalized.slice(1);
230
291
  for (const subUsage of normalized[0].terms) terms.push([...subUsage, ...rest]);
231
- } else terms.push(normalized);
292
+ } else if (normalized.length > 0 || !containsMalformedLeaf(usage)) terms.push(normalized);
232
293
  }
233
294
  return {
234
295
  type: "exclusive",
235
296
  terms
236
297
  };
237
- } else return term;
298
+ } else {
299
+ if (term.type === "option") return {
300
+ ...term,
301
+ names: [...term.names]
302
+ };
303
+ else if (term.type === "command") {
304
+ if (term.usageLine == null || typeof term.usageLine === "function") return { ...term };
305
+ return {
306
+ ...term,
307
+ usageLine: cloneUsage(term.usageLine)
308
+ };
309
+ }
310
+ return { ...term };
311
+ }
312
+ }
313
+ function isNonDegenerateTerm(term) {
314
+ if (term.type === "option") return term.names.length > 0;
315
+ if (term.type === "command") return term.name !== "";
316
+ if (term.type === "argument") return term.metavar.length > 0;
317
+ if (term.type === "optional" || term.type === "multiple" || term.type === "exclusive") return term.terms.length > 0;
318
+ return true;
319
+ }
320
+ function containsMalformedLeaf(usage) {
321
+ for (const term of usage) {
322
+ if (term.type === "option" && term.names.length === 0) return true;
323
+ if (term.type === "command" && term.name === "") return true;
324
+ if (term.type === "argument" && term.metavar.length === 0) return true;
325
+ if (term.type === "optional" || term.type === "multiple") {
326
+ if (containsMalformedLeaf(term.terms)) return true;
327
+ }
328
+ if (term.type === "exclusive") {
329
+ for (const branch of term.terms) if (containsMalformedLeaf(branch)) return true;
330
+ }
331
+ }
332
+ return false;
333
+ }
334
+ /**
335
+ * Creates a deep clone of a single {@link UsageTerm}. Recursive term
336
+ * variants (`optional`, `multiple`, `exclusive`) are cloned recursively.
337
+ * For `command` terms, a function-valued `usageLine` is preserved by
338
+ * reference (functions are stateless callbacks), while an array-valued
339
+ * `usageLine` is deep-cloned.
340
+ *
341
+ * @param term The usage term to clone.
342
+ * @returns A structurally equal but referentially distinct copy.
343
+ * @since 1.0.0
344
+ */
345
+ function cloneUsageTerm(term) {
346
+ switch (term.type) {
347
+ case "argument": return { ...term };
348
+ case "option": return {
349
+ ...term,
350
+ names: [...term.names]
351
+ };
352
+ case "command": {
353
+ if (term.usageLine == null || typeof term.usageLine === "function") return { ...term };
354
+ return {
355
+ ...term,
356
+ usageLine: term.usageLine.map(cloneUsageTerm)
357
+ };
358
+ }
359
+ case "optional": return {
360
+ type: "optional",
361
+ terms: term.terms.map(cloneUsageTerm)
362
+ };
363
+ case "multiple": return {
364
+ type: "multiple",
365
+ terms: term.terms.map(cloneUsageTerm),
366
+ min: term.min
367
+ };
368
+ case "exclusive": return {
369
+ type: "exclusive",
370
+ terms: term.terms.map((u) => u.map(cloneUsageTerm))
371
+ };
372
+ case "literal":
373
+ case "passthrough":
374
+ case "ellipsis": return { ...term };
375
+ }
376
+ }
377
+ /**
378
+ * Creates a deep clone of a {@link Usage} array and all of its terms.
379
+ *
380
+ * @param usage The usage array to clone.
381
+ * @returns A mutable array of deeply cloned usage terms.
382
+ * @since 1.0.0
383
+ */
384
+ function cloneUsage(usage) {
385
+ return usage.map(cloneUsageTerm);
238
386
  }
239
- function filterUsageForDisplay(usage) {
387
+ function filterUsageForDisplay(usage, isHidden = isUsageHidden) {
240
388
  const terms = [];
241
389
  for (const term of usage) {
242
- if ((term.type === "argument" || term.type === "option" || term.type === "command" || term.type === "passthrough") && isUsageHidden(term.hidden)) continue;
390
+ if ((term.type === "argument" || term.type === "option" || term.type === "command" || term.type === "passthrough") && isHidden(term.hidden)) continue;
391
+ if (term.type === "option" && term.names.length === 0) continue;
392
+ if (term.type === "command" && term.name === "") continue;
393
+ if (term.type === "argument" && term.metavar.length === 0) continue;
394
+ if (term.type === "literal" && term.value === "") continue;
243
395
  if (term.type === "optional") {
244
- const filtered = filterUsageForDisplay(term.terms);
396
+ const filtered = filterUsageForDisplay(term.terms, isHidden);
245
397
  if (filtered.length > 0) terms.push({
246
398
  type: "optional",
247
399
  terms: filtered
@@ -249,7 +401,7 @@ function filterUsageForDisplay(usage) {
249
401
  continue;
250
402
  }
251
403
  if (term.type === "multiple") {
252
- const filtered = filterUsageForDisplay(term.terms);
404
+ const filtered = filterUsageForDisplay(term.terms, isHidden);
253
405
  if (filtered.length > 0) terms.push({
254
406
  type: "multiple",
255
407
  terms: filtered,
@@ -260,8 +412,8 @@ function filterUsageForDisplay(usage) {
260
412
  if (term.type === "exclusive") {
261
413
  const filteredBranches = term.terms.map((branch) => {
262
414
  const first = branch[0];
263
- if (first?.type === "command" && isUsageHidden(first.hidden)) return [];
264
- return filterUsageForDisplay(branch);
415
+ if (first?.type === "command" && isHidden(first.hidden)) return [];
416
+ return filterUsageForDisplay(branch, isHidden);
265
417
  }).filter((branch) => branch.length > 0);
266
418
  if (filteredBranches.length > 0) terms.push({
267
419
  type: "exclusive",
@@ -276,7 +428,6 @@ function filterUsageForDisplay(usage) {
276
428
  function* formatUsageTerms(terms, options) {
277
429
  let i = 0;
278
430
  for (const t of terms) {
279
- if ("hidden" in t && (t.type === "argument" || t.type === "option" || t.type === "command" || t.type === "passthrough") && isUsageHidden(t.hidden)) continue;
280
431
  if (i > 0) yield {
281
432
  text: " ",
282
433
  width: 1
@@ -295,12 +446,14 @@ function* formatUsageTerms(terms, options) {
295
446
  * @returns A formatted string representation of the usage term.
296
447
  */
297
448
  function formatUsageTerm(term, options = {}) {
298
- const visibleTerms = filterUsageForDisplay([term]);
449
+ const hiddenCheck = options.context === "doc" ? isDocHidden : isUsageHidden;
450
+ const visibleTerms = filterUsageForDisplay([term], hiddenCheck);
299
451
  if (visibleTerms.length < 1) return "";
300
452
  let lineWidth = 0;
301
453
  let output = "";
302
454
  for (const { text, width } of formatUsageTermInternal(visibleTerms[0], options)) {
303
- if (options.maxWidth != null && lineWidth + width > options.maxWidth) {
455
+ if (options.maxWidth != null && lineWidth > 0 && lineWidth + width > options.maxWidth) {
456
+ if (output.endsWith(" ")) output = output.slice(0, -1);
304
457
  output += "\n";
305
458
  lineWidth = 0;
306
459
  if (text === " ") continue;
@@ -314,24 +467,24 @@ function* formatUsageTermInternal(term, options) {
314
467
  const optionsSeparator = options.optionsSeparator ?? "/";
315
468
  if (term.type === "argument") yield {
316
469
  text: options?.colors ? `\x1b[4m${term.metavar}\x1b[0m` : term.metavar,
317
- width: term.metavar.length
470
+ width: getDisplayWidth(term.metavar)
318
471
  };
319
472
  else if (term.type === "option") if (options?.onlyShortestOptions) {
320
- const shortestName = term.names.reduce((a, b) => a.length <= b.length ? a : b);
473
+ const shortestName = term.names.reduce((a, b) => getDisplayWidth(a) <= getDisplayWidth(b) ? a : b);
321
474
  yield {
322
475
  text: options?.colors ? `\x1b[3m${shortestName}\x1b[0m` : shortestName,
323
- width: shortestName.length
476
+ width: getDisplayWidth(shortestName)
324
477
  };
325
478
  } else {
326
479
  let i = 0;
327
480
  for (const optionName of term.names) {
328
481
  if (i > 0) yield {
329
482
  text: options?.colors ? `\x1b[2m${optionsSeparator}\x1b[0m` : optionsSeparator,
330
- width: optionsSeparator.length
483
+ width: getDisplayWidth(optionsSeparator)
331
484
  };
332
485
  yield {
333
486
  text: options?.colors ? `\x1b[3m${optionName}\x1b[0m` : optionName,
334
- width: optionName.length
487
+ width: getDisplayWidth(optionName)
335
488
  };
336
489
  i++;
337
490
  }
@@ -342,13 +495,13 @@ function* formatUsageTermInternal(term, options) {
342
495
  };
343
496
  yield {
344
497
  text: options?.colors ? `\x1b[4m\x1b[2m${term.metavar}\x1b[0m` : term.metavar,
345
- width: term.metavar.length
498
+ width: getDisplayWidth(term.metavar)
346
499
  };
347
500
  }
348
501
  }
349
502
  else if (term.type === "command") yield {
350
503
  text: options?.colors ? `\x1b[1m${term.name}\x1b[0m` : term.name,
351
- width: term.name.length
504
+ width: getDisplayWidth(term.name)
352
505
  };
353
506
  else if (term.type === "optional") {
354
507
  yield {
@@ -410,7 +563,7 @@ function* formatUsageTermInternal(term, options) {
410
563
  };
411
564
  } else if (term.type === "literal") yield {
412
565
  text: term.value,
413
- width: term.value.length
566
+ width: getDisplayWidth(term.value)
414
567
  };
415
568
  else if (term.type === "passthrough") {
416
569
  const text = "[...]";
@@ -428,4 +581,4 @@ function* formatUsageTermInternal(term, options) {
428
581
  }
429
582
 
430
583
  //#endregion
431
- export { extractArgumentMetavars, extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, isDocHidden, isSuggestionHidden, isUsageHidden, mergeHidden, normalizeUsage };
584
+ export { cloneUsage, cloneUsageTerm, extractArgumentMetavars, extractCommandNames, extractLiteralValues, extractOptionNames, formatUsage, formatUsageTerm, isDocHidden, isSuggestionHidden, isUsageHidden, mergeHidden, normalizeUsage };