@redpanda-data/docs-extensions-and-macros
Version:
Antora extensions and macros developed for Redpanda documentation.
217 lines (182 loc) • 6.35 kB
JavaScript
/**
* Updates modules/ROOT/nav.adoc with new connector entries
*/
const fs = require('fs');
const path = require('path');
/**
* Parse nav.adoc and extract connector sections
* @param {string} content - nav.adoc content
* @returns {Object} Sections with their connectors
*/
function parseNav(content) {
const lines = content.split('\n');
const sections = {};
let currentSection = null;
let sectionStartLine = -1;
let sectionEndLine = -1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Detect component type sections
if (line.match(/^\*\* xref:components:(inputs|outputs|processors|scanners|caches|rate_limits|buffers|metrics|tracers)\/about\.adoc\[\]/)) {
// Save previous section end
if (currentSection && sectionStartLine !== -1) {
sections[currentSection].endLine = i - 1;
}
// Extract type
const match = line.match(/components:(\w+)\//);
currentSection = match ? match[1] : null;
if (currentSection) {
sections[currentSection] = {
startLine: i,
endLine: -1,
connectors: []
};
sectionStartLine = i;
}
}
// Collect connector entries
if (currentSection && line.match(/^\*\*\* xref:components:\w+\/[\w_]+\.adoc\[\]/)) {
const match = line.match(/xref:components:\w+\/([\w_]+)\.adoc/);
if (match) {
sections[currentSection].connectors.push({
name: match[1],
line: i,
text: line
});
}
}
// Detect section end (next major section starts)
if (currentSection && line.match(/^\*\* xref:/) && !line.includes(`components:${currentSection}`)) {
sections[currentSection].endLine = i - 1;
currentSection = null;
}
}
// Close last section if needed
if (currentSection && sections[currentSection].endLine === -1) {
sections[currentSection].endLine = lines.length - 1;
}
return { lines, sections };
}
/**
* Insert connectors into nav.adoc in alphabetical order
* @param {string} navPath - Path to nav.adoc
* @param {Array} newConnectors - Array of {type, name} objects
* @returns {Object} Update result
*/
function updateNav(navPath, newConnectors) {
if (!fs.existsSync(navPath)) {
throw new Error(`nav.adoc not found at ${navPath}`);
}
const content = fs.readFileSync(navPath, 'utf8');
const { lines, sections } = parseNav(content);
const updates = [];
const skipped = [];
// Group new connectors by type
const byType = {};
newConnectors.forEach(conn => {
if (!byType[conn.type]) byType[conn.type] = [];
byType[conn.type].push(conn);
});
// Process each type
Object.entries(byType).forEach(([type, connectors]) => {
if (!sections[type]) {
console.warn(`Warning: Section not found for type: ${type}`);
connectors.forEach(c => skipped.push({ ...c, reason: 'section not found' }));
return;
}
const section = sections[type];
const existingNames = new Set(section.connectors.map(c => c.name));
connectors.forEach(conn => {
// Skip if already exists
if (existingNames.has(conn.name)) {
skipped.push({ ...conn, reason: 'already exists' });
return;
}
// Find insertion point (alphabetical order)
const newEntry = `*** xref:components:${type}/${conn.name}.adoc[]`;
let insertIndex = -1;
for (let i = 0; i < section.connectors.length; i++) {
if (conn.name.localeCompare(section.connectors[i].name) < 0) {
insertIndex = section.connectors[i].line;
break;
}
}
// If no insertion point found, append at end of section
if (insertIndex === -1) {
// Find last connector line in section
if (section.connectors.length > 0) {
insertIndex = section.connectors[section.connectors.length - 1].line + 1;
} else {
// Empty section, insert after section header
insertIndex = section.startLine + 1;
}
}
updates.push({
type,
name: conn.name,
insertIndex,
entry: newEntry
});
});
});
// Apply updates (in reverse order to maintain line numbers)
updates.sort((a, b) => b.insertIndex - a.insertIndex);
updates.forEach(update => {
lines.splice(update.insertIndex, 0, update.entry);
});
// Write updated nav.adoc
fs.writeFileSync(navPath, lines.join('\n'));
return {
updated: updates.length,
skippedCount: skipped.length,
updates,
skipped
};
}
/**
* Update nav.adoc from draft files
* @param {Array} draftFiles - Array of draft file objects
* @param {string} navPath - Path to nav.adoc (optional, auto-detects)
* @returns {Object} Update result
*/
function updateNavFromDrafts(draftFiles, navPath = null) {
// Auto-detect nav.adoc location if not provided
if (!navPath) {
const possiblePaths = [
path.resolve(process.cwd(), 'modules/ROOT/nav.adoc'),
path.resolve(process.cwd(), '../rp-connect-docs/modules/ROOT/nav.adoc'),
// Optional: Set RP_CONNECT_DOCS_PATH env var to specify custom location
process.env.RP_CONNECT_DOCS_PATH && path.resolve(process.env.RP_CONNECT_DOCS_PATH, 'modules/ROOT/nav.adoc')
].filter(Boolean);
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
navPath = p;
break;
}
}
if (!navPath) {
console.warn('Warning: Could not find nav.adoc, skipping navigation update');
return { updated: 0, skippedCount: draftFiles.length, error: 'nav.adoc not found' };
}
}
console.log(`📝 Updating navigation: ${navPath}`);
// Filter out cloud-only connectors (they go in partials, not nav)
const nonCloudOnlyFiles = draftFiles.filter(draft => !draft.cloudOnly);
const cloudOnlyFiles = draftFiles.filter(draft => draft.cloudOnly);
if (cloudOnlyFiles.length > 0) {
console.log(` ℹ️ Skipping ${cloudOnlyFiles.length} cloud-only connectors (partials don't need nav entries):`);
cloudOnlyFiles.forEach(draft => {
console.log(` • ${draft.type}/${draft.name}`);
});
}
const connectors = nonCloudOnlyFiles.map(draft => ({
type: draft.type,
name: draft.name
}));
return updateNav(navPath, connectors);
}
module.exports = {
parseNav,
updateNav,
updateNavFromDrafts
};