@optique/core 1.4.0-dev.2897 → 1.4.0-dev.2928
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/dist/constructs.cjs +18 -5
- package/dist/constructs.js +18 -5
- package/dist/doc-pages.cjs +480 -0
- package/dist/doc-pages.d.cts +98 -0
- package/dist/doc-pages.d.ts +98 -0
- package/dist/doc-pages.js +479 -0
- package/dist/facade.cjs +10 -3
- package/dist/facade.d.cts +11 -0
- package/dist/facade.d.ts +11 -0
- package/dist/facade.js +11 -4
- package/dist/internal/doc-metadata.cjs +121 -0
- package/dist/internal/doc-metadata.d.cts +72 -0
- package/dist/internal/doc-metadata.d.ts +72 -0
- package/dist/internal/doc-metadata.js +118 -0
- package/dist/modifiers.cjs +16 -3
- package/dist/modifiers.js +16 -3
- package/dist/primitives.cjs +33 -4
- package/dist/primitives.js +33 -4
- package/dist/usage-internals.cjs +104 -1
- package/dist/usage-internals.js +104 -2
- package/dist/valueparser.cjs +20 -10
- package/dist/valueparser.js +20 -10
- package/package.json +10 -2
- package/skills/optique/SKILL.md +8 -8
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { HiddenVisibility } from "./usage.js";
|
|
2
|
+
import { DocPage } from "./doc.js";
|
|
3
|
+
import { Mode, Parser } from "./internal/parser.js";
|
|
4
|
+
|
|
5
|
+
//#region src/doc-pages.d.ts
|
|
6
|
+
|
|
7
|
+
/** A command page with an explicit canonical path. @since 1.4.0 */
|
|
8
|
+
interface CommandDocPage {
|
|
9
|
+
/** Canonical tokens, excluding program name and positional values. */
|
|
10
|
+
readonly path: readonly string[];
|
|
11
|
+
/** Constructed documentation, with command tokens already in usage. */
|
|
12
|
+
readonly page: DocPage;
|
|
13
|
+
/** Visible aliases of the final command token. */
|
|
14
|
+
readonly aliases?: readonly string[];
|
|
15
|
+
/** Accepted alternative names reserved for validation, never display. */
|
|
16
|
+
readonly hiddenAliases?: readonly string[];
|
|
17
|
+
/** Documentation visibility; hidden ancestors suppress descendants. */
|
|
18
|
+
readonly hidden?: HiddenVisibility;
|
|
19
|
+
/** Default alias display for this command. */
|
|
20
|
+
readonly showAliases?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** An author-supplied replacement or additional page. @since 1.4.0 */
|
|
23
|
+
interface SuppliedCommandDocPage extends CommandDocPage {
|
|
24
|
+
/**
|
|
25
|
+
* A page assertion covers this page only. A subtree assertion declares that
|
|
26
|
+
* the author supplied every visible descendant too; opaque descendants cannot
|
|
27
|
+
* be verified independently. Defaults to page.
|
|
28
|
+
*/
|
|
29
|
+
readonly coverage?: "page" | "subtree";
|
|
30
|
+
}
|
|
31
|
+
/** A collected page and the origin of its documentation. @since 1.4.0 */
|
|
32
|
+
interface CollectedCommandDocPage extends CommandDocPage {
|
|
33
|
+
/** Whether metadata or the author provided this page. */
|
|
34
|
+
readonly source: "automatic" | "supplied";
|
|
35
|
+
/** Coverage asserted by the author, for supplied pages only. */
|
|
36
|
+
readonly coverage?: "page" | "subtree";
|
|
37
|
+
}
|
|
38
|
+
/** A visible scope whose documentation could not be collected. @since 1.4.0 */
|
|
39
|
+
interface DocPageCollectionGap {
|
|
40
|
+
/** Affected page or visible enclosing opaque scope; never positional values. */
|
|
41
|
+
readonly path: readonly string[];
|
|
42
|
+
/** Whether missing information affects this page or an opaque subtree. */
|
|
43
|
+
readonly scope: "page" | "subtree";
|
|
44
|
+
/** A stable diagnostic for the unsupported operation. */
|
|
45
|
+
readonly reason: "unregistered-parser" | "value-metadata-unavailable" | "static-default-format-unavailable" | "functional-usage" | "ambiguous-command-path" | "missing-command-page";
|
|
46
|
+
}
|
|
47
|
+
/** Result data for the supplied parser, without runner augmentation. @since 1.4.0 */
|
|
48
|
+
interface DocPageCollectionResult {
|
|
49
|
+
/** Root and supported commands in definition order, followed by additions. */
|
|
50
|
+
readonly entries: readonly CollectedCommandDocPage[];
|
|
51
|
+
/** Uncovered visible scopes; an empty list does not inspect author imports. */
|
|
52
|
+
readonly unresolved: readonly DocPageCollectionGap[];
|
|
53
|
+
/** Runner-added help/version/completion are outside this parser scope. */
|
|
54
|
+
readonly scope: "parser";
|
|
55
|
+
}
|
|
56
|
+
/** Options for metadata-only collection. @since 1.4.0 */
|
|
57
|
+
interface DocPageCollectionOptions {
|
|
58
|
+
/** Throw for uncovered scopes. Defaults to true. */
|
|
59
|
+
readonly strict?: boolean;
|
|
60
|
+
/** Replacements win at their canonical paths; duplicate supplied paths fail. */
|
|
61
|
+
readonly entries?: readonly SuppliedCommandDocPage[];
|
|
62
|
+
}
|
|
63
|
+
/** Strict collection failed with explicit partial result data. @since 1.4.0 */
|
|
64
|
+
declare class DocPageCollectionError extends TypeError {
|
|
65
|
+
/** Collected pages and every unresolved visible scope. */
|
|
66
|
+
readonly result: DocPageCollectionResult;
|
|
67
|
+
/**
|
|
68
|
+
* Constructs an error from a partial collection.
|
|
69
|
+
* @param result Collected pages and gaps.
|
|
70
|
+
*/
|
|
71
|
+
constructor(result: DocPageCollectionResult);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Collects documentation through core-owned constructor metadata, without
|
|
75
|
+
* parsing, defaults, normalization, validators, sources, suggestions, handlers,
|
|
76
|
+
* or custom parser/value-parser documentation/formatting methods.
|
|
77
|
+
*
|
|
78
|
+
* Supported constructors are command/or/object/seq, option/flag/argument,
|
|
79
|
+
* optional/multiple/withDefault; safe value metadata comes from string/integer/
|
|
80
|
+
* choice. Unregistered wrappers are opaque. Custom value parsers retain only
|
|
81
|
+
* enclosing primitive metadata captured at construction. Function defaults are
|
|
82
|
+
* omitted unless withDefault supplies a documentation message. Unsupported
|
|
83
|
+
* static default formatting is reported instead of evaluated.
|
|
84
|
+
*
|
|
85
|
+
* Imports and definition/page construction remain application-owned and are
|
|
86
|
+
* not sandboxed. Supply inert definitions separately from executable startup.
|
|
87
|
+
*
|
|
88
|
+
* @param parser The parser definition to document.
|
|
89
|
+
* @param options Strictness and author-supplied page/subtree replacements.
|
|
90
|
+
* @returns Canonical pages and explicit unresolved scopes for the user parser.
|
|
91
|
+
* @throws {DocPageCollectionError} If strict mode finds unresolved scopes.
|
|
92
|
+
* @throws {TypeError} If supplied paths/aliases are invalid or ambiguous,
|
|
93
|
+
* or supplied canonical paths are duplicated.
|
|
94
|
+
* @since 1.4.0
|
|
95
|
+
*/
|
|
96
|
+
declare function collectDocPages<M extends Mode, T, S>(parser: Parser<M, T, S>, options?: DocPageCollectionOptions): DocPageCollectionResult;
|
|
97
|
+
//#endregion
|
|
98
|
+
export { CollectedCommandDocPage, CommandDocPage, DocPageCollectionError, DocPageCollectionGap, DocPageCollectionOptions, DocPageCollectionResult, SuppliedCommandDocPage, collectDocPages };
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { validateCommandNames } from "./validate.js";
|
|
2
|
+
import { cloneUsageTerm, isDocHidden, mergeHidden } from "./usage.js";
|
|
3
|
+
import { cloneMessage } from "./message.js";
|
|
4
|
+
import { cloneDocEntry, deduplicateDocFragments } from "./doc.js";
|
|
5
|
+
import { getDocNode, getDocValue } from "./internal/doc-metadata.js";
|
|
6
|
+
|
|
7
|
+
//#region src/doc-pages.ts
|
|
8
|
+
/** Strict collection failed with explicit partial result data. @since 1.4.0 */
|
|
9
|
+
var DocPageCollectionError = class extends TypeError {
|
|
10
|
+
/** Collected pages and every unresolved visible scope. */
|
|
11
|
+
result;
|
|
12
|
+
/**
|
|
13
|
+
* Constructs an error from a partial collection.
|
|
14
|
+
* @param result Collected pages and gaps.
|
|
15
|
+
*/
|
|
16
|
+
constructor(result) {
|
|
17
|
+
super("Documentation collection has unresolved scopes.");
|
|
18
|
+
this.name = "DocPageCollectionError";
|
|
19
|
+
this.result = result;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const emptyProjection = {
|
|
23
|
+
usage: [],
|
|
24
|
+
fragments: []
|
|
25
|
+
};
|
|
26
|
+
function key(path) {
|
|
27
|
+
if (path.length > 0) validateCommandNames(path, "Documentation path");
|
|
28
|
+
if (path.some((token) => !token.isWellFormed())) throw new TypeError("Documentation paths must contain well-formed Unicode tokens.");
|
|
29
|
+
return JSON.stringify(path);
|
|
30
|
+
}
|
|
31
|
+
function prefix(parent, child) {
|
|
32
|
+
return parent.length <= child.length && parent.every((token, index) => token === child[index]);
|
|
33
|
+
}
|
|
34
|
+
function hasPath(parser, path) {
|
|
35
|
+
const node = getDocNode(parser);
|
|
36
|
+
if (node == null || path.length === 0 || isDocHidden(node.hidden) || ambiguousSequence(node)) return false;
|
|
37
|
+
if (node.kind === "command") return !isDocHidden(node.hidden) && node.name === path[0] && (path.length === 1 || hasPath(node.children[0], path.slice(1)));
|
|
38
|
+
return node.children?.some((child) => hasPath(child, path)) ?? false;
|
|
39
|
+
}
|
|
40
|
+
function hasCommands(parser) {
|
|
41
|
+
const node = getDocNode(parser);
|
|
42
|
+
if (node == null || isDocHidden(node.hidden)) return false;
|
|
43
|
+
return node.kind === "command" || node.children?.some(hasCommands) === true;
|
|
44
|
+
}
|
|
45
|
+
function ambiguousSequence(node) {
|
|
46
|
+
return node.kind === "seq" && (node.children?.filter(hasCommands).length ?? 0) > 1;
|
|
47
|
+
}
|
|
48
|
+
function hideUsage(usage, pathTerms) {
|
|
49
|
+
return usage.map((term) => {
|
|
50
|
+
if (pathTerms.includes(term)) return term;
|
|
51
|
+
if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") return {
|
|
52
|
+
...term,
|
|
53
|
+
terms: hideUsage(term.terms, pathTerms)
|
|
54
|
+
};
|
|
55
|
+
if (term.type === "exclusive") return {
|
|
56
|
+
...term,
|
|
57
|
+
terms: term.terms.map((terms) => hideUsage(terms, pathTerms))
|
|
58
|
+
};
|
|
59
|
+
if (term.type === "option" || term.type === "argument" || term.type === "command" || term.type === "passthrough") return {
|
|
60
|
+
...term,
|
|
61
|
+
hidden: mergeHidden(term.hidden, "usage")
|
|
62
|
+
};
|
|
63
|
+
return term;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function leafTerm(usage) {
|
|
67
|
+
for (const term of usage) if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") {
|
|
68
|
+
const found = leafTerm(term.terms);
|
|
69
|
+
if (found != null) return found;
|
|
70
|
+
} else return term;
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
function staticDefault(parser, value) {
|
|
74
|
+
const node = getDocNode(parser);
|
|
75
|
+
if (node == null) return void 0;
|
|
76
|
+
if (node.kind !== "leaf") {
|
|
77
|
+
const preservesValue = node.kind === "command" || node.kind === "or" || node.kind === "optional" || node.kind === "default";
|
|
78
|
+
return preservesValue && node.children?.length === 1 ? staticDefault(node.children[0], value) : void 0;
|
|
79
|
+
}
|
|
80
|
+
if (node.boolean && typeof value === "boolean") return [{
|
|
81
|
+
type: "text",
|
|
82
|
+
text: String(value)
|
|
83
|
+
}];
|
|
84
|
+
const metadata = node.valueParser == null ? void 0 : getDocValue(node.valueParser);
|
|
85
|
+
if (metadata == null || metadata.canFormatDefault === false || typeof value !== metadata.kind) return void 0;
|
|
86
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint") return void 0;
|
|
87
|
+
return [{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: metadata.preserveNegativeZero && Object.is(value, -0) ? "-0" : String(value)
|
|
90
|
+
}];
|
|
91
|
+
}
|
|
92
|
+
function fragmentsEntries(fragments) {
|
|
93
|
+
return fragments.flatMap((fragment) => fragment.type === "entry" ? [fragment] : fragment.entries);
|
|
94
|
+
}
|
|
95
|
+
function defaultFragments(fragments, value) {
|
|
96
|
+
return fragments.map((fragment) => fragment.type === "entry" ? {
|
|
97
|
+
...fragment,
|
|
98
|
+
default: value
|
|
99
|
+
} : {
|
|
100
|
+
...fragment,
|
|
101
|
+
entries: fragment.entries.map((entry) => ({
|
|
102
|
+
...entry,
|
|
103
|
+
default: value
|
|
104
|
+
}))
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function sections(fragments) {
|
|
108
|
+
const result = [];
|
|
109
|
+
const named = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const fragment of deduplicateDocFragments(fragments)) {
|
|
111
|
+
const title = fragment.type === "entry" ? void 0 : fragment.title;
|
|
112
|
+
let entries = named.get(title);
|
|
113
|
+
if (entries == null) {
|
|
114
|
+
entries = [];
|
|
115
|
+
named.set(title, entries);
|
|
116
|
+
result.push({
|
|
117
|
+
title,
|
|
118
|
+
entries
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
entries.push(...(fragment.type === "entry" ? [fragment] : fragment.entries).map(cloneDocEntry));
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
function publicUsage(usage) {
|
|
126
|
+
return usage.map((term) => {
|
|
127
|
+
if (term.type === "command") return {
|
|
128
|
+
type: "command",
|
|
129
|
+
name: term.name,
|
|
130
|
+
...term.hidden != null && { hidden: term.hidden }
|
|
131
|
+
};
|
|
132
|
+
if (term.type === "exclusive") return {
|
|
133
|
+
...term,
|
|
134
|
+
terms: term.terms.map(publicUsage)
|
|
135
|
+
};
|
|
136
|
+
if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") return {
|
|
137
|
+
...term,
|
|
138
|
+
terms: publicUsage(term.terms)
|
|
139
|
+
};
|
|
140
|
+
return cloneUsageTerm(term);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function docHidden(entry, entries) {
|
|
144
|
+
for (let length = 1; length <= entry.path.length; length++) if (isDocHidden(entries.get(key(entry.path.slice(0, length)))?.hidden)) return true;
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Collects documentation through core-owned constructor metadata, without
|
|
149
|
+
* parsing, defaults, normalization, validators, sources, suggestions, handlers,
|
|
150
|
+
* or custom parser/value-parser documentation/formatting methods.
|
|
151
|
+
*
|
|
152
|
+
* Supported constructors are command/or/object/seq, option/flag/argument,
|
|
153
|
+
* optional/multiple/withDefault; safe value metadata comes from string/integer/
|
|
154
|
+
* choice. Unregistered wrappers are opaque. Custom value parsers retain only
|
|
155
|
+
* enclosing primitive metadata captured at construction. Function defaults are
|
|
156
|
+
* omitted unless withDefault supplies a documentation message. Unsupported
|
|
157
|
+
* static default formatting is reported instead of evaluated.
|
|
158
|
+
*
|
|
159
|
+
* Imports and definition/page construction remain application-owned and are
|
|
160
|
+
* not sandboxed. Supply inert definitions separately from executable startup.
|
|
161
|
+
*
|
|
162
|
+
* @param parser The parser definition to document.
|
|
163
|
+
* @param options Strictness and author-supplied page/subtree replacements.
|
|
164
|
+
* @returns Canonical pages and explicit unresolved scopes for the user parser.
|
|
165
|
+
* @throws {DocPageCollectionError} If strict mode finds unresolved scopes.
|
|
166
|
+
* @throws {TypeError} If supplied paths/aliases are invalid or ambiguous,
|
|
167
|
+
* or supplied canonical paths are duplicated.
|
|
168
|
+
* @since 1.4.0
|
|
169
|
+
*/
|
|
170
|
+
function collectDocPages(parser, options = {}) {
|
|
171
|
+
const discovered = new Map([["[]", { path: [] }]]);
|
|
172
|
+
const reports = /* @__PURE__ */ new Map();
|
|
173
|
+
const report = (path, scope, reason) => {
|
|
174
|
+
const gap = {
|
|
175
|
+
path: [...path],
|
|
176
|
+
scope,
|
|
177
|
+
reason
|
|
178
|
+
};
|
|
179
|
+
reports.set(JSON.stringify(gap), gap);
|
|
180
|
+
};
|
|
181
|
+
function enumerate(input, path) {
|
|
182
|
+
const node = getDocNode(input);
|
|
183
|
+
if (node == null) {
|
|
184
|
+
report(path, "subtree", "unregistered-parser");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (isDocHidden(node.hidden)) return;
|
|
188
|
+
if (ambiguousSequence(node)) {
|
|
189
|
+
report(path, "subtree", "ambiguous-command-path");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (node.kind === "command") {
|
|
193
|
+
if (isDocHidden(node.hidden)) return;
|
|
194
|
+
const childPath = [...path, node.name];
|
|
195
|
+
const pathKey = key(childPath);
|
|
196
|
+
if (discovered.has(pathKey)) report(childPath, "subtree", "ambiguous-command-path");
|
|
197
|
+
else discovered.set(pathKey, {
|
|
198
|
+
path: childPath,
|
|
199
|
+
node
|
|
200
|
+
});
|
|
201
|
+
enumerate(node.children[0], childPath);
|
|
202
|
+
} else for (const child of node.children ?? []) enumerate(child, path);
|
|
203
|
+
}
|
|
204
|
+
enumerate(parser, []);
|
|
205
|
+
function project(input, target, path, pagePath) {
|
|
206
|
+
const node = getDocNode(input);
|
|
207
|
+
if (node == null) {
|
|
208
|
+
report(path, "subtree", "unregistered-parser");
|
|
209
|
+
if (key(path) !== key(pagePath)) report(pagePath, "page", "unregistered-parser");
|
|
210
|
+
return {
|
|
211
|
+
...emptyProjection,
|
|
212
|
+
unresolvedUsage: true
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (isDocHidden(node.hidden)) return {
|
|
216
|
+
...emptyProjection,
|
|
217
|
+
usage: node.usage
|
|
218
|
+
};
|
|
219
|
+
if (ambiguousSequence(node)) {
|
|
220
|
+
report(path, "subtree", "ambiguous-command-path");
|
|
221
|
+
if (key(path) !== key(pagePath)) report(pagePath, "page", "ambiguous-command-path");
|
|
222
|
+
return {
|
|
223
|
+
...emptyProjection,
|
|
224
|
+
unresolvedUsage: true
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (node.kind === "leaf") {
|
|
228
|
+
const term = leafTerm(node.usage);
|
|
229
|
+
if (term == null || "hidden" in term && isDocHidden(term.hidden)) return {
|
|
230
|
+
...emptyProjection,
|
|
231
|
+
usage: node.usage
|
|
232
|
+
};
|
|
233
|
+
const metadata = node.valueParser == null ? void 0 : getDocValue(node.valueParser);
|
|
234
|
+
if (node.valueParser != null && metadata == null) report(pagePath, "page", "value-metadata-unavailable");
|
|
235
|
+
const choices = metadata?.choices == null ? void 0 : [{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: metadata.choices.map((value) => metadata.preserveNegativeZero && Object.is(value, -0) ? "-0" : String(value)).join(", ")
|
|
238
|
+
}];
|
|
239
|
+
return {
|
|
240
|
+
usage: node.usage,
|
|
241
|
+
fragments: [{
|
|
242
|
+
type: "entry",
|
|
243
|
+
term,
|
|
244
|
+
description: node.description,
|
|
245
|
+
choices
|
|
246
|
+
}]
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (node.kind === "command") {
|
|
250
|
+
if (isDocHidden(node.hidden)) return {
|
|
251
|
+
...emptyProjection,
|
|
252
|
+
usage: node.usage
|
|
253
|
+
};
|
|
254
|
+
if (target.length === 0) return {
|
|
255
|
+
usage: node.usage,
|
|
256
|
+
fragments: [{
|
|
257
|
+
type: "entry",
|
|
258
|
+
term: {
|
|
259
|
+
type: "command",
|
|
260
|
+
name: node.name,
|
|
261
|
+
aliases: node.aliases,
|
|
262
|
+
hidden: node.hidden
|
|
263
|
+
},
|
|
264
|
+
description: node.brief ?? node.description,
|
|
265
|
+
showAliases: node.showAliases
|
|
266
|
+
}]
|
|
267
|
+
};
|
|
268
|
+
if (target[0] !== node.name) return emptyProjection;
|
|
269
|
+
const selected = project(node.children[0], target.slice(1), [...path, node.name], pagePath);
|
|
270
|
+
let usage$1 = selected.usage;
|
|
271
|
+
let unresolvedUsage = selected.unresolvedUsage;
|
|
272
|
+
if (target.length === 1 && node.usageLine != null) if (typeof node.usageLine === "function") {
|
|
273
|
+
report([...path, node.name], "page", "functional-usage");
|
|
274
|
+
unresolvedUsage = true;
|
|
275
|
+
} else {
|
|
276
|
+
usage$1 = node.usageLine;
|
|
277
|
+
unresolvedUsage = false;
|
|
278
|
+
}
|
|
279
|
+
const pathTerm = {
|
|
280
|
+
type: "command",
|
|
281
|
+
name: node.name
|
|
282
|
+
};
|
|
283
|
+
return {
|
|
284
|
+
...selected,
|
|
285
|
+
usage: [pathTerm, ...usage$1],
|
|
286
|
+
pathTerms: [pathTerm, ...selected.pathTerms ?? []],
|
|
287
|
+
unresolvedUsage,
|
|
288
|
+
brief: selected.brief ?? node.brief,
|
|
289
|
+
description: selected.description ?? node.description,
|
|
290
|
+
footer: selected.footer ?? node.footer,
|
|
291
|
+
showUsage: selected.showUsage ?? node.showUsage
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (node.kind === "optional" || node.kind === "multiple" || node.kind === "default") {
|
|
295
|
+
const inner = project(node.children[0], target, path, pagePath);
|
|
296
|
+
if (node.kind === "default") {
|
|
297
|
+
const usage$1 = [{
|
|
298
|
+
type: "optional",
|
|
299
|
+
terms: inner.usage
|
|
300
|
+
}];
|
|
301
|
+
if (node.defaultMessage != null) return {
|
|
302
|
+
...inner,
|
|
303
|
+
usage: usage$1,
|
|
304
|
+
fragments: defaultFragments(inner.fragments, node.defaultMessage)
|
|
305
|
+
};
|
|
306
|
+
if (typeof node.defaultValue !== "function" && fragmentsEntries(inner.fragments).length > 0) {
|
|
307
|
+
const formatted = staticDefault(node.children[0], node.defaultValue);
|
|
308
|
+
if (formatted != null) return {
|
|
309
|
+
...inner,
|
|
310
|
+
usage: usage$1,
|
|
311
|
+
fragments: defaultFragments(inner.fragments, formatted)
|
|
312
|
+
};
|
|
313
|
+
report(pagePath, "page", "static-default-format-unavailable");
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
...inner,
|
|
317
|
+
usage: usage$1
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
...inner,
|
|
322
|
+
usage: [node.kind === "optional" ? {
|
|
323
|
+
type: "optional",
|
|
324
|
+
terms: inner.usage
|
|
325
|
+
} : {
|
|
326
|
+
...node.usage.find((term) => term.type === "multiple"),
|
|
327
|
+
type: "multiple",
|
|
328
|
+
terms: inner.usage,
|
|
329
|
+
min: node.min ?? 0
|
|
330
|
+
}]
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const children = node.children ?? [];
|
|
334
|
+
if (node.kind === "or") {
|
|
335
|
+
if (target.length > 0) {
|
|
336
|
+
const selected = children.filter((child) => hasPath(child, target));
|
|
337
|
+
if (selected.length !== 1) {
|
|
338
|
+
report(path, "subtree", "ambiguous-command-path");
|
|
339
|
+
if (key(path) !== key(pagePath)) report(pagePath, "page", "ambiguous-command-path");
|
|
340
|
+
return {
|
|
341
|
+
...emptyProjection,
|
|
342
|
+
unresolvedUsage: true
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
return project(selected[0], target, path, pagePath);
|
|
346
|
+
}
|
|
347
|
+
const projected$1 = children.map((child) => project(child, [], path, pagePath));
|
|
348
|
+
return {
|
|
349
|
+
usage: node.usage,
|
|
350
|
+
fragments: projected$1.flatMap((child) => child.fragments),
|
|
351
|
+
unresolvedUsage: projected$1.some((child) => child.unresolvedUsage)
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const projected = children.map((child) => project(child, hasPath(child, target) ? target : [], path, pagePath));
|
|
355
|
+
const fragments = projected.flatMap((child) => child.fragments);
|
|
356
|
+
const pathTerms = projected.flatMap((child) => child.pathTerms ?? []);
|
|
357
|
+
const usage = node.kind === "seq" ? [{
|
|
358
|
+
type: "sequence",
|
|
359
|
+
terms: projected.flatMap((child) => child.usage)
|
|
360
|
+
}] : projected.flatMap((child) => child.usage);
|
|
361
|
+
return {
|
|
362
|
+
usage: node.hidden === "usage" ? hideUsage(usage, pathTerms) : usage,
|
|
363
|
+
pathTerms,
|
|
364
|
+
fragments: node.label == null ? fragments : [...fragments.filter((fragment) => fragment.type === "section" && fragment.title != null), {
|
|
365
|
+
type: "section",
|
|
366
|
+
title: node.label,
|
|
367
|
+
entries: fragmentsEntries(fragments.filter((fragment) => fragment.type === "entry" || fragment.title == null))
|
|
368
|
+
}],
|
|
369
|
+
unresolvedUsage: projected.some((child) => child.unresolvedUsage),
|
|
370
|
+
brief: projected.find((child) => child.brief != null)?.brief,
|
|
371
|
+
description: projected.find((child) => child.description != null)?.description,
|
|
372
|
+
footer: projected.find((child) => child.footer != null)?.footer,
|
|
373
|
+
showUsage: projected.find((child) => child.showUsage != null)?.showUsage
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const entries = /* @__PURE__ */ new Map();
|
|
377
|
+
for (const { path, node } of discovered.values()) {
|
|
378
|
+
const page = project(parser, path, [], path);
|
|
379
|
+
entries.set(key(path), {
|
|
380
|
+
path,
|
|
381
|
+
aliases: node?.aliases?.filter((alias) => !node.hiddenAliases?.includes(alias)),
|
|
382
|
+
hiddenAliases: node?.hiddenAliases?.slice(),
|
|
383
|
+
hidden: node?.hidden,
|
|
384
|
+
showAliases: node?.showAliases,
|
|
385
|
+
source: "automatic",
|
|
386
|
+
page: {
|
|
387
|
+
usage: page.unresolvedUsage ? void 0 : publicUsage(page.usage),
|
|
388
|
+
sections: sections(page.fragments),
|
|
389
|
+
brief: page.brief == null ? void 0 : cloneMessage(page.brief),
|
|
390
|
+
description: page.description == null ? void 0 : cloneMessage(page.description),
|
|
391
|
+
footer: page.footer == null ? void 0 : cloneMessage(page.footer),
|
|
392
|
+
showUsage: page.showUsage
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
const supplied = /* @__PURE__ */ new Map();
|
|
397
|
+
for (const entry of options.entries ?? []) {
|
|
398
|
+
const entryKey = key(entry.path);
|
|
399
|
+
if (supplied.has(entryKey)) throw new TypeError(`Duplicate supplied command path: ${entryKey}`);
|
|
400
|
+
if (entry.aliases?.length || entry.hiddenAliases?.length) validateCommandNames([...entry.aliases ?? [], ...entry.hiddenAliases ?? []], "Documentation alias");
|
|
401
|
+
if (entry.path.length === 0 && (entry.aliases?.length || entry.hiddenAliases?.length || entry.hidden)) throw new TypeError("The root page cannot have aliases or hidden visibility.");
|
|
402
|
+
if (entry.coverage != null && entry.coverage !== "page" && entry.coverage !== "subtree") throw new TypeError("Supplied coverage must be page or subtree.");
|
|
403
|
+
supplied.set(entryKey, entry);
|
|
404
|
+
entries.set(entryKey, {
|
|
405
|
+
...entries.get(entryKey),
|
|
406
|
+
...entry,
|
|
407
|
+
path: [...entry.path],
|
|
408
|
+
source: "supplied",
|
|
409
|
+
coverage: entry.coverage ?? "page"
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const visible = [...entries.values()].filter((entry) => !docHidden(entry, entries));
|
|
413
|
+
const unresolved = [...reports.values()].filter((gap) => {
|
|
414
|
+
const enclosing = entries.get(key(gap.path));
|
|
415
|
+
if (enclosing != null && docHidden(enclosing, entries)) return false;
|
|
416
|
+
if (gap.reason === "functional-usage") return supplied.get(key(gap.path))?.page.usage == null;
|
|
417
|
+
if (!supplied.has(key(gap.path))) return true;
|
|
418
|
+
if (gap.scope === "page") return false;
|
|
419
|
+
return ![...supplied.values()].some((entry) => entry.coverage === "subtree" && prefix(entry.path, gap.path));
|
|
420
|
+
});
|
|
421
|
+
const missing = /* @__PURE__ */ new Map();
|
|
422
|
+
const spellings = /* @__PURE__ */ new Map();
|
|
423
|
+
for (const entry of entries.values()) {
|
|
424
|
+
if (entry.path.length === 0) continue;
|
|
425
|
+
for (const spelling of [
|
|
426
|
+
entry.path.at(-1),
|
|
427
|
+
...entry.aliases ?? [],
|
|
428
|
+
...entry.hiddenAliases ?? []
|
|
429
|
+
]) {
|
|
430
|
+
const spellingKey = key([...entry.path.slice(0, -1), spelling]);
|
|
431
|
+
const previous = spellings.get(spellingKey);
|
|
432
|
+
if (previous != null && previous !== key(entry.path)) throw new TypeError(`Ambiguous command spelling: ${spellingKey}`);
|
|
433
|
+
spellings.set(spellingKey, key(entry.path));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const entry of visible) {
|
|
437
|
+
for (let length = 0; length < entry.path.length; length++) {
|
|
438
|
+
const parent = entry.path.slice(0, length);
|
|
439
|
+
if (!entries.has(key(parent))) missing.set(key(parent), parent);
|
|
440
|
+
}
|
|
441
|
+
for (const item of entry.page.sections.flatMap((section) => section.entries)) {
|
|
442
|
+
if (item.term.type !== "command" || isDocHidden(item.term.hidden)) continue;
|
|
443
|
+
const child = [...entry.path, item.term.name];
|
|
444
|
+
if (!entries.has(key(child))) missing.set(key(child), child);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
for (const path of missing.values()) unresolved.push({
|
|
448
|
+
path,
|
|
449
|
+
scope: "page",
|
|
450
|
+
reason: "missing-command-page"
|
|
451
|
+
});
|
|
452
|
+
const filtered = visible.map((entry) => {
|
|
453
|
+
const pageSections = entry.page.sections.map((section) => ({
|
|
454
|
+
...section,
|
|
455
|
+
entries: section.entries.filter((item) => {
|
|
456
|
+
if (item.term.type !== "command") return true;
|
|
457
|
+
const target = entries.get(key([...entry.path, item.term.name]));
|
|
458
|
+
return target == null || !docHidden(target, entries);
|
|
459
|
+
})
|
|
460
|
+
}));
|
|
461
|
+
return pageSections.some((section, index) => section.entries.length !== entry.page.sections[index].entries.length) ? {
|
|
462
|
+
...entry,
|
|
463
|
+
page: {
|
|
464
|
+
...entry.page,
|
|
465
|
+
sections: pageSections
|
|
466
|
+
}
|
|
467
|
+
} : entry;
|
|
468
|
+
});
|
|
469
|
+
const result = {
|
|
470
|
+
entries: filtered,
|
|
471
|
+
unresolved,
|
|
472
|
+
scope: "parser"
|
|
473
|
+
};
|
|
474
|
+
if ((options.strict ?? true) && unresolved.length > 0) throw new DocPageCollectionError(result);
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
//#endregion
|
|
479
|
+
export { DocPageCollectionError, collectDocPages };
|
package/dist/facade.cjs
CHANGED
|
@@ -818,7 +818,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
818
818
|
options = optionsParam ?? {};
|
|
819
819
|
}
|
|
820
820
|
require_validate.validateProgramName(programName);
|
|
821
|
-
const { colors, messageFormatter, theme: requestedTheme, maxWidth: requestedMaxWidth, termWidth, showDefault, showChoices, showAliases, showEnvironment, sectionOrder, helpSections, showUsage, usageLine, commandList = "recursive", aboveError = "usage", onError = () => {
|
|
821
|
+
const { colors, messageFormatter, theme: requestedTheme, maxWidth: requestedMaxWidth, termWidth, showDefault, showChoices, showAliases, showEnvironment, sectionOrder, helpSections, showUsage, usageLine, commandList = "recursive", aboveError = "usage", errorUsage = "root", onError = () => {
|
|
822
822
|
throw new RunParserError("Failed to parse command line arguments.");
|
|
823
823
|
}, stderr = console.error, stdout = console.log, brief, description, examples, author, bugs, footer } = options;
|
|
824
824
|
let maxWidth = requestedMaxWidth;
|
|
@@ -1117,8 +1117,15 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
1117
1117
|
})));
|
|
1118
1118
|
}
|
|
1119
1119
|
if (effectiveAboveError === "usage") {
|
|
1120
|
-
|
|
1121
|
-
|
|
1120
|
+
const rootUsage = augmentedParser.usage;
|
|
1121
|
+
const optionCount = Number(helpParsers.helpOption != null) + Number(versionParsers.versionOption != null);
|
|
1122
|
+
const alternativeCount = optionCount + Number(helpParsers.helpCommand != null) + Number(versionParsers.versionCommand != null) + Number(completionParsers.completionCommand != null);
|
|
1123
|
+
const errorUsageTerms = aboveError === "usage" && errorUsage === "command" ? require_usage_internals.projectCommandUsage(rootUsage, classified.commandPath, alternativeCount === 0 ? void 0 : {
|
|
1124
|
+
optionCount,
|
|
1125
|
+
alternativeCount
|
|
1126
|
+
}) ?? rootUsage : rootUsage;
|
|
1127
|
+
resolveUsageWidth(errorUsageTerms);
|
|
1128
|
+
stderr(`${usageLabel()}${indentLines(require_usage.formatUsage(programName, errorUsageTerms, {
|
|
1122
1129
|
theme,
|
|
1123
1130
|
colors,
|
|
1124
1131
|
maxWidth: maxWidth == null ? void 0 : maxWidth - usageLabelWidth(),
|
package/dist/facade.d.cts
CHANGED
|
@@ -326,6 +326,17 @@ interface RunOptions<THelp, TError> {
|
|
|
326
326
|
* @default `"usage"`
|
|
327
327
|
*/
|
|
328
328
|
readonly aboveError?: "usage" | "help" | "none";
|
|
329
|
+
/**
|
|
330
|
+
* Scope of the compact preamble when `aboveError` is `"usage"`.
|
|
331
|
+
* `"command"` uses the failed parse's canonical command path and usage
|
|
332
|
+
* metadata, retaining parent options. Hidden or unresolved paths and
|
|
333
|
+
* unsupported compositions fall back to root usage. Neither mode applies
|
|
334
|
+
* help-only `usageLine` overrides or evaluates the parser again.
|
|
335
|
+
*
|
|
336
|
+
* @default `"root"`
|
|
337
|
+
* @since 1.4.0
|
|
338
|
+
*/
|
|
339
|
+
readonly errorUsage?: "root" | "command";
|
|
329
340
|
/**
|
|
330
341
|
* Callback invoked after error output for a parse failure, an invalid
|
|
331
342
|
* command path before help, or a missing or unsupported completion shell.
|
package/dist/facade.d.ts
CHANGED
|
@@ -326,6 +326,17 @@ interface RunOptions<THelp, TError> {
|
|
|
326
326
|
* @default `"usage"`
|
|
327
327
|
*/
|
|
328
328
|
readonly aboveError?: "usage" | "help" | "none";
|
|
329
|
+
/**
|
|
330
|
+
* Scope of the compact preamble when `aboveError` is `"usage"`.
|
|
331
|
+
* `"command"` uses the failed parse's canonical command path and usage
|
|
332
|
+
* metadata, retaining parent options. Hidden or unresolved paths and
|
|
333
|
+
* unsupported compositions fall back to root usage. Neither mode applies
|
|
334
|
+
* help-only `usageLine` overrides or evaluates the parser again.
|
|
335
|
+
*
|
|
336
|
+
* @default `"root"`
|
|
337
|
+
* @since 1.4.0
|
|
338
|
+
*/
|
|
339
|
+
readonly errorUsage?: "root" | "command";
|
|
329
340
|
/**
|
|
330
341
|
* Callback invoked after error output for a parse failure, an invalid
|
|
331
342
|
* command path before help, or a missing or unsupported completion shell.
|
package/dist/facade.js
CHANGED
|
@@ -12,7 +12,7 @@ import { collectExplicitSourceValues, collectExplicitSourceValuesAsync, createDe
|
|
|
12
12
|
import { createInputTrace } from "./input-trace.js";
|
|
13
13
|
import { attemptParseAsync, attemptParseSync, createEffectfulCompletionSession, createParserContext, getDocPage } from "./internal/parser.js";
|
|
14
14
|
import { bash, fish, nu, pwsh, zsh } from "./completion.js";
|
|
15
|
-
import { collectActiveOptionNames, collectRootOptionNames } from "./usage-internals.js";
|
|
15
|
+
import { collectActiveOptionNames, collectRootOptionNames, projectCommandUsage } from "./usage-internals.js";
|
|
16
16
|
import { allowDuplicateLeadingCommandNamesKey, hiddenCommandAliasesKey } from "./internal/command-alias.js";
|
|
17
17
|
import { completeOrExtractPhase2Seed } from "./phase2-seed.js";
|
|
18
18
|
import { multiple, optional, withDefault } from "./modifiers.js";
|
|
@@ -818,7 +818,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
818
818
|
options = optionsParam ?? {};
|
|
819
819
|
}
|
|
820
820
|
validateProgramName(programName);
|
|
821
|
-
const { colors, messageFormatter, theme: requestedTheme, maxWidth: requestedMaxWidth, termWidth, showDefault, showChoices, showAliases, showEnvironment, sectionOrder, helpSections, showUsage, usageLine, commandList = "recursive", aboveError = "usage", onError = () => {
|
|
821
|
+
const { colors, messageFormatter, theme: requestedTheme, maxWidth: requestedMaxWidth, termWidth, showDefault, showChoices, showAliases, showEnvironment, sectionOrder, helpSections, showUsage, usageLine, commandList = "recursive", aboveError = "usage", errorUsage = "root", onError = () => {
|
|
822
822
|
throw new RunParserError("Failed to parse command line arguments.");
|
|
823
823
|
}, stderr = console.error, stdout = console.log, brief, description, examples, author, bugs, footer } = options;
|
|
824
824
|
let maxWidth = requestedMaxWidth;
|
|
@@ -1117,8 +1117,15 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
1117
1117
|
})));
|
|
1118
1118
|
}
|
|
1119
1119
|
if (effectiveAboveError === "usage") {
|
|
1120
|
-
|
|
1121
|
-
|
|
1120
|
+
const rootUsage = augmentedParser.usage;
|
|
1121
|
+
const optionCount = Number(helpParsers.helpOption != null) + Number(versionParsers.versionOption != null);
|
|
1122
|
+
const alternativeCount = optionCount + Number(helpParsers.helpCommand != null) + Number(versionParsers.versionCommand != null) + Number(completionParsers.completionCommand != null);
|
|
1123
|
+
const errorUsageTerms = aboveError === "usage" && errorUsage === "command" ? projectCommandUsage(rootUsage, classified.commandPath, alternativeCount === 0 ? void 0 : {
|
|
1124
|
+
optionCount,
|
|
1125
|
+
alternativeCount
|
|
1126
|
+
}) ?? rootUsage : rootUsage;
|
|
1127
|
+
resolveUsageWidth(errorUsageTerms);
|
|
1128
|
+
stderr(`${usageLabel()}${indentLines(formatUsage(programName, errorUsageTerms, {
|
|
1122
1129
|
theme,
|
|
1123
1130
|
colors,
|
|
1124
1131
|
maxWidth: maxWidth == null ? void 0 : maxWidth - usageLabelWidth(),
|