moxygen
Version:
Doxygen XML to Markdown converter
1,132 lines • 39.4 kB
JavaScript
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { Parser as XmlParser } from 'xml2js';
import { createCompound, findCompound } from './compound.js';
import { inline, getAnchor } from './helpers.js';
import { log } from './logger.js';
import * as md from './markdown.js';
let parserOptions;
const references = {};
let root;
function currentContextName(context) {
return context[context.length - 1]?.['#name'];
}
function headingLevel(context) {
const parent = context[context.length - 1];
const match = parent?.['#name']?.match(/^sect([1-6])$/);
return match ? Math.min(Number(match[1]) + 1, 6) : 2;
}
function parameterListTitle(kind) {
switch (kind) {
case 'exception':
return 'Exceptions';
case 'retval':
return 'Return Values';
case 'templateparam':
return 'Template Parameters';
case 'param':
default:
return 'Parameters';
}
}
/**
* Convert a Doxygen XML element tree to Markdown.
*/
function toMarkdown(element, context = [], options = {}) {
if (typeof element === 'string') {
return element;
}
if (Array.isArray(element)) {
return element.map((v) => toMarkdown(v, context, options)).join('');
}
if (typeof element !== 'object' || element === null) {
return '';
}
const el = element;
let s = '';
// Opening
switch (el['#name']) {
case 'ref':
if (currentContextName(context) === 'programlisting') {
return s + toMarkdown(el.$$, context, options);
}
return s + md.refLink(toMarkdown(el.$$, context, options), el.$?.refid ?? '');
case '__text__':
s = el._ ?? '';
break;
case 'emphasis':
s = '*';
break;
case 'bold':
s = '**';
break;
case 'parametername':
case 'computeroutput':
s = '`';
break;
case 'parameterlist':
if (options.skipParameterLists)
return '';
s = `\n#### ${parameterListTitle(el.$?.kind)}\n`;
break;
case 'parameteritem':
s = '* ';
break;
case 'programlisting': {
const lang = el.$?.filename?.match(/\.([a-z0-9]+)$/i)?.[1] ?? 'cpp';
context.push(el);
s = `\n\`\`\`${lang}\n`;
break;
}
case 'verbatim':
s = '\n```\n';
break;
case 'orderedlist':
context.push(el);
s = '\n\n';
break;
case 'itemizedlist':
context.push(el);
s = '\n\n';
break;
case 'variablelist':
context.push(el);
s = '\n\n';
break;
case 'varlistentry':
s = '\n* ';
break;
case 'term':
s = '**';
break;
case 'listitem':
s = currentContextName(context) === 'orderedlist'
? '1. '
: currentContextName(context) === 'variablelist'
? ': '
: '* ';
break;
case 'sp':
s = ' ';
break;
case 'heading':
s = '## ';
break;
case 'xrefsect':
s += '\n> ';
break;
case 'simplesect': {
const kind = el.$?.kind;
context.push(el);
if (kind === 'attention' || kind === 'warning') {
s = '\n:::warning\n';
}
else if (kind === 'note' || kind === 'remark') {
s = '\n:::note\n';
}
else if (kind === 'deprecated') {
s = '\n:::warning\n**Deprecated.** ';
}
else if (kind === 'return') {
s = '\n#### Returns\n';
}
else if (kind === 'see') {
s = '**See also**: ';
}
else if (kind === 'since') {
s = '**Since**: ';
}
else if (kind === 'pre') {
s = '\n#### Preconditions\n';
}
else if (kind === 'post') {
s = '\n#### Postconditions\n';
}
else if (kind === 'invariant') {
s = '\n#### Invariants\n';
}
else if (kind === 'author') {
s = '**Author**: ';
}
else if (kind === 'copyright') {
s = '**Copyright**: ';
}
else if (kind === 'par') {
const title = el.$$?.find((child) => child['#name'] === 'title');
const titleText = title ? trim(toMarkdown(title.$$, context, options)) || trim(title._ ?? '') : '';
s = titleText ? `\n#### ${titleText}\n` : '\n#### Notes\n';
}
else {
log.warn(`simplesect kind '${kind}' not supported`);
}
break;
}
case 'formula': {
let formula = trim(el._ ?? '');
if (formula.startsWith('$') && formula.endsWith('$'))
return formula;
if (formula.startsWith('\\[') && formula.endsWith('\\]')) {
formula = trim(formula.substring(2, formula.length - 2));
}
return `\n$$\n${formula}\n$$\n`;
}
case 'preformatted':
s = '\n<pre>';
break;
case 'anchor':
s = getAnchor(el.$?.id ?? '', parserOptions);
break;
case 'image': {
const name = el.$?.name ?? '';
const caption = trim(el.$$ ? toMarkdown(el.$$, context, options) : (el._ ?? ''));
return name ? `` : caption;
}
case 'sect1':
case 'sect2':
case 'sect3':
case 'sect4':
case 'sect5':
case 'sect6':
context.push(el);
s = `\n${getAnchor(el.$?.id ?? '', parserOptions)}\n`;
break;
case 'title': {
if (currentContextName(context) === 'simplesect') {
return '';
}
const level = '#'.repeat(headingLevel(context));
const title = trim(el.$$ ? toMarkdown(el.$$, context, options) : (el._ ?? ''));
return `\n${level} ${title}\n`;
}
case 'mdash':
s = '—';
break;
case 'ndash':
s = '–';
break;
case 'linebreak':
s = '<br/>';
break;
case 'xreftitle':
case 'entry':
case 'row':
case 'ulink':
case 'codeline':
case 'highlight':
case 'table':
case 'para':
case 'parameterdescription':
case 'parameternamelist':
case 'xrefdescription':
case 'hruler':
case undefined:
break;
default:
log.warn(`${el['#name']}: not yet supported`);
}
// Recurse on children
if (el.$$) {
s += toMarkdown(el.$$, context, options);
}
// Closing
switch (el['#name']) {
case 'simplesect': {
const closeKind = el.$?.kind;
if (closeKind === 'attention' || closeKind === 'warning' || closeKind === 'note' || closeKind === 'remark' || closeKind === 'deprecated') {
s += '\n:::\n\n';
}
else {
s += '\n\n';
}
context.pop();
break;
}
case 'parameterlist':
case 'para':
s += '\n\n';
break;
case 'emphasis':
s += '*';
break;
case 'bold':
s += '**';
break;
case 'parameteritem':
s += '\n';
break;
case 'computeroutput':
s += '`';
break;
case 'parametername':
s += '` ';
break;
case 'entry':
s = md.escape.cell(s) + '|';
break;
case 'programlisting':
s += '```\n';
context.pop();
break;
case 'verbatim':
if (s && !s.endsWith('\n'))
s += '\n';
s += '```\n';
break;
case 'codeline':
s += '\n';
break;
case 'ulink':
s = md.safeLink(s, el.$?.url ?? '');
break;
case 'orderedlist':
context.pop();
s += '\n';
break;
case 'itemizedlist':
context.pop();
s += '\n';
break;
case 'variablelist':
context.pop();
s += '\n';
break;
case 'listitem':
s += '\n';
break;
case 'term':
s += '**';
break;
case 'xreftitle':
s += ': ';
break;
case 'preformatted':
s += '</pre>\n';
break;
case 'sect1':
case 'sect2':
case 'sect3':
case 'sect4':
case 'sect5':
case 'sect6':
context.pop();
s += '\n';
break;
case 'row':
s = '\n' + md.escape.row(s);
if (el.$$ && el.$$[0]?.$?.thead === 'yes') {
s += el.$$.map((_, i) => (i ? ' | ' : '\n') + '---------').join('');
}
break;
}
return s;
}
function trim(text) {
return text.trim();
}
function textContent(element) {
if (typeof element === 'string')
return element;
if (Array.isArray(element))
return element.map(textContent).join('');
if (typeof element !== 'object' || element === null)
return '';
const el = element;
return el._ ?? (el.$$ ? textContent(el.$$) : '');
}
/** Convert a named XML field to trimmed Markdown. */
function mdField(def, property, options = {}) {
return trim(toMarkdown(def[property], [], options));
}
/** Extract a summary from brief or detailed description. */
function mdSummary(def, options = {}) {
let summary = trim(toMarkdown(def['briefdescription'], [], options));
if (!summary) {
summary = trim(toMarkdown(def['detaileddescription'], [], options));
if (summary) {
const firstSentence = summary.split('\n', 1)[0];
if (firstSentence)
summary = firstSentence;
}
}
return cleanSummary(summary);
}
function cleanSummary(summary) {
const lines = summary.split(/\n+/);
const kept = [];
for (const line of lines) {
if (/^#{2,6}\s+(Parameters|Template Parameters|Exceptions|Returns?|Return Values)\b/i.test(line.trim())) {
break;
}
kept.push(line);
}
return kept.join(' ').replace(/\s+/g, ' ').trim();
}
function parseTemplateParams(def, descriptions = {}) {
const result = [];
const tpl = def.templateparamlist;
if (!tpl?.length || !tpl[0].param)
return result;
const params = tpl[0].param;
for (const param of params) {
const name = param.declname ? trim(toMarkdown(param.declname)) : '';
result.push({
type: trim(toMarkdown(param.type)),
name,
description: descriptions[name] ?? '',
defaultValue: param.defval ? trim(toMarkdown(param.defval)) : undefined,
});
}
return result;
}
function parseRelationRefs(def, property) {
const refs = def[property];
if (!refs?.length)
return [];
return refs.map((ref) => {
const attrs = (ref.$ ?? {});
return {
name: trim(toMarkdown(ref)) || String(ref._ ?? ''),
refid: attrs.refid,
compoundref: attrs.compoundref,
startline: attrs.startline,
endline: attrs.endline,
};
}).filter((ref) => ref.name || ref.refid);
}
function normalizeQualifier(value) {
return value.replace(/\s+/g, ' ').trim();
}
function pushUnique(list, value) {
const normalized = normalizeQualifier(value ?? '');
if (normalized && !list.includes(normalized))
list.push(normalized);
}
function extractBalancedCall(source, keyword) {
const match = new RegExp(`\\b${keyword}\\b`).exec(source);
if (!match)
return undefined;
let index = match.index + keyword.length;
while (/\s/.test(source[index] ?? ''))
index += 1;
if (source[index] !== '(')
return keyword;
const start = index;
let depth = 0;
for (; index < source.length; index += 1) {
const char = source[index];
if (char === '(')
depth += 1;
if (char === ')') {
depth -= 1;
if (depth === 0) {
return `${keyword}${source.slice(start, index + 1)}`;
}
}
}
return keyword;
}
function extractRequiresClause(source) {
const match = /\brequires\b/.exec(source);
if (!match)
return undefined;
let clause = source.slice(match.index).replace(/\s*=\s*(?:delete|default)\s*;?$/, '');
clause = clause.replace(/\s*(?:override|final)\s*$/, '');
return clause;
}
function parseMembers(compound, props, membersdef) {
Object.assign(compound, props);
references[compound.refid] = compound;
if (membersdef) {
for (const memberdef of membersdef) {
const member = {
name: memberdef.name[0],
parent: compound,
refid: '',
kind: '',
section: '',
prot: '',
static: '',
virtual: '',
proto: '',
briefdescription: '',
detaileddescription: '',
summary: '',
enumvalue: [],
returnType: '',
params: [],
returnValues: [],
exceptions: [],
templateParams: [],
qualifiers: [],
prefixQualifiers: [],
definition: '',
argsstring: '',
initializer: '',
isConst: false,
isInline: false,
isExplicit: false,
isStatic: false,
isVirtual: false,
isNodiscard: false,
isConstexpr: false,
isConsteval: false,
references: [],
referencedBy: [],
reimplements: [],
reimplementedBy: [],
};
compound.members.push(member);
const attrs = memberdef.$;
if (attrs) {
Object.assign(member, attrs);
}
references[member.refid] = member;
}
}
}
function extractDocListItem(item) {
if (!item.$$)
return undefined;
let name = '';
let description = '';
for (const part of item.$$) {
if (part['#name'] === 'parameternamelist' && part.$$) {
const names = part.$$
.filter((pn) => pn['#name'] === 'parametername')
.map((pn) => trim(textContent(pn)))
.filter(Boolean);
name = names.join(', ');
}
else if (part['#name'] === 'parameterdescription') {
description = trim(toMarkdown(part, [], { skipParameterLists: true }));
}
}
return name ? { name, description } : undefined;
}
function collectDocLists(element, lists) {
if (element['#name'] === 'parameterlist') {
const kind = element.$?.kind ?? 'param';
for (const child of element.$$ ?? []) {
if (child['#name'] !== 'parameteritem')
continue;
const item = extractDocListItem(child);
if (!item)
continue;
if (kind === 'retval') {
lists.returnValues.push(item);
}
else if (kind === 'exception') {
lists.exceptions.push(item);
}
else if (kind === 'templateparam') {
lists.templateParamDescriptions[item.name] = item.description;
}
else {
lists.paramDescriptions[item.name] = item.description;
}
}
return;
}
for (const child of element.$$ ?? []) {
collectDocLists(child, lists);
}
}
/** Extract structured parameter-style lists from detaileddescription XML. */
function extractDocLists(memberdef) {
const lists = {
paramDescriptions: {},
templateParamDescriptions: {},
returnValues: [],
exceptions: [],
};
const detailed = memberdef.detaileddescription;
if (!detailed)
return lists;
const roots = Array.isArray(detailed) ? detailed : [detailed];
for (const rootElement of roots) {
for (const child of rootElement.$$ ?? []) {
collectDocLists(child, lists);
}
}
return lists;
}
function parseMember(member, section, memberdef) {
log.verbose(`Processing member ${member.kind} ${member.name}`);
member.section = section;
const docLists = extractDocLists(memberdef);
member.briefdescription = mdField(memberdef, 'briefdescription');
member.detaileddescription = mdField(memberdef, 'detaileddescription', { skipParameterLists: true });
member.summary = mdSummary(memberdef, { skipParameterLists: true });
const attrs = memberdef.$;
// Populate structured fields
member.isConst = attrs['const'] === 'yes';
member.isInline = attrs.inline === 'yes';
member.isExplicit = attrs.explicit === 'yes';
member.isStatic = attrs.static === 'yes';
member.isVirtual = attrs.virt === 'virtual' || attrs.virt === 'pure-virtual';
member.isNodiscard = attrs.nodiscard === 'yes';
member.isConstexpr = attrs.constexpr === 'yes';
member.isConsteval = attrs.consteval === 'yes';
member.returnType = trim(toMarkdown(memberdef.type));
if (memberdef.location) {
const locations = memberdef.location;
const locationAttrs = locations?.[0]?.$;
const location = locationAttrs?.file;
if (location) {
member.location = location;
}
if (locationAttrs?.line) {
member.locationLine = locationAttrs.line;
}
}
// Definition and argsstring
if (memberdef.definition) {
const defArr = memberdef.definition;
member.definition = defArr?.[0]?._ ?? '';
}
if (memberdef.argsstring) {
const argsArr = memberdef.argsstring;
member.argsstring = argsArr?.[0]?._ ?? '';
}
member.returnValues = docLists.returnValues;
member.exceptions = docLists.exceptions;
member.templateParams = parseTemplateParams(memberdef, docLists.templateParamDescriptions);
member.references = parseRelationRefs(memberdef, 'references');
member.referencedBy = parseRelationRefs(memberdef, 'referencedby');
member.reimplements = parseRelationRefs(memberdef, 'reimplements');
member.reimplementedBy = parseRelationRefs(memberdef, 'reimplementedby');
// Function parameters
const hasCallableArgs = member.kind !== 'friend' || !!member.argsstring.trim();
if (memberdef.param && hasCallableArgs && (member.kind === 'function' || member.kind === 'signal' || member.kind === 'slot' || member.kind === 'friend')) {
const params = memberdef.param;
for (const param of params) {
const paramName = param.declname ? trim(toMarkdown(param.declname)) : '';
member.params.push({
type: trim(toMarkdown(param.type)),
name: paramName,
description: docLists.paramDescriptions[paramName] ?? '',
defaultValue: param.defval ? trim(toMarkdown(param.defval)) : undefined,
});
}
}
// Qualifiers
const prefixQualifiers = [];
const qualifiers = [];
if (member.isNodiscard)
prefixQualifiers.push('[[nodiscard]]');
if (member.isConsteval)
prefixQualifiers.push('consteval');
if (member.isConstexpr)
prefixQualifiers.push('constexpr');
if (member.isConst)
qualifiers.push('const');
if (member.argsstring) {
const refQualifierMatch = member.argsstring.match(/\)\s*(?:const\s*)?(?:noexcept(?:\s*\([^)]*\))?\s*)?(&&?)(?=\s*(?:noexcept|override|final|->|requires|=|$))/);
if (refQualifierMatch)
pushUnique(qualifiers, refQualifierMatch[1]);
pushUnique(qualifiers, extractBalancedCall(member.argsstring, 'noexcept'));
const trailingReturnMatch = member.argsstring.match(/->\s*(.+?)(?=\s*(?:requires|noexcept|override|final|=|$))/);
if (trailingReturnMatch)
pushUnique(qualifiers, `-> ${trailingReturnMatch[1].trim()}`);
if (/\boverride\b/.test(member.argsstring))
pushUnique(qualifiers, 'override');
if (/\bfinal\b/.test(member.argsstring))
pushUnique(qualifiers, 'final');
pushUnique(qualifiers, extractRequiresClause(member.argsstring));
if (/=\s*delete$/.test(member.argsstring))
pushUnique(qualifiers, '= delete');
if (/=\s*default/.test(member.argsstring))
pushUnique(qualifiers, '= default');
}
if (attrs.noexcept === 'yes')
pushUnique(qualifiers, 'noexcept');
if (memberdef.requiresclause)
pushUnique(qualifiers, `requires ${mdField(memberdef, 'requiresclause')}`);
if (memberdef.qualifier) {
const xmlQualifiers = memberdef.qualifier;
for (const qualifier of xmlQualifiers) {
pushUnique(qualifiers, trim(toMarkdown(qualifier)));
}
}
member.prefixQualifiers = prefixQualifiers;
member.qualifiers = qualifiers;
// Build legacy proto string (for classic templates)
let m = [];
switch (member.kind) {
case 'friend':
m.push(attrs.prot, ' ');
if (member.templateParams.length > 0) {
m.push('template<');
member.templateParams.forEach((tp, i) => {
if (i > 0)
m.push(',');
m.push(tp.type);
if (tp.name)
m.push(' ', tp.name);
if (tp.defaultValue)
m.push(' = ', tp.defaultValue);
});
m.push('> \n');
}
m.push('friend ');
if (member.returnType)
m.push(member.returnType, ' ');
m.push(md.refLink(member.name, member.refid));
if (member.argsstring) {
m.push('(');
member.params.forEach((param, i) => {
if (i > 0)
m.push(', ');
m.push(param.type);
if (param.name)
m.push(' ', param.name);
if (param.defaultValue)
m.push(' = ', param.defaultValue);
});
m.push(')');
}
for (const q of qualifiers) {
m.push(' ', q);
}
break;
case 'signal':
case 'slot':
m.push(`{${member.kind}} `);
// fallthrough
case 'function':
m.push(attrs.prot, ' ');
if (member.templateParams.length > 0) {
m.push('template<');
member.templateParams.forEach((tp, i) => {
if (i > 0)
m.push(',');
m.push(tp.type);
if (tp.name)
m.push(' ', tp.name);
if (tp.defaultValue)
m.push(' = ', tp.defaultValue);
});
m.push('> \n');
}
for (const q of prefixQualifiers) {
m.push(q, ' ');
}
if (member.isInline)
m.push('inline', ' ');
if (member.isStatic)
m.push('static', ' ');
if (member.isVirtual)
m.push('virtual', ' ');
m.push(toMarkdown(memberdef.type), ' ');
if (member.isExplicit)
m.push('explicit', ' ');
m.push(md.refLink(member.name, member.refid));
m.push('(');
member.params.forEach((param, i) => {
if (i > 0)
m.push(', ');
m.push(param.type);
if (param.name)
m.push(' ', param.name);
if (param.defaultValue)
m.push(' = ', param.defaultValue);
});
m.push(')');
for (const q of qualifiers) {
m.push(' ', q);
}
break;
case 'variable':
m.push(attrs.prot, ' ');
if (member.isStatic)
m.push('static', ' ');
if (attrs.mutable === 'yes')
m.push('mutable', ' ');
m.push(toMarkdown(memberdef.type), ' ');
m.push(md.refLink(member.name, member.refid));
if (memberdef.initializer) {
const init = memberdef.initializer[0]?._ ?? '';
if (init) {
m.push(' ', init);
member.initializer = init;
}
}
break;
case 'property':
m.push(`{${member.kind}} `);
m.push(toMarkdown(memberdef.type), ' ');
m.push(md.refLink(member.name, member.refid));
break;
case 'typedef':
if (/^using\b/.test(member.definition)) {
m.push('using ', md.refLink(member.name, member.refid), ' = ', toMarkdown(memberdef.type));
}
else {
m.push('typedef ', toMarkdown(memberdef.type), ' ', md.refLink(member.name, member.refid));
}
break;
case 'enum':
member.enumvalue = [];
if (memberdef.enumvalue) {
for (const param of memberdef.enumvalue) {
member.enumvalue.push({
name: mdField(param, 'name'),
briefdescription: mdField(param, 'briefdescription'),
detaileddescription: mdField(param, 'detaileddescription'),
summary: mdSummary(param),
});
}
}
m.push(member.kind, ' ', md.refLink(member.name, member.refid));
break;
default:
m.push(member.kind, ' ', md.refLink(member.name, member.refid));
break;
}
member.proto = inline(m);
}
function assignToNamespace(compound, child) {
if (compound.name !== child.namespace) {
log.warn(`namespace mismatch: ${compound.name} != ${child.namespace}`);
}
if (child.parent) {
delete child.parent.compounds[child.id];
}
compound.compounds[child.id] = child;
child.parent = compound;
}
function assignCompoundGroup(compound, group) {
compound.groupid = group.id;
compound.groupname = group.name;
for (const member of compound.members) {
member.groupid = group.id;
member.groupname = group.name;
}
for (const child of Object.values(compound.compounds)) {
assignCompoundGroup(child, group);
}
}
function pruneGroupTopLevelDuplicates(group) {
const topLevelRefids = new Set(Object.values(group.compounds).map((compound) => compound.refid));
for (const [id, compound] of Object.entries(group.compounds)) {
let current = compound.parent;
while (current) {
if (topLevelRefids.has(current.refid) && current.groupid === group.id) {
delete group.compounds[id];
break;
}
current = current.parent;
}
}
}
function assignNamespaceToGroup(compound, child) {
compound.compounds[child.id] = child;
for (const id of Object.keys(child.compounds)) {
delete compound.compounds[id];
}
}
function assignClassToGroup(compound, child) {
compound.compounds[child.id] = child;
assignCompoundGroup(child, compound);
}
function assignGroupToGroup(compound, child) {
if (child.parent) {
delete child.parent.compounds[child.id];
}
compound.compounds[child.id] = child;
child.parent = compound;
}
function extractPageSections(page, elements) {
for (const element of elements) {
if (element['#name'] === 'sect1' || element['#name'] === 'sect2' || element['#name'] === 'sect3') {
const id = element.$?.id ?? '';
const member = {
section: element['#name'],
id,
name: id,
refid: id,
parent: page,
kind: '',
prot: '',
static: '',
virtual: '',
proto: '',
briefdescription: '',
detaileddescription: '',
summary: '',
enumvalue: [],
returnType: '',
params: [],
returnValues: [],
exceptions: [],
templateParams: [],
qualifiers: [],
prefixQualifiers: [],
definition: '',
argsstring: '',
initializer: '',
isConst: false,
isInline: false,
isExplicit: false,
isStatic: false,
isVirtual: false,
isNodiscard: false,
isConstexpr: false,
isConsteval: false,
references: [],
referencedBy: [],
reimplements: [],
reimplementedBy: [],
};
page.members.push(member);
references[member.refid] = member;
}
if (element.$$) {
extractPageSections(page, element.$$);
}
}
}
function parseCompound(compound, compounddef) {
log.verbose(`Processing compound ${compound.name}`);
const attrs = compounddef.$;
Object.assign(compound, attrs);
compound.fullname = compounddef.compoundname[0]._ ??
compounddef.compoundname[0] ?? '';
// Short name: strip namespace prefix
const nameParts = compound.fullname.split('::');
compound.shortname = nameParts[nameParts.length - 1] || compound.name;
// Group title (from @defgroup "Title")
if (compounddef.title) {
const titleArr = compounddef.title;
const title = titleArr?.[0]?._ ?? titleArr?.[0] ?? '';
if (title)
compound.shortname = title;
}
// Language from XML attribute
if (attrs.language) {
compound.language = attrs.language;
}
// Include file
if (compounddef.includes) {
const includes = compounddef.includes;
if (includes.length > 0) {
compound.includes = includes[0]._ ?? '';
}
}
compound.briefdescription = mdField(compounddef, 'briefdescription');
compound.detaileddescription = mdField(compounddef, 'detaileddescription');
compound.summary = mdSummary(compounddef);
compound.templateParams = parseTemplateParams(compounddef);
if (compounddef.basecompoundref) {
for (const ref of compounddef.basecompoundref) {
const refAttrs = ref.$;
compound.basecompoundref.push({
prot: refAttrs.prot,
name: ref._,
refid: refAttrs.refid,
virt: refAttrs.virt,
});
}
}
if (compounddef.derivedcompoundref) {
for (const ref of compounddef.derivedcompoundref) {
const refAttrs = ref.$;
compound.derivedcompoundref.push({
prot: refAttrs.prot,
name: ref._,
refid: refAttrs.refid,
virt: refAttrs.virt,
});
}
}
if (compounddef.sectiondef) {
for (const section of compounddef.sectiondef) {
const sectionAttrs = section.$;
if (section.memberdef) {
for (const memberdef of section.memberdef) {
const memberAttrs = memberdef.$;
const member = references[memberAttrs.id];
if (!member)
continue;
if (compound.kind === 'group') {
member.groupid = compound.id;
member.groupname = compound.name;
}
else if (compound.kind === 'file') {
root.members.push(member);
}
parseMember(member, sectionAttrs.kind, memberdef);
}
}
}
}
compound.proto = inline([compound.kind, ' ', md.refLink(compound.name, compound.refid)]);
if (compounddef.location) {
const locations = compounddef.location;
const locationAttrs = locations?.[0]?.$;
const location = locationAttrs?.file;
if (location) {
compound.location = location;
}
if (locationAttrs?.line) {
compound.locationLine = locationAttrs.line;
}
}
// Handle innerclass for any compound that can contain nested types
if (compounddef.innerclass) {
const innerclassRefs = [];
for (const innerclassdef of compounddef.innerclass) {
const innerAttrs = innerclassdef.$;
innerclassRefs.push(innerAttrs.refid);
const ref = references[innerAttrs.refid];
if (!ref)
continue;
switch (compound.kind) {
case 'namespace':
assignToNamespace(compound, ref);
break;
case 'group':
assignClassToGroup(compound, ref);
break;
case 'file':
break;
default:
// class, interface, enum - nest inner classes directly
compound.compounds[ref.id] = ref;
ref.parent = compound;
break;
}
}
if (compound.kind === 'file') {
compound.fileCompoundRefs = innerclassRefs;
}
}
if (compounddef.innernamespace && compound.kind === 'file') {
const innernamespaceRefs = [];
for (const namespacedef of compounddef.innernamespace) {
const nsAttrs = namespacedef.$;
innernamespaceRefs.push(nsAttrs.refid);
}
compound.fileNamespaceRefs = innernamespaceRefs;
}
switch (compound.kind) {
case 'class':
case 'struct':
case 'union':
case 'typedef':
case 'interface':
case 'enum': {
const parts = compound.name.split('::');
compound.namespace = parts.slice(0, -1).join('::');
break;
}
case 'page':
extractPageSections(compound, compounddef.$$);
break;
case 'namespace':
case 'group':
if (compound.kind === 'group') {
compound.groupid = compound.id;
compound.groupname = compound.name;
}
if (compounddef.innernamespace) {
compound.innernamespaces = [];
for (const namespacedef of compounddef.innernamespace) {
const nsAttrs = namespacedef.$;
if (compound.kind === 'group') {
const ref = references[nsAttrs.refid];
if (ref)
assignNamespaceToGroup(compound, ref);
}
}
}
if (compound.kind === 'group' && compounddef.innergroup) {
for (const groupdef of compounddef.innergroup) {
const groupAttrs = groupdef.$;
const ref = references[groupAttrs.refid];
if (ref)
assignGroupToGroup(compound, ref);
}
}
if (compound.kind === 'group') {
pruneGroupTopLevelDuplicates(compound);
}
break;
default:
break;
}
}
function parseIndex(rootCompound, index, options) {
// Pass 1: register all compounds and their members in the references map.
// Sort by refid length (ascending) so parent namespaces are created before children.
const sorted = [...index].sort((a, b) => {
const aRefid = a.$.refid ?? '';
const bRefid = b.$.refid ?? '';
return aRefid.length - bRefid.length;
});
for (const element of sorted) {
const attrs = element.$;
const name = element.name?.[0] ?? '';
const compound = findCompound(rootCompound, attrs.refid, name, true);
parseMembers(compound, attrs, element.member);
}
// Pass 2: parse compound XML files. All refs are now registered so
// innerclass/innernamespace lookups succeed regardless of order.
for (const element of sorted) {
const attrs = element.$;
const compound = references[attrs.refid];
if (!compound)
continue;
const xmlPath = join(options.directory, `${compound.refid}.xml`);
log.verbose(`Parsing ${xmlPath}`);
try {
const xml = readFileSync(xmlPath, 'utf8');
const xmlParser = new XmlParser({
explicitChildren: true,
preserveChildrenOrder: true,
charsAsChildren: true,
});
xmlParser.parseString(xml, (err, data) => {
if (err) {
log.warn(`parse error for file: ${xmlPath}`);
return;
}
const doxygen = data.doxygen;
const compounddefs = doxygen.compounddef;
parseCompound(compound, compounddefs[0]);
});
}
catch (e) {
log.warn(`failed to read: ${xmlPath}`);
}
}
}
/**
* Load and parse Doxygen XML index.
*/
export async function loadIndex(options) {
parserOptions = options;
root = createCompound();
root.kind = 'index';
// Clear references
for (const key of Object.keys(references)) {
delete references[key];
}
const indexPath = join(options.directory, 'index.xml');
log.verbose(`Parsing ${indexPath}`);
const xml = readFileSync(indexPath, 'utf8');
return new Promise((resolve, reject) => {
const xmlParser = new XmlParser();
xmlParser.parseString(xml, (err, result) => {
if (err) {
reject(new Error(`Failed to parse doxygen XML: ${err}`));
return;
}
const doxygenindex = result.doxygenindex;
const compounds = doxygenindex.compound;
parseIndex(root, compounds, options);
resolve({ root, references });
});
});
}
//# sourceMappingURL=parser.js.map