politty 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{arg-registry-BoqVZRFO.d.ts → arg-registry-DDJpsUea.d.cts} +24 -2
- package/dist/arg-registry-DDJpsUea.d.cts.map +1 -0
- package/dist/{arg-registry-BoqVZRFO.d.cts → arg-registry-DDJpsUea.d.ts} +24 -2
- package/dist/arg-registry-DDJpsUea.d.ts.map +1 -0
- package/dist/augment.d.cts +1 -1
- package/dist/augment.d.ts +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/completion/index.d.cts +2 -2
- package/dist/completion/index.d.ts +2 -2
- package/dist/docs/index.cjs +827 -43
- package/dist/docs/index.cjs.map +1 -1
- package/dist/docs/index.d.cts +45 -3
- package/dist/docs/index.d.cts.map +1 -1
- package/dist/docs/index.d.ts +45 -3
- package/dist/docs/index.d.ts.map +1 -1
- package/dist/docs/index.js +827 -43
- package/dist/docs/index.js.map +1 -1
- package/dist/{index-Csk1VFou.d.ts → index-DKGn3lIl.d.ts} +2 -2
- package/dist/{index-Csk1VFou.d.ts.map → index-DKGn3lIl.d.ts.map} +1 -1
- package/dist/{index-Ct48_myg.d.cts → index-WyViqW59.d.cts} +2 -2
- package/dist/{index-Ct48_myg.d.cts.map → index-WyViqW59.d.cts.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/prompt/clack/index.d.cts +1 -1
- package/dist/prompt/clack/index.d.ts +1 -1
- package/dist/prompt/index.d.cts +1 -1
- package/dist/prompt/index.d.ts +1 -1
- package/dist/prompt/inquirer/index.d.cts +1 -1
- package/dist/prompt/inquirer/index.d.ts +1 -1
- package/dist/{runner--Zn4KN9B.js → runner-D43SkHt5.js} +119 -18
- package/dist/runner-D43SkHt5.js.map +1 -0
- package/dist/{runner-BloFWJEB.cjs → runner-DvFvokV6.cjs} +119 -18
- package/dist/runner-DvFvokV6.cjs.map +1 -0
- package/package.json +6 -6
- package/dist/arg-registry-BoqVZRFO.d.cts.map +0 -1
- package/dist/arg-registry-BoqVZRFO.d.ts.map +0 -1
- package/dist/runner--Zn4KN9B.js.map +0 -1
- package/dist/runner-BloFWJEB.cjs.map +0 -1
package/dist/docs/index.cjs
CHANGED
|
@@ -487,8 +487,10 @@ function renderSubcommandsTableFromArray(subcommands, info, generateAnchors = tr
|
|
|
487
487
|
let cmdCell;
|
|
488
488
|
if (generateAnchors) {
|
|
489
489
|
const anchor = generateAnchor$1(sub.fullPath);
|
|
490
|
-
const
|
|
490
|
+
const hasSubFile = fileMap !== void 0 && Object.prototype.hasOwnProperty.call(fileMap, subCommandPath);
|
|
491
|
+
const subFile = hasSubFile ? fileMap[subCommandPath] : void 0;
|
|
491
492
|
if (currentFile && subFile && currentFile !== subFile) cmdCell = `[\`${fullName}\`](${getRelativePath(currentFile, subFile)}#${anchor})`;
|
|
493
|
+
else if (fileMap && !hasSubFile) cmdCell = `\`${fullName}\``;
|
|
492
494
|
else cmdCell = `[\`${fullName}\`](#${anchor})`;
|
|
493
495
|
} else cmdCell = `\`${fullName}\``;
|
|
494
496
|
if (hasAliases) lines.push(`| ${cmdCell} | ${aliasCell} | ${desc} |`);
|
|
@@ -551,14 +553,15 @@ function getGlobalOptionsLink(info) {
|
|
|
551
553
|
return `See [Global Options](${info.rootDocPath && info.filePath && info.filePath !== info.rootDocPath ? `${getRelativePath(info.filePath, info.rootDocPath)}#global-options` : "#global-options"}) for options available to all commands.`;
|
|
552
554
|
}
|
|
553
555
|
function createCommandRenderer(options = {}) {
|
|
554
|
-
const { headingLevel = 1, optionStyle = "table", generateAnchors = true, includeSubcommandDetails = true, renderDescription: customRenderDescription, renderUsage: customRenderUsage, renderArguments: customRenderArguments, renderOptions: customRenderOptions, renderSubcommands: customRenderSubcommands, renderNotes: customRenderNotes, renderFooter: customRenderFooter, renderExamples: customRenderExamples } = options;
|
|
556
|
+
const { headingLevel = 1, optionStyle = "table", generateAnchors = true, includeSubcommandDetails = true, markerless = false, renderDescription: customRenderDescription, renderUsage: customRenderUsage, renderArguments: customRenderArguments, renderOptions: customRenderOptions, renderSubcommands: customRenderSubcommands, renderNotes: customRenderNotes, renderFooter: customRenderFooter, renderExamples: customRenderExamples } = options;
|
|
557
|
+
const wrap = markerless ? (_type, _scope, content) => content : wrapWithMarker;
|
|
555
558
|
return (info) => {
|
|
556
559
|
const sections = [];
|
|
557
560
|
const scope = info.commandPath;
|
|
558
561
|
const effectiveLevel = Math.min(headingLevel + (info.depth - 1), 6);
|
|
559
562
|
const h = "#".repeat(effectiveLevel);
|
|
560
563
|
const title = info.commandPath || info.name;
|
|
561
|
-
sections.push(
|
|
564
|
+
sections.push(wrap("heading", scope, `${h} ${title}`));
|
|
562
565
|
{
|
|
563
566
|
const parts = [];
|
|
564
567
|
if (info.description) parts.push(info.description);
|
|
@@ -570,7 +573,7 @@ function createCommandRenderer(options = {}) {
|
|
|
570
573
|
info
|
|
571
574
|
};
|
|
572
575
|
const content = customRenderDescription ? customRenderDescription(context) : context.content;
|
|
573
|
-
if (content) sections.push(
|
|
576
|
+
if (content) sections.push(wrap("description", scope, content));
|
|
574
577
|
}
|
|
575
578
|
}
|
|
576
579
|
{
|
|
@@ -580,7 +583,7 @@ function createCommandRenderer(options = {}) {
|
|
|
580
583
|
info
|
|
581
584
|
};
|
|
582
585
|
const content = customRenderUsage ? customRenderUsage(context) : context.content;
|
|
583
|
-
if (content) sections.push(
|
|
586
|
+
if (content) sections.push(wrap("usage", scope, content));
|
|
584
587
|
}
|
|
585
588
|
if (info.positionalArgs.length > 0) {
|
|
586
589
|
const renderArgs = (args, opts) => {
|
|
@@ -596,7 +599,7 @@ function createCommandRenderer(options = {}) {
|
|
|
596
599
|
info
|
|
597
600
|
};
|
|
598
601
|
const content = customRenderArguments ? customRenderArguments(context) : renderArgs(context.args);
|
|
599
|
-
if (content) sections.push(
|
|
602
|
+
if (content) sections.push(wrap("arguments", scope, content));
|
|
600
603
|
}
|
|
601
604
|
if (info.options.length > 0) {
|
|
602
605
|
const renderOpts = (opts, renderOpts) => {
|
|
@@ -616,11 +619,11 @@ function createCommandRenderer(options = {}) {
|
|
|
616
619
|
info
|
|
617
620
|
};
|
|
618
621
|
const content = customRenderOptions ? customRenderOptions(context) : renderOpts(context.options);
|
|
619
|
-
if (content) sections.push(
|
|
622
|
+
if (content) sections.push(wrap("options", scope, content));
|
|
620
623
|
}
|
|
621
624
|
{
|
|
622
625
|
const globalLink = getGlobalOptionsLink(info);
|
|
623
|
-
if (globalLink) sections.push(
|
|
626
|
+
if (globalLink) sections.push(wrap("global-options-link", scope, globalLink));
|
|
624
627
|
}
|
|
625
628
|
if (info.subCommands.length > 0) {
|
|
626
629
|
const effectiveAnchors = generateAnchors && includeSubcommandDetails;
|
|
@@ -637,7 +640,7 @@ function createCommandRenderer(options = {}) {
|
|
|
637
640
|
info
|
|
638
641
|
};
|
|
639
642
|
const content = customRenderSubcommands ? customRenderSubcommands(context) : renderSubs(context.subcommands);
|
|
640
|
-
if (content) sections.push(
|
|
643
|
+
if (content) sections.push(wrap("subcommands", scope, content));
|
|
641
644
|
}
|
|
642
645
|
if (info.examples && info.examples.length > 0) {
|
|
643
646
|
const renderEx = (examples, results, opts) => {
|
|
@@ -656,7 +659,7 @@ function createCommandRenderer(options = {}) {
|
|
|
656
659
|
info
|
|
657
660
|
};
|
|
658
661
|
const content = customRenderExamples ? customRenderExamples(context) : renderEx(context.examples, context.results);
|
|
659
|
-
if (content) sections.push(
|
|
662
|
+
if (content) sections.push(wrap("examples", scope, content));
|
|
660
663
|
}
|
|
661
664
|
if (info.notes) {
|
|
662
665
|
const context = {
|
|
@@ -665,7 +668,7 @@ function createCommandRenderer(options = {}) {
|
|
|
665
668
|
info
|
|
666
669
|
};
|
|
667
670
|
const content = customRenderNotes ? customRenderNotes(context) : context.content;
|
|
668
|
-
if (content) sections.push(
|
|
671
|
+
if (content) sections.push(wrap("notes", scope, content));
|
|
669
672
|
}
|
|
670
673
|
{
|
|
671
674
|
const context = {
|
|
@@ -873,7 +876,7 @@ async function executeSingleExample(example, rootCommand, commandPath) {
|
|
|
873
876
|
collector.start();
|
|
874
877
|
let success = true;
|
|
875
878
|
try {
|
|
876
|
-
const { runCommand } = await Promise.resolve().then(() => require("../runner-
|
|
879
|
+
const { runCommand } = await Promise.resolve().then(() => require("../runner-DvFvokV6.cjs")).then((n) => n.runner_exports);
|
|
877
880
|
const result = await runCommand(rootCommand, argv);
|
|
878
881
|
success = result.success;
|
|
879
882
|
if (!result.success && result.error) console.error(result.error.message);
|
|
@@ -1092,6 +1095,11 @@ function generateAnchor(commandPath) {
|
|
|
1092
1095
|
function isLeafCommand(info) {
|
|
1093
1096
|
return info.subCommands.length === 0;
|
|
1094
1097
|
}
|
|
1098
|
+
function isSubcommandOf$1(childPath, parentPath) {
|
|
1099
|
+
if (childPath === parentPath) return true;
|
|
1100
|
+
if (parentPath === "") return childPath !== "";
|
|
1101
|
+
return childPath.startsWith(parentPath + " ");
|
|
1102
|
+
}
|
|
1095
1103
|
/**
|
|
1096
1104
|
* Expand commands to include their subcommands
|
|
1097
1105
|
* If a command has subcommands, recursively find all commands under it
|
|
@@ -1122,13 +1130,23 @@ function renderCategory(category, allCommands, headingLevel, leafOnly) {
|
|
|
1122
1130
|
lines.push("");
|
|
1123
1131
|
lines.push(category.description);
|
|
1124
1132
|
lines.push("");
|
|
1125
|
-
const commandPaths = expandCommands(category.commands, allCommands, leafOnly);
|
|
1133
|
+
const commandPaths = category.noExpand ? category.commands : expandCommands(category.commands, allCommands, leafOnly);
|
|
1134
|
+
let visibleCommandPaths = commandPaths;
|
|
1135
|
+
const fallbackCommandPaths = /* @__PURE__ */ new Set();
|
|
1136
|
+
if (category.allowedCommands) {
|
|
1137
|
+
const allowed = new Set(category.allowedCommands);
|
|
1138
|
+
visibleCommandPaths = commandPaths.filter((cmdPath) => allowed.has(cmdPath));
|
|
1139
|
+
for (const configuredPath of category.commands) if (allowed.has(configuredPath) && !visibleCommandPaths.some((cmdPath) => isSubcommandOf$1(cmdPath, configuredPath))) {
|
|
1140
|
+
visibleCommandPaths.push(configuredPath);
|
|
1141
|
+
fallbackCommandPaths.add(configuredPath);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1126
1144
|
lines.push("| Command | Description |");
|
|
1127
1145
|
lines.push("|---------|-------------|");
|
|
1128
|
-
for (const cmdPath of
|
|
1146
|
+
for (const cmdPath of visibleCommandPaths) {
|
|
1129
1147
|
const info = allCommands.get(cmdPath);
|
|
1130
1148
|
if (!info) continue;
|
|
1131
|
-
if (leafOnly && !isLeafCommand(info)) continue;
|
|
1149
|
+
if (!category.noExpand && leafOnly && !fallbackCommandPaths.has(cmdPath) && !isLeafCommand(info)) continue;
|
|
1132
1150
|
const displayName = cmdPath || info.name;
|
|
1133
1151
|
const anchor = generateAnchor(displayName);
|
|
1134
1152
|
const desc = escapeTableCell(info.description ?? "");
|
|
@@ -1194,6 +1212,181 @@ function isTruthyEnv(envKey) {
|
|
|
1194
1212
|
const value = process.env[envKey];
|
|
1195
1213
|
return value === "true" || value === "1";
|
|
1196
1214
|
}
|
|
1215
|
+
function extractYamlFrontMatter(content) {
|
|
1216
|
+
const lines = content.split(/\r?\n/);
|
|
1217
|
+
if (lines[0] !== "---") return null;
|
|
1218
|
+
const frontMatterLines = [];
|
|
1219
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1220
|
+
const line = lines[i];
|
|
1221
|
+
if (line === "---" || line === "...") return frontMatterLines.join("\n");
|
|
1222
|
+
frontMatterLines.push(line ?? "");
|
|
1223
|
+
}
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
function stripPolittyFrontMatterForOutput(content) {
|
|
1227
|
+
const lineEnding = detectLineEnding(content);
|
|
1228
|
+
const lines = content.split(/\r?\n/);
|
|
1229
|
+
if (lines[0] !== "---") return content;
|
|
1230
|
+
let endIndex = -1;
|
|
1231
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1232
|
+
const line = lines[i];
|
|
1233
|
+
if (line === "---" || line === "...") {
|
|
1234
|
+
endIndex = i;
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (endIndex === -1) return content;
|
|
1239
|
+
const frontMatterLines = lines.slice(1, endIndex);
|
|
1240
|
+
const keptFrontMatterLines = [];
|
|
1241
|
+
for (let i = 0; i < frontMatterLines.length; i++) {
|
|
1242
|
+
const line = frontMatterLines[i] ?? "";
|
|
1243
|
+
if (!/^politty\s*:\s*(.*)$/.test(line)) {
|
|
1244
|
+
keptFrontMatterLines.push(line);
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
while (i + 1 < frontMatterLines.length) {
|
|
1248
|
+
const nextLine = frontMatterLines[i + 1] ?? "";
|
|
1249
|
+
if (nextLine.trim() !== "" && !nextLine.startsWith(" ") && !nextLine.startsWith(" ")) break;
|
|
1250
|
+
i++;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const bodyLines = lines.slice(endIndex + 1);
|
|
1254
|
+
if (!keptFrontMatterLines.some((line) => line.trim() !== "")) return bodyLines.join(lineEnding).replace(new RegExp(`^${lineEnding}`), "");
|
|
1255
|
+
return [
|
|
1256
|
+
"---",
|
|
1257
|
+
...keptFrontMatterLines,
|
|
1258
|
+
lines[endIndex] ?? "---",
|
|
1259
|
+
...bodyLines
|
|
1260
|
+
].join(lineEnding);
|
|
1261
|
+
}
|
|
1262
|
+
function stripYamlScalarQuotes(value) {
|
|
1263
|
+
const trimmed = value.trim();
|
|
1264
|
+
if (trimmed.length >= 2 && (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'"))) return trimmed.slice(1, -1);
|
|
1265
|
+
return trimmed;
|
|
1266
|
+
}
|
|
1267
|
+
function normalizeTemplatePlaceholderKey(value) {
|
|
1268
|
+
let normalized = stripYamlScalarQuotes(value);
|
|
1269
|
+
if (normalized === "") return null;
|
|
1270
|
+
const fullPlaceholder = normalized.match(/^\{\{politty:([^{}]*)\}\}$/);
|
|
1271
|
+
if (fullPlaceholder) normalized = fullPlaceholder[1] ?? "";
|
|
1272
|
+
else if (normalized.startsWith("politty:")) normalized = normalized.slice(8);
|
|
1273
|
+
return normalized === "" ? null : normalized;
|
|
1274
|
+
}
|
|
1275
|
+
function templatePlaceholderKey(placeholder) {
|
|
1276
|
+
return placeholder.slice(2, -2).slice(8);
|
|
1277
|
+
}
|
|
1278
|
+
function splitFrontMatterListValue(value) {
|
|
1279
|
+
const trimmed = value.trim();
|
|
1280
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return [trimmed];
|
|
1281
|
+
return trimmed.slice(1, -1).split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
1282
|
+
}
|
|
1283
|
+
function addTemplatePlaceholderExclusion(exclusions, value) {
|
|
1284
|
+
const normalized = normalizeTemplatePlaceholderKey(value);
|
|
1285
|
+
if (normalized !== null) exclusions.add(normalized);
|
|
1286
|
+
}
|
|
1287
|
+
function collectExcludedTemplatePlaceholders(templateContent) {
|
|
1288
|
+
const exclusions = /* @__PURE__ */ new Set();
|
|
1289
|
+
const frontMatter = extractYamlFrontMatter(templateContent);
|
|
1290
|
+
if (frontMatter === null) return exclusions;
|
|
1291
|
+
let inPolittyBlock = false;
|
|
1292
|
+
let inExcludeList = false;
|
|
1293
|
+
let excludeIndent = 0;
|
|
1294
|
+
for (const line of frontMatter.split(/\r?\n/)) {
|
|
1295
|
+
const trimmed = line.trim();
|
|
1296
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
1297
|
+
const topLevelPolitty = line.match(/^politty\s*:\s*(.*)$/);
|
|
1298
|
+
if (topLevelPolitty) {
|
|
1299
|
+
inPolittyBlock = (topLevelPolitty[1]?.trim() ?? "") === "";
|
|
1300
|
+
inExcludeList = false;
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
if (!line.startsWith(" ") && !line.startsWith(" ")) {
|
|
1304
|
+
inPolittyBlock = false;
|
|
1305
|
+
inExcludeList = false;
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
if (!inPolittyBlock) continue;
|
|
1309
|
+
const excludeEntry = line.match(/^(\s+)(?:exclude|excludes)\s*:\s*(.*)$/);
|
|
1310
|
+
if (excludeEntry) {
|
|
1311
|
+
const value = excludeEntry[2]?.trim() ?? "";
|
|
1312
|
+
if (value === "") {
|
|
1313
|
+
inExcludeList = true;
|
|
1314
|
+
excludeIndent = excludeEntry[1]?.length ?? 0;
|
|
1315
|
+
} else {
|
|
1316
|
+
inExcludeList = false;
|
|
1317
|
+
for (const item of splitFrontMatterListValue(value)) addTemplatePlaceholderExclusion(exclusions, item);
|
|
1318
|
+
}
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
if (!inExcludeList) continue;
|
|
1322
|
+
const listItem = line.match(/^(\s*)-\s*(.+)$/);
|
|
1323
|
+
if (!listItem || (listItem[1]?.length ?? 0) <= excludeIndent) {
|
|
1324
|
+
inExcludeList = false;
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
addTemplatePlaceholderExclusion(exclusions, listItem[2] ?? "");
|
|
1328
|
+
}
|
|
1329
|
+
return exclusions;
|
|
1330
|
+
}
|
|
1331
|
+
function collectTemplateIndexMetadata(templateContent) {
|
|
1332
|
+
const frontMatter = extractYamlFrontMatter(templateContent);
|
|
1333
|
+
if (frontMatter === null) return {};
|
|
1334
|
+
let inPolittyBlock = false;
|
|
1335
|
+
let inIndexBlock = false;
|
|
1336
|
+
let indexIndent = 0;
|
|
1337
|
+
const metadata = {};
|
|
1338
|
+
for (const line of frontMatter.split(/\r?\n/)) {
|
|
1339
|
+
const trimmed = line.trim();
|
|
1340
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
1341
|
+
const topLevelPolitty = line.match(/^politty\s*:\s*(.*)$/);
|
|
1342
|
+
if (topLevelPolitty) {
|
|
1343
|
+
inPolittyBlock = (topLevelPolitty[1]?.trim() ?? "") === "";
|
|
1344
|
+
inIndexBlock = false;
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
if (!line.startsWith(" ") && !line.startsWith(" ")) {
|
|
1348
|
+
inPolittyBlock = false;
|
|
1349
|
+
inIndexBlock = false;
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
if (!inPolittyBlock) continue;
|
|
1353
|
+
const indexEntry = line.match(/^(\s+)index\s*:\s*(.*)$/);
|
|
1354
|
+
if (indexEntry) {
|
|
1355
|
+
inIndexBlock = (indexEntry[2]?.trim() ?? "") === "";
|
|
1356
|
+
indexIndent = indexEntry[1]?.length ?? 0;
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if (!inIndexBlock) continue;
|
|
1360
|
+
if ((line.match(/^(\s*)/)?.[1]?.length ?? 0) <= indexIndent) {
|
|
1361
|
+
inIndexBlock = false;
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
const property = line.match(/^\s+(title|description)\s*:\s*(.+)$/);
|
|
1365
|
+
if (!property) continue;
|
|
1366
|
+
const key = property[1];
|
|
1367
|
+
const value = stripYamlScalarQuotes(property[2] ?? "");
|
|
1368
|
+
if (key === "title") metadata.title = value;
|
|
1369
|
+
else if (key === "description") metadata.description = value;
|
|
1370
|
+
}
|
|
1371
|
+
return metadata;
|
|
1372
|
+
}
|
|
1373
|
+
function createTemplateExclusions(rawKeys) {
|
|
1374
|
+
return {
|
|
1375
|
+
rawKeys,
|
|
1376
|
+
commandScopes: /* @__PURE__ */ new Set(),
|
|
1377
|
+
commandSections: /* @__PURE__ */ new Map(),
|
|
1378
|
+
globalOptions: false,
|
|
1379
|
+
index: false
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
function setFileMapEntry(fileMap, commandPath, filePath) {
|
|
1383
|
+
Object.defineProperty(fileMap, commandPath, {
|
|
1384
|
+
value: filePath,
|
|
1385
|
+
enumerable: true,
|
|
1386
|
+
configurable: true,
|
|
1387
|
+
writable: true
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1197
1390
|
/**
|
|
1198
1391
|
* Normalize file mapping entry to FileConfig
|
|
1199
1392
|
*/
|
|
@@ -1548,6 +1741,227 @@ function removeCommandSections(content, commandPath) {
|
|
|
1548
1741
|
return content;
|
|
1549
1742
|
}
|
|
1550
1743
|
/**
|
|
1744
|
+
* Strip politty marker lines from content, then collapse the blank-line gaps the removed markers
|
|
1745
|
+
* leave behind (outside fenced code blocks only, so intentional blank lines inside generated
|
|
1746
|
+
* example/code blocks are preserved) and trim leading/trailing blank lines.
|
|
1747
|
+
*/
|
|
1748
|
+
function stripPolittyMarkers(content) {
|
|
1749
|
+
let result = collapseBlankLinesOutsideCodeFences(content.split("\n").filter((line) => !/^<!-- politty:.*-->$/.test(line.trim())).join("\n"));
|
|
1750
|
+
result = result.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
1751
|
+
return result;
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Collapse runs of 3+ newlines to 2, but only outside fenced code blocks so that intentional
|
|
1755
|
+
* blank lines inside handwritten code samples are preserved. Fences are lines whose trimmed
|
|
1756
|
+
* content starts with ``` or ~~~.
|
|
1757
|
+
*/
|
|
1758
|
+
function collapseBlankLinesOutsideCodeFences(content) {
|
|
1759
|
+
const lines = content.split("\n");
|
|
1760
|
+
const out = [];
|
|
1761
|
+
let inFence = false;
|
|
1762
|
+
let blankRun = 0;
|
|
1763
|
+
for (const line of lines) {
|
|
1764
|
+
const trimmed = line.trim();
|
|
1765
|
+
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
1766
|
+
inFence = !inFence;
|
|
1767
|
+
blankRun = 0;
|
|
1768
|
+
out.push(line);
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
if (!inFence && line.trim() === "") {
|
|
1772
|
+
blankRun++;
|
|
1773
|
+
if (blankRun >= 2) continue;
|
|
1774
|
+
} else if (!inFence) blankRun = 0;
|
|
1775
|
+
out.push(line);
|
|
1776
|
+
}
|
|
1777
|
+
return out.join("\n");
|
|
1778
|
+
}
|
|
1779
|
+
function detectLineEnding(content) {
|
|
1780
|
+
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
1781
|
+
}
|
|
1782
|
+
function countLineBreaks(value) {
|
|
1783
|
+
return (value.match(/\n/g) ?? []).length;
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Type guard for SectionType values parsed from template placeholders.
|
|
1787
|
+
*/
|
|
1788
|
+
function isSectionType(value) {
|
|
1789
|
+
return SECTION_TYPES.some((type) => type === value);
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Clamp a numeric heading level to the valid HeadingLevel range (1–6).
|
|
1793
|
+
* Uses a switch to return a literal union member, avoiding `as` assertions.
|
|
1794
|
+
*/
|
|
1795
|
+
function clampHeadingLevel(level) {
|
|
1796
|
+
switch (Math.min(6, Math.max(1, Math.trunc(level)))) {
|
|
1797
|
+
case 1: return 1;
|
|
1798
|
+
case 2: return 2;
|
|
1799
|
+
case 3: return 3;
|
|
1800
|
+
case 4: return 4;
|
|
1801
|
+
case 5: return 5;
|
|
1802
|
+
default: return 6;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
function resolveTemplateCommandScope(tokens, allCommands) {
|
|
1806
|
+
if (tokens.length === 0) return allCommands === void 0 || allCommands.has("") ? "" : null;
|
|
1807
|
+
const exactScope = tokens.join(":");
|
|
1808
|
+
if (allCommands?.has(exactScope)) return exactScope;
|
|
1809
|
+
const colonSeparatedScope = tokens.join(" ");
|
|
1810
|
+
if (allCommands?.has(colonSeparatedScope)) return colonSeparatedScope;
|
|
1811
|
+
return allCommands === void 0 ? colonSeparatedScope : null;
|
|
1812
|
+
}
|
|
1813
|
+
function templateScopeFallback(tokens) {
|
|
1814
|
+
return tokens.join(" ");
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Parse a single {{politty:...}} placeholder string into a discriminated structure.
|
|
1818
|
+
* The `placeholder` argument should be the full `{{politty:...}}` text.
|
|
1819
|
+
*
|
|
1820
|
+
* Uses String.match / String.replace internally (not .exec) to avoid lastIndex
|
|
1821
|
+
* state issues from the shared TEMPLATE_PLACEHOLDER_REGEX constant.
|
|
1822
|
+
*/
|
|
1823
|
+
function parsePlaceholder(placeholder, allCommands) {
|
|
1824
|
+
const tokens = placeholder.slice(2, -2).split(":");
|
|
1825
|
+
const directive = tokens[1];
|
|
1826
|
+
if (directive === "command") {
|
|
1827
|
+
const rest = tokens.slice(2);
|
|
1828
|
+
if (rest.length === 1 && rest[0] === "") return {
|
|
1829
|
+
kind: "invalid",
|
|
1830
|
+
reason: `Trailing colon in "${placeholder}"; use {{politty:command}} for the root command.`
|
|
1831
|
+
};
|
|
1832
|
+
const fullScope = resolveTemplateCommandScope(rest, allCommands);
|
|
1833
|
+
if (fullScope !== null) return {
|
|
1834
|
+
kind: "command",
|
|
1835
|
+
scope: fullScope,
|
|
1836
|
+
type: void 0
|
|
1837
|
+
};
|
|
1838
|
+
if (rest.length >= 2) {
|
|
1839
|
+
const last = rest[rest.length - 1];
|
|
1840
|
+
const scopeTokens = rest.slice(0, -1);
|
|
1841
|
+
const sectionScope = resolveTemplateCommandScope(scopeTokens, allCommands);
|
|
1842
|
+
if (last !== void 0 && isSectionType(last)) return {
|
|
1843
|
+
kind: "command",
|
|
1844
|
+
scope: sectionScope ?? templateScopeFallback(scopeTokens),
|
|
1845
|
+
type: last
|
|
1846
|
+
};
|
|
1847
|
+
if (last !== void 0 && sectionScope !== null) return {
|
|
1848
|
+
kind: "invalid",
|
|
1849
|
+
reason: `Unknown section type "${last}" for command scope "${formatCommandPath(sectionScope)}". Valid section types: ${SECTION_TYPES.join(", ")}`
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
return {
|
|
1853
|
+
kind: "command",
|
|
1854
|
+
scope: templateScopeFallback(rest),
|
|
1855
|
+
type: void 0
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
if (directive === "global-options") {
|
|
1859
|
+
if (tokens.length !== 2) return {
|
|
1860
|
+
kind: "invalid",
|
|
1861
|
+
reason: `Malformed placeholder "${placeholder}". Expected {{politty:global-options}}.`
|
|
1862
|
+
};
|
|
1863
|
+
return { kind: "global-options" };
|
|
1864
|
+
}
|
|
1865
|
+
if (directive === "index") {
|
|
1866
|
+
if (tokens.length !== 2) return {
|
|
1867
|
+
kind: "invalid",
|
|
1868
|
+
reason: `Malformed placeholder "${placeholder}". Expected {{politty:index}}.`
|
|
1869
|
+
};
|
|
1870
|
+
return { kind: "index" };
|
|
1871
|
+
}
|
|
1872
|
+
return {
|
|
1873
|
+
kind: "invalid",
|
|
1874
|
+
reason: `Unknown politty directive "${directive ?? ""}" in "${placeholder}". Valid directives: command, global-options, index`
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
function buildTemplateExclusions(rawKeys, allCommands) {
|
|
1878
|
+
const exclusions = createTemplateExclusions(rawKeys);
|
|
1879
|
+
for (const key of rawKeys) {
|
|
1880
|
+
const parsed = parsePlaceholder(`{{politty:${key}}}`, allCommands);
|
|
1881
|
+
if (parsed.kind === "command") if (parsed.type === void 0) exclusions.commandScopes.add(parsed.scope);
|
|
1882
|
+
else {
|
|
1883
|
+
let sections = exclusions.commandSections.get(parsed.scope);
|
|
1884
|
+
if (!sections) {
|
|
1885
|
+
sections = /* @__PURE__ */ new Set();
|
|
1886
|
+
exclusions.commandSections.set(parsed.scope, sections);
|
|
1887
|
+
}
|
|
1888
|
+
sections.add(parsed.type);
|
|
1889
|
+
}
|
|
1890
|
+
else if (parsed.kind === "global-options") exclusions.globalOptions = true;
|
|
1891
|
+
else if (parsed.kind === "index") exclusions.index = true;
|
|
1892
|
+
}
|
|
1893
|
+
return exclusions;
|
|
1894
|
+
}
|
|
1895
|
+
function isCommandScopeExcluded(commandPath, excludedCommandScopes) {
|
|
1896
|
+
for (const excludedScope of excludedCommandScopes) if (isSubcommandOf(commandPath, excludedScope)) return true;
|
|
1897
|
+
return false;
|
|
1898
|
+
}
|
|
1899
|
+
function isCommandSectionExcluded(commandPath, sectionType, exclusions) {
|
|
1900
|
+
if (isCommandScopeExcluded(commandPath, exclusions.commandScopes)) return true;
|
|
1901
|
+
return exclusions.commandSections.get(commandPath)?.has(sectionType) ?? false;
|
|
1902
|
+
}
|
|
1903
|
+
function getTemplateCommandTreePaths(commandPath, allCommands, ignores, exclusions) {
|
|
1904
|
+
return sortDepthFirst(filterIgnoredCommands(expandCommandPaths([commandPath], allCommands), ignores).filter((path) => !isCommandScopeExcluded(path, exclusions.commandScopes)), [commandPath]);
|
|
1905
|
+
}
|
|
1906
|
+
function shouldSkipTemplatePlaceholder(placeholder, parsed, exclusions) {
|
|
1907
|
+
if (exclusions.rawKeys.has(templatePlaceholderKey(placeholder))) return true;
|
|
1908
|
+
if (parsed.kind === "command") {
|
|
1909
|
+
if (isCommandScopeExcluded(parsed.scope, exclusions.commandScopes)) return true;
|
|
1910
|
+
return parsed.type !== void 0 && (exclusions.commandSections.get(parsed.scope)?.has(parsed.type) ?? false);
|
|
1911
|
+
}
|
|
1912
|
+
if (parsed.kind === "global-options") return exclusions.globalOptions;
|
|
1913
|
+
if (parsed.kind === "index") return exclusions.index;
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
function isRawCommandPlaceholderUnderExcludedScope(key, exclusions) {
|
|
1917
|
+
if (!key.startsWith("command:")) return false;
|
|
1918
|
+
const tokens = key.slice(8).split(":");
|
|
1919
|
+
for (const excludedScope of exclusions.commandScopes) {
|
|
1920
|
+
if (excludedScope === "") return true;
|
|
1921
|
+
const spaceTokens = excludedScope.split(" ");
|
|
1922
|
+
if (tokens.slice(0, spaceTokens.length).join(" ") === excludedScope) return true;
|
|
1923
|
+
const colonTokens = excludedScope.split(":");
|
|
1924
|
+
if (tokens.slice(0, colonTokens.length).join(":") === excludedScope) return true;
|
|
1925
|
+
}
|
|
1926
|
+
return false;
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Regex matching {{politty:...}} placeholders.
|
|
1930
|
+
* NOTE: only use with String.match / String.replace, never with .exec in a loop,
|
|
1931
|
+
* because the /g flag makes the regex stateful via lastIndex.
|
|
1932
|
+
*/
|
|
1933
|
+
const TEMPLATE_PLACEHOLDER_REGEX = /\{\{politty:[^{}]*\}\}/g;
|
|
1934
|
+
function validateTemplatePlaceholderSyntax(templateContent, templatePath) {
|
|
1935
|
+
const validPlaceholderStarts = /* @__PURE__ */ new Set();
|
|
1936
|
+
for (const match of templateContent.matchAll(TEMPLATE_PLACEHOLDER_REGEX)) {
|
|
1937
|
+
const start = match.index;
|
|
1938
|
+
const end = start + match[0].length;
|
|
1939
|
+
if (templateContent[start - 1] === "{" || templateContent[end] === "}") {
|
|
1940
|
+
const snippet = templateContent.slice(Math.max(0, start - 1), Math.min(templateContent.length, end + 1)).split("\n")[0];
|
|
1941
|
+
throw new Error(`Malformed politty placeholder in template "${templatePath}": "${snippet}". Expected {{politty:...}}.`);
|
|
1942
|
+
}
|
|
1943
|
+
validPlaceholderStarts.add(start);
|
|
1944
|
+
}
|
|
1945
|
+
let searchIndex = 0;
|
|
1946
|
+
while (true) {
|
|
1947
|
+
const placeholderStart = templateContent.indexOf("{{politty:", searchIndex);
|
|
1948
|
+
if (placeholderStart === -1) return;
|
|
1949
|
+
if (!validPlaceholderStarts.has(placeholderStart)) {
|
|
1950
|
+
const snippet = templateContent.slice(placeholderStart, placeholderStart + 80).split("\n")[0];
|
|
1951
|
+
throw new Error(`Malformed politty placeholder in template "${templatePath}": "${snippet}". Expected {{politty:...}}.`);
|
|
1952
|
+
}
|
|
1953
|
+
searchIndex = placeholderStart + 10;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function getUnknownSectionTypeError(scope, allCommands) {
|
|
1957
|
+
const separatorIndex = scope.lastIndexOf(":");
|
|
1958
|
+
if (separatorIndex === -1) return null;
|
|
1959
|
+
const commandScope = scope.slice(0, separatorIndex);
|
|
1960
|
+
const sectionType = scope.slice(separatorIndex + 1);
|
|
1961
|
+
if (sectionType === "" || !allCommands.has(commandScope)) return null;
|
|
1962
|
+
return `Unknown section type "${sectionType}" for command scope "${formatCommandPath(commandScope)}". Valid section types: ${SECTION_TYPES.join(", ")}`;
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1551
1965
|
* Extract a marker section from content
|
|
1552
1966
|
* Returns the content between start and end markers (including markers)
|
|
1553
1967
|
*/
|
|
@@ -1607,6 +2021,17 @@ function normalizeGlobalOptions(config) {
|
|
|
1607
2021
|
return isGlobalOptionsConfigWithOptions(config) ? config : { args: config };
|
|
1608
2022
|
}
|
|
1609
2023
|
/**
|
|
2024
|
+
* Derive an ArgsShape from a globalArgs Zod schema, retaining only non-positional option fields.
|
|
2025
|
+
* Returns undefined when globalArgs is undefined or contains no option fields.
|
|
2026
|
+
* Used to build globalOptionDefinitions from globalArgs when rootDoc is not available.
|
|
2027
|
+
*/
|
|
2028
|
+
function deriveGlobalArgsShape(globalArgs) {
|
|
2029
|
+
if (!globalArgs) return void 0;
|
|
2030
|
+
const optionFields = require_schema_extractor.extractFields(globalArgs).fields.filter((f) => !f.positional);
|
|
2031
|
+
if (optionFields.length === 0) return void 0;
|
|
2032
|
+
return Object.fromEntries(optionFields.map((f) => [f.name, f.schema]));
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
1610
2035
|
* Collect global option definitions from rootDoc.
|
|
1611
2036
|
* Global options are intentionally applied to all generated command sections.
|
|
1612
2037
|
*/
|
|
@@ -1635,12 +2060,38 @@ function deriveIndexFromFiles(files, rootDocPath, allCommands, ignores) {
|
|
|
1635
2060
|
title: fileConfig?.title ?? cmdInfo?.name ?? node_path.basename(filePath, node_path.extname(filePath)),
|
|
1636
2061
|
description: fileConfig?.description ?? cmdInfo?.description ?? "",
|
|
1637
2062
|
commands: topLevelCommands,
|
|
2063
|
+
allowedCommands: commandPaths,
|
|
1638
2064
|
docPath
|
|
1639
2065
|
});
|
|
1640
2066
|
}
|
|
1641
2067
|
return categories;
|
|
1642
2068
|
}
|
|
1643
2069
|
/**
|
|
2070
|
+
* Build index categories for the {{politty:index}} placeholder from other template outputs.
|
|
2071
|
+
* Each category lists exactly the heading-producing scopes of that output (noExpand), so the
|
|
2072
|
+
* index never links to commands that template mode did not render.
|
|
2073
|
+
*/
|
|
2074
|
+
function deriveIndexFromTemplateOutputs(templateMeta, currentOutputPath, indexFilePath, allCommands) {
|
|
2075
|
+
const normalizedCurrent = normalizeDocPathForComparison(currentOutputPath);
|
|
2076
|
+
const categories = [];
|
|
2077
|
+
for (const [outputPath, meta] of templateMeta.entries()) {
|
|
2078
|
+
if (normalizeDocPathForComparison(outputPath) === normalizedCurrent) continue;
|
|
2079
|
+
const scopes = meta.headingScopes;
|
|
2080
|
+
if (scopes.length === 0) continue;
|
|
2081
|
+
const docPath = "./" + node_path.relative(node_path.dirname(indexFilePath), outputPath).replace(/\\/g, "/");
|
|
2082
|
+
const firstScope = scopes[0];
|
|
2083
|
+
const cmdInfo = firstScope !== void 0 ? allCommands.get(firstScope) : void 0;
|
|
2084
|
+
categories.push({
|
|
2085
|
+
title: meta.indexTitle ?? cmdInfo?.name ?? node_path.basename(outputPath, node_path.extname(outputPath)),
|
|
2086
|
+
description: meta.indexDescription ?? cmdInfo?.description ?? "",
|
|
2087
|
+
commands: scopes,
|
|
2088
|
+
docPath,
|
|
2089
|
+
noExpand: true
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
return categories;
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
1644
2095
|
* Collect command paths that are actually documented in configured files.
|
|
1645
2096
|
*/
|
|
1646
2097
|
function collectDocumentedCommandPaths(files, allCommands, ignores) {
|
|
@@ -1662,6 +2113,15 @@ function collectTargetDocumentedCommandPaths(targetCommands, files, allCommands,
|
|
|
1662
2113
|
}
|
|
1663
2114
|
return documentedTargetCommandPaths;
|
|
1664
2115
|
}
|
|
2116
|
+
function commandPathMatchesTarget(commandPath, targetCommands) {
|
|
2117
|
+
return targetCommands.some((targetCommand) => isSubcommandOf(commandPath, targetCommand));
|
|
2118
|
+
}
|
|
2119
|
+
function templateMetaReferencesCommandTarget(meta, targetCommands) {
|
|
2120
|
+
return meta.referencedScopes.some((scope) => commandPathMatchesTarget(scope, targetCommands));
|
|
2121
|
+
}
|
|
2122
|
+
function templateMetaShouldProcessForTarget(meta, targetCommands) {
|
|
2123
|
+
return meta.emitsIndex || meta.emitsGlobalOptions || templateMetaReferencesCommandTarget(meta, targetCommands);
|
|
2124
|
+
}
|
|
1665
2125
|
/**
|
|
1666
2126
|
* Validate that excluded command options match globalOptions definitions.
|
|
1667
2127
|
*/
|
|
@@ -1680,16 +2140,19 @@ function validateGlobalOptionCompatibility(documentedCommandPaths, allCommands,
|
|
|
1680
2140
|
if (conflicts.length > 0) throw new Error(`Invalid globalOptions configuration:\n - ${conflicts.join("\n - ")}`);
|
|
1681
2141
|
}
|
|
1682
2142
|
/**
|
|
2143
|
+
* Build global options content (anchor + args table) without markers
|
|
2144
|
+
*/
|
|
2145
|
+
function buildGlobalOptionsContent(config) {
|
|
2146
|
+
return ["<a id=\"global-options\"></a>", renderArgsTable(config.args, config.options)].join("\n");
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
1683
2149
|
* Generate global options section content with markers
|
|
1684
2150
|
*/
|
|
1685
2151
|
function generateGlobalOptionsSection(config) {
|
|
1686
|
-
const startMarker = globalOptionsStartMarker();
|
|
1687
|
-
const endMarker = globalOptionsEndMarker();
|
|
1688
2152
|
return [
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
endMarker
|
|
2153
|
+
globalOptionsStartMarker(),
|
|
2154
|
+
buildGlobalOptionsContent(config),
|
|
2155
|
+
globalOptionsEndMarker()
|
|
1693
2156
|
].join("\n");
|
|
1694
2157
|
}
|
|
1695
2158
|
/**
|
|
@@ -1909,29 +2372,74 @@ function findTargetCommandsInFile(targetCommands, filePath, files, allCommands,
|
|
|
1909
2372
|
/**
|
|
1910
2373
|
* Generate a single command section (already contains section markers from renderer)
|
|
1911
2374
|
*/
|
|
1912
|
-
function generateCommandSection(cmdPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions) {
|
|
2375
|
+
function generateCommandSection(cmdPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions, ignores = [], excludeOptionNames, templateExclusions) {
|
|
1913
2376
|
const info = allCommands.get(cmdPath);
|
|
1914
2377
|
if (!info) return null;
|
|
2378
|
+
if (templateExclusions && isCommandScopeExcluded(info.commandPath, templateExclusions.commandScopes)) return null;
|
|
1915
2379
|
const enriched = {
|
|
1916
2380
|
...info,
|
|
1917
2381
|
filePath,
|
|
1918
2382
|
fileMap,
|
|
1919
2383
|
rootDocPath
|
|
1920
2384
|
};
|
|
2385
|
+
if (ignores.length > 0 || templateExclusions && templateExclusions.commandScopes.size > 0) enriched.subCommands = info.subCommands.filter((sub) => {
|
|
2386
|
+
const subCommandPath = sub.fullPath.join(" ");
|
|
2387
|
+
if (ignores.some((pattern) => matchesIgnorePattern(subCommandPath, pattern))) return false;
|
|
2388
|
+
return !(templateExclusions && isCommandScopeExcluded(subCommandPath, templateExclusions.commandScopes));
|
|
2389
|
+
});
|
|
1921
2390
|
if (hasGlobalOptions !== void 0) enriched.hasGlobalOptions = hasGlobalOptions;
|
|
1922
|
-
|
|
2391
|
+
if (excludeOptionNames && excludeOptionNames.size > 0) {
|
|
2392
|
+
enriched.options = info.options.filter((opt) => !excludeOptionNames.has(opt.name));
|
|
2393
|
+
if (info.extracted) enriched.extracted = filterExtractedFields(info.extracted, excludeOptionNames);
|
|
2394
|
+
}
|
|
2395
|
+
let rendered = render(enriched);
|
|
2396
|
+
if (templateExclusions) for (const [scope, sectionTypes] of templateExclusions.commandSections) {
|
|
2397
|
+
if (scope !== info.commandPath) continue;
|
|
2398
|
+
for (const sectionType of sectionTypes) {
|
|
2399
|
+
const section = extractSectionMarker(rendered, sectionType, scope);
|
|
2400
|
+
if (section !== null) rendered = rendered.replace(section, "");
|
|
2401
|
+
}
|
|
2402
|
+
rendered = collapseBlankLinesOutsideCodeFences(rendered);
|
|
2403
|
+
}
|
|
2404
|
+
return rendered;
|
|
2405
|
+
}
|
|
2406
|
+
function generateCommandTreeMarkdown(cmdPath, allCommands, render, ignores, filePath, fileMap, rootDocPath, hasGlobalOptions, excludeOptionNames, templateExclusions) {
|
|
2407
|
+
const commandPaths = getTemplateCommandTreePaths(cmdPath, allCommands, ignores, templateExclusions);
|
|
2408
|
+
const sections = [];
|
|
2409
|
+
for (const commandPath of commandPaths) {
|
|
2410
|
+
const section = generateCommandSection(commandPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions, ignores, excludeOptionNames, templateExclusions);
|
|
2411
|
+
if (section !== null) sections.push(section);
|
|
2412
|
+
}
|
|
2413
|
+
return sections.length === 0 ? null : sections.join("\n");
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
2416
|
+
* Return a copy of ExtractedFields with the named options removed from every field collection
|
|
2417
|
+
* (top-level fields, union options, and discriminated-union variants). Used to exclude global
|
|
2418
|
+
* options from grouped option tables rendered directly from `extracted`.
|
|
2419
|
+
*/
|
|
2420
|
+
function filterExtractedFields(extracted, excludeOptionNames) {
|
|
2421
|
+
const result = {
|
|
2422
|
+
...extracted,
|
|
2423
|
+
fields: extracted.fields.filter((f) => !excludeOptionNames.has(f.name))
|
|
2424
|
+
};
|
|
2425
|
+
if (extracted.unionOptions) result.unionOptions = extracted.unionOptions.map((opt) => filterExtractedFields(opt, excludeOptionNames));
|
|
2426
|
+
if (extracted.variants) result.variants = extracted.variants.map((variant) => ({
|
|
2427
|
+
...variant,
|
|
2428
|
+
fields: variant.fields.filter((f) => !excludeOptionNames.has(f.name))
|
|
2429
|
+
}));
|
|
2430
|
+
return result;
|
|
1923
2431
|
}
|
|
1924
2432
|
/**
|
|
1925
2433
|
* Generate markdown for a file containing multiple commands
|
|
1926
2434
|
* Each command section is wrapped with markers for partial validation
|
|
1927
2435
|
*/
|
|
1928
|
-
function generateFileMarkdown(commandPaths, allCommands, render, filePath, fileMap, specifiedOrder, fileConfig, rootDocPath, hasGlobalOptions) {
|
|
2436
|
+
function generateFileMarkdown(commandPaths, allCommands, render, filePath, fileMap, specifiedOrder, fileConfig, rootDocPath, hasGlobalOptions, ignores = []) {
|
|
1929
2437
|
const sections = [];
|
|
1930
2438
|
const header = fileConfig ? generateFileHeader(fileConfig) : null;
|
|
1931
2439
|
if (header) sections.push(header);
|
|
1932
2440
|
const sortedPaths = sortDepthFirst(commandPaths, specifiedOrder ?? []);
|
|
1933
2441
|
for (const cmdPath of sortedPaths) {
|
|
1934
|
-
const section = generateCommandSection(cmdPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions);
|
|
2442
|
+
const section = generateCommandSection(cmdPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions, ignores);
|
|
1935
2443
|
if (section) sections.push(section);
|
|
1936
2444
|
}
|
|
1937
2445
|
return `${sections.join("\n")}\n`;
|
|
@@ -1943,7 +2451,7 @@ function buildFileMap(files, allCommands, ignores) {
|
|
|
1943
2451
|
const fileMap = {};
|
|
1944
2452
|
for (const [filePath, fileConfigRaw] of Object.entries(files)) {
|
|
1945
2453
|
const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
|
|
1946
|
-
for (const cmdPath of commandPaths) fileMap
|
|
2454
|
+
for (const cmdPath of commandPaths) setFileMapEntry(fileMap, cmdPath, filePath);
|
|
1947
2455
|
}
|
|
1948
2456
|
return fileMap;
|
|
1949
2457
|
}
|
|
@@ -1996,7 +2504,7 @@ function pathToFiles(pathConfig, allCommands) {
|
|
|
1996
2504
|
* Generate documentation from command definition
|
|
1997
2505
|
*/
|
|
1998
2506
|
async function generateDoc(config) {
|
|
1999
|
-
const { command, ignores = [], format = {}, formatter, examples: examplesConfig, targetCommands, globalArgs } = config;
|
|
2507
|
+
const { command, ignores = [], format = {}, formatter, examples: examplesConfig, targetCommands, globalArgs, customizable = false } = config;
|
|
2000
2508
|
const allCommands = await collectAllCommands(command);
|
|
2001
2509
|
let files;
|
|
2002
2510
|
let usingPathConfig = false;
|
|
@@ -2008,7 +2516,8 @@ async function generateDoc(config) {
|
|
|
2008
2516
|
resolvedRootDocPath = converted.rootDocPath;
|
|
2009
2517
|
usingPathConfig = true;
|
|
2010
2518
|
} else if (config.files !== void 0) files = config.files;
|
|
2011
|
-
else
|
|
2519
|
+
else if (config.templates !== void 0) files = {};
|
|
2520
|
+
else throw new Error("Either \"path\", \"files\", or \"templates\" must be specified.");
|
|
2012
2521
|
let rootDoc = config.rootDoc;
|
|
2013
2522
|
if (!rootDoc && usingPathConfig && (globalArgs || config.rootInfo)) rootDoc = { path: resolvedRootDocPath };
|
|
2014
2523
|
if (globalArgs && rootDoc && !rootDoc.globalOptions) {
|
|
@@ -2030,12 +2539,14 @@ async function generateDoc(config) {
|
|
|
2030
2539
|
}
|
|
2031
2540
|
if (examplesConfig) await executeConfiguredExamples(allCommands, examplesConfig, command);
|
|
2032
2541
|
const hasTargetCommands = targetCommands !== void 0 && targetCommands.length > 0;
|
|
2033
|
-
if (hasTargetCommands) {
|
|
2034
|
-
for (const targetCommand of targetCommands) if (!findFileForCommand(targetCommand, files, allCommands, ignores)) throw new Error(`Target command "${targetCommand}" not found in any file configuration`);
|
|
2035
|
-
}
|
|
2036
2542
|
const globalOptionDefinitions = collectGlobalOptionDefinitions(rootDoc);
|
|
2037
|
-
|
|
2038
|
-
if (globalOptionDefinitions.size > 0) for (const
|
|
2543
|
+
const templateGlobalOptionFields = /* @__PURE__ */ new Map();
|
|
2544
|
+
if (config.templates) if (globalOptionDefinitions.size > 0) for (const [name, field] of globalOptionDefinitions) templateGlobalOptionFields.set(name, field);
|
|
2545
|
+
else {
|
|
2546
|
+
const shape = deriveGlobalArgsShape(globalArgs);
|
|
2547
|
+
if (shape) for (const field of collectRenderableGlobalOptionFields(shape)) templateGlobalOptionFields.set(field.name, field);
|
|
2548
|
+
}
|
|
2549
|
+
const documentedCommandPaths = hasTargetCommands ? collectTargetDocumentedCommandPaths(targetCommands, files, allCommands, ignores) : collectDocumentedCommandPaths(files, allCommands, ignores);
|
|
2039
2550
|
const allFilesCommands = [];
|
|
2040
2551
|
for (const fileConfigRaw of Object.values(files)) {
|
|
2041
2552
|
const fileConfig = normalizeFileConfig(fileConfigRaw);
|
|
@@ -2044,6 +2555,159 @@ async function generateDoc(config) {
|
|
|
2044
2555
|
validateIgnoresExist(ignores, allCommands);
|
|
2045
2556
|
validateNoConflicts(allFilesCommands, ignores, allCommands);
|
|
2046
2557
|
const fileMap = buildFileMap(files, allCommands, ignores);
|
|
2558
|
+
const templateContents = /* @__PURE__ */ new Map();
|
|
2559
|
+
const templateExclusions = /* @__PURE__ */ new Map();
|
|
2560
|
+
if (config.templates) for (const [outputPath, templatePath] of Object.entries(config.templates)) {
|
|
2561
|
+
const templateContent = readFile(templatePath);
|
|
2562
|
+
templateContents.set(outputPath, templateContent);
|
|
2563
|
+
if (templateContent !== null) templateExclusions.set(outputPath, buildTemplateExclusions(collectExcludedTemplatePlaceholders(templateContent), allCommands));
|
|
2564
|
+
}
|
|
2565
|
+
const templateEntries = Object.entries(config.templates ?? {});
|
|
2566
|
+
const templateMeta = /* @__PURE__ */ new Map();
|
|
2567
|
+
const templateValidationErrors = /* @__PURE__ */ new Map();
|
|
2568
|
+
if (templateEntries.length > 0) {
|
|
2569
|
+
const normalizedRootDocPath = rootDoc ? normalizeDocPathForComparison(rootDoc.path) : null;
|
|
2570
|
+
const normalizedFileKeys = new Set(Object.keys(files).map(normalizeDocPathForComparison));
|
|
2571
|
+
const normalizedTemplateOutputs = /* @__PURE__ */ new Set();
|
|
2572
|
+
const allNormalizedTemplateOutputs = new Set(templateEntries.map(([outputPath]) => normalizeDocPathForComparison(outputPath)));
|
|
2573
|
+
for (const [outputPath, templatePath] of templateEntries) {
|
|
2574
|
+
const normalizedOutput = normalizeDocPathForComparison(outputPath);
|
|
2575
|
+
const normalizedSource = normalizeDocPathForComparison(templatePath);
|
|
2576
|
+
if (normalizedFileKeys.has(normalizedOutput)) throw new Error(`Template output path "${outputPath}" conflicts with an existing files key.`);
|
|
2577
|
+
if (normalizedRootDocPath && normalizedOutput === normalizedRootDocPath) throw new Error(`Template output path "${outputPath}" conflicts with rootDoc.path "${rootDoc.path}".`);
|
|
2578
|
+
if (normalizedTemplateOutputs.has(normalizedOutput)) throw new Error(`Duplicate template output path: "${outputPath}".`);
|
|
2579
|
+
normalizedTemplateOutputs.add(normalizedOutput);
|
|
2580
|
+
if (normalizedSource === normalizedOutput) throw new Error(`Template output path "${outputPath}" must not be the same as its source template path.`);
|
|
2581
|
+
if (normalizedFileKeys.has(normalizedSource)) throw new Error(`Template source path "${templatePath}" conflicts with a files output key.`);
|
|
2582
|
+
if (normalizedRootDocPath && normalizedSource === normalizedRootDocPath) throw new Error(`Template source path "${templatePath}" conflicts with rootDoc.path "${rootDoc.path}".`);
|
|
2583
|
+
if (allNormalizedTemplateOutputs.has(normalizedSource)) throw new Error(`Template source path "${templatePath}" conflicts with a template output path.`);
|
|
2584
|
+
}
|
|
2585
|
+
const availableCommandPaths = Array.from(allCommands.keys()).join(", ");
|
|
2586
|
+
for (const [outputPath, templatePath] of templateEntries) {
|
|
2587
|
+
const templateContent = templateContents.get(outputPath) ?? null;
|
|
2588
|
+
const validationErrors = [];
|
|
2589
|
+
if (templateContent === null) {
|
|
2590
|
+
templateMeta.set(outputPath, {
|
|
2591
|
+
referencedScopes: [],
|
|
2592
|
+
headingScopes: [],
|
|
2593
|
+
commandTreeRoots: [],
|
|
2594
|
+
emitsGlobalOptions: false,
|
|
2595
|
+
emitsIndex: false
|
|
2596
|
+
});
|
|
2597
|
+
templateValidationErrors.set(outputPath, validationErrors);
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
try {
|
|
2601
|
+
validateTemplatePlaceholderSyntax(templateContent, templatePath);
|
|
2602
|
+
} catch (error) {
|
|
2603
|
+
validationErrors.push(error instanceof Error ? error.message : String(error));
|
|
2604
|
+
}
|
|
2605
|
+
const placeholders = Array.from(new Set(templateContent.match(TEMPLATE_PLACEHOLDER_REGEX) ?? []));
|
|
2606
|
+
const scopes = /* @__PURE__ */ new Set();
|
|
2607
|
+
const headingScopes = /* @__PURE__ */ new Set();
|
|
2608
|
+
const commandTreeRoots = /* @__PURE__ */ new Set();
|
|
2609
|
+
let emitsGlobalOptions = false;
|
|
2610
|
+
let emitsIndex = false;
|
|
2611
|
+
const exclusions = templateExclusions.get(outputPath) ?? createTemplateExclusions(/* @__PURE__ */ new Set());
|
|
2612
|
+
const indexMetadata = collectTemplateIndexMetadata(templateContent);
|
|
2613
|
+
for (const placeholder of placeholders) {
|
|
2614
|
+
const placeholderKey = templatePlaceholderKey(placeholder);
|
|
2615
|
+
if (exclusions.rawKeys.has(placeholderKey) || isRawCommandPlaceholderUnderExcludedScope(placeholderKey, exclusions)) continue;
|
|
2616
|
+
const parsed = parsePlaceholder(placeholder, allCommands);
|
|
2617
|
+
if (shouldSkipTemplatePlaceholder(placeholder, parsed, exclusions)) continue;
|
|
2618
|
+
if (parsed.kind === "invalid") {
|
|
2619
|
+
validationErrors.push(`${parsed.reason} (in template "${templatePath}")`);
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2622
|
+
if (parsed.kind === "command") {
|
|
2623
|
+
const { scope, type } = parsed;
|
|
2624
|
+
if (!allCommands.has(scope)) {
|
|
2625
|
+
const sectionTypeError = getUnknownSectionTypeError(scope, allCommands);
|
|
2626
|
+
if (sectionTypeError) {
|
|
2627
|
+
validationErrors.push(`${sectionTypeError} (in template "${templatePath}")`);
|
|
2628
|
+
continue;
|
|
2629
|
+
}
|
|
2630
|
+
validationErrors.push(`Unknown command scope "${scope}" in template "${templatePath}". Available: ${availableCommandPaths}`);
|
|
2631
|
+
continue;
|
|
2632
|
+
}
|
|
2633
|
+
if (ignores.some((pattern) => matchesIgnorePattern(scope, pattern))) {
|
|
2634
|
+
validationErrors.push(`Command scope "${scope}" in template "${templatePath}" conflicts with ignores configuration.`);
|
|
2635
|
+
continue;
|
|
2636
|
+
}
|
|
2637
|
+
if (type === void 0) {
|
|
2638
|
+
const commandTreePaths = getTemplateCommandTreePaths(scope, allCommands, ignores, exclusions);
|
|
2639
|
+
if (!isCommandSectionExcluded(scope, "heading", exclusions)) commandTreeRoots.add(scope);
|
|
2640
|
+
for (const commandTreePath of commandTreePaths) {
|
|
2641
|
+
scopes.add(commandTreePath);
|
|
2642
|
+
if (!isCommandSectionExcluded(commandTreePath, "heading", exclusions)) headingScopes.add(commandTreePath);
|
|
2643
|
+
}
|
|
2644
|
+
} else {
|
|
2645
|
+
scopes.add(scope);
|
|
2646
|
+
if (type === "heading" && !isCommandSectionExcluded(scope, "heading", exclusions)) {
|
|
2647
|
+
headingScopes.add(scope);
|
|
2648
|
+
commandTreeRoots.add(scope);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
} else if (parsed.kind === "global-options") emitsGlobalOptions = true;
|
|
2652
|
+
else if (parsed.kind === "index") emitsIndex = true;
|
|
2653
|
+
}
|
|
2654
|
+
if (emitsGlobalOptions) {
|
|
2655
|
+
if (!(!!rootDoc?.globalOptions || deriveGlobalArgsShape(globalArgs) !== void 0)) validationErrors.push(`Template "${templatePath}" uses {{politty:global-options}} but no global options are configured (neither rootDoc.globalOptions nor globalArgs with non-positional options).`);
|
|
2656
|
+
}
|
|
2657
|
+
templateMeta.set(outputPath, {
|
|
2658
|
+
referencedScopes: Array.from(scopes),
|
|
2659
|
+
headingScopes: Array.from(headingScopes),
|
|
2660
|
+
commandTreeRoots: Array.from(commandTreeRoots),
|
|
2661
|
+
emitsGlobalOptions,
|
|
2662
|
+
emitsIndex,
|
|
2663
|
+
...indexMetadata.title !== void 0 ? { indexTitle: indexMetadata.title } : {},
|
|
2664
|
+
...indexMetadata.description !== void 0 ? { indexDescription: indexMetadata.description } : {}
|
|
2665
|
+
});
|
|
2666
|
+
templateValidationErrors.set(outputPath, validationErrors);
|
|
2667
|
+
}
|
|
2668
|
+
for (const meta of templateMeta.values()) {
|
|
2669
|
+
if (hasTargetCommands && !templateMetaShouldProcessForTarget(meta, targetCommands)) continue;
|
|
2670
|
+
for (const scope of meta.referencedScopes) documentedCommandPaths.add(scope);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
if (hasTargetCommands) for (const targetCommand of targetCommands) {
|
|
2674
|
+
const targetFilePath = findFileForCommand(targetCommand, files, allCommands, ignores);
|
|
2675
|
+
const targetTemplatePath = Array.from(templateMeta.values()).some((meta) => templateMetaReferencesCommandTarget(meta, [targetCommand]));
|
|
2676
|
+
if (!targetFilePath && !targetTemplatePath) throw new Error(`Target command "${targetCommand}" not found in any file or template configuration`);
|
|
2677
|
+
}
|
|
2678
|
+
const activeTemplateMeta = hasTargetCommands && config.templates ? new Map(Array.from(templateMeta.entries()).filter(([, meta]) => templateMetaShouldProcessForTarget(meta, targetCommands))) : templateMeta;
|
|
2679
|
+
for (const [outputPath, validationErrors] of templateValidationErrors.entries()) if (validationErrors.length > 0 && activeTemplateMeta.has(outputPath)) throw new Error(validationErrors.join("\n"));
|
|
2680
|
+
const templateGlobalOptionsProviderPaths = Array.from(templateMeta.entries()).filter(([, meta]) => meta.emitsGlobalOptions).map(([outputPath]) => outputPath);
|
|
2681
|
+
const templateGlobalOptionsProviderPath = templateGlobalOptionsProviderPaths.length === 1 ? templateGlobalOptionsProviderPaths[0] : void 0;
|
|
2682
|
+
validateGlobalOptionCompatibility(documentedCommandPaths, allCommands, globalOptionDefinitions);
|
|
2683
|
+
if (globalOptionDefinitions.size === 0 && templateGlobalOptionFields.size > 0) {
|
|
2684
|
+
const emittingTemplateScopes = /* @__PURE__ */ new Set();
|
|
2685
|
+
for (const meta of activeTemplateMeta.values()) {
|
|
2686
|
+
if (!meta.emitsGlobalOptions && templateGlobalOptionsProviderPath === void 0) continue;
|
|
2687
|
+
for (const scope of meta.referencedScopes) emittingTemplateScopes.add(scope);
|
|
2688
|
+
}
|
|
2689
|
+
validateGlobalOptionCompatibility(emittingTemplateScopes, allCommands, templateGlobalOptionFields);
|
|
2690
|
+
}
|
|
2691
|
+
if (globalOptionDefinitions.size > 0) for (const info of allCommands.values()) {
|
|
2692
|
+
info.options = info.options.filter((opt) => !globalOptionDefinitions.has(opt.name));
|
|
2693
|
+
if (info.extracted) info.extracted = filterExtractedFields(info.extracted, new Set(globalOptionDefinitions.keys()));
|
|
2694
|
+
}
|
|
2695
|
+
const templateFileMap = {};
|
|
2696
|
+
for (const [scope, outputPath] of Object.entries(fileMap)) setFileMapEntry(templateFileMap, scope, outputPath);
|
|
2697
|
+
const scopeRootLength = (root) => root === "" ? 0 : root.split(" ").length;
|
|
2698
|
+
const templateOwners = /* @__PURE__ */ new Map();
|
|
2699
|
+
for (const [templateOutputPath, meta] of templateMeta.entries()) for (const scope of meta.headingScopes) {
|
|
2700
|
+
if (Object.prototype.hasOwnProperty.call(fileMap, scope)) continue;
|
|
2701
|
+
let bestRootLen = -1;
|
|
2702
|
+
for (const root of meta.commandTreeRoots) if (isSubcommandOf(scope, root)) bestRootLen = Math.max(bestRootLen, scopeRootLength(root));
|
|
2703
|
+
if (bestRootLen < 0) continue;
|
|
2704
|
+
const existing = templateOwners.get(scope);
|
|
2705
|
+
if (!existing || bestRootLen > existing.rootLen) templateOwners.set(scope, {
|
|
2706
|
+
outputPath: templateOutputPath,
|
|
2707
|
+
rootLen: bestRootLen
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
for (const [scope, { outputPath }] of templateOwners) setFileMapEntry(templateFileMap, scope, outputPath);
|
|
2047
2711
|
const results = [];
|
|
2048
2712
|
let hasError = false;
|
|
2049
2713
|
for (const [filePath, fileConfigRaw] of Object.entries(files)) {
|
|
@@ -2056,18 +2720,20 @@ async function generateDoc(config) {
|
|
|
2056
2720
|
const diffs = [];
|
|
2057
2721
|
const minDepth = Math.min(...commandPaths.map((p) => allCommands.get(p)?.depth ?? 1));
|
|
2058
2722
|
const adjustedHeadingLevel = Math.max(1, (format?.headingLevel ?? 1) - (minDepth - 1));
|
|
2723
|
+
const isRootDocFile = usingPathConfig && rootDoc && normalizeDocPathForComparison(filePath) === normalizeDocPathForComparison(rootDoc.path);
|
|
2724
|
+
const fileUsesMarkers = usingPathConfig || customizable;
|
|
2059
2725
|
const fileRenderer = createCommandRenderer({
|
|
2060
2726
|
...format,
|
|
2061
|
-
headingLevel: adjustedHeadingLevel
|
|
2727
|
+
headingLevel: adjustedHeadingLevel,
|
|
2728
|
+
markerless: !fileUsesMarkers
|
|
2062
2729
|
});
|
|
2063
2730
|
const render = fileConfig.render ?? fileRenderer;
|
|
2064
|
-
|
|
2065
|
-
if (hasTargetCommands || isRootDocFile) {
|
|
2731
|
+
if (Boolean(isRootDocFile) || hasTargetCommands && fileUsesMarkers) {
|
|
2066
2732
|
let existingContent = readFile(filePath);
|
|
2067
2733
|
const sortedCommandPaths = sortDepthFirst(commandPaths, specifiedCommands);
|
|
2068
2734
|
const effectiveTargetCommands = hasTargetCommands ? fileTargetCommands : commandPaths;
|
|
2069
2735
|
for (const targetCommand of effectiveTargetCommands) {
|
|
2070
|
-
const rawSection = generateCommandSection(targetCommand, allCommands, render, filePath,
|
|
2736
|
+
const rawSection = generateCommandSection(targetCommand, allCommands, render, filePath, templateFileMap, rootDoc?.path, globalOptionDefinitions.size > 0, ignores);
|
|
2071
2737
|
if (!rawSection) throw new Error(`Target command "${targetCommand}" not found in commands`);
|
|
2072
2738
|
const generatedSection = await applyFormatter(rawSection, formatter);
|
|
2073
2739
|
if (!existingContent) {
|
|
@@ -2129,23 +2795,23 @@ async function generateDoc(config) {
|
|
|
2129
2795
|
diffs.push(formatDiff(existingSection, generatedSectionPart));
|
|
2130
2796
|
}
|
|
2131
2797
|
}
|
|
2132
|
-
if (doctorMode) {
|
|
2798
|
+
if (doctorMode || customizable) {
|
|
2133
2799
|
const generatedMarkers = collectSectionMarkers(generatedSection, targetCommand);
|
|
2134
2800
|
const existingMarkerSet = new Set(existingMarkers);
|
|
2135
2801
|
for (const sectionType of generatedMarkers) {
|
|
2136
2802
|
if (existingMarkerSet.has(sectionType)) continue;
|
|
2137
2803
|
const generatedSectionPart = extractSectionMarker(generatedSection, sectionType, targetCommand);
|
|
2138
2804
|
if (!generatedSectionPart) continue;
|
|
2139
|
-
if (updateMode) {
|
|
2805
|
+
if (doctorMode && updateMode) {
|
|
2140
2806
|
existingContent = insertSectionMarkerAtOrder(existingContent, sectionType, targetCommand, generatedSectionPart);
|
|
2141
2807
|
writeFile(filePath, existingContent);
|
|
2142
2808
|
if (fileStatus !== "created") fileStatus = "updated";
|
|
2143
|
-
} else {
|
|
2809
|
+
} else if (doctorMode) {
|
|
2144
2810
|
hasError = true;
|
|
2145
2811
|
hasDoctorIssues = true;
|
|
2146
2812
|
fileStatus = "diff";
|
|
2147
2813
|
diffs.push(`[doctor] Missing section marker "${sectionType}" for command "${formatCommandPath(targetCommand)}". Run with ${DOCTOR_ENV}=true ${UPDATE_GOLDEN_ENV}=true to insert.\n${generatedSectionPart}`);
|
|
2148
|
-
}
|
|
2814
|
+
} else console.warn(`[politty] Missing "${sectionType}" section for command "${formatCommandPath(targetCommand)}" in ${filePath}. Run with ${DOCTOR_ENV}=true ${UPDATE_GOLDEN_ENV}=true to insert it, or leave it removed to opt that section out.`);
|
|
2149
2815
|
}
|
|
2150
2816
|
}
|
|
2151
2817
|
}
|
|
@@ -2169,7 +2835,7 @@ async function generateDoc(config) {
|
|
|
2169
2835
|
}
|
|
2170
2836
|
}
|
|
2171
2837
|
} else {
|
|
2172
|
-
const generatedMarkdown = await applyFormatter(generateFileMarkdown(commandPaths, allCommands, render, filePath,
|
|
2838
|
+
const generatedMarkdown = await applyFormatter(generateFileMarkdown(commandPaths, allCommands, render, filePath, templateFileMap, specifiedCommands, fileConfig, rootDoc?.path, globalOptionDefinitions.size > 0, ignores), formatter);
|
|
2173
2839
|
const comparison = compareWithExisting(generatedMarkdown, filePath);
|
|
2174
2840
|
if (comparison.match) {} else if (updateMode) {
|
|
2175
2841
|
writeFile(filePath, generatedMarkdown);
|
|
@@ -2187,6 +2853,112 @@ async function generateDoc(config) {
|
|
|
2187
2853
|
diff: diffs.length > 0 ? diffs.join("\n\n") : void 0
|
|
2188
2854
|
});
|
|
2189
2855
|
}
|
|
2856
|
+
let normalizedTemplateGlobalOptions;
|
|
2857
|
+
if (rootDoc?.globalOptions) normalizedTemplateGlobalOptions = normalizeGlobalOptions(rootDoc.globalOptions);
|
|
2858
|
+
else {
|
|
2859
|
+
const shape = deriveGlobalArgsShape(globalArgs);
|
|
2860
|
+
if (shape) normalizedTemplateGlobalOptions = { args: shape };
|
|
2861
|
+
}
|
|
2862
|
+
for (const [outputPath, templatePath] of templateEntries) {
|
|
2863
|
+
if (!activeTemplateMeta.has(outputPath)) continue;
|
|
2864
|
+
const templateContent = templateContents.get(outputPath) ?? null;
|
|
2865
|
+
if (templateContent === null) {
|
|
2866
|
+
hasError = true;
|
|
2867
|
+
results.push({
|
|
2868
|
+
path: outputPath,
|
|
2869
|
+
status: "diff",
|
|
2870
|
+
diff: `Template file not found: ${templatePath}`
|
|
2871
|
+
});
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
const meta = templateMeta.get(outputPath);
|
|
2875
|
+
const templateLineEnding = detectLineEnding(templateContent);
|
|
2876
|
+
const outputTemplateContent = stripPolittyFrontMatterForOutput(templateContent);
|
|
2877
|
+
const headingDepths = (meta?.headingScopes ?? []).map((s) => allCommands.get(s)?.depth ?? 1);
|
|
2878
|
+
const minDepth = headingDepths.length > 0 ? Math.min(...headingDepths) : 1;
|
|
2879
|
+
const adjustedHeadingLevel = clampHeadingLevel((format?.headingLevel ?? 1) - (minDepth - 1));
|
|
2880
|
+
const templateRenderer = createCommandRenderer({
|
|
2881
|
+
...format,
|
|
2882
|
+
headingLevel: adjustedHeadingLevel
|
|
2883
|
+
});
|
|
2884
|
+
const outputEmitsGlobalOptions = meta?.emitsGlobalOptions ?? false;
|
|
2885
|
+
const excludeOptionNames = (rootDoc !== void 0 && globalOptionDefinitions.size > 0 || outputEmitsGlobalOptions || templateGlobalOptionsProviderPath !== void 0) && templateGlobalOptionFields.size > 0 ? new Set(templateGlobalOptionFields.keys()) : void 0;
|
|
2886
|
+
const sectionHasGlobalOptions = excludeOptionNames !== void 0;
|
|
2887
|
+
const effectiveRootDocPath = outputEmitsGlobalOptions ? outputPath : rootDoc?.path ?? templateGlobalOptionsProviderPath;
|
|
2888
|
+
const placeholders = Array.from(new Set(outputTemplateContent.match(TEMPLATE_PLACEHOLDER_REGEX) ?? []));
|
|
2889
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
2890
|
+
const exclusions = templateExclusions.get(outputPath) ?? createTemplateExclusions(/* @__PURE__ */ new Set());
|
|
2891
|
+
for (const placeholder of placeholders) {
|
|
2892
|
+
const placeholderKey = templatePlaceholderKey(placeholder);
|
|
2893
|
+
if (exclusions.rawKeys.has(placeholderKey) || isRawCommandPlaceholderUnderExcludedScope(placeholderKey, exclusions)) {
|
|
2894
|
+
replacements.set(placeholder, "");
|
|
2895
|
+
continue;
|
|
2896
|
+
}
|
|
2897
|
+
const parsed = parsePlaceholder(placeholder, allCommands);
|
|
2898
|
+
if (shouldSkipTemplatePlaceholder(placeholder, parsed, exclusions)) {
|
|
2899
|
+
replacements.set(placeholder, "");
|
|
2900
|
+
continue;
|
|
2901
|
+
}
|
|
2902
|
+
if (parsed.kind === "invalid") throw new Error(`Internal error: unresolved placeholder "${placeholder}" in template "${templatePath}": ${parsed.reason}`);
|
|
2903
|
+
if (parsed.kind === "command") {
|
|
2904
|
+
const { scope, type } = parsed;
|
|
2905
|
+
if (type === void 0) {
|
|
2906
|
+
const rawSection = generateCommandTreeMarkdown(scope, allCommands, templateRenderer, ignores, outputPath, templateFileMap, effectiveRootDocPath, sectionHasGlobalOptions, excludeOptionNames, exclusions);
|
|
2907
|
+
if (rawSection === null) {
|
|
2908
|
+
replacements.set(placeholder, "");
|
|
2909
|
+
continue;
|
|
2910
|
+
}
|
|
2911
|
+
replacements.set(placeholder, stripPolittyMarkers(rawSection));
|
|
2912
|
+
} else {
|
|
2913
|
+
const rawSection = generateCommandSection(scope, allCommands, templateRenderer, outputPath, templateFileMap, effectiveRootDocPath, sectionHasGlobalOptions, ignores, excludeOptionNames, exclusions);
|
|
2914
|
+
if (rawSection === null) {
|
|
2915
|
+
replacements.set(placeholder, "");
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2918
|
+
const extracted = extractSectionMarker(rawSection, type, scope);
|
|
2919
|
+
replacements.set(placeholder, extracted === null ? "" : stripPolittyMarkers(extracted));
|
|
2920
|
+
}
|
|
2921
|
+
} else if (parsed.kind === "global-options") if (normalizedTemplateGlobalOptions) replacements.set(placeholder, buildGlobalOptionsContent(normalizedTemplateGlobalOptions));
|
|
2922
|
+
else replacements.set(placeholder, "");
|
|
2923
|
+
else if (parsed.kind === "index") {
|
|
2924
|
+
const indexContent = await renderCommandIndex(command, [...deriveIndexFromFiles(files, outputPath, allCommands, ignores), ...deriveIndexFromTemplateOutputs(templateMeta, outputPath, outputPath, allCommands)], rootDoc?.index);
|
|
2925
|
+
replacements.set(placeholder, indexContent);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
let generated = outputTemplateContent.replace(/((?:\r?\n)*)([ \t]*)(\{\{politty:[^{}]*\}\})([ \t]*)((?:\r?\n)*)/g, (match, leadNl, leadWs, placeholder, trailWs, trailNl, offset, fullString) => {
|
|
2929
|
+
const replacement = replacements.get(placeholder);
|
|
2930
|
+
if (replacement === void 0) throw new Error(`Internal error: unresolved placeholder "${placeholder}" in template "${templatePath}".`);
|
|
2931
|
+
const startsLine = leadNl !== "" || offset === 0 || fullString[offset - 1] === "\n";
|
|
2932
|
+
const endsLine = trailNl !== "" || offset + match.length === fullString.length;
|
|
2933
|
+
if (replacement === "" && startsLine && endsLine) {
|
|
2934
|
+
if (leadNl === "" || trailNl === "") return "";
|
|
2935
|
+
const leadBreaks = countLineBreaks(leadNl);
|
|
2936
|
+
const trailBreaks = countLineBreaks(trailNl);
|
|
2937
|
+
const widest = Math.max(leadBreaks, trailBreaks);
|
|
2938
|
+
const lineEnding = leadBreaks >= trailBreaks ? detectLineEnding(leadNl) : detectLineEnding(trailNl);
|
|
2939
|
+
return widest >= 2 ? lineEnding + lineEnding : widest === 1 ? lineEnding : "";
|
|
2940
|
+
}
|
|
2941
|
+
return `${leadNl}${leadWs}${replacement}${trailWs}${trailNl}`;
|
|
2942
|
+
});
|
|
2943
|
+
generated = `${generated.trimEnd()}${templateLineEnding}`;
|
|
2944
|
+
generated = await applyFormatter(generated, formatter);
|
|
2945
|
+
const comparison = compareWithExisting(generated, outputPath);
|
|
2946
|
+
let templateStatus = "match";
|
|
2947
|
+
let templateDiff;
|
|
2948
|
+
if (comparison.match) {} else if (updateMode) {
|
|
2949
|
+
writeFile(outputPath, generated);
|
|
2950
|
+
templateStatus = comparison.fileExists ? "updated" : "created";
|
|
2951
|
+
} else {
|
|
2952
|
+
hasError = true;
|
|
2953
|
+
templateStatus = "diff";
|
|
2954
|
+
if (comparison.diff) templateDiff = comparison.diff;
|
|
2955
|
+
}
|
|
2956
|
+
results.push({
|
|
2957
|
+
path: outputPath,
|
|
2958
|
+
status: templateStatus,
|
|
2959
|
+
diff: templateDiff
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2190
2962
|
if (rootDoc) {
|
|
2191
2963
|
const rootDocFilePath = rootDoc.path;
|
|
2192
2964
|
let rootDocStatus = "match";
|
|
@@ -2292,7 +3064,19 @@ async function assertDocMatch(config) {
|
|
|
2292
3064
|
function initDocFile(config, fileSystem) {
|
|
2293
3065
|
if (!isTruthyEnv("POLITTY_DOCS_UPDATE")) return;
|
|
2294
3066
|
if (typeof config === "string") deleteFile(config, fileSystem);
|
|
2295
|
-
else
|
|
3067
|
+
else {
|
|
3068
|
+
const protectedPaths = new Set(Object.values(config.templates ?? {}).map(normalizeDocPathForComparison));
|
|
3069
|
+
if (config.rootDoc) protectedPaths.add(normalizeDocPathForComparison(config.rootDoc.path));
|
|
3070
|
+
const isProtectedPath = (p) => protectedPaths.has(normalizeDocPathForComparison(p));
|
|
3071
|
+
if (config.files) for (const filePath of Object.keys(config.files)) {
|
|
3072
|
+
if (isProtectedPath(filePath)) continue;
|
|
3073
|
+
deleteFile(filePath, fileSystem);
|
|
3074
|
+
}
|
|
3075
|
+
if (config.templates) for (const outputPath of Object.keys(config.templates)) {
|
|
3076
|
+
if (isProtectedPath(outputPath)) continue;
|
|
3077
|
+
deleteFile(outputPath, fileSystem);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
2296
3080
|
}
|
|
2297
3081
|
|
|
2298
3082
|
//#endregion
|