281 lines
9.2 KiB
TypeScript
281 lines
9.2 KiB
TypeScript
import type { MDOutput } from "./common";
|
|
|
|
export interface CommandArg {
|
|
arg: {
|
|
name: string;
|
|
values: string[];
|
|
description: string;
|
|
};
|
|
}
|
|
|
|
export interface CommandGroup {
|
|
preamble: string; // the base command keyword
|
|
mainFunc: string; // the function that handles this command
|
|
subFuncs: string[]; // the subfunction that handles this command
|
|
syn: string[]; // different valid syntax patterns for the command
|
|
desc: string; // command description
|
|
args: CommandArg[]; // detailed argument descriptions for each syntax
|
|
examples: string[]; // usage examples for the command
|
|
tags: string[]; // command category tag(s)
|
|
}
|
|
|
|
const githubFunctionUrl = "https://git.jabber.space/devs/profanity/src/branch/master/src/command/cmd_defs.c";
|
|
|
|
export class CommandParser {
|
|
private buffer: string;
|
|
private readonly preamblePattern: RegExp =
|
|
/CMD_PREAMBLE\(\s*"([^"]+)"\s*,\s*([\w_]+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([^)]+)\)/ms;
|
|
private readonly mainFuncPattern: RegExp = /CMD_MAINFUNC\((\w+)\)/;
|
|
private readonly subFuncPattern: RegExp = /CMD_SUBFUNC\((\w+)\)/;
|
|
private readonly tagsPattern: RegExp = /CMD_TAGS\(([^)]+)\)/;
|
|
private readonly synPattern: RegExp = /CMD_SYN\(\s*([\s\S]*?[^\\]\")\s*\)/m;
|
|
private readonly descPattern: RegExp = /CMD_DESC\(([\s\S]*?[^\\]\")\s*\)/m;
|
|
private readonly argsPattern: RegExp = /CMD_ARGS\(([\s\S]*?)\)/m;
|
|
private readonly examplesPattern: RegExp = /CMD_EXAMPLES\(([\s\S]*?)\)/m;
|
|
private commands: CommandGroup[] = [];
|
|
|
|
constructor(buffer: string) {
|
|
this.buffer = buffer;
|
|
this.parse();
|
|
}
|
|
|
|
private parse() {
|
|
const buffer = this.buffer;
|
|
const commandDefsMatch = buffer.match(/command_defs\[\]\s*=\s*\{([\s\S]*)\};/);
|
|
|
|
if (!commandDefsMatch) {
|
|
console.error("Could not find command_defs[]");
|
|
process.exit(1);
|
|
}
|
|
|
|
const commandDefsContent = commandDefsMatch[1];
|
|
const commandBlocks = this.extractBlocks(commandDefsContent);
|
|
for (const block of commandBlocks) {
|
|
const group = this.parseBlock(block);
|
|
this.commands.push(group);
|
|
}
|
|
}
|
|
|
|
private extractBlocks(content: string) {
|
|
const blocks: string[] = [];
|
|
let pos = 0;
|
|
const len = content.length;
|
|
while (pos < len) {
|
|
const start = content.indexOf("{", pos);
|
|
if (start === -1) break;
|
|
let braceCount = 1;
|
|
let end = start + 1;
|
|
while (end < len && braceCount > 0) {
|
|
if (content[end] === "{") {
|
|
braceCount++;
|
|
} else if (content[end] === "}") {
|
|
braceCount--;
|
|
}
|
|
end++;
|
|
}
|
|
if (braceCount === 0) {
|
|
const block = content.slice(start, end);
|
|
blocks.push(block.trim());
|
|
pos = end;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return blocks;
|
|
}
|
|
|
|
private extractMultilineStrings(input: string): string[] {
|
|
const regex = /"((?:[^"\\]|\\.)*)"/g;
|
|
const matches = [];
|
|
let match;
|
|
while ((match = regex.exec(input)) !== null) {
|
|
matches.push(match[1].replace(/\\"/g, '"'));
|
|
}
|
|
return matches;
|
|
}
|
|
|
|
private parseArgNameAndValues(argStr: string): {
|
|
name: string;
|
|
values: string[];
|
|
} {
|
|
return {
|
|
name: argStr,
|
|
values: []
|
|
};
|
|
}
|
|
|
|
private extractArgs = (input: string): CommandArg[] => {
|
|
const matches = [...input.matchAll(/\{\s*"([^"]+?)",\s*"([^"]+?)"\s*\}/gs)];
|
|
return matches.map((m) => {
|
|
const rawArg = m[1];
|
|
const description = m[2];
|
|
const { name, values } = this.parseArgNameAndValues(rawArg);
|
|
return {
|
|
arg: {
|
|
name,
|
|
values,
|
|
description
|
|
}
|
|
};
|
|
});
|
|
};
|
|
private parseBlock(block: string): CommandGroup {
|
|
const preambleMatch = block.match(this.preamblePattern);
|
|
const mainFuncMatch = block.match(this.mainFuncPattern);
|
|
const subFuncMatch = block.match(this.subFuncPattern);
|
|
const tagsMatch = block.match(this.tagsPattern);
|
|
const synMatch = block.match(this.synPattern);
|
|
const descMatch = block.match(this.descPattern);
|
|
const argsMatch = block.match(this.argsPattern);
|
|
const examplesMatch = block.match(this.examplesPattern);
|
|
|
|
return {
|
|
preamble: preambleMatch?.[1] || "",
|
|
mainFunc: mainFuncMatch?.[1] || "",
|
|
subFuncs: subFuncMatch?.[1].split(",") || [],
|
|
syn: synMatch ? this.extractMultilineStrings(synMatch[1]) : [],
|
|
|
|
desc: descMatch ? this.extractMultilineStrings(descMatch[1]).join(" ") : "",
|
|
args: argsMatch ? this.extractArgs(argsMatch[1]) : [],
|
|
examples: examplesMatch ? this.extractMultilineStrings(examplesMatch[1]) : [],
|
|
tags: tagsMatch ? [...tagsMatch[1].matchAll(/CMD_TAG_([A-Z_]+)/g)].map((m) => m[1]?.toLowerCase()) : []
|
|
};
|
|
}
|
|
|
|
private capitalizeFirstLetter(str: string) {
|
|
if (!str) return "";
|
|
if (str.charAt(0) === "/") {
|
|
if (str.length > 1) {
|
|
return str.charAt(0) + str.charAt(1).toUpperCase() + str.slice(2);
|
|
} else {
|
|
return str;
|
|
}
|
|
} else {
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
}
|
|
}
|
|
|
|
toMD() {
|
|
let stream = "";
|
|
const output: MDOutput = { filename: "command-reference.md", buffer: "" };
|
|
stream += this.writeFrontMatter("Command Reference", "Command Reference");
|
|
|
|
// Sort commands alphabetically by command name
|
|
this.commands = this.commands.sort((a, b) => { if (a.preamble < b.preamble) return -1; if (a.preamble > b.preamble) return 1; return 0; });
|
|
|
|
for (const group of this.commands) {
|
|
if (group.preamble) {
|
|
stream += this.writeCommandHeader(group.preamble);
|
|
} else {
|
|
console.error("No preamble for group", group);
|
|
process.exit(1);
|
|
}
|
|
if (group.desc) {
|
|
// No need for title, it's obvious that it's a description (right under the command's title)
|
|
// stream += this.writeSection("📄 Description", true);
|
|
stream += group.desc;
|
|
stream += "\n\n";
|
|
}
|
|
if (group.syn) {
|
|
stream += this.writeSynopsisBlock(group.syn);
|
|
}
|
|
if (group.args.length > 0) {
|
|
stream += this.writeArgsBlock(group.args);
|
|
}
|
|
if (group.examples.length > 0) {
|
|
stream += this.writeExamplesBlock(group.examples);
|
|
}
|
|
stream += "\n\n";
|
|
}
|
|
|
|
output.buffer = stream;
|
|
return output;
|
|
}
|
|
|
|
private beginBlock() {
|
|
return `---\n`;
|
|
}
|
|
|
|
private endBlock() {
|
|
return `---\n\n`;
|
|
}
|
|
|
|
private writeSection(sectionName: string, isChild = false) {
|
|
return `${isChild ? "####" : "##"} ${!isChild ? "`" : ""}${sectionName}${!isChild ? "`" : ""}\n\n`;
|
|
}
|
|
|
|
private writeGenericBlock(blockName: string, content: string) {
|
|
return `**${blockName}** ${content}<br/>\n`;
|
|
}
|
|
|
|
private writeSynopsisBlock(syntaxList: string[]) {
|
|
let stream = "";
|
|
// No need for Synopsis section, it's self-explanatory
|
|
// stream += this.writeSection("🔧 Synopsis", true);
|
|
stream += "```bash\n";
|
|
stream += syntaxList.join("\n");
|
|
stream += "\n";
|
|
stream += "```\n\n";
|
|
return stream;
|
|
}
|
|
|
|
private writeExamplesBlock(exampleList: string[]) {
|
|
let stream = "";
|
|
stream += this.writeSection("💡 Examples", true);
|
|
stream += "```bash\n";
|
|
stream += exampleList.join("\n");
|
|
stream += "\n";
|
|
stream += "```";
|
|
stream += "\n\n";
|
|
return stream;
|
|
}
|
|
|
|
private escapeMarkdown(text: string): string {
|
|
return text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/\|/g, "|")
|
|
.replace(/\[/g, "[")
|
|
.replace(/\]/g, "]");
|
|
}
|
|
|
|
private generateMarkdownTable(headers: string[], rows: string[][], escapeMarkdownForRows=true): string {
|
|
const escapedRows = rows.map((row) => row.map((cell) => escapeMarkdownForRows ? this.escapeMarkdown(cell) : cell));
|
|
const escapedHeaders = headers.map((header) => this.escapeMarkdown(header));
|
|
const colWidths = headers.map((_, i) =>
|
|
Math.max(escapedHeaders[i].length, ...escapedRows.map((row) => (row[i] || "").length))
|
|
);
|
|
const pad = (text: string, width: number) => text + " ".repeat(width - text.length);
|
|
|
|
const lines = [];
|
|
lines.push("| " + escapedHeaders.map((h, i) => pad(h, colWidths[i])).join(" | ") + " |");
|
|
lines.push("| " + colWidths.map((w) => "-".repeat(w)).join(" | ") + " |");
|
|
|
|
for (const row of escapedRows) {
|
|
lines.push("| " + row.map((cell, i) => pad(cell, colWidths[i])).join(" | ") + " |");
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
private writeArgsBlock(args: CommandArg[]) {
|
|
let stream = "";
|
|
stream += this.writeSection("📝 Arguments", true);
|
|
const headers = ["Argument", "Description"];
|
|
const rows = args.map((arg) => [`\`${arg.arg.name}\``, this.escapeMarkdown(arg.arg.description)]);
|
|
stream += this.generateMarkdownTable(headers, rows, false);
|
|
stream += "\n\n";
|
|
return stream;
|
|
}
|
|
|
|
private writeFrontMatter(title: string, description: string) {
|
|
return `${this.beginBlock()}title: ${title}\ndescription: ${description || title
|
|
}\ntableOfContents:\n minHeadingLevel: 1\n maxHeadingLevel: 2\n${this.endBlock()}`;
|
|
}
|
|
|
|
private writeCommandHeader(title: string) {
|
|
return this.writeSection(title);
|
|
}
|
|
}
|