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}
\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, "]"); } 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); } }