Initial commit

This commit is contained in:
2025-07-09 12:25:44 +02:00
commit c3aec55da4
109 changed files with 10797 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\|/g, "&#124;")
.replace(/\[/g, "&#91;")
.replace(/\]/g, "&#93;");
}
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);
}
}