Initial commit
This commit is contained in:
280
src/scripts/parser/command-parser.ts
Normal file
280
src/scripts/parser/command-parser.ts
Normal 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, "&")
|
||||
.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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user