@optique/man 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/README.md +17 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/cli.cjs +21 -17
- package/dist/cli.js +6 -3
- package/dist/{generator-OV4-0NLg.cjs → generator-DYAqAqpo.cjs} +3 -2
- package/dist/generator.cjs +2 -2
- package/dist/index.cjs +2 -2
- package/dist/{man-Dp2YQIdn.cjs → man-ChbwsIhG.cjs} +4 -32
- package/dist/man.cjs +1 -1
- package/dist/markdown.cjs +248 -0
- package/dist/markdown.d.cts +81 -0
- package/dist/markdown.d.ts +81 -0
- package/dist/markdown.js +246 -0
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -98,6 +98,23 @@ const manPage = generateManPage(parser, {
|
|
|
98
98
|
~~~~
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
Markdown command references
|
|
102
|
+
---------------------------
|
|
103
|
+
|
|
104
|
+
Import `formatCommandReference()` from `@optique/man/markdown` for one linked
|
|
105
|
+
reference containing explicitly supplied command pages. Supported built-in
|
|
106
|
+
definitions can supply pages through `collectDocPages()` from
|
|
107
|
+
`@optique/core/doc-pages`, with strict reporting for unsupported scopes. Neither
|
|
108
|
+
subpath is imported by ordinary parser or runner entry points.
|
|
109
|
+
|
|
110
|
+
See the [Markdown reference guide] and the [runnable example]. Coverage is
|
|
111
|
+
author-declared, including whether runner-added help/version/completion forms
|
|
112
|
+
are documented.
|
|
113
|
+
|
|
114
|
+
[Markdown reference guide]: https://optique.dev/concepts/markdown-reference
|
|
115
|
+
[runnable example]: https://github.com/dahlia/optique/tree/main/packages/man/test/fixtures/markdown-reference
|
|
116
|
+
|
|
117
|
+
|
|
101
118
|
Documentation
|
|
102
119
|
-------------
|
|
103
120
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
|
|
25
|
+
Object.defineProperty(exports, '__toESM', {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
get: function () {
|
|
28
|
+
return __toESM;
|
|
29
|
+
}
|
|
30
|
+
});
|
package/dist/cli.cjs
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const
|
|
2
|
+
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
3
3
|
require('./roff-C8MvQ2tL.cjs');
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
4
|
+
require('./man-ChbwsIhG.cjs');
|
|
5
|
+
const require_generator = require('./generator-DYAqAqpo.cjs');
|
|
6
|
+
const __optique_core_constructs = require_chunk.__toESM(require("@optique/core/constructs"));
|
|
7
|
+
const __optique_core_primitives = require_chunk.__toESM(require("@optique/core/primitives"));
|
|
8
|
+
const __optique_core_valueparser = require_chunk.__toESM(require("@optique/core/valueparser"));
|
|
9
|
+
const __optique_core_modifiers = require_chunk.__toESM(require("@optique/core/modifiers"));
|
|
10
|
+
const __optique_core_message = require_chunk.__toESM(require("@optique/core/message"));
|
|
11
|
+
const __optique_core_program = require_chunk.__toESM(require("@optique/core/program"));
|
|
12
|
+
const __optique_run = require_chunk.__toESM(require("@optique/run"));
|
|
13
|
+
const node_fs = require_chunk.__toESM(require("node:fs"));
|
|
14
|
+
const node_fs_promises = require_chunk.__toESM(require("node:fs/promises"));
|
|
15
|
+
const node_path = require_chunk.__toESM(require("node:path"));
|
|
16
|
+
const node_process = require_chunk.__toESM(require("node:process"));
|
|
17
|
+
const node_url = require_chunk.__toESM(require("node:url"));
|
|
17
18
|
|
|
18
19
|
//#region deno.json
|
|
19
20
|
var name = "@optique/man";
|
|
20
|
-
var version = "1.4.0-dev.
|
|
21
|
+
var version = "1.4.0-dev.2928+80c003f2";
|
|
21
22
|
var license = "MIT";
|
|
22
23
|
var exports$1 = {
|
|
23
24
|
".": "./src/index.ts",
|
|
24
25
|
"./roff": "./src/roff.ts",
|
|
25
26
|
"./man": "./src/man.ts",
|
|
26
|
-
"./cli": "./src/cli.ts"
|
|
27
|
+
"./cli": "./src/cli.ts",
|
|
28
|
+
"./markdown": "./src/markdown.ts"
|
|
27
29
|
};
|
|
28
30
|
var imports = {
|
|
29
31
|
"#src/": "./src/",
|
|
@@ -47,6 +49,7 @@ var tasks = {
|
|
|
47
49
|
"test:bun"
|
|
48
50
|
] }
|
|
49
51
|
};
|
|
52
|
+
var publish = { "exclude": ["test/prototypes/", "test/fixtures/markdown-reference/"] };
|
|
50
53
|
var deno_default = {
|
|
51
54
|
name,
|
|
52
55
|
version,
|
|
@@ -54,7 +57,8 @@ var deno_default = {
|
|
|
54
57
|
exports: exports$1,
|
|
55
58
|
imports,
|
|
56
59
|
exclude,
|
|
57
|
-
tasks
|
|
60
|
+
tasks,
|
|
61
|
+
publish
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
//#endregion
|
package/dist/cli.js
CHANGED
|
@@ -17,13 +17,14 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
17
17
|
|
|
18
18
|
//#region deno.json
|
|
19
19
|
var name = "@optique/man";
|
|
20
|
-
var version = "1.4.0-dev.
|
|
20
|
+
var version = "1.4.0-dev.2928+80c003f2";
|
|
21
21
|
var license = "MIT";
|
|
22
22
|
var exports = {
|
|
23
23
|
".": "./src/index.ts",
|
|
24
24
|
"./roff": "./src/roff.ts",
|
|
25
25
|
"./man": "./src/man.ts",
|
|
26
|
-
"./cli": "./src/cli.ts"
|
|
26
|
+
"./cli": "./src/cli.ts",
|
|
27
|
+
"./markdown": "./src/markdown.ts"
|
|
27
28
|
};
|
|
28
29
|
var imports = {
|
|
29
30
|
"#src/": "./src/",
|
|
@@ -47,6 +48,7 @@ var tasks = {
|
|
|
47
48
|
"test:bun"
|
|
48
49
|
] }
|
|
49
50
|
};
|
|
51
|
+
var publish = { "exclude": ["test/prototypes/", "test/fixtures/markdown-reference/"] };
|
|
50
52
|
var deno_default = {
|
|
51
53
|
name,
|
|
52
54
|
version,
|
|
@@ -54,7 +56,8 @@ var deno_default = {
|
|
|
54
56
|
exports,
|
|
55
57
|
imports,
|
|
56
58
|
exclude,
|
|
57
|
-
tasks
|
|
59
|
+
tasks,
|
|
60
|
+
publish
|
|
58
61
|
};
|
|
59
62
|
|
|
60
63
|
//#endregion
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
1
|
+
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
|
+
const require_man = require('./man-ChbwsIhG.cjs');
|
|
3
|
+
const __optique_core_parser = require_chunk.__toESM(require("@optique/core/parser"));
|
|
3
4
|
|
|
4
5
|
//#region src/generator.ts
|
|
5
6
|
/**
|
package/dist/generator.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
require('./man-Dp2YQIdn.cjs');
|
|
2
1
|
require('./roff-C8MvQ2tL.cjs');
|
|
3
|
-
|
|
2
|
+
require('./man-ChbwsIhG.cjs');
|
|
3
|
+
const require_generator = require('./generator-DYAqAqpo.cjs');
|
|
4
4
|
|
|
5
5
|
exports.generateManPage = require_generator.generateManPage;
|
|
6
6
|
exports.generateManPageAsync = require_generator.generateManPageAsync;
|
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const require_man = require('./man-Dp2YQIdn.cjs');
|
|
2
1
|
const require_roff = require('./roff-C8MvQ2tL.cjs');
|
|
3
|
-
const
|
|
2
|
+
const require_man = require('./man-ChbwsIhG.cjs');
|
|
3
|
+
const require_generator = require('./generator-DYAqAqpo.cjs');
|
|
4
4
|
|
|
5
5
|
exports.escapeHyphens = require_roff.escapeHyphens;
|
|
6
6
|
exports.escapeQuotedValue = require_roff.escapeQuotedValue;
|
|
@@ -1,30 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
-
key = keys[i];
|
|
11
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
-
get: ((k) => from[k]).bind(null, key),
|
|
13
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
-
value: mod,
|
|
20
|
-
enumerable: true
|
|
21
|
-
}) : target, mod));
|
|
22
|
-
|
|
23
|
-
//#endregion
|
|
1
|
+
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
24
2
|
const require_roff = require('./roff-C8MvQ2tL.cjs');
|
|
25
|
-
const __optique_core_doc = __toESM(require("@optique/core/doc"));
|
|
26
|
-
const __optique_core_usage = __toESM(require("@optique/core/usage"));
|
|
27
|
-
const __optique_core_internal_usage = __toESM(require("@optique/core/internal/usage"));
|
|
3
|
+
const __optique_core_doc = require_chunk.__toESM(require("@optique/core/doc"));
|
|
4
|
+
const __optique_core_usage = require_chunk.__toESM(require("@optique/core/usage"));
|
|
5
|
+
const __optique_core_internal_usage = require_chunk.__toESM(require("@optique/core/internal/usage"));
|
|
28
6
|
|
|
29
7
|
//#region src/man.ts
|
|
30
8
|
/**
|
|
@@ -449,12 +427,6 @@ function formatDocPageAsMan(page, options) {
|
|
|
449
427
|
}
|
|
450
428
|
|
|
451
429
|
//#endregion
|
|
452
|
-
Object.defineProperty(exports, '__toESM', {
|
|
453
|
-
enumerable: true,
|
|
454
|
-
get: function () {
|
|
455
|
-
return __toESM;
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
430
|
Object.defineProperty(exports, 'formatDateForMan', {
|
|
459
431
|
enumerable: true,
|
|
460
432
|
get: function () {
|
package/dist/man.cjs
CHANGED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
|
+
const __optique_core_usage = require_chunk.__toESM(require("@optique/core/usage"));
|
|
3
|
+
|
|
4
|
+
//#region src/markdown.ts
|
|
5
|
+
function validateToken(token) {
|
|
6
|
+
if (typeof token !== "string" || token.length === 0 || /[\s\x00-\x1f\x7f-\x9f]/u.test(token) || !token.isWellFormed()) throw new TypeError("Command paths and aliases must contain nonempty tokens without whitespace or control characters.");
|
|
7
|
+
}
|
|
8
|
+
function pathKey(path) {
|
|
9
|
+
for (const token of path) validateToken(token);
|
|
10
|
+
return JSON.stringify(path);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Returns an injective anchor from the UTF-8 bytes of a JSON canonical path.
|
|
14
|
+
* The dot separates these IDs from GitHub/VitePress heading slugs.
|
|
15
|
+
* @param path Canonical command tokens; an empty array identifies root.
|
|
16
|
+
* @returns The explicit anchor ID, without a leading hash.
|
|
17
|
+
* @throws {TypeError} If a token is empty, ill-formed, or contains whitespace
|
|
18
|
+
* or control characters.
|
|
19
|
+
* @since 1.4.0
|
|
20
|
+
*/
|
|
21
|
+
function getCommandReferenceAnchor(path) {
|
|
22
|
+
const bytes = new TextEncoder().encode(pathKey(path));
|
|
23
|
+
return "optique-command." + Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
24
|
+
}
|
|
25
|
+
function singleLine(input) {
|
|
26
|
+
return input.replace(/[\r\n\t]/g, " ").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
27
|
+
}
|
|
28
|
+
function code(input) {
|
|
29
|
+
const content = singleLine(input);
|
|
30
|
+
if (content === "") return "<code></code>";
|
|
31
|
+
const fence = "`".repeat(Math.max(0, ...Array.from(content.matchAll(/`+/g), (match) => match[0].length)) + 1);
|
|
32
|
+
const padding = content.startsWith("`") || content.endsWith("`") || /^ .* $/u.test(content) ? " " : "";
|
|
33
|
+
return fence + padding + content + padding + fence;
|
|
34
|
+
}
|
|
35
|
+
function prose(input) {
|
|
36
|
+
return input.replace(/\r\n?/g, "\n").split("\n").map((line) => {
|
|
37
|
+
const escaped = singleLine(line).replace(/[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/g, (char) => {
|
|
38
|
+
if (char === "&") return "&";
|
|
39
|
+
if (char === "<") return "<";
|
|
40
|
+
if (char === ">") return ">";
|
|
41
|
+
return "\\" + char;
|
|
42
|
+
});
|
|
43
|
+
return escaped.replace(/^ +/, (spaces) => " ".repeat(spaces.length));
|
|
44
|
+
}).join("\\\n");
|
|
45
|
+
}
|
|
46
|
+
function formatMessageTerm(term) {
|
|
47
|
+
switch (term.type) {
|
|
48
|
+
case "text": return prose(term.text);
|
|
49
|
+
case "optionName": return code(term.optionName);
|
|
50
|
+
case "optionNames": return term.optionNames.map(code).join(", ");
|
|
51
|
+
case "metavar": return code(term.metavar);
|
|
52
|
+
case "value": return code(term.value);
|
|
53
|
+
case "values": return term.values.map(code).join(" ");
|
|
54
|
+
case "envVar": return code(term.envVar);
|
|
55
|
+
case "commandLine": return code(term.commandLine);
|
|
56
|
+
case "url": return code(term.url.href);
|
|
57
|
+
case "lineBreak": return "\\\n";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function formatMessage(message) {
|
|
61
|
+
let previousCode = false;
|
|
62
|
+
return message.map((term) => {
|
|
63
|
+
const content = formatMessageTerm(term);
|
|
64
|
+
const semanticCode = term.type !== "text" && term.type !== "lineBreak";
|
|
65
|
+
const separator = previousCode && semanticCode && content !== "" ? "<!-- -->" : "";
|
|
66
|
+
if (content !== "") previousCode = semanticCode;
|
|
67
|
+
return separator + content;
|
|
68
|
+
}).join("").replace(/(?:\\\n)+$/, "");
|
|
69
|
+
}
|
|
70
|
+
function link(label, path) {
|
|
71
|
+
return `<a href="#${getCommandReferenceAnchor(path)}"><code>${prose(singleLine(label))}</code></a>`;
|
|
72
|
+
}
|
|
73
|
+
function visibleAliases(entry) {
|
|
74
|
+
const hidden = new Set(entry.hiddenAliases);
|
|
75
|
+
return [...new Set(entry.aliases)].filter((alias) => !hidden.has(alias) && alias !== entry.path.at(-1));
|
|
76
|
+
}
|
|
77
|
+
function hiddenInTree(entry, index) {
|
|
78
|
+
for (let length = 1; length <= entry.path.length; length++) if ((0, __optique_core_usage.isDocHidden)(index.get(pathKey(entry.path.slice(0, length)))?.hidden)) return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function validateEntries(entries) {
|
|
82
|
+
const index = /* @__PURE__ */ new Map();
|
|
83
|
+
const spellings = /* @__PURE__ */ new Map();
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const key = pathKey(entry.path);
|
|
86
|
+
if (index.has(key)) throw new TypeError(`Duplicate command path: ${key}`);
|
|
87
|
+
index.set(key, entry);
|
|
88
|
+
for (const alias of [...entry.aliases ?? [], ...entry.hiddenAliases ?? []]) validateToken(alias);
|
|
89
|
+
if (entry.path.length === 0) {
|
|
90
|
+
if (entry.aliases?.length || entry.hiddenAliases?.length || entry.hidden) throw new TypeError("The root page cannot have aliases or hidden visibility.");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
for (const name of [
|
|
94
|
+
entry.path.at(-1),
|
|
95
|
+
...entry.aliases ?? [],
|
|
96
|
+
...entry.hiddenAliases ?? []
|
|
97
|
+
]) {
|
|
98
|
+
const spelling = pathKey([...entry.path.slice(0, -1), name]);
|
|
99
|
+
const destination = spellings.get(spelling);
|
|
100
|
+
if (destination != null && destination !== key) throw new TypeError(`Ambiguous command spelling: ${spelling}`);
|
|
101
|
+
spellings.set(spelling, key);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return index;
|
|
105
|
+
}
|
|
106
|
+
function commandTarget(entry, parent, index) {
|
|
107
|
+
return entry.term.type === "command" ? index.get(pathKey([...parent.path, entry.term.name])) : void 0;
|
|
108
|
+
}
|
|
109
|
+
function entryHidden(entry) {
|
|
110
|
+
return "hidden" in entry.term && (0, __optique_core_usage.isDocHidden)(entry.term.hidden);
|
|
111
|
+
}
|
|
112
|
+
function validateCoverage(coverage) {
|
|
113
|
+
if (coverage == null || !["all-visible", "partial"].includes(coverage.commands) || [
|
|
114
|
+
coverage.help,
|
|
115
|
+
coverage.version,
|
|
116
|
+
coverage.completion
|
|
117
|
+
].some((value) => value !== "included" && value !== "excluded")) throw new TypeError("Reference coverage must declare commands and runner-added help, version, and completion inputs.");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Renders supplied command pages in caller order as one Markdown reference.
|
|
121
|
+
*
|
|
122
|
+
* Coverage is always author-declared. Visible child references and canonical
|
|
123
|
+
* prefixes are checked, but commands absent from every supplied page cannot be
|
|
124
|
+
* discovered. The renderer never collects pages or evaluates parser callbacks.
|
|
125
|
+
* Messages are documentation text rather than Markdown. Generated anchors and
|
|
126
|
+
* internal links require raw HTML support; bare web/email text may be linkified
|
|
127
|
+
* by a consumer. Every page's usage already includes its command tokens.
|
|
128
|
+
*
|
|
129
|
+
* @param entries Canonical command pages, with explicit aliases and visibility.
|
|
130
|
+
* @param options Program metadata, coverage declaration and presentation policy.
|
|
131
|
+
* @returns A deterministic Markdown document ending in one newline.
|
|
132
|
+
* @throws {TypeError} If paths/aliases are invalid or ambiguous, a canonical
|
|
133
|
+
* path is duplicated, metadata is invalid, or declared complete coverage has
|
|
134
|
+
* missing visible command pages.
|
|
135
|
+
* @since 1.4.0
|
|
136
|
+
*/
|
|
137
|
+
function formatCommandReference(entries, options) {
|
|
138
|
+
(0, __optique_core_usage.formatUsage)(options.name, [], { colors: false });
|
|
139
|
+
validateCoverage(options.coverage);
|
|
140
|
+
const index = validateEntries(entries);
|
|
141
|
+
const visible = entries.filter((entry) => !hiddenInTree(entry, index));
|
|
142
|
+
const unresolved = options.unresolved?.filter((gap) => !hiddenInTree({
|
|
143
|
+
path: gap.path,
|
|
144
|
+
page: { sections: [] }
|
|
145
|
+
}, index)) ?? [];
|
|
146
|
+
for (const gap of unresolved) {
|
|
147
|
+
pathKey(gap.path);
|
|
148
|
+
if (gap.scope !== "page" && gap.scope !== "subtree") throw new TypeError("An unresolved scope must be page or subtree.");
|
|
149
|
+
}
|
|
150
|
+
if (unresolved.length > 0 && options.coverage.commands === "all-visible") throw new TypeError("Unresolved command documentation requires partial coverage.");
|
|
151
|
+
const missing = /* @__PURE__ */ new Map();
|
|
152
|
+
if (!index.has("[]")) missing.set("[]", []);
|
|
153
|
+
for (const entry of visible) {
|
|
154
|
+
for (let length = 0; length < entry.path.length; length++) {
|
|
155
|
+
const prefix = entry.path.slice(0, length);
|
|
156
|
+
if (!index.has(pathKey(prefix))) missing.set(pathKey(prefix), prefix);
|
|
157
|
+
}
|
|
158
|
+
for (const section of entry.page.sections) for (const item of section.entries) {
|
|
159
|
+
if (entryHidden(item) || item.term.type !== "command") continue;
|
|
160
|
+
const target = commandTarget(item, entry, index);
|
|
161
|
+
if (target == null) {
|
|
162
|
+
const path = [...entry.path, item.term.name];
|
|
163
|
+
missing.set(pathKey(path), path);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (missing.size > 0 && options.coverage.commands === "all-visible") throw new TypeError(`Missing command pages: ${[...missing.values()].map((path) => JSON.stringify(path)).join(", ")}`);
|
|
168
|
+
const output = [`# ${code(options.name)} command reference`];
|
|
169
|
+
const addMessage = (content) => {
|
|
170
|
+
if (content != null && content.length > 0) output.push(formatMessage(content));
|
|
171
|
+
};
|
|
172
|
+
if (options.version != null) output.push(`Version: ${code(options.version)}`);
|
|
173
|
+
addMessage(options.brief);
|
|
174
|
+
addMessage(options.description);
|
|
175
|
+
output.push(`Coverage: author-declared ${options.coverage.commands === "all-visible" ? "all visible commands" : "partial command reference"}.`);
|
|
176
|
+
output.push("Runner inputs: " + [
|
|
177
|
+
"help",
|
|
178
|
+
"version",
|
|
179
|
+
"completion"
|
|
180
|
+
].map((feature) => `${feature}: ${options.coverage[feature] === "included" ? "included" : "not documented here (if configured)"}`).join("; ") + ".");
|
|
181
|
+
if (missing.size > 0) output.push("Missing command pages: " + [...missing.values()].map((path) => code([options.name, ...path].join(" "))).join(", ") + ".");
|
|
182
|
+
if (unresolved.length > 0) output.push("## Unresolved documentation", unresolved.map((gap) => ` - ${code([options.name, ...gap.path].join(" "))} (${gap.scope}): ${code(gap.reason)}`).join("\n"));
|
|
183
|
+
output.push("## Contents");
|
|
184
|
+
output.push(visible.map((entry) => {
|
|
185
|
+
const fullName = [options.name, ...entry.path].join(" ");
|
|
186
|
+
const aliases = entry.showAliases ?? options.showAliases ?? false ? visibleAliases(entry).map((alias) => link([
|
|
187
|
+
options.name,
|
|
188
|
+
...entry.path.slice(0, -1),
|
|
189
|
+
alias
|
|
190
|
+
].join(" "), entry.path)) : [];
|
|
191
|
+
return ` - ${link(fullName, entry.path)}${aliases.length > 0 ? ` (aliases: ${aliases.join(", ")})` : ""}`;
|
|
192
|
+
}).join("\n"));
|
|
193
|
+
for (const entry of visible) {
|
|
194
|
+
const page = entry.page;
|
|
195
|
+
output.push(`<a id="${getCommandReferenceAnchor(entry.path)}"></a>`);
|
|
196
|
+
output.push(`## ${code([options.name, ...entry.path].join(" "))}`);
|
|
197
|
+
addMessage(page.brief);
|
|
198
|
+
if ((options.showUsage ?? page.showUsage ?? true) && page.usage != null) {
|
|
199
|
+
const usage = (0, __optique_core_usage.formatUsage)(options.name, page.usage, { colors: false }).replace(/\r\n?/g, "\n").split("\n").map(singleLine).join("\n");
|
|
200
|
+
const fence = "~".repeat(Math.max(3, ...Array.from(usage.matchAll(/~+/g), (match) => match[0].length + 1)));
|
|
201
|
+
output.push("### Usage", `${fence} text\n${usage}\n${fence}`);
|
|
202
|
+
}
|
|
203
|
+
addMessage(page.description);
|
|
204
|
+
for (const section of page.sections) {
|
|
205
|
+
const items = [];
|
|
206
|
+
for (const item of section.entries) {
|
|
207
|
+
if (entryHidden(item)) continue;
|
|
208
|
+
const target = commandTarget(item, entry, index);
|
|
209
|
+
if (target != null && hiddenInTree(target, index)) continue;
|
|
210
|
+
const label = (0, __optique_core_usage.formatUsageTerm)(item.term, {
|
|
211
|
+
context: "doc",
|
|
212
|
+
colors: false
|
|
213
|
+
});
|
|
214
|
+
if (label === "") continue;
|
|
215
|
+
const aliases = target != null && (item.showAliases ?? target.showAliases ?? options.showAliases ?? false) ? visibleAliases(target).map((alias) => link(alias, target.path)) : [];
|
|
216
|
+
let content = " - " + (target == null ? code(label) : link(label, target.path));
|
|
217
|
+
if (aliases.length > 0) content += ` (aliases: ${aliases.join(", ")})`;
|
|
218
|
+
const details = [];
|
|
219
|
+
if (item.description != null) details.push(formatMessage(item.description));
|
|
220
|
+
if (item.default != null) details.push("Default: " + formatMessage(item.default));
|
|
221
|
+
if (item.choices != null) details.push("Choices: " + formatMessage(item.choices));
|
|
222
|
+
if (item.envVars?.length) details.push("Environment: " + item.envVars.map(code).join(", "));
|
|
223
|
+
if (details.length > 0) content += "\n\n" + details.join("\n\n").split("\n").map((line) => " " + line).join("\n");
|
|
224
|
+
items.push(content);
|
|
225
|
+
}
|
|
226
|
+
if (items.length > 0) output.push("### " + prose(singleLine(section.title ?? "Inputs")), items.join("\n\n"));
|
|
227
|
+
}
|
|
228
|
+
const bindings = page.environmentBindings?.filter((binding) => !(0, __optique_core_usage.isDocHidden)(binding.hidden));
|
|
229
|
+
if (bindings?.length) output.push("### Environment", bindings.map((binding) => ` - ${code(binding.name)}${binding.description == null ? "" : ": " + formatMessage(binding.description).replaceAll("\n", "\n ")}`).join("\n"));
|
|
230
|
+
for (const [title, content] of [
|
|
231
|
+
["Examples", page.examples],
|
|
232
|
+
["Author", page.author],
|
|
233
|
+
["Bugs", page.bugs],
|
|
234
|
+
["Notes", page.footer]
|
|
235
|
+
]) if (content != null && content.length > 0) output.push(`### ${title}`, formatMessage(content));
|
|
236
|
+
}
|
|
237
|
+
for (const [title, content] of [
|
|
238
|
+
["Examples", options.examples],
|
|
239
|
+
["Author", options.author],
|
|
240
|
+
["Bugs", options.bugs],
|
|
241
|
+
["Notes", options.footer]
|
|
242
|
+
]) if (content != null && content.length > 0) output.push(`## ${title}`, formatMessage(content));
|
|
243
|
+
return output.filter((part) => part !== "").join("\n\n") + "\n";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
//#endregion
|
|
247
|
+
exports.formatCommandReference = formatCommandReference;
|
|
248
|
+
exports.getCommandReferenceAnchor = getCommandReferenceAnchor;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ProgramMetadata } from "@optique/core/program";
|
|
2
|
+
import { DocPage } from "@optique/core/doc";
|
|
3
|
+
import { HiddenVisibility } from "@optique/core/usage";
|
|
4
|
+
|
|
5
|
+
//#region src/markdown.d.ts
|
|
6
|
+
|
|
7
|
+
/** A supplied page and its canonical identity. @since 1.4.0 */
|
|
8
|
+
interface MarkdownCommandEntry {
|
|
9
|
+
/** Canonical command tokens, excluding the program name. Empty for root. */
|
|
10
|
+
readonly path: readonly string[];
|
|
11
|
+
/** Already constructed documentation; usage includes command tokens. */
|
|
12
|
+
readonly page: DocPage;
|
|
13
|
+
/** Visible alternative names of this command under its canonical parent. */
|
|
14
|
+
readonly aliases?: readonly string[];
|
|
15
|
+
/** Accepted names that must never be displayed. */
|
|
16
|
+
readonly hiddenAliases?: readonly string[];
|
|
17
|
+
/** Documentation-hidden parents suppress every supplied descendant. */
|
|
18
|
+
readonly hidden?: HiddenVisibility;
|
|
19
|
+
/** Alias display override for this command's contents entry. */
|
|
20
|
+
readonly showAliases?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Author assertions about the supplied reference's scope. @since 1.4.0 */
|
|
23
|
+
interface MarkdownReferenceCoverage {
|
|
24
|
+
/** Whether the author has supplied every visible command in this scope. */
|
|
25
|
+
readonly commands: "all-visible" | "partial";
|
|
26
|
+
/** Whether all configured help forms in this scope are documented. */
|
|
27
|
+
readonly help: "included" | "excluded";
|
|
28
|
+
/** Whether all configured version forms in this scope are documented. */
|
|
29
|
+
readonly version: "included" | "excluded";
|
|
30
|
+
/** Whether all configured completion forms in this scope are documented. */
|
|
31
|
+
readonly completion: "included" | "excluded";
|
|
32
|
+
}
|
|
33
|
+
/** Program metadata and presentation options for a reference. @since 1.4.0 */
|
|
34
|
+
interface MarkdownReferenceOptions extends ProgramMetadata {
|
|
35
|
+
/** Required author declaration, printed in the artifact. */
|
|
36
|
+
readonly coverage: MarkdownReferenceCoverage;
|
|
37
|
+
/** Default alias display, overridden by command entries and DocEntries. */
|
|
38
|
+
readonly showAliases?: boolean;
|
|
39
|
+
/** Override each supplied page's showUsage setting. Defaults to that page. */
|
|
40
|
+
readonly showUsage?: boolean;
|
|
41
|
+
/** Collection omissions to print in a partial reference. Hidden scopes skip them. */
|
|
42
|
+
readonly unresolved?: readonly {
|
|
43
|
+
/** Visible enclosing canonical path. */
|
|
44
|
+
readonly path: readonly string[];
|
|
45
|
+
/** Whether the gap affects one page or an opaque subtree. */
|
|
46
|
+
readonly scope: "page" | "subtree";
|
|
47
|
+
/** Documentation-only diagnostic, rendered as text. */
|
|
48
|
+
readonly reason: string;
|
|
49
|
+
}[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns an injective anchor from the UTF-8 bytes of a JSON canonical path.
|
|
53
|
+
* The dot separates these IDs from GitHub/VitePress heading slugs.
|
|
54
|
+
* @param path Canonical command tokens; an empty array identifies root.
|
|
55
|
+
* @returns The explicit anchor ID, without a leading hash.
|
|
56
|
+
* @throws {TypeError} If a token is empty, ill-formed, or contains whitespace
|
|
57
|
+
* or control characters.
|
|
58
|
+
* @since 1.4.0
|
|
59
|
+
*/
|
|
60
|
+
declare function getCommandReferenceAnchor(path: readonly string[]): string;
|
|
61
|
+
/**
|
|
62
|
+
* Renders supplied command pages in caller order as one Markdown reference.
|
|
63
|
+
*
|
|
64
|
+
* Coverage is always author-declared. Visible child references and canonical
|
|
65
|
+
* prefixes are checked, but commands absent from every supplied page cannot be
|
|
66
|
+
* discovered. The renderer never collects pages or evaluates parser callbacks.
|
|
67
|
+
* Messages are documentation text rather than Markdown. Generated anchors and
|
|
68
|
+
* internal links require raw HTML support; bare web/email text may be linkified
|
|
69
|
+
* by a consumer. Every page's usage already includes its command tokens.
|
|
70
|
+
*
|
|
71
|
+
* @param entries Canonical command pages, with explicit aliases and visibility.
|
|
72
|
+
* @param options Program metadata, coverage declaration and presentation policy.
|
|
73
|
+
* @returns A deterministic Markdown document ending in one newline.
|
|
74
|
+
* @throws {TypeError} If paths/aliases are invalid or ambiguous, a canonical
|
|
75
|
+
* path is duplicated, metadata is invalid, or declared complete coverage has
|
|
76
|
+
* missing visible command pages.
|
|
77
|
+
* @since 1.4.0
|
|
78
|
+
*/
|
|
79
|
+
declare function formatCommandReference(entries: readonly MarkdownCommandEntry[], options: MarkdownReferenceOptions): string;
|
|
80
|
+
//#endregion
|
|
81
|
+
export { MarkdownCommandEntry, MarkdownReferenceCoverage, MarkdownReferenceOptions, formatCommandReference, getCommandReferenceAnchor };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ProgramMetadata } from "@optique/core/program";
|
|
2
|
+
import { DocPage } from "@optique/core/doc";
|
|
3
|
+
import { HiddenVisibility } from "@optique/core/usage";
|
|
4
|
+
|
|
5
|
+
//#region src/markdown.d.ts
|
|
6
|
+
|
|
7
|
+
/** A supplied page and its canonical identity. @since 1.4.0 */
|
|
8
|
+
interface MarkdownCommandEntry {
|
|
9
|
+
/** Canonical command tokens, excluding the program name. Empty for root. */
|
|
10
|
+
readonly path: readonly string[];
|
|
11
|
+
/** Already constructed documentation; usage includes command tokens. */
|
|
12
|
+
readonly page: DocPage;
|
|
13
|
+
/** Visible alternative names of this command under its canonical parent. */
|
|
14
|
+
readonly aliases?: readonly string[];
|
|
15
|
+
/** Accepted names that must never be displayed. */
|
|
16
|
+
readonly hiddenAliases?: readonly string[];
|
|
17
|
+
/** Documentation-hidden parents suppress every supplied descendant. */
|
|
18
|
+
readonly hidden?: HiddenVisibility;
|
|
19
|
+
/** Alias display override for this command's contents entry. */
|
|
20
|
+
readonly showAliases?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Author assertions about the supplied reference's scope. @since 1.4.0 */
|
|
23
|
+
interface MarkdownReferenceCoverage {
|
|
24
|
+
/** Whether the author has supplied every visible command in this scope. */
|
|
25
|
+
readonly commands: "all-visible" | "partial";
|
|
26
|
+
/** Whether all configured help forms in this scope are documented. */
|
|
27
|
+
readonly help: "included" | "excluded";
|
|
28
|
+
/** Whether all configured version forms in this scope are documented. */
|
|
29
|
+
readonly version: "included" | "excluded";
|
|
30
|
+
/** Whether all configured completion forms in this scope are documented. */
|
|
31
|
+
readonly completion: "included" | "excluded";
|
|
32
|
+
}
|
|
33
|
+
/** Program metadata and presentation options for a reference. @since 1.4.0 */
|
|
34
|
+
interface MarkdownReferenceOptions extends ProgramMetadata {
|
|
35
|
+
/** Required author declaration, printed in the artifact. */
|
|
36
|
+
readonly coverage: MarkdownReferenceCoverage;
|
|
37
|
+
/** Default alias display, overridden by command entries and DocEntries. */
|
|
38
|
+
readonly showAliases?: boolean;
|
|
39
|
+
/** Override each supplied page's showUsage setting. Defaults to that page. */
|
|
40
|
+
readonly showUsage?: boolean;
|
|
41
|
+
/** Collection omissions to print in a partial reference. Hidden scopes skip them. */
|
|
42
|
+
readonly unresolved?: readonly {
|
|
43
|
+
/** Visible enclosing canonical path. */
|
|
44
|
+
readonly path: readonly string[];
|
|
45
|
+
/** Whether the gap affects one page or an opaque subtree. */
|
|
46
|
+
readonly scope: "page" | "subtree";
|
|
47
|
+
/** Documentation-only diagnostic, rendered as text. */
|
|
48
|
+
readonly reason: string;
|
|
49
|
+
}[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns an injective anchor from the UTF-8 bytes of a JSON canonical path.
|
|
53
|
+
* The dot separates these IDs from GitHub/VitePress heading slugs.
|
|
54
|
+
* @param path Canonical command tokens; an empty array identifies root.
|
|
55
|
+
* @returns The explicit anchor ID, without a leading hash.
|
|
56
|
+
* @throws {TypeError} If a token is empty, ill-formed, or contains whitespace
|
|
57
|
+
* or control characters.
|
|
58
|
+
* @since 1.4.0
|
|
59
|
+
*/
|
|
60
|
+
declare function getCommandReferenceAnchor(path: readonly string[]): string;
|
|
61
|
+
/**
|
|
62
|
+
* Renders supplied command pages in caller order as one Markdown reference.
|
|
63
|
+
*
|
|
64
|
+
* Coverage is always author-declared. Visible child references and canonical
|
|
65
|
+
* prefixes are checked, but commands absent from every supplied page cannot be
|
|
66
|
+
* discovered. The renderer never collects pages or evaluates parser callbacks.
|
|
67
|
+
* Messages are documentation text rather than Markdown. Generated anchors and
|
|
68
|
+
* internal links require raw HTML support; bare web/email text may be linkified
|
|
69
|
+
* by a consumer. Every page's usage already includes its command tokens.
|
|
70
|
+
*
|
|
71
|
+
* @param entries Canonical command pages, with explicit aliases and visibility.
|
|
72
|
+
* @param options Program metadata, coverage declaration and presentation policy.
|
|
73
|
+
* @returns A deterministic Markdown document ending in one newline.
|
|
74
|
+
* @throws {TypeError} If paths/aliases are invalid or ambiguous, a canonical
|
|
75
|
+
* path is duplicated, metadata is invalid, or declared complete coverage has
|
|
76
|
+
* missing visible command pages.
|
|
77
|
+
* @since 1.4.0
|
|
78
|
+
*/
|
|
79
|
+
declare function formatCommandReference(entries: readonly MarkdownCommandEntry[], options: MarkdownReferenceOptions): string;
|
|
80
|
+
//#endregion
|
|
81
|
+
export { MarkdownCommandEntry, MarkdownReferenceCoverage, MarkdownReferenceOptions, formatCommandReference, getCommandReferenceAnchor };
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { formatUsage, formatUsageTerm, isDocHidden } from "@optique/core/usage";
|
|
2
|
+
|
|
3
|
+
//#region src/markdown.ts
|
|
4
|
+
function validateToken(token) {
|
|
5
|
+
if (typeof token !== "string" || token.length === 0 || /[\s\x00-\x1f\x7f-\x9f]/u.test(token) || !token.isWellFormed()) throw new TypeError("Command paths and aliases must contain nonempty tokens without whitespace or control characters.");
|
|
6
|
+
}
|
|
7
|
+
function pathKey(path) {
|
|
8
|
+
for (const token of path) validateToken(token);
|
|
9
|
+
return JSON.stringify(path);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Returns an injective anchor from the UTF-8 bytes of a JSON canonical path.
|
|
13
|
+
* The dot separates these IDs from GitHub/VitePress heading slugs.
|
|
14
|
+
* @param path Canonical command tokens; an empty array identifies root.
|
|
15
|
+
* @returns The explicit anchor ID, without a leading hash.
|
|
16
|
+
* @throws {TypeError} If a token is empty, ill-formed, or contains whitespace
|
|
17
|
+
* or control characters.
|
|
18
|
+
* @since 1.4.0
|
|
19
|
+
*/
|
|
20
|
+
function getCommandReferenceAnchor(path) {
|
|
21
|
+
const bytes = new TextEncoder().encode(pathKey(path));
|
|
22
|
+
return "optique-command." + Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
23
|
+
}
|
|
24
|
+
function singleLine(input) {
|
|
25
|
+
return input.replace(/[\r\n\t]/g, " ").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
26
|
+
}
|
|
27
|
+
function code(input) {
|
|
28
|
+
const content = singleLine(input);
|
|
29
|
+
if (content === "") return "<code></code>";
|
|
30
|
+
const fence = "`".repeat(Math.max(0, ...Array.from(content.matchAll(/`+/g), (match) => match[0].length)) + 1);
|
|
31
|
+
const padding = content.startsWith("`") || content.endsWith("`") || /^ .* $/u.test(content) ? " " : "";
|
|
32
|
+
return fence + padding + content + padding + fence;
|
|
33
|
+
}
|
|
34
|
+
function prose(input) {
|
|
35
|
+
return input.replace(/\r\n?/g, "\n").split("\n").map((line) => {
|
|
36
|
+
const escaped = singleLine(line).replace(/[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/g, (char) => {
|
|
37
|
+
if (char === "&") return "&";
|
|
38
|
+
if (char === "<") return "<";
|
|
39
|
+
if (char === ">") return ">";
|
|
40
|
+
return "\\" + char;
|
|
41
|
+
});
|
|
42
|
+
return escaped.replace(/^ +/, (spaces) => " ".repeat(spaces.length));
|
|
43
|
+
}).join("\\\n");
|
|
44
|
+
}
|
|
45
|
+
function formatMessageTerm(term) {
|
|
46
|
+
switch (term.type) {
|
|
47
|
+
case "text": return prose(term.text);
|
|
48
|
+
case "optionName": return code(term.optionName);
|
|
49
|
+
case "optionNames": return term.optionNames.map(code).join(", ");
|
|
50
|
+
case "metavar": return code(term.metavar);
|
|
51
|
+
case "value": return code(term.value);
|
|
52
|
+
case "values": return term.values.map(code).join(" ");
|
|
53
|
+
case "envVar": return code(term.envVar);
|
|
54
|
+
case "commandLine": return code(term.commandLine);
|
|
55
|
+
case "url": return code(term.url.href);
|
|
56
|
+
case "lineBreak": return "\\\n";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function formatMessage(message) {
|
|
60
|
+
let previousCode = false;
|
|
61
|
+
return message.map((term) => {
|
|
62
|
+
const content = formatMessageTerm(term);
|
|
63
|
+
const semanticCode = term.type !== "text" && term.type !== "lineBreak";
|
|
64
|
+
const separator = previousCode && semanticCode && content !== "" ? "<!-- -->" : "";
|
|
65
|
+
if (content !== "") previousCode = semanticCode;
|
|
66
|
+
return separator + content;
|
|
67
|
+
}).join("").replace(/(?:\\\n)+$/, "");
|
|
68
|
+
}
|
|
69
|
+
function link(label, path) {
|
|
70
|
+
return `<a href="#${getCommandReferenceAnchor(path)}"><code>${prose(singleLine(label))}</code></a>`;
|
|
71
|
+
}
|
|
72
|
+
function visibleAliases(entry) {
|
|
73
|
+
const hidden = new Set(entry.hiddenAliases);
|
|
74
|
+
return [...new Set(entry.aliases)].filter((alias) => !hidden.has(alias) && alias !== entry.path.at(-1));
|
|
75
|
+
}
|
|
76
|
+
function hiddenInTree(entry, index) {
|
|
77
|
+
for (let length = 1; length <= entry.path.length; length++) if (isDocHidden(index.get(pathKey(entry.path.slice(0, length)))?.hidden)) return true;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function validateEntries(entries) {
|
|
81
|
+
const index = /* @__PURE__ */ new Map();
|
|
82
|
+
const spellings = /* @__PURE__ */ new Map();
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
const key = pathKey(entry.path);
|
|
85
|
+
if (index.has(key)) throw new TypeError(`Duplicate command path: ${key}`);
|
|
86
|
+
index.set(key, entry);
|
|
87
|
+
for (const alias of [...entry.aliases ?? [], ...entry.hiddenAliases ?? []]) validateToken(alias);
|
|
88
|
+
if (entry.path.length === 0) {
|
|
89
|
+
if (entry.aliases?.length || entry.hiddenAliases?.length || entry.hidden) throw new TypeError("The root page cannot have aliases or hidden visibility.");
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
for (const name of [
|
|
93
|
+
entry.path.at(-1),
|
|
94
|
+
...entry.aliases ?? [],
|
|
95
|
+
...entry.hiddenAliases ?? []
|
|
96
|
+
]) {
|
|
97
|
+
const spelling = pathKey([...entry.path.slice(0, -1), name]);
|
|
98
|
+
const destination = spellings.get(spelling);
|
|
99
|
+
if (destination != null && destination !== key) throw new TypeError(`Ambiguous command spelling: ${spelling}`);
|
|
100
|
+
spellings.set(spelling, key);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return index;
|
|
104
|
+
}
|
|
105
|
+
function commandTarget(entry, parent, index) {
|
|
106
|
+
return entry.term.type === "command" ? index.get(pathKey([...parent.path, entry.term.name])) : void 0;
|
|
107
|
+
}
|
|
108
|
+
function entryHidden(entry) {
|
|
109
|
+
return "hidden" in entry.term && isDocHidden(entry.term.hidden);
|
|
110
|
+
}
|
|
111
|
+
function validateCoverage(coverage) {
|
|
112
|
+
if (coverage == null || !["all-visible", "partial"].includes(coverage.commands) || [
|
|
113
|
+
coverage.help,
|
|
114
|
+
coverage.version,
|
|
115
|
+
coverage.completion
|
|
116
|
+
].some((value) => value !== "included" && value !== "excluded")) throw new TypeError("Reference coverage must declare commands and runner-added help, version, and completion inputs.");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Renders supplied command pages in caller order as one Markdown reference.
|
|
120
|
+
*
|
|
121
|
+
* Coverage is always author-declared. Visible child references and canonical
|
|
122
|
+
* prefixes are checked, but commands absent from every supplied page cannot be
|
|
123
|
+
* discovered. The renderer never collects pages or evaluates parser callbacks.
|
|
124
|
+
* Messages are documentation text rather than Markdown. Generated anchors and
|
|
125
|
+
* internal links require raw HTML support; bare web/email text may be linkified
|
|
126
|
+
* by a consumer. Every page's usage already includes its command tokens.
|
|
127
|
+
*
|
|
128
|
+
* @param entries Canonical command pages, with explicit aliases and visibility.
|
|
129
|
+
* @param options Program metadata, coverage declaration and presentation policy.
|
|
130
|
+
* @returns A deterministic Markdown document ending in one newline.
|
|
131
|
+
* @throws {TypeError} If paths/aliases are invalid or ambiguous, a canonical
|
|
132
|
+
* path is duplicated, metadata is invalid, or declared complete coverage has
|
|
133
|
+
* missing visible command pages.
|
|
134
|
+
* @since 1.4.0
|
|
135
|
+
*/
|
|
136
|
+
function formatCommandReference(entries, options) {
|
|
137
|
+
formatUsage(options.name, [], { colors: false });
|
|
138
|
+
validateCoverage(options.coverage);
|
|
139
|
+
const index = validateEntries(entries);
|
|
140
|
+
const visible = entries.filter((entry) => !hiddenInTree(entry, index));
|
|
141
|
+
const unresolved = options.unresolved?.filter((gap) => !hiddenInTree({
|
|
142
|
+
path: gap.path,
|
|
143
|
+
page: { sections: [] }
|
|
144
|
+
}, index)) ?? [];
|
|
145
|
+
for (const gap of unresolved) {
|
|
146
|
+
pathKey(gap.path);
|
|
147
|
+
if (gap.scope !== "page" && gap.scope !== "subtree") throw new TypeError("An unresolved scope must be page or subtree.");
|
|
148
|
+
}
|
|
149
|
+
if (unresolved.length > 0 && options.coverage.commands === "all-visible") throw new TypeError("Unresolved command documentation requires partial coverage.");
|
|
150
|
+
const missing = /* @__PURE__ */ new Map();
|
|
151
|
+
if (!index.has("[]")) missing.set("[]", []);
|
|
152
|
+
for (const entry of visible) {
|
|
153
|
+
for (let length = 0; length < entry.path.length; length++) {
|
|
154
|
+
const prefix = entry.path.slice(0, length);
|
|
155
|
+
if (!index.has(pathKey(prefix))) missing.set(pathKey(prefix), prefix);
|
|
156
|
+
}
|
|
157
|
+
for (const section of entry.page.sections) for (const item of section.entries) {
|
|
158
|
+
if (entryHidden(item) || item.term.type !== "command") continue;
|
|
159
|
+
const target = commandTarget(item, entry, index);
|
|
160
|
+
if (target == null) {
|
|
161
|
+
const path = [...entry.path, item.term.name];
|
|
162
|
+
missing.set(pathKey(path), path);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (missing.size > 0 && options.coverage.commands === "all-visible") throw new TypeError(`Missing command pages: ${[...missing.values()].map((path) => JSON.stringify(path)).join(", ")}`);
|
|
167
|
+
const output = [`# ${code(options.name)} command reference`];
|
|
168
|
+
const addMessage = (content) => {
|
|
169
|
+
if (content != null && content.length > 0) output.push(formatMessage(content));
|
|
170
|
+
};
|
|
171
|
+
if (options.version != null) output.push(`Version: ${code(options.version)}`);
|
|
172
|
+
addMessage(options.brief);
|
|
173
|
+
addMessage(options.description);
|
|
174
|
+
output.push(`Coverage: author-declared ${options.coverage.commands === "all-visible" ? "all visible commands" : "partial command reference"}.`);
|
|
175
|
+
output.push("Runner inputs: " + [
|
|
176
|
+
"help",
|
|
177
|
+
"version",
|
|
178
|
+
"completion"
|
|
179
|
+
].map((feature) => `${feature}: ${options.coverage[feature] === "included" ? "included" : "not documented here (if configured)"}`).join("; ") + ".");
|
|
180
|
+
if (missing.size > 0) output.push("Missing command pages: " + [...missing.values()].map((path) => code([options.name, ...path].join(" "))).join(", ") + ".");
|
|
181
|
+
if (unresolved.length > 0) output.push("## Unresolved documentation", unresolved.map((gap) => ` - ${code([options.name, ...gap.path].join(" "))} (${gap.scope}): ${code(gap.reason)}`).join("\n"));
|
|
182
|
+
output.push("## Contents");
|
|
183
|
+
output.push(visible.map((entry) => {
|
|
184
|
+
const fullName = [options.name, ...entry.path].join(" ");
|
|
185
|
+
const aliases = entry.showAliases ?? options.showAliases ?? false ? visibleAliases(entry).map((alias) => link([
|
|
186
|
+
options.name,
|
|
187
|
+
...entry.path.slice(0, -1),
|
|
188
|
+
alias
|
|
189
|
+
].join(" "), entry.path)) : [];
|
|
190
|
+
return ` - ${link(fullName, entry.path)}${aliases.length > 0 ? ` (aliases: ${aliases.join(", ")})` : ""}`;
|
|
191
|
+
}).join("\n"));
|
|
192
|
+
for (const entry of visible) {
|
|
193
|
+
const page = entry.page;
|
|
194
|
+
output.push(`<a id="${getCommandReferenceAnchor(entry.path)}"></a>`);
|
|
195
|
+
output.push(`## ${code([options.name, ...entry.path].join(" "))}`);
|
|
196
|
+
addMessage(page.brief);
|
|
197
|
+
if ((options.showUsage ?? page.showUsage ?? true) && page.usage != null) {
|
|
198
|
+
const usage = formatUsage(options.name, page.usage, { colors: false }).replace(/\r\n?/g, "\n").split("\n").map(singleLine).join("\n");
|
|
199
|
+
const fence = "~".repeat(Math.max(3, ...Array.from(usage.matchAll(/~+/g), (match) => match[0].length + 1)));
|
|
200
|
+
output.push("### Usage", `${fence} text\n${usage}\n${fence}`);
|
|
201
|
+
}
|
|
202
|
+
addMessage(page.description);
|
|
203
|
+
for (const section of page.sections) {
|
|
204
|
+
const items = [];
|
|
205
|
+
for (const item of section.entries) {
|
|
206
|
+
if (entryHidden(item)) continue;
|
|
207
|
+
const target = commandTarget(item, entry, index);
|
|
208
|
+
if (target != null && hiddenInTree(target, index)) continue;
|
|
209
|
+
const label = formatUsageTerm(item.term, {
|
|
210
|
+
context: "doc",
|
|
211
|
+
colors: false
|
|
212
|
+
});
|
|
213
|
+
if (label === "") continue;
|
|
214
|
+
const aliases = target != null && (item.showAliases ?? target.showAliases ?? options.showAliases ?? false) ? visibleAliases(target).map((alias) => link(alias, target.path)) : [];
|
|
215
|
+
let content = " - " + (target == null ? code(label) : link(label, target.path));
|
|
216
|
+
if (aliases.length > 0) content += ` (aliases: ${aliases.join(", ")})`;
|
|
217
|
+
const details = [];
|
|
218
|
+
if (item.description != null) details.push(formatMessage(item.description));
|
|
219
|
+
if (item.default != null) details.push("Default: " + formatMessage(item.default));
|
|
220
|
+
if (item.choices != null) details.push("Choices: " + formatMessage(item.choices));
|
|
221
|
+
if (item.envVars?.length) details.push("Environment: " + item.envVars.map(code).join(", "));
|
|
222
|
+
if (details.length > 0) content += "\n\n" + details.join("\n\n").split("\n").map((line) => " " + line).join("\n");
|
|
223
|
+
items.push(content);
|
|
224
|
+
}
|
|
225
|
+
if (items.length > 0) output.push("### " + prose(singleLine(section.title ?? "Inputs")), items.join("\n\n"));
|
|
226
|
+
}
|
|
227
|
+
const bindings = page.environmentBindings?.filter((binding) => !isDocHidden(binding.hidden));
|
|
228
|
+
if (bindings?.length) output.push("### Environment", bindings.map((binding) => ` - ${code(binding.name)}${binding.description == null ? "" : ": " + formatMessage(binding.description).replaceAll("\n", "\n ")}`).join("\n"));
|
|
229
|
+
for (const [title, content] of [
|
|
230
|
+
["Examples", page.examples],
|
|
231
|
+
["Author", page.author],
|
|
232
|
+
["Bugs", page.bugs],
|
|
233
|
+
["Notes", page.footer]
|
|
234
|
+
]) if (content != null && content.length > 0) output.push(`### ${title}`, formatMessage(content));
|
|
235
|
+
}
|
|
236
|
+
for (const [title, content] of [
|
|
237
|
+
["Examples", options.examples],
|
|
238
|
+
["Author", options.author],
|
|
239
|
+
["Bugs", options.bugs],
|
|
240
|
+
["Notes", options.footer]
|
|
241
|
+
]) if (content != null && content.length > 0) output.push(`## ${title}`, formatMessage(content));
|
|
242
|
+
return output.filter((part) => part !== "").join("\n\n") + "\n";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
//#endregion
|
|
246
|
+
export { formatCommandReference, getCommandReferenceAnchor };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optique/man",
|
|
3
|
-
"version": "1.4.0-dev.
|
|
4
|
-
"description": "Man
|
|
3
|
+
"version": "1.4.0-dev.2928",
|
|
4
|
+
"description": "Man pages and Markdown command references for Optique",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"CLI",
|
|
7
7
|
"command-line",
|
|
@@ -77,6 +77,14 @@
|
|
|
77
77
|
},
|
|
78
78
|
"import": "./dist/cli.js",
|
|
79
79
|
"require": "./dist/cli.cjs"
|
|
80
|
+
},
|
|
81
|
+
"./markdown": {
|
|
82
|
+
"types": {
|
|
83
|
+
"import": "./dist/markdown.d.ts",
|
|
84
|
+
"require": "./dist/markdown.d.cts"
|
|
85
|
+
},
|
|
86
|
+
"import": "./dist/markdown.js",
|
|
87
|
+
"require": "./dist/markdown.cjs"
|
|
80
88
|
}
|
|
81
89
|
},
|
|
82
90
|
"imports": {
|
|
@@ -90,11 +98,11 @@
|
|
|
90
98
|
"optique-man": "./dist/cli.js"
|
|
91
99
|
},
|
|
92
100
|
"dependencies": {
|
|
93
|
-
"@optique/core": "1.4.0-dev.
|
|
94
|
-
"@optique/run": "1.4.0-dev.
|
|
101
|
+
"@optique/core": "1.4.0-dev.2928+80c003f2",
|
|
102
|
+
"@optique/run": "1.4.0-dev.2928+80c003f2"
|
|
95
103
|
},
|
|
96
104
|
"devDependencies": {
|
|
97
|
-
"@optique/testing": "1.4.0-dev.
|
|
105
|
+
"@optique/testing": "1.4.0-dev.2928+80c003f2",
|
|
98
106
|
"@types/node": "^24.0.0",
|
|
99
107
|
"tsdown": "^0.13.0",
|
|
100
108
|
"tsx": "^4.21.0",
|