Files
jabber.space/src/scripts/parser/apidoc/c-apidoc-parser.ts
2025-09-30 10:44:16 +02:00

421 lines
13 KiB
TypeScript

import { readFileSync, existsSync, writeFileSync } from "fs";
import { fileURLToPath } from "url";
import path, { dirname } from "path";
import { ensureArray } from "./common";
import { XMLParser } from "fast-xml-parser";
export interface FunctionParam {
type: string;
declname: string;
}
export interface DetailedParam {
paramName: string;
paramDescription: string;
}
export interface FunctionDetailedParamDescription {
description: string;
params: DetailedParam[];
returns: string;
}
export interface FunctionDoc {
name: string;
definition: string;
argsstring: string;
param: FunctionParam[];
briefDescription: string;
detailedDescription: FunctionDetailedParamDescription;
location: {
definition: {
file: string;
line: number;
column: number;
};
declaration: {
file: string;
line: number;
column: number;
};
};
}
export interface TypeDefDoc {
type: string;
definition: string;
argsstring: string;
name: string;
briefDescription: string;
detailedDescription: string;
location: {
file: string;
line: number;
column: number;
};
}
export interface DocFile {
name: string;
briefDescription: string;
detailedDescription: string;
functions: FunctionDoc[];
typedefs: TypeDefDoc[];
location: string;
}
export interface ApiDoc {
files: DocFile[];
}
export interface MDOutput {
filename: string;
buffer: string;
}
export class CApiDocParser {
private xmlParser: XMLParser;
private buffer: string;
private docPath: string;
private repoPath: string;
private fileCache: Record<string, any> = {};
private sourceFiles: Record<string, string> = {};
private apiDoc: ApiDoc;
private outputs: MDOutput[] = [];
constructor(docEntry: { buffer: string; docPath: string; repoPath: string }) {
this.buffer = docEntry.buffer;
this.docPath = docEntry.docPath;
this.repoPath = docEntry.repoPath;
this.xmlParser = new XMLParser({ ignoreAttributes: false });
this.apiDoc = { files: [] };
}
private preloadAllXmlFiles(fileList: string[]) {
for (const file of fileList) {
this.fileCache[file] = readFileSync(path.join(this.docPath, `${file}.xml`), "utf-8");
}
}
private getXmlFileNameFromRefId(refId: string): string {
return refId;
}
parse() {
const buffer = this.buffer;
const parser = this.xmlParser;
const obj = parser.parse(buffer);
const compounds = obj.doxygenindex.compound;
const importPaths = (compounds as any[])
.filter((c) => c["@_kind"] === "file")
.map((c) => c["@_refid"])
.filter((refid): refid is string => typeof refid === "string");
this.preloadAllXmlFiles(importPaths);
for (const compound of compounds) {
const compoundType = compound["@_kind"];
if (compoundType === "page") continue;
else if (compoundType === "file") {
const file = this.parseDocFile(compound);
this.apiDoc.files.push(file);
}
}
}
// Parses a Doxygen description structure into Markdown-formatted string.
private parseDescriptionSection(description: any): string {
let md = "";
const items = ensureArray(description);
console.log(JSON.stringify(items, null, 2));
for (const item of items) {
// Handle plain text or detailedDescription
if (typeof item === "string") {
md += this.escapeMarkdown(item) + "\n\n";
} else if (item["#text"]) {
md += this.escapeMarkdown(this.extractText(item["#text"])) + "\n\n";
}
}
return md.trim();
}
private parseDocFile(compound: any): DocFile {
const functions: FunctionDoc[] = [];
const typedefs: TypeDefDoc[] = [];
const refId = compound["@_refid"];
const fileName = this.getXmlFileNameFromRefId(refId);
const file = this.fileCache[fileName];
if (!file) {
console.error(`File not found: ${fileName}`);
}
const parsedFile = this.xmlParser.parse(file);
const compounddef = parsedFile?.doxygen?.compounddef;
const compoundName = compounddef?.compoundname;
const briefDescription = compounddef?.briefdescription?.para || "";
const detailedDescription = this.parseDescriptionSection(compounddef?.detaileddescription?.para || []);
const location = compounddef?.location?.["@_file"] || "";
const sectionDefs = ensureArray(compounddef?.sectiondef);
for (const sectionDef of sectionDefs) {
const sectionType = sectionDef["@_kind"];
if (sectionType === "typedef") {
const memberDefs = ensureArray(sectionDef?.memberdef);
for (const memberDef of memberDefs) {
const typedef = this.parseTypeDef(memberDef);
typedefs.push(typedef);
}
} else if (sectionType === "func") {
const memberDefs = ensureArray(sectionDef?.memberdef);
for (const memberDef of memberDefs) {
const func = this.parseFunction(memberDef);
functions.push(func);
}
}
}
return {
name: compoundName,
briefDescription,
detailedDescription,
functions,
typedefs,
location
};
}
private parseFunction(memberDef: any): FunctionDoc {
const type = memberDef["@_kind"];
const functionType = memberDef?.type || "";
const definition = memberDef?.definition || "";
const argsstring = memberDef?.argsstring || "";
const name = memberDef?.name || "";
const params = Array.isArray(memberDef?.param) ? memberDef?.param : [memberDef?.param];
const parsedParams: FunctionParam[] = [];
for (const param of params) {
parsedParams.push(this.parseFunctionParam(param));
}
const briefDescription = memberDef?.briefDescription || "";
const detailedDescription: FunctionDetailedParamDescription = this.parseFunctionDetailedDescription(
memberDef?.detaileddescription
);
const location = memberDef?.location;
const parsedLocation = {
definition: {
file: location?.["@_file"] || "",
line: Number(location?.["@_line"]) || 0,
column: Number(location?.["@_column"]) || 0
},
declaration: {
file: location?.["@_declfile"] || "",
line: Number(location?.["@_declline"]) || 0,
column: Number(location?.["@_declcolumn"]) || 0
}
};
return {
name,
definition,
argsstring,
param: parsedParams,
briefDescription,
detailedDescription,
location: parsedLocation
};
}
private parseFunctionParam(param: any): FunctionParam {
return {
type: param?.type,
declname: param?.declname
};
}
private parseFunctionDetailedDescription(description: any): FunctionDetailedParamDescription {
let descriptionText = "";
const params: DetailedParam[] = [];
let returnsText = "void";
if (description?.para) {
const para = description.para;
if (para["#text"]) {
descriptionText = para["#text"];
}
if (para.parameterlist && para.parameterlist["@_kind"] === "param") {
const parameterList = para.parameterlist;
const parameterItems = ensureArray(parameterList.parameteritem);
for (const parameterItem of parameterItems) {
const parameterName = parameterItem?.parameternamelist?.parametername || "";
const parameterDescription = parameterItem?.parameterdescription?.para || "";
params.push({
paramName: parameterName,
paramDescription: parameterDescription
});
}
}
if (para.simplesect && para.simplesect["@_kind"] === "return") {
returnsText = para.simplesect.para || "";
}
}
return {
description: descriptionText,
params,
returns: returnsText
};
}
private parseTypeDef(memberDef: any): TypeDefDoc {
const type = memberDef["@_kind"];
const typeDef = memberDef?.type || "";
const definition: string = memberDef?.definition || "";
const argsstring = memberDef?.argsstring || "";
const name = memberDef?.name || "";
const briefDescription = memberDef?.briefdescription || "";
const detailedDescription = memberDef?.detaileddescription?.para || "";
const location = {
file: memberDef?.location?.["@_file"] || "",
line: Number(memberDef?.location?.["@_line"]) || 0,
column: Number(memberDef?.location?.["@_column"]) || 0
};
return {
type: typeDef,
name,
definition,
argsstring,
briefDescription,
detailedDescription,
location
};
}
toMD(): MDOutput[] {
for (const docFile of this.apiDoc.files) {
this.writeFile(docFile);
}
return this.outputs;
}
private writeFile(docFile: DocFile) {
let stream = "";
stream += this.writeFrontMatter(docFile.name, docFile.briefDescription || `${docFile.name} File Reference`);
stream += `${docFile.detailedDescription}\n\n`;
if (docFile.typedefs.length > 0) {
stream += "## Typedefs\n\n";
for (const typedef of docFile.typedefs) {
stream += this.writeTypedef(typedef);
}
}
if (docFile.functions.length > 0) {
stream += "## Functions\n\n";
for (const func of docFile.functions) {
stream += this.writeFunction(func);
}
}
this.outputs.push({
filename: docFile.name.replace(/\.h$/, ".md").replace(/\.c$/, ".md"),
buffer: 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[][]): string {
const escapedRows = rows.map((row) => row.map((cell) => this.escapeMarkdown(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 extractText(content: any): string {
if (!content) return "";
if (typeof content === "string") return content;
if (typeof content === "object") {
if ("#text" in content) return content["#text"];
return JSON.stringify(content);
}
return String(content);
}
private extractReturnType(definition: string): string {
const parts = definition.trim().split(/\s+/);
if (parts.length < 2) return "";
return parts.slice(0, -1).join(" ");
}
private writeFunction(func: FunctionDoc) {
let stream = "";
stream += "### " + func.name + "\n\n";
if (func.definition && func.argsstring) {
stream += "```c\n" + func.definition + func.argsstring + "\n```\n";
}
if (func.detailedDescription) {
if (func.detailedDescription.description) {
stream += "\n\n" + func.detailedDescription.description + "\n\n";
}
if (func.detailedDescription.params.length > 0) {
stream += "##### Parameters\n\n";
const params = func.detailedDescription.params.map((param) => [
param.paramName,
this.extractText(param.paramDescription)
]);
stream += this.generateMarkdownTable(["Parameter", "Description"], params) + "\n\n";
}
stream += "##### ⏎ Return Value\n\n";
if (func.detailedDescription.returns) {
stream += "`" + this.extractReturnType(func.definition) + "`: " + func.detailedDescription.returns + "\n\n";
}
}
return stream;
}
private writeTypedef(typedef: TypeDefDoc) {
let stream = "";
stream += "### " + typedef.name + "\n\n";
if (typedef.definition) {
stream += "```c\n" + typedef.definition + "\n```\n";
}
if (typedef.detailedDescription) {
stream += "\n\n" + typedef.detailedDescription + "\n\n";
}
return stream;
}
private beginBlock() {
return `---\n`;
}
private endBlock() {
return `---\n\n`;
}
private writeFrontMatter(title: string, description: string) {
return `${this.beginBlock()}title: ${title}\ndescription: ${description || title}\n${this.endBlock()}`;
}
}