UNPKG

moxygen

Version:

Doxygen XML to Markdown converter

259 lines 8.51 kB
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