moxygen
Version:
Doxygen XML to Markdown converter
259 lines • 8.51 kB
JavaScript
import { writeFileSync, mkdirSync } from 'node:fs';
import { dirname, relative } from 'node:path';
import { format as utilFormat } from 'node:util';
import { log } from './logger.js';
/**
* Wrap code segments in backticks, preserving markdown links and line breaks.
*/
export function inline(code) {
if (!Array.isArray(code)) {
return `\`${code}\``;
}
let s = '';
let isInline = false;
for (const segment of code) {
const refs = segment.split(/(\[.*?\]\(.*?\)|\n|\s{2}\n)/g);
for (const fragment of refs) {
if (fragment.charAt(0) === '[') {
const match = fragment.match(/\[(.*?)\]\((.*?)\)/);
if (match) {
if (isInline) {
s += '`';
isInline = false;
}
s += `[\`${match[1]}\`](${match[2]})`;
}
}
else if (fragment === '\n' || fragment === ' \n') {
if (isInline) {
s += '`';
isInline = false;
}
s += fragment;
}
else if (fragment) {
if (!isInline) {
s += '`';
isInline = true;
}
s += fragment;
}
}
}
return s + (isInline ? '`' : '');
}
/**
* Strip markdown links from generated type/signature text while preserving labels.
*/
export function stripMarkdownLinks(text) {
return (text || '').replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
}
/**
* Convert a C++ symbol into a filesystem- and URL-safe path segment.
*/
export function safePathSegment(name) {
return (name || 'unknown')
.replace(/::/g, '-')
.replace(/[^A-Za-z0-9_-]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '') || 'unknown';
}
/**
* Generate an anchor string based on options.
*/
export function getAnchor(name, options) {
if (options.anchors) {
return `{#${name}}`;
}
if (options.htmlAnchors) {
return `<a id="${name}"></a>`;
}
return '';
}
/**
* Find the nearest parent compound matching one of the given kinds.
*/
export function findParent(compound, kinds) {
let current = compound;
while (current) {
if (kinds.includes(current.kind))
return current;
current = current.parent;
}
return undefined;
}
/**
* Convert a name to a clean URL-safe anchor ID.
*/
export function cleanId(name) {
return name
.toLowerCase()
.replace(/::/g, '-')
.replace(/[^a-z0-9_-]/g, '')
.replace(/^-+|-+$/g, '') || 'unknown';
}
/**
* Build a map from Doxygen refids to clean, human-readable anchor IDs.
* Handles deduplication for overloaded names.
*/
export function buildCleanAnchorMap(compounds) {
const map = new Map();
const used = new Set();
const seen = new Set();
function dedup(base) {
let id = base;
let i = 1;
while (used.has(id)) {
id = `${base}-${i++}`;
}
used.add(id);
return id;
}
function visit(compound) {
if (seen.has(compound.refid)) {
return;
}
seen.add(compound.refid);
const compClean = dedup(cleanId(compound.shortname || compound.name));
map.set(compound.refid, compClean);
for (const member of compound.filtered?.members || compound.members || []) {
if (seen.has(member.refid)) {
continue;
}
seen.add(member.refid);
const memberClean = dedup(cleanId(member.name));
map.set(member.refid, memberClean);
}
for (const child of Object.values(compound.compounds)) {
visit(child);
}
}
for (const compound of compounds) {
visit(compound);
}
return map;
}
export function resolveRefs(content, compound, references, options, anchorMap, slugMap, pagePathMap) {
function anchor(refid) {
return anchorMap?.get(refid) ?? refid;
}
function outputPath(dest) {
const mappedPath = pagePathMap?.get(dest.refid);
if (mappedPath) {
return mappedPath;
}
if (slugMap) {
const slug = slugMap.get(dest.refid);
if (slug)
return `${slug}.html`;
// Fallback: try parent group/namespace
if (dest.parent) {
const parentSlug = slugMap.get(dest.parent.refid);
if (parentSlug)
return `${parentSlug}.html`;
}
}
return compoundPath(dest, options);
}
function hrefTo(dest, refid) {
const targetPath = outputPath(dest);
const currentPath = outputPath(compound);
if (targetPath === currentPath) {
return `#${anchor(refid)}`;
}
if (slugMap) {
return `${targetPath}#${anchor(refid)}`;
}
const relPath = relative(dirname(currentPath), targetPath).replace(/\\/g, '/');
return `${relPath || targetPath}#${anchor(refid)}`;
}
function mappedHref(refid) {
const targetPath = pagePathMap?.get(refid);
if (!targetPath)
return undefined;
const currentPath = outputPath(compound);
if (targetPath === currentPath) {
return `#${anchor(refid)}`;
}
if (slugMap) {
return `${targetPath}#${anchor(refid)}`;
}
const relPath = relative(dirname(currentPath), targetPath).replace(/\\/g, '/');
return `${relPath || targetPath}#${anchor(refid)}`;
}
return content.replace(/\{#ref ([^ ]+) #\}/g, (_, refid) => {
const ref = references[refid];
if (!ref)
return `#${anchor(refid)}`;
const page = findParent(ref, ['page']);
if (page) {
return hrefTo(page, refid);
}
const directHref = mappedHref(refid);
if (directHref) {
return directHref;
}
if (options.groups || slugMap) {
const dest = findParent(ref, ['class', 'struct', 'interface', 'enum', 'group', 'namespace']);
if (!dest)
return `#${anchor(refid)}`;
return hrefTo(dest, refid);
}
if (options.classes) {
const dest = findParent(ref, ['namespace', 'class', 'struct']);
if (!dest)
return `#${anchor(refid)}`;
return hrefTo(dest, refid);
}
if (compound.kind === 'page') {
return hrefTo(compound.parent, refid);
}
return `#${anchor(refid)}`;
});
}
/**
* Calculate the output file path for a compound.
*/
export function compoundPath(compound, options) {
if (compound.kind === 'page') {
return `${dirname(options.output)}/page-${compound.name}.md`;
}
if (compound.kind === 'index' && (options.groups || options.classes)) {
return `${dirname(options.output)}/api.md`;
}
if (options.groups && compound.kind === 'group') {
return utilFormat(options.output, compound.groupname);
}
if (options.classes) {
return utilFormat(options.output, safePathSegment(compound.name));
}
if (options.groups) {
return utilFormat(options.output, compound.groupname);
}
return options.output;
}
/**
* Render a compound's contents to a string, resolving refs.
*/
export function renderCompound(compound, contents, references, options, anchorMap, slugMap, pagePathMap) {
const resolved = contents.map((content) => content ? resolveRefs(content, compound, references, options, anchorMap, slugMap, pagePathMap) : '');
return resolved.filter(Boolean).join('');
}
/**
* Write a compound's rendered contents to file, resolving refs first.
*/
export function writeCompound(compound, contents, references, options, anchorMap, pagePathMap) {
const filepath = compoundPath(compound, options);
const output = renderCompound(compound, contents, references, options, anchorMap, undefined, pagePathMap);
writeFile(filepath, [output]);
}
/**
* Write content array to a file.
*/
export function writeFile(filepath, contents) {
log.verbose(`Writing: ${filepath}`);
mkdirSync(dirname(filepath), { recursive: true });
const output = contents.filter(Boolean).join('');
writeFileSync(filepath, output, 'utf8');
}
//# sourceMappingURL=helpers.js.map