421 lines
13 KiB
TypeScript
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, "&")
|
|
.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()}`;
|
|
}
|
|
}
|