@optique/core 1.0.0-dev.921 → 1.0.1

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 -525
  46. package/dist/facade.d.cts +59 -15
  47. package/dist/facade.d.ts +59 -15
  48. package/dist/facade.js +718 -525
  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 +1270 -187
  106. package/dist/valueparser.d.cts +320 -14
  107. package/dist/valueparser.d.ts +320 -14
  108. package/dist/valueparser.js +1269 -188
  109. package/package.json +9 -9
package/dist/doc.d.ts CHANGED
@@ -103,6 +103,53 @@ interface DocFragments {
103
103
  */
104
104
  readonly footer?: Message;
105
105
  }
106
+ /**
107
+ * Returns whether a doc entry's term is hidden from documentation.
108
+ * Only term types with a `hidden` field (argument, option, command,
109
+ * passthrough) are checked; other types always return `false`.
110
+ *
111
+ * @param entry The doc entry to check.
112
+ * @returns `true` if the entry should be hidden from documentation.
113
+ * @since 1.0.0
114
+ */
115
+ declare function isDocEntryHidden(entry: DocEntry): boolean;
116
+ /**
117
+ * Removes duplicate {@link DocEntry} values that share the same surface
118
+ * syntax (same term type and identifying names). Doc-hidden entries are
119
+ * filtered out first so they cannot influence the ordering of visible
120
+ * entries. Among the remaining visible entries, the first occurrence is
121
+ * kept and later duplicates are discarded.
122
+ *
123
+ * Positional argument entries are never deduplicated because they are
124
+ * distinguished by position, not by metavar, and {@link DocEntry} does
125
+ * not carry position information.
126
+ *
127
+ * @param entries The entries to deduplicate.
128
+ * @returns A new array with hidden entries removed and duplicates
129
+ * collapsed, preserving insertion order of visible entries.
130
+ * @since 1.0.0
131
+ */
132
+ declare function deduplicateDocEntries(entries: readonly DocEntry[]): DocEntry[];
133
+ /**
134
+ * Removes duplicate entries from a list of {@link DocFragment} values.
135
+ * Entry-type fragments are deduplicated by their surface syntax key.
136
+ * Section-type fragments have their entries deduplicated internally.
137
+ *
138
+ * @param fragments The fragments to deduplicate.
139
+ * @returns A new array with duplicate entries removed.
140
+ * @since 1.0.0
141
+ */
142
+ declare function deduplicateDocFragments(fragments: readonly DocFragment[]): DocFragment[];
143
+ /**
144
+ * Creates a deep clone of a {@link DocEntry}. The `term` is cloned via
145
+ * {@link cloneUsageTerm}, and `description`, `default`, and `choices`
146
+ * messages are cloned via {@link cloneMessage}.
147
+ *
148
+ * @param entry The documentation entry to clone.
149
+ * @returns A structurally equal but referentially distinct copy.
150
+ * @since 1.0.0
151
+ */
152
+ declare function cloneDocEntry(entry: DocEntry): DocEntry;
106
153
  /**
107
154
  * Configuration for customizing default value display formatting.
108
155
  *
@@ -148,9 +195,10 @@ interface ShowChoicesOptions {
148
195
  readonly label?: string;
149
196
  /**
150
197
  * Maximum number of choice values to display before truncating with
151
- * `...`. Set to `Infinity` to show all choices.
198
+ * `...`. Must be at least `1`. Set to `Infinity` to show all choices.
152
199
  *
153
200
  * @default `8`
201
+ * @throws {RangeError} If the value is less than `1`.
154
202
  */
155
203
  readonly maxItems?: number;
156
204
  }
