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 = {}; private sourceFiles: Record = {}; 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, "&") .replace(//g, ">") .replace(/\|/g, "|") .replace(/\[/g, "[") .replace(/\]/g, "]"); } 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()}`; } }