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