Initial commit
This commit is contained in:
402
src/scripts/parser/apidoc/c-apidoc-parser.ts
Normal file
402
src/scripts/parser/apidoc/c-apidoc-parser.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 || "";
|
||||
const detailedDescription = 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`);
|
||||
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, "[")
|
||||
.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 += "#### 📄 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 += "#### 📄 Description";
|
||||
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()}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user