@xpack/docusaurus-plugin-doxygen
Version:
A Docusaurus plugin to integrate Doxygen into a Docusaurus project.
525 lines • 23.8 kB
JavaScript
/*
* This file is part of the xPack project (http://xpack.github.io).
* Copyright (c) 2025 Liviu Ionescu. All rights reserved.
*
* Permission to use, copy, modify, and/or distribute this software
* for any purpose is hereby granted, under the terms of the MIT license.
*
* If a copy of the license was not distributed with this file, it can
* be obtained from https://opensource.org/licenses/MIT.
*/
// ----------------------------------------------------------------------------
import * as util from 'node:util';
import assert from 'node:assert';
import * as fs from 'node:fs/promises';
import path from 'node:path';
import { Groups } from './view-model/groups-vm.js';
import { Classes } from './view-model/classes-vm.js';
import { DoxygenFileOptions } from './view-model/options.js';
import { escapeMdx, getPermalinkAnchor, stripPermalinkAnchor } from './utils.js';
import { Namespaces } from './view-model/namespaces-vm.js';
import { FilesAndFolders } from './view-model/files-and-folders-vm.js';
import { Pages } from './view-model/pages-vm.js';
import { pluginName } from '../plugin/docusaurus.js';
import { Member } from './view-model/members-vm.js';
import { Renderers } from './elements-renderers/renderers.js';
// ----------------------------------------------------------------------------
// <xsd:simpleType name="DoxCompoundKind">
// <xsd:restriction base="xsd:string">
// <xsd:enumeration value="class" />
// <xsd:enumeration value="struct" />
// <xsd:enumeration value="union" />
// <xsd:enumeration value="interface" />
// <xsd:enumeration value="protocol" />
// <xsd:enumeration value="category" />
// <xsd:enumeration value="exception" />
// <xsd:enumeration value="service" />
// <xsd:enumeration value="singleton" />
// <xsd:enumeration value="module" />
// <xsd:enumeration value="type" />
// <xsd:enumeration value="file" />
// <xsd:enumeration value="namespace" />
// <xsd:enumeration value="group" />
// <xsd:enumeration value="page" />
// <xsd:enumeration value="example" />
// <xsd:enumeration value="dir" />
// <xsd:enumeration value="concept" />
// </xsd:restriction>
// </xsd:simpleType>
// ----------------------------------------------------------------------------
export class Workspace {
// --------------------------------------------------------------------------
constructor({ dataModel, pluginOptions, siteConfig, pluginActions = undefined }) {
this.collectionNamesByKind = {
class: 'classes',
struct: 'classes',
union: 'classes',
// interface
// protocol
// category
// exception
// service
// singleton
// module
// type
file: 'files',
namespace: 'namespaces',
group: 'groups',
page: 'pages',
// example
dir: 'files'
// concept
};
// The order of entries in the sidebar and in the top menu dropdown.
this.sidebarCollectionNames = ['groups', 'namespaces', 'classes', 'files', 'pages'];
// View model objects.
this.compoundsById = new Map();
this.membersById = new Map();
this.writtenMdxFilesCounter = 0;
this.writtenHtmlFilesCounter = 0;
console.log();
this.dataModel = dataModel;
this.pluginOptions = pluginOptions;
this.siteConfig = siteConfig;
this.pluginActions = pluginActions;
this.doxygenOptions = new DoxygenFileOptions(this.dataModel.doxyfile?.options);
const docsFolderPath = this.pluginOptions.docsFolderPath.replace(/^[/]/, '').replace(/[/]$/, '');
const apiFolderPath = this.pluginOptions.apiFolderPath.replace(/^[/]/, '').replace(/[/]$/, '');
this.outputFolderPath = `${docsFolderPath}/${apiFolderPath}/`;
this.sidebarBaseId = `${apiFolderPath}/`;
const docsBaseUrl = this.pluginOptions.docsBaseUrl.replace(/^[/]/, '').replace(/[/]$/, '');
const apiBaseUrl = this.pluginOptions.apiBaseUrl.replace(/^[/]/, '').replace(/[/]$/, '');
this.absoluteBaseUrl = `${this.siteConfig.baseUrl}${docsBaseUrl}/${apiBaseUrl}/`;
this.pageBaseUrl = `${this.siteConfig.baseUrl}${docsBaseUrl}/${apiBaseUrl}/`;
this.slugBaseUrl = `/${apiBaseUrl}/`;
this.menuBaseUrl = `/${docsBaseUrl}/${apiBaseUrl}/`;
// console.log('absoluteBaseUrl:', this.absoluteBaseUrl)
// Create the view-model objects.
this.viewModel = new Map();
this.viewModel.set('groups', new Groups(this));
this.viewModel.set('namespaces', new Namespaces(this));
this.viewModel.set('classes', new Classes(this));
this.viewModel.set('files', new FilesAndFolders(this));
this.viewModel.set('pages', new Pages(this));
this.elementRenderers = new Renderers(this);
this.createVieModelObjects();
this.createCompoundsHierarchies();
this.createMembersMap();
this.initializeCompoundsLate();
this.initializeMemberLate();
this.validatePermalinks();
this.cleanups();
}
// --------------------------------------------------------------------------
createVieModelObjects() {
console.log('Creating view model objects...');
for (const compoundDefDataModel of this.dataModel.compoundDefs) {
let added = false;
const collectionName = this.collectionNamesByKind[compoundDefDataModel.kind];
if (collectionName !== undefined) {
const collection = this.viewModel.get(collectionName);
if (collection !== undefined) {
// Create the compound object and add it to the parent collection.
// console.log(compoundDefDataModel.kind, compoundDefDataModel.compoundName)
const compound = collection.addChild(compoundDefDataModel);
// Also add it to the global compounds map.
this.compoundsById.set(compound.id, compound);
// console.log('compoundsById.set', compound.kind, compound.id)
added = true;
}
}
if (!added) {
// console.error(util.inspect(compoundDefDataModel, { compact: false, depth: 999 }))
console.error('compoundDefDataModel', compoundDefDataModel.kind, 'not implemented yet in', this.constructor.name);
}
}
if (this.pluginOptions.verbose) {
console.log(this.compoundsById.size, 'compound definitions');
}
}
// --------------------------------------------------------------------------
createCompoundsHierarchies() {
console.log('Creating compounds hierarchies...');
for (const [collectionName, collection] of this.viewModel) {
// console.log('createHierarchies:', collectionName)
collection.createCompoundsHierarchies();
}
}
// --------------------------------------------------------------------------
// Required since references can be resolved only after all objects are in.
initializeCompoundsLate() {
console.log('Performing compounds late initializations...');
for (const [collectionName, collection] of this.viewModel) {
// console.log('createHierarchies:', collectionName)
for (const [compoundId, compound] of collection.collectionCompoundsById) {
this.currentCompound = compound;
if (this.pluginOptions.debug) {
console.log(compound.kind, compound.compoundName);
}
compound.initializeLate();
}
}
this.currentCompound = undefined;
}
// --------------------------------------------------------------------------
createMembersMap() {
console.log('Creating member definitions map...');
for (const [, compound] of this.compoundsById) {
// console.log(compound.kind, compound.compoundName, compound.id)
if (compound.sections !== undefined) {
for (const section of compound.sections) {
if (section.indexMembers !== undefined) {
// console.log(' ', sectionDef.kind)
for (const member of section.indexMembers) {
if (member instanceof Member) {
const memberCompoundId = stripPermalinkAnchor(member.id);
if (memberCompoundId !== compound.id) {
// Skip member definitions from different compounds.
// Hopefully they are defined properly there.
// console.log('member from another compound', compoundId, 'skipped')
}
else {
// console.log(' ', memberDef.kind, memberDef.id)
if (this.membersById.has(member.id)) {
if (this.pluginOptions.verbose) {
console.warn('member already in map', member.id, 'in', this.membersById.get(member.id)?.name);
}
}
else {
this.membersById.set(member.id, member);
}
}
}
}
}
}
}
}
if (this.pluginOptions.verbose) {
console.log(this.membersById.size, 'member definitions');
}
}
// --------------------------------------------------------------------------
// Required since references can be resolved only after all objects are in.
initializeMemberLate() {
console.log('Performing members late initializations...');
for (const [, compound] of this.compoundsById) {
if (this.pluginOptions.debug) {
console.log(compound.kind, compound.compoundName, compound.id);
}
this.currentCompound = compound;
if (compound.sections !== undefined) {
for (const section of compound.sections) {
section.initializeLate();
if (section.indexMembers !== undefined) {
if (this.pluginOptions.debug) {
console.log(' ', section.kind);
}
for (const member of section.indexMembers) {
if (member instanceof Member) {
if (this.pluginOptions.debug) {
console.log(' ', member.kind, member.id);
}
member.initializeLate();
}
}
}
}
}
}
this.currentCompound = undefined;
}
// --------------------------------------------------------------------------
/**
* @brief Validate the uniqueness of permalinks.
*/
validatePermalinks() {
console.log('Validating permalinks...');
const pagePermalinksById = new Map();
const compoundsByPermalink = new Map();
for (const compoundDefDataModel of this.dataModel.compoundDefs) {
// console.log(compoundDefDataModel.kind, compoundDefDataModel.compoundName)
const compoundDefDataModelId = compoundDefDataModel.id;
if (pagePermalinksById.has(compoundDefDataModelId)) {
console.warn('Duplicate id', compoundDefDataModelId);
}
const compound = this.compoundsById.get(compoundDefDataModelId);
if (compound === undefined) {
console.error('compoundDefDataModel', compoundDefDataModelId, 'not yet processed in', this.constructor.name, 'validatePermalinks');
continue;
}
const permalink = compound.relativePermalink;
if (permalink !== undefined) {
// console.log('permalink:', permalink)
let compoundsMap = compoundsByPermalink.get(permalink);
if (compoundsMap === undefined) {
compoundsMap = new Map();
compoundsByPermalink.set(permalink, compoundsMap);
}
pagePermalinksById.set(compoundDefDataModelId, permalink);
if (!compoundsMap.has(compound.id)) {
compoundsMap.set(compound.id, compound);
}
}
}
for (const [permalink, compoundsMap] of compoundsByPermalink) {
if (compoundsMap.size > 1) {
if (this.pluginOptions.verbose) {
console.warn('Permalink', permalink, 'has', compoundsMap.size, 'occurrences:');
}
let count = 1;
for (const [compoundId, compound] of compoundsMap) {
const suffix = `-${count}`;
count += 1;
compound.relativePermalink += suffix;
compound.docusaurusId += suffix;
if (this.pluginOptions.verbose) {
console.warn('-', compound.relativePermalink, compound.id);
}
}
}
}
}
// --------------------------------------------------------------------------
cleanups() {
for (const [, compound] of this.compoundsById) {
compound._private._compoundDef = undefined;
}
}
// --------------------------------------------------------------------------
async writeMdxFile({ filePath, bodyLines, frontMatter, frontMatterCodeLines, title, pagePermalink }) {
const lines = [];
lines.push('');
lines.push('<DoxygenPage pluginConfig={pluginConfig}>');
lines.push('');
lines.push(...bodyLines);
lines.push('');
lines.push('</DoxygenPage>');
lines.push('');
// Hack to prevent Docusaurus replace legit content with emojis.
let text = lines.join('\n');
if (pagePermalink !== undefined && pagePermalink.length > 0) {
// Strip local page permalink from anchors.
text = text.replaceAll(`"${pagePermalink}/#`, '"#');
}
text = text.replaceAll(':thread:', "{':thread:'}").replaceAll(':flags:', "{':flags:'}");
// https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs#markdown-front-matter
const frontMatterLines = [];
frontMatterLines.push('---');
frontMatterLines.push('');
frontMatterLines.push('# DO NOT EDIT!');
frontMatterLines.push('# Automatically generated via docusaurus-plugin-doxygen by Doxygen.');
frontMatterLines.push('');
for (const [key, value] of Object.entries(frontMatter)) {
if (Array.isArray(value)) {
frontMatterLines.push(`${key}:`);
for (const arrayValue of frontMatter[key]) {
frontMatterLines.push(` - ${arrayValue}`);
}
}
else if (typeof value === 'boolean') {
frontMatterLines.push(`${key}: ${value ? 'true' : 'false'}`);
}
else {
frontMatterLines.push(`${key}: ${value}`);
}
}
frontMatterLines.push('');
// Skip date, to avoid unnecessary git commits.
// frontMatterText += `date: ${formatDate(new Date())}\n`
// frontMatterText += '\n'
frontMatterLines.push('---');
frontMatterLines.push('');
if (text.includes('<Link')) {
frontMatterLines.push('import Link from \'@docusaurus/Link\'');
}
// Theme components.
if (text.includes('<CodeBlock')) {
frontMatterLines.push('import CodeBlock from \'@theme/CodeBlock\'');
}
if (text.includes('<Admonition')) {
frontMatterLines.push('import Admonition from \'@theme/Admonition\'');
}
frontMatterLines.push('');
const componentNames = [
'CodeLine',
'CollapsibleTreeTable',
'DoxygenPage',
'EnumerationList',
'EnumerationListItem',
'GeneratedByDoxygen',
'Highlight',
'IncludesList',
'IncludesListItem',
'MemberDefinition',
'MembersIndex',
'MembersIndexItem',
'ParametersList',
'ParametersListItem',
'ProgramListing',
'Reference',
'SectionDefinition',
'SectionUser',
'TreeTable',
'TreeTableRow',
'XrefSect'
];
// Add includes for the plugin components.
for (const componentName of componentNames) {
if (text.includes(`<${componentName}`)) {
frontMatterLines.push(`import ${componentName} from '${pluginName}/components/${componentName}'`);
}
}
if (frontMatterCodeLines !== undefined && frontMatterCodeLines.length > 0) {
frontMatterLines.push('');
for (const line of frontMatterCodeLines) {
frontMatterLines.push(line);
}
}
frontMatterLines.push('');
frontMatterLines.push('import pluginConfig from \'@site/docusaurus-plugin-doxygen-config.json\'');
frontMatterLines.push('');
if (frontMatter.title === undefined && title !== undefined) {
frontMatterLines.push(`# ${title}`);
frontMatterLines.push('');
}
await fs.mkdir(path.dirname(filePath), { recursive: true });
const fileHandle = await fs.open(filePath, 'ax');
await fileHandle.write(frontMatterLines.join('\n'));
await fileHandle.write(text);
await fileHandle.close();
this.writtenMdxFilesCounter += 1;
}
// --------------------------------------------------------------------------
renderElementsToMdxLines(elements) {
if (!Array.isArray(elements)) {
return [];
}
const lines = [];
for (const element of elements) {
lines.push(...this.renderElementToMdxLines(element));
}
return lines;
}
renderElementToMdxLines(element) {
if (element === undefined) {
return [];
}
if (typeof element === 'string') {
return [escapeMdx(element)];
}
if (Array.isArray(element)) {
const lines = [];
for (const elementOfArray of element) {
lines.push(...this.renderElementToMdxLines(elementOfArray));
}
return lines;
}
const linesRenderer = this.elementRenderers.getElementLinesRenderer(element);
if (linesRenderer !== undefined) {
return linesRenderer.renderToMdxLines(element);
}
const textRenderer = this.elementRenderers.getElementTextRenderer(element);
if (textRenderer !== undefined) {
return [textRenderer.renderToMdxText(element)];
}
console.error(util.inspect(element, { compact: false, depth: 999 }));
console.error('no element lines renderer for', element.constructor.name, 'in', this.constructor.name, 'renderElementToMdxLines');
assert(false);
}
renderElementsToMdxText(elements) {
if (elements === undefined) {
return '';
}
let text = '';
for (const element of elements) {
text += this.renderElementToMdxText(element);
}
return text;
}
renderElementToMdxText(element) {
if (element === undefined) {
return '';
}
if (typeof element === 'string') {
return escapeMdx(element);
}
if (Array.isArray(element)) {
let text = '';
for (const elementOfArray of element) {
text += this.renderElementToMdxText(elementOfArray);
}
return text;
}
const textRenderer = this.elementRenderers.getElementTextRenderer(element);
if (textRenderer !== undefined) {
return textRenderer.renderToMdxText(element);
}
// console.warn('trying element lines renderer for', element.constructor.name, 'in', this.constructor.name, 'renderElementToMdxText')
const linesRenderer = this.elementRenderers.getElementLinesRenderer(element);
if (linesRenderer !== undefined) {
return linesRenderer.renderToMdxLines(element).join('\n');
}
console.error(util.inspect(element, { compact: false, depth: 999 }));
console.error('no element text renderer for', element.constructor.name, 'in', this.constructor.name, 'renderElementToMdxText');
return '';
}
// --------------------------------------------------------------------------
getPermalink({ refid, kindref }) {
// console.log(refid, kindref)
// if (refid.endsWith('ga45942bdeee4fb61db5a7dc3747cb7193')) {
// console.log(refid, kindref)
// }
let permalink;
if (kindref === 'compound') {
permalink = this.getPagePermalink(refid);
}
else if (kindref === 'member') {
const compoundId = stripPermalinkAnchor(refid);
// console.log('compoundId:', compoundId)
// if (this.currentCompound !== undefined && compoundId === this.currentCompound.id) {
// permalink = `#${getPermalinkAnchor(refid)}`
// } else {
permalink = `${this.getPagePermalink(compoundId)}/#${getPermalinkAnchor(refid)}`;
// }
}
else {
console.error('Unsupported kindref', kindref, 'for', refid, 'in', this.constructor.name, 'getPermalink');
}
// if (refid.endsWith('ga45942bdeee4fb61db5a7dc3747cb7193')) {
// console.log(permalink)
// }
return permalink;
}
getPagePermalink(refid) {
const dataObject = this.compoundsById.get(refid);
if (dataObject === undefined) {
if (this.pluginOptions.debug) {
console.warn('refid', refid, 'is not a known compound, no permalink');
}
return undefined;
}
const pagePermalink = dataObject.relativePermalink;
if (pagePermalink === undefined) {
if (this.pluginOptions.verbose) {
console.warn('refid', refid, 'has no permalink');
}
return undefined;
}
assert(pagePermalink !== undefined);
return `${this.pageBaseUrl}${pagePermalink}`;
}
getXrefPermalink(id) {
// console.log('1', id, this.currentCompoundDef.id)
const pagePart = id.replace(/_1.*/, '');
const anchorPart = id.replace(/.*_1/, '');
// console.log('2', part1, part2)
// if (this.currentCompound !== undefined && pagePart === this.currentCompound.id) {
// return `#${anchorPart}`
// } else {
return `${this.pageBaseUrl}pages/${pagePart}/#${anchorPart}`;
// }
}
}
// ----------------------------------------------------------------------------
//# sourceMappingURL=workspace.js.map