@@ -263,6 +311,13 @@ interface DocPageFormatOptions {
263
311
  * @param page The documentation page to format
264
312
  * @param options Formatting options to customize the output
265
313
  * @returns A formatted string representation of the documentation page
314
+ * @throws {TypeError} If `programName` is not a string, is empty,
315
+ * whitespace-only, or contains control characters, if any non-empty
316
+ * section's title is not a string, is empty, whitespace-only, or contains
317
+ * control characters, or if `maxWidth` is not a finite integer.
318
+ * @throws {RangeError} If any entry needs a description column and `maxWidth`
319
+ * is too small to fit the minimum layout (less than `termIndent + 4`), or if
320
+ * `showChoices.maxItems` is less than `1`.
266
321
  *
267
322
  * @example
268
323
  * ```typescript
@@ -284,4 +339,4 @@ interface DocPageFormatOptions {
284
339
  */
285
340
  declare function formatDocPage(programName: string, page: DocPage, options?: DocPageFormatOptions): string;
286
341
  //#endregion
287
- export { DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, ShowChoicesOptions, ShowDefaultOptions, formatDocPage };
342
+ export { DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, ShowChoicesOptions, ShowDefaultOptions, cloneDocEntry, deduplicateDocEntries, deduplicateDocFragments, formatDocPage, isDocEntryHidden };
package/dist/doc.js CHANGED
@@ -1,8 +1,145 @@
1
- import { formatMessage, text } from "./message.js";
2
- import { formatUsage, formatUsageTerm } from "./usage.js";
1
+ import { getDisplayWidth } from "./displaywidth.js";
2
+ import { cloneMessage, formatMessage, text } from "./message.js";
3
+ import { validateLabel, validateProgramName } from "./validate.js";
4
+ import { cloneUsageTerm, formatUsage, formatUsageTerm, isDocHidden, isUsageHidden } from "./usage.js";
3
5
 
4
6
  //#region src/doc.ts
5
7
  /**
8
+ * Returns whether a doc entry's term is hidden from documentation.
9
+ * Only term types with a `hidden` field (argument, option, command,
10
+ * passthrough) are checked; other types always return `false`.
11
+ *
12
+ * @param entry The doc entry to check.
13
+ * @returns `true` if the entry should be hidden from documentation.
14
+ * @since 1.0.0
15
+ */
16
+ function isDocEntryHidden(entry) {
17
+ const term = entry.term;
18
+ if (term.type === "argument" || term.type === "option" || term.type === "command" || term.type === "passthrough") return isDocHidden(term.hidden);
19
+ return false;
20
+ }
21
+ function getDocEntryKey(entry) {
22
+ const term = entry.term;
23
+ switch (term.type) {
24
+ case "command": return `command:${term.name}`;
25
+ case "option": return `option:${[...term.names].sort().join(",")}:${term.metavar ?? ""}`;
26
+ case "argument": return `argument:${term.metavar}`;
27
+ default: return JSON.stringify(term);
28
+ }
29
+ }
30
+ /**
31
+ * Removes duplicate {@link DocEntry} values that share the same surface
32
+ * syntax (same term type and identifying names). Doc-hidden entries are
33
+ * filtered out first so they cannot influence the ordering of visible
34
+ * entries. Among the remaining visible entries, the first occurrence is
35
+ * kept and later duplicates are discarded.
36
+ *
37
+ * Positional argument entries are never deduplicated because they are
38
+ * distinguished by position, not by metavar, and {@link DocEntry} does
39
+ * not carry position information.
40
+ *
41
+ * @param entries The entries to deduplicate.
42
+ * @returns A new array with hidden entries removed and duplicates
43
+ * collapsed, preserving insertion order of visible entries.
44
+ * @since 1.0.0
45
+ */
46
+ function deduplicateDocEntries(entries) {
47
+ const seen = /* @__PURE__ */ new Set();
48
+ const result = [];
49
+ for (const entry of entries) {
50
+ if (isDocEntryHidden(entry)) continue;
51
+ if (entry.term.type === "argument") {
52
+ result.push(entry);
53
+ continue;
54
+ }
55
+ const key = getDocEntryKey(entry);
56
+ if (!seen.has(key)) {
57
+ seen.add(key);
58
+ result.push(entry);
59
+ }
60
+ }
61
+ return result;
62
+ }
63
+ /**
64
+ * Removes duplicate entries from a list of {@link DocFragment} values.
65
+ * Entry-type fragments are deduplicated by their surface syntax key.
66
+ * Section-type fragments have their entries deduplicated internally.
67
+ *
68
+ * @param fragments The fragments to deduplicate.
69
+ * @returns A new array with duplicate entries removed.
70
+ * @since 1.0.0
71
+ */
72
+ function deduplicateDocFragments(fragments) {
73
+ const untitledSeen = /* @__PURE__ */ new Set();
74
+ const titledSectionMap = /* @__PURE__ */ new Map();
75
+ const titledSectionPositioned = /* @__PURE__ */ new Set();
76
+ const slots = [];
77
+ for (const fragment of fragments) if (fragment.type === "entry") {
78
+ if (isDocEntryHidden(fragment)) continue;
79
+ if (fragment.term.type === "argument") slots.push(fragment);
80
+ else {
81
+ const key = getDocEntryKey(fragment);
82
+ if (!untitledSeen.has(key)) {
83
+ untitledSeen.add(key);
84
+ slots.push(fragment);
85
+ }
86
+ }
87
+ } else if (fragment.title == null) {
88
+ const dedupedEntries = [];
89
+ for (const entry of fragment.entries) {
90
+ if (isDocEntryHidden(entry)) continue;
91
+ if (entry.term.type === "argument") {
92
+ dedupedEntries.push(entry);
93
+ continue;
94
+ }
95
+ const key = getDocEntryKey(entry);
96
+ if (!untitledSeen.has(key)) {
97
+ untitledSeen.add(key);
98
+ dedupedEntries.push(entry);
99
+ }
100
+ }
101
+ if (dedupedEntries.length > 0) slots.push({
102
+ ...fragment,
103
+ type: "section",
104
+ entries: dedupedEntries
105
+ });
106
+ } else {
107
+ if (!titledSectionMap.has(fragment.title)) titledSectionMap.set(fragment.title, []);
108
+ if (!titledSectionPositioned.has(fragment.title) && fragment.entries.some((e) => !isDocEntryHidden(e))) {
109
+ titledSectionPositioned.add(fragment.title);
110
+ slots.push(fragment.title);
111
+ }
112
+ titledSectionMap.get(fragment.title).push(...fragment.entries);
113
+ }
114
+ const result = [];
115
+ for (const slot of slots) if (typeof slot === "string") {
116
+ const entries = deduplicateDocEntries(titledSectionMap.get(slot));
117
+ if (entries.length > 0) result.push({
118
+ type: "section",
119
+ title: slot,
120
+ entries
121
+ });
122
+ } else result.push(slot);
123
+ return result;
124
+ }
125
+ /**
126
+ * Creates a deep clone of a {@link DocEntry}. The `term` is cloned via
127
+ * {@link cloneUsageTerm}, and `description`, `default`, and `choices`
128
+ * messages are cloned via {@link cloneMessage}.
129
+ *
130
+ * @param entry The documentation entry to clone.
131
+ * @returns A structurally equal but referentially distinct copy.
132
+ * @since 1.0.0
133
+ */
134
+ function cloneDocEntry(entry) {
135
+ return {
136
+ term: cloneUsageTerm(entry.term),
137
+ ...entry.description != null && { description: cloneMessage(entry.description) },
138
+ ...entry.default != null && { default: cloneMessage(entry.default) },
139
+ ...entry.choices != null && { choices: cloneMessage(entry.choices) }
140
+ };
141
+ }
142
+ /**
6
143
  * Classifies a {@link DocSection} by its content type for use in the
7
144
  * default smart sort.
8
145
  *
@@ -47,6 +184,13 @@ function defaultSectionOrder(a, b) {
47
184
  * @param page The documentation page to format
48
185
  * @param options Formatting options to customize the output
49
186
  * @returns A formatted string representation of the documentation page
187
+ * @throws {TypeError} If `programName` is not a string, is empty,
188
+ * whitespace-only, or contains control characters, if any non-empty
189
+ * section's title is not a string, is empty, whitespace-only, or contains
190
+ * control characters, or if `maxWidth` is not a finite integer.
191
+ * @throws {RangeError} If any entry needs a description column and `maxWidth`
192
+ * is too small to fit the minimum layout (less than `termIndent + 4`), or if
193
+ * `showChoices.maxItems` is less than `1`.
50
194
  *
51
195
  * @example
52
196
  * ```typescript
@@ -67,10 +211,70 @@ function defaultSectionOrder(a, b) {
67
211
  * ```
68
212
  */
69
213
  function formatDocPage(programName, page, options = {}) {
214
+ validateProgramName(programName);
70
215
  const termIndent = options.termIndent ?? 2;
71
216
  const termWidth = options.termWidth ?? 26;
217
+ if (options.maxWidth != null && (!Number.isFinite(options.maxWidth) || !Number.isInteger(options.maxWidth))) throw new TypeError(`maxWidth must be a finite integer, got ${options.maxWidth}.`);
218
+ const filteredSections = page.sections.map((s) => ({
219
+ ...s,
220
+ entries: s.entries.filter((e) => {
221
+ const rendered = formatUsageTerm(e.term, { context: "doc" });
222
+ return rendered.trim() !== "";
223
+ })
224
+ }));
225
+ page = {
226
+ ...page,
227
+ sections: filteredSections
228
+ };
229
+ if (typeof options.showChoices === "object" && options.showChoices.maxItems != null) {
230
+ const maxItems = options.showChoices.maxItems;
231
+ if (maxItems < 1) throw new RangeError(`showChoices.maxItems must be at least 1, but got ${maxItems}.`);
232
+ }
233
+ const hasContent = (msg) => Array.isArray(msg) && msg.length > 0;
234
+ if (options.maxWidth != null) {
235
+ const hasEntries = page.sections.some((s) => s.entries.length > 0);
236
+ const needsDescColumn = hasEntries && page.sections.some((s) => s.entries.some((e) => hasContent(e.description) || options.showDefault && hasContent(e.default) || options.showChoices && hasContent(e.choices)));
237
+ let minDescWidth = 1;
238
+ if (needsDescColumn) {
239
+ if (options.showDefault && page.sections.some((s) => s.entries.some((e) => hasContent(e.default)))) {
240
+ const prefix = typeof options.showDefault === "object" ? options.showDefault.prefix ?? " [" : " [";
241
+ minDescWidth = Math.max(minDescWidth, getDisplayWidth(prefix));
242
+ }
243
+ if (options.showChoices && page.sections.some((s) => s.entries.some((e) => hasContent(e.choices)))) {
244
+ const prefix = typeof options.showChoices === "object" ? options.showChoices.prefix ?? " (" : " (";
245
+ const label = typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ";
246
+ minDescWidth = Math.max(minDescWidth, getDisplayWidth(prefix) + getDisplayWidth(label));
247
+ }
248
+ }
249
+ const splitEntryMin = termIndent + 2 + Math.max(2, 2 * minDescWidth - 1);
250
+ const fixedEntryMin = termIndent + 2 + termWidth + minDescWidth;
251
+ const entryMin = needsDescColumn ? Math.min(splitEntryMin, fixedEntryMin) : hasEntries ? termIndent + 1 : 1;
252
+ const programNameWidth = getDisplayWidth(programName);
253
+ const usageMin = page.usage != null ? 7 + Math.max(programNameWidth, Math.min(maxVisibleAtomicWidth(page.usage), programNameWidth + 7)) : 1;
254
+ let sectionMin = 1;
255
+ if (hasContent(page.examples)) sectionMin = Math.max(sectionMin, 9);
256
+ if (hasContent(page.author)) sectionMin = Math.max(sectionMin, 7);
257
+ if (hasContent(page.bugs)) sectionMin = Math.max(sectionMin, 5);
258
+ const minWidth = Math.max(entryMin, usageMin, sectionMin);
259
+ if (options.maxWidth < minWidth) throw new RangeError(`maxWidth must be at least ${minWidth}, got ${options.maxWidth}.`);
260
+ if (needsDescColumn && minDescWidth > 1) {
261
+ const avail = options.maxWidth - termIndent - 2;
262
+ const effTW = avail >= termWidth + 1 ? termWidth : Math.max(1, Math.floor(avail / 2));
263
+ const descW = avail - effTW;
264
+ if (descW < minDescWidth) {
265
+ const needed = termIndent + termWidth + 2 + minDescWidth;
266
+ throw new RangeError(`maxWidth must be at least ${needed}, got ${options.maxWidth}.`);
267
+ }
268
+ }
269
+ }
270
+ let effectiveTermWidth;
271
+ if (options.maxWidth == null) effectiveTermWidth = termWidth;
272
+ else {
273
+ const availableForColumns = options.maxWidth - termIndent - 2;
274
+ effectiveTermWidth = availableForColumns >= termWidth + 1 ? termWidth : Math.max(1, Math.floor(availableForColumns / 2));
275
+ }
72
276
  let output = "";
73
- if (page.brief != null) {
277
+ if (hasContent(page.brief)) {
74
278
  output += formatMessage(page.brief, {
75
279
  colors: options.colors,
76
280
  maxWidth: options.maxWidth,
@@ -88,7 +292,7 @@ function formatDocPage(programName, page, options = {}) {
88
292
  }), 7);
89
293
  output += "\n";
90
294
  }
91
- if (page.description != null) {
295
+ if (hasContent(page.description)) {
92
296
  output += "\n";
93
297
  output += formatMessage(page.description, {
94
298
  colors: options.colors,
@@ -110,6 +314,7 @@ function formatDocPage(programName, page, options = {}) {
110
314
  if (section.entries.length < 1) continue;
111
315
  output += "\n";
112
316
  if (section.title != null) {
317
+ validateLabel(section.title);
113
318
  const sectionLabel = options.colors ? `\x1b[1;2m${section.title}:\x1b[0m\n` : `${section.title}:\n`;
114
319
  output += sectionLabel;
115
320
  }
@@ -117,11 +322,12 @@ function formatDocPage(programName, page, options = {}) {
117
322
  const term = formatUsageTerm(entry.term, {
118
323
  colors: options.colors,
119
324
  optionsSeparator: ", ",
325
+ context: "doc",
120
326
  maxWidth: options.maxWidth == null ? void 0 : options.maxWidth - termIndent
121
327
  });
122
- const descColumnWidth = options.maxWidth == null ? void 0 : options.maxWidth - termIndent - termWidth - 2;
328
+ const descColumnWidth = options.maxWidth == null ? void 0 : options.maxWidth - termIndent - effectiveTermWidth - 2;
123
329
  const termVisibleWidth = lastLineVisibleLength(term);
124
- const extraTermOffset = descColumnWidth != null ? Math.max(0, termVisibleWidth - termWidth) : 0;
330
+ const extraTermOffset = descColumnWidth != null ? Math.max(0, termVisibleWidth - effectiveTermWidth) : 0;
125
331
  const currentExtraOffset = () => description.includes("\n") ? 0 : extraTermOffset;
126
332
  const descFormatOptions = {
127
333
  colors: options.colors,
@@ -130,22 +336,24 @@ function formatDocPage(programName, page, options = {}) {
130
336
  startWidth: extraTermOffset > 0 ? extraTermOffset : void 0
131
337
  };
132
338
  let description = entry.description == null ? "" : formatMessage(entry.description, descFormatOptions);
133
- if (options.showDefault && entry.default != null) {
339
+ if (options.showDefault && hasContent(entry.default)) {
134
340
  const prefix = typeof options.showDefault === "object" ? options.showDefault.prefix ?? " [" : " [";
135
341
  const suffix = typeof options.showDefault === "object" ? options.showDefault.suffix ?? "]" : "]";
342
+ const prefixWidth = getDisplayWidth(prefix);
343
+ const suffixWidth = getDisplayWidth(suffix);
136
344
  let defaultStartWidth;
137
345
  if (descColumnWidth != null) {
138
346
  const lastW = lastLineVisibleLength(description);
139
347
  const effectiveLastW = lastW + currentExtraOffset();
140
- if (effectiveLastW + prefix.length >= descColumnWidth) {
348
+ if (effectiveLastW + prefixWidth >= descColumnWidth) {
141
349
  description += "\n";
142
- defaultStartWidth = prefix.length;
143
- } else defaultStartWidth = effectiveLastW + prefix.length;
350
+ defaultStartWidth = prefixWidth;
351
+ } else defaultStartWidth = effectiveLastW + prefixWidth;
144
352
  }
145
353
  const defaultFormatOptions = {
146
354
  colors: options.colors ? { resetSuffix: "\x1B[2m" } : false,
147
355
  quotes: !options.colors,
148
- maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - suffix.length,
356
+ maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - suffixWidth,
149
357
  startWidth: defaultStartWidth
150
358
  };
151
359
  const defaultContent = formatMessage(entry.default, defaultFormatOptions);
@@ -153,7 +361,7 @@ function formatDocPage(programName, page, options = {}) {
153
361
  const formattedDefault = options.colors ? `\x1b[2m${defaultText}\x1b[0m` : defaultText;
154
362
  description += formattedDefault;
155
363
  }
156
- if (options.showChoices && entry.choices != null) {
364
+ if (options.showChoices && hasContent(entry.choices)) {
157
365
  const prefix = typeof options.showChoices === "object" ? options.showChoices.prefix ?? " (" : " (";
158
366
  const suffix = typeof options.showChoices === "object" ? options.showChoices.suffix ?? ")" : ")";
159
367
  const label = typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ";
@@ -174,11 +382,14 @@ function formatDocPage(programName, page, options = {}) {
174
382
  }
175
383
  if (truncated) truncatedTerms = [...terms.slice(0, cutIndex), text(", ...")];
176
384
  }
385
+ const choicesPrefixWidth = getDisplayWidth(prefix);
386
+ const choicesSuffixWidth = getDisplayWidth(suffix);
387
+ const choicesLabelWidth = getDisplayWidth(label);
177
388
  let choicesStartWidth;
178
389
  if (descColumnWidth != null) {
179
390
  const lastW = lastLineVisibleLength(description);
180
391
  const effectiveLastW = lastW + currentExtraOffset();
181
- const prefixLabelLen = prefix.length + label.length;
392
+ const prefixLabelLen = choicesPrefixWidth + choicesLabelWidth;
182
393
  if (effectiveLastW + prefixLabelLen >= descColumnWidth) {
183
394
  description += "\n";
184
395
  choicesStartWidth = prefixLabelLen;
@@ -187,7 +398,7 @@ function formatDocPage(programName, page, options = {}) {
187
398
  const choicesFormatOptions = {
188
399
  colors: options.colors ? { resetSuffix: "\x1B[2m" } : false,
189
400
  quotes: false,
190
- maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - suffix.length,
401
+ maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - choicesSuffixWidth,
191
402
  startWidth: choicesStartWidth
192
403
  };
193
404
  const choicesDisplay = formatMessage(truncatedTerms, choicesFormatOptions);
@@ -195,10 +406,10 @@ function formatDocPage(programName, page, options = {}) {
195
406
  const formattedChoices = options.colors ? `\x1b[2m${choicesText}\x1b[0m` : choicesText;
196
407
  description += formattedChoices;
197
408
  }
198
- output += `${" ".repeat(termIndent)}${ansiAwareRightPad(term, termWidth)} ${description === "" ? "" : indentLines(description, termIndent + termWidth + 2)}\n`;
409
+ output += `${" ".repeat(termIndent)}${ansiAwareRightPad(term, effectiveTermWidth)}${description === "" ? "" : ` ${indentLines(description, termIndent + effectiveTermWidth + 2)}`}\n`;
199
410
  }
200
411
  }
201
- if (page.examples != null) {
412
+ if (hasContent(page.examples)) {
202
413
  output += "\n";
203
414
  const examplesLabel = options.colors ? "\x1B[1;2mExamples:\x1B[0m\n" : "Examples:\n";
204
415
  output += examplesLabel;
@@ -210,7 +421,7 @@ function formatDocPage(programName, page, options = {}) {
210
421
  output += " " + indentLines(examplesContent, 2);
211
422
  output += "\n";
212
423
  }
213
- if (page.author != null) {
424
+ if (hasContent(page.author)) {
214
425
  output += "\n";
215
426
  const authorLabel = options.colors ? "\x1B[1;2mAuthor:\x1B[0m\n" : "Author:\n";
216
427
  output += authorLabel;
@@ -222,7 +433,7 @@ function formatDocPage(programName, page, options = {}) {
222
433
  output += " " + indentLines(authorContent, 2);
223
434
  output += "\n";
224
435
  }
225
- if (page.bugs != null) {
436
+ if (hasContent(page.bugs)) {
226
437
  output += "\n";
227
438
  const bugsLabel = options.colors ? "\x1B[1;2mBugs:\x1B[0m\n" : "Bugs:\n";
228
439
  output += bugsLabel;
@@ -234,7 +445,7 @@ function formatDocPage(programName, page, options = {}) {
234
445
  output += " " + indentLines(bugsContent, 2);
235
446
  output += "\n";
236
447
  }
237
- if (page.footer != null) {
448
+ if (hasContent(page.footer)) {
238
449
  output += "\n";
239
450
  output += formatMessage(page.footer, {
240
451
  colors: options.colors,
@@ -247,17 +458,64 @@ function formatDocPage(programName, page, options = {}) {
247
458
  function indentLines(text$1, indent) {
248
459
  return text$1.split("\n").join("\n" + " ".repeat(indent));
249
460
  }
250
- const ansiEscapeCodeRegex = /\x1B\[[0-9;]*[a-zA-Z]/g;
461
+ /**
462
+ * Returns the width of the widest non-breakable segment among visible
463
+ * (non-usage-hidden) terms in a usage tree. Hidden terms are excluded
464
+ * because they are filtered out before rendering, so they do not
465
+ * contribute to the rendered width.
466
+ */
467
+ function maxVisibleAtomicWidth(usage) {
468
+ let max = 0;
469
+ for (const term of usage) switch (term.type) {
470
+ case "argument":
471
+ if (!isUsageHidden(term.hidden)) max = Math.max(max, getDisplayWidth(term.metavar));
472
+ break;
473
+ case "option":
474
+ if (!isUsageHidden(term.hidden) && term.names.length > 0) {
475
+ for (const name of term.names) max = Math.max(max, getDisplayWidth(name));
476
+ if (term.metavar != null) max = Math.max(max, getDisplayWidth(term.metavar));
477
+ }
478
+ break;
479
+ case "command":
480
+ if (!isUsageHidden(term.hidden)) max = Math.max(max, getDisplayWidth(term.name));
481
+ break;
482
+ case "passthrough":
483
+ if (!isUsageHidden(term.hidden)) max = Math.max(max, 5);
484
+ break;
485
+ case "optional":
486
+ max = Math.max(max, maxVisibleAtomicWidth(term.terms));
487
+ break;
488
+ case "multiple": {
489
+ const innerMax = maxVisibleAtomicWidth(term.terms);
490
+ if (innerMax > 0) max = Math.max(max, 3, innerMax);
491
+ break;
492
+ }
493
+ case "exclusive":
494
+ for (const branch of term.terms) {
495
+ const first = branch[0];
496
+ if (first?.type === "command" && isUsageHidden(first.hidden)) continue;
497
+ max = Math.max(max, maxVisibleAtomicWidth(branch));
498
+ }
499
+ break;
500
+ case "literal":
501
+ if (term.value !== "") max = Math.max(max, getDisplayWidth(term.value));
502
+ break;
503
+ case "ellipsis":
504
+ max = Math.max(max, 3);
505
+ break;
506
+ }
507
+ return max;
508
+ }
251
509
  function ansiAwareRightPad(text$1, length, char = " ") {
252
- const strippedText = text$1.replace(ansiEscapeCodeRegex, "");
253
- if (strippedText.length >= length) return text$1;
254
- return text$1 + char.repeat(length - strippedText.length);
510
+ const visibleWidth = lastLineVisibleLength(text$1);
511
+ if (visibleWidth >= length) return text$1;
512
+ return text$1 + char.repeat(length - visibleWidth);
255
513
  }
256
514
  function lastLineVisibleLength(text$1) {
257
515
  const lastNewline = text$1.lastIndexOf("\n");
258
516
  const lastLine = lastNewline === -1 ? text$1 : text$1.slice(lastNewline + 1);
259
- return lastLine.replace(ansiEscapeCodeRegex, "").length;
517
+ return getDisplayWidth(lastLine);
260
518
  }
261
519
 
262
520
  //#endregion
263
- export { formatDocPage };
521
+ export { cloneDocEntry, deduplicateDocEntries, deduplicateDocFragments, formatDocPage, isDocEntryHidden };
@@ -0,0 +1,56 @@
1
+
2
+ //#region src/execution-context.ts
3
+ /**
4
+ * Appends a child parser segment to the current execution path.
5
+ * @internal
6
+ */
7
+ function withChildExecPath(exec, segment) {
8
+ if (exec == null) return void 0;
9
+ return {
10
+ ...exec,
11
+ path: [...exec.path ?? [], segment]
12
+ };
13
+ }
14
+ /**
15
+ * Merges child-owned execution fields back into the parent execution context.
16
+ * @internal
17
+ */
18
+ function mergeChildExec(parent, child) {
19
+ if (parent == null) return child;
20
+ if (child == null) return parent;
21
+ return {
22
+ ...parent,
23
+ trace: child.trace ?? parent.trace,
24
+ dependencyRuntime: child.dependencyRuntime ?? parent.dependencyRuntime,
25
+ dependencyRegistry: child.dependencyRegistry ?? parent.dependencyRegistry,
26
+ commandPath: child.commandPath ?? parent.commandPath,
27
+ preCompletedByParser: child.preCompletedByParser ?? parent.preCompletedByParser,
28
+ excludedSourceFields: child.excludedSourceFields ?? parent.excludedSourceFields
29
+ };
30
+ }
31
+ /**
32
+ * Creates a child parser context while keeping flat and nested execution data
33
+ * aligned.
34
+ * @internal
35
+ */
36
+ function withChildContext(context, segment, state, usage) {
37
+ const exec = withChildExecPath(context.exec, segment);
38
+ const dependencyRegistry = context.dependencyRegistry ?? exec?.dependencyRegistry;
39
+ return {
40
+ ...context,
41
+ state,
42
+ ...usage != null ? { usage } : {},
43
+ ...exec != null ? {
44
+ exec: dependencyRegistry === exec.dependencyRegistry ? exec : {
45
+ ...exec,
46
+ dependencyRegistry
47
+ },
48
+ dependencyRegistry
49
+ } : {}
50
+ };
51
+ }
52
+
53
+ //#endregion
54
+ exports.mergeChildExec = mergeChildExec;
55
+ exports.withChildContext = withChildContext;
56
+ exports.withChildExecPath = withChildExecPath;
@@ -0,0 +1,53 @@
1
+ //#region src/execution-context.ts
2
+ /**
3
+ * Appends a child parser segment to the current execution path.
4
+ * @internal
5
+ */
6
+ function withChildExecPath(exec, segment) {
7
+ if (exec == null) return void 0;
8
+ return {
9
+ ...exec,
10
+ path: [...exec.path ?? [], segment]
11
+ };
12
+ }
13
+ /**
14
+ * Merges child-owned execution fields back into the parent execution context.
15
+ * @internal
16
+ */
17
+ function mergeChildExec(parent, child) {
18
+ if (parent == null) return child;
19
+ if (child == null) return parent;
20
+ return {
21
+ ...parent,
22
+ trace: child.trace ?? parent.trace,
23
+ dependencyRuntime: child.dependencyRuntime ?? parent.dependencyRuntime,
24
+ dependencyRegistry: child.dependencyRegistry ?? parent.dependencyRegistry,
25
+ commandPath: child.commandPath ?? parent.commandPath,
26
+ preCompletedByParser: child.preCompletedByParser ?? parent.preCompletedByParser,
27
+ excludedSourceFields: child.excludedSourceFields ?? parent.excludedSourceFields
28
+ };
29
+ }
30
+ /**
31
+ * Creates a child parser context while keeping flat and nested execution data
32
+ * aligned.
33
+ * @internal
34
+ */
35
+ function withChildContext(context, segment, state, usage) {
36
+ const exec = withChildExecPath(context.exec, segment);
37
+ const dependencyRegistry = context.dependencyRegistry ?? exec?.dependencyRegistry;
38
+ return {
39
+ ...context,
40
+ state,
41
+ ...usage != null ? { usage } : {},
42
+ ...exec != null ? {
43
+ exec: dependencyRegistry === exec.dependencyRegistry ? exec : {
44
+ ...exec,
45
+ dependencyRegistry
46
+ },
47
+ dependencyRegistry
48
+ } : {}
49
+ };
50
+ }
51
+
52
+ //#endregion
53
+ export { mergeChildExec, withChildContext, withChildExecPath };