From 357d3fd773a8854e13de5c720cff3da2f904cb16 Mon Sep 17 00:00:00 2001 From: John Krug Date: Fri, 21 Nov 2025 18:33:08 +0100 Subject: [PATCH 1/3] Extend vlp by adding a processor for xref:latest or xref:dev Signed-off-by: John Krug --- extensions/versions-latest-prerelease/vlp.js | 161 ++++++++++++++----- 1 file changed, 122 insertions(+), 39 deletions(-) diff --git a/extensions/versions-latest-prerelease/vlp.js b/extensions/versions-latest-prerelease/vlp.js index ba98cac..38dc201 100644 --- a/extensions/versions-latest-prerelease/vlp.js +++ b/extensions/versions-latest-prerelease/vlp.js @@ -13,30 +13,36 @@ // https://docs.antora.org/antora/latest/playbook/configure-urls/ // Author: John Krug +// See: https://docs.antora.org/antora/latest/playbook/configure-urls/ // Requires 'semver' in package.json for version parsing const semver = require("semver"); const path = require("node:path"); const fs = require("node:fs"); -// Enable debug output if VLP_DEBUG env var is set +// Enable debug output if VLP_DEBUG environment variable is set const debug = process.env.VLP_DEBUG === "true"; -/** - * Prints debug messages if debugging is enabled. - * @param {...any} args - Arguments to print. - */ +// Output directory and version info for symlink/file creation +let outputDir = null; +const latestVersionsList = []; +let startPageVersion = null; +let startPageComponent = null; + +// Symlink and file names used for version pointers +const LATEST_SYMLINK = "latest"; +const DEV_SYMLINK = "dev"; +const LATEST_DEV_FILE = "latest_dev.txt"; + +// Print debug messages if enabled, using dynamic filename label. +const debugLabel = `[${require("node:path").basename(__filename)}]`; function dprint(...args) { if (debug) { - console.log("[vlp.js]", ...args); + console.log(debugLabel, ...args); } } -/** - * Creates a symlink at symlinkPath pointing to targetPath. - * If symlinkPath exists and is not a directory, it is removed first. - * Directories are never overwritten to avoid accidental data loss. - */ +// Create symlink, avoiding directories. function createSymlink(targetPath, symlinkPath) { if (fs.existsSync(symlinkPath)) { if (!fs.lstatSync(symlinkPath).isDirectory()) { @@ -50,29 +56,75 @@ function createSymlink(targetPath, symlinkPath) { fs.symlinkSync(targetPath, symlinkPath); } -/** - * Ensures symlink targets are within the intended output directory. - * Prevents directory traversal vulnerabilities. - */ +// Ensure symlink targets are safe. function isSafePath(base, target) { const relative = path.relative(base, target); return !relative.startsWith("..") && !path.isAbsolute(relative); } -// Output directory and version info for symlink/file creation -let outputDir = null; -const latestVersionsList = []; -let startPageVersion = null; -let startPageComponent = null; +// Rewrite xrefs to latest/dev versions. +function modifyXrefsInText(fileText, file, latestVersionsList) { + const regex = new RegExp( + `xref:(${LATEST_SYMLINK}|${DEV_SYMLINK})@([^:]+):([^\s[]+)`, + "g", + ); + let match = regex.exec(fileText); + let newFileText = fileText; + let xrefsModified = 0; + while (match !== null) { + const versionType = match[1]; + const targetComponent = match[2]; + const targetFile = match[3]; + dprint( + `[MODIFIABLE XREF FOUND] Version: ${versionType}, Target Component: ` + + `${targetComponent}, Target File: ${targetFile}, File: ${file.src?.path}`, + ); -// Symlink and file names used for version pointers -const LATEST_SYMLINK = "latest"; -const DEV_SYMLINK = "dev"; -const LATEST_DEV_FILE = "latest_dev.txt"; + // Look up the correct version from latestVersionsList + const compEntry = latestVersionsList.find( + (e) => e.componentName === targetComponent, + ); + let actualVersion = null; + if (compEntry) { + if (versionType === LATEST_SYMLINK && compEntry.latestStableObj) { + actualVersion = compEntry.latestStableObj.version; + } else if (versionType === DEV_SYMLINK && compEntry.latestPrereleaseObj) { + actualVersion = compEntry.latestPrereleaseObj.version; + } + } + if (actualVersion) { + // Build the new xref with full filename + const newXref = `xref:${actualVersion}@${targetComponent}:${targetFile}`; + dprint(`[XREF MODIFIED] ${newXref}`); + // Replace the original xref in newFileText + const originalXref = match[0]; + // Find the line containing the original xref + const lines = newFileText.split(/\r?\n/); + const modifiedLine = lines.find((line) => line.includes(originalXref)); + if (modifiedLine) { + dprint(`[MODIFIED LINE] ${modifiedLine}`); + } + newFileText = newFileText.replace(originalXref, newXref); + xrefsModified++; + } else { + dprint( + `[XREF MODIFIED] No version found for ${versionType} in component ` + + `${targetComponent}`, + ); + } + match = regex.exec(newFileText); + } + // Print summary if any xrefs were modified + if (xrefsModified > 0) { + console.log( + `${debugLabel} Modified ${xrefsModified} xref(s) in ` + + `file: ${file.src?.path}`, + ); + } + return newFileText; +} -/** - * Writes latest_dev.txt for a component, creating the directory if needed. - */ +// Write latest_dev.txt for a component. function writeLatestDevFile(dir, content) { try { fs.mkdirSync(dir, { recursive: true }); @@ -82,15 +134,8 @@ function writeLatestDevFile(dir, content) { } } -/** Antora extension entry point. Registers event handlers for playbookBuilt, - * contentClassified, and sitePublished. - * - * - playbookBuilt: Captures output directory from playbook config - * - contentClassified: Determines latest stable and prerelease versions for - * each component and writes latest_dev.txt files - * - sitePublished: Creates symlinks ('latest', 'dev') for each component to - * their respective versions - */ +// Extension entry point: hooks into playbookBuilt, contentClassified, +// sitePublished. module.exports.register = function () { // Capture output directory for later symlink and file creation @@ -170,6 +215,43 @@ module.exports.register = function () { ); writeLatestDevFile(dirName, fileContent); }); + + contentCatalog.findBy({ mediaType: "text/asciidoc" }).forEach((file) => { + try { + // Skip nav.adoc files, files in the shared component, or undefined + // filename + const basename = file.src?.basename; + const compName = file.component?.name || file.src?.component; + const filename = file.src?.path; + + // Skip files that are nav.adoc, in the shared component, or have no + // filename + if (!filename || basename === "nav.adoc" || compName === "shared") { + return; + } + + // Output component name and version from file.src if available + if (file.src?.component && file.src?.version) { + dprint( + `[SRC COMPONENT INFO] Component: ${file.src.component}, ` + + `Version: ${file.src.version}, File: ${file.src.path}`, + ); + } + + // Scan file for 'xref:latest@' or 'xref:dev@' and modify them + const fileText = file.contents?.toString(); + if (fileText) { + const newFileText = modifyXrefsInText( + fileText, + file, + latestVersionsList, + ); + file.contents = Buffer.from(newFileText); + } + } catch (err) { + console.error(`[vlp.js] Error processing file: ${file.src?.path}`, err); + } + }); }); this.once("sitePublished", () => { @@ -228,8 +310,8 @@ module.exports.register = function () { if (fs.existsSync(latestPath)) { dprint( `Updating index.html: ` + - `Replacing /${startPageComponent}/${startPageVersion}/ ` + - `with /${startPageComponent}/latest/` + `Replacing /${startPageComponent}/${startPageVersion}/ ` + + `with /${startPageComponent}/latest/`, ); // Build a regex to match URLs containing the version // for this component @@ -239,7 +321,8 @@ module.exports.register = function () { fs.copyFileSync(indexPath, backupPath); dprint(`Backed up index.html to ${backupPath}`); const versionPattern = new RegExp( - `(${startPageComponent})/${startPageVersion}(/|\b)`, "g" + `(${startPageComponent})/${startPageVersion}(/|\b)`, + "g", ); // Perform the replacement in index.html content indexContent = indexContent.replace(versionPattern, "$1/latest$2"); From a541e5340223bdfbac0e2679a6a3d0ac403f81e9 Mon Sep 17 00:00:00 2001 From: John Krug Date: Tue, 25 Nov 2025 21:21:21 +0100 Subject: [PATCH 2/3] A little comment tweaking. Signed-off-by: John Krug --- extensions/versions-latest-prerelease/vlp.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/versions-latest-prerelease/vlp.js b/extensions/versions-latest-prerelease/vlp.js index 38dc201..795f037 100644 --- a/extensions/versions-latest-prerelease/vlp.js +++ b/extensions/versions-latest-prerelease/vlp.js @@ -9,6 +9,11 @@ // be configured to 'FollowSymlinks'. This extension puts those symlinks in // place, in the build directory structure, after the Antora build. +// Additionally, this extension modifies 'xref:' links in AsciiDoc files that +// point to 'latest' or 'dev' versions to point to the actual latest stable or +// prerelease version numbers. This ensures that cross-references resolve +// correctly during the Antora build. + // Docs for the standard mechanism: // https://docs.antora.org/antora/latest/playbook/configure-urls/ @@ -218,8 +223,6 @@ module.exports.register = function () { contentCatalog.findBy({ mediaType: "text/asciidoc" }).forEach((file) => { try { - // Skip nav.adoc files, files in the shared component, or undefined - // filename const basename = file.src?.basename; const compName = file.component?.name || file.src?.component; const filename = file.src?.path; From 446cce3f0cf83cba22ab775fa2422e0960e8ca2d Mon Sep 17 00:00:00 2001 From: John Krug Date: Thu, 27 Nov 2025 11:07:21 +0100 Subject: [PATCH 3/3] Adjusted comments and some more debug printing. Regex tweak. Signed-off-by: John Krug --- extensions/versions-latest-prerelease/vlp.js | 75 +++++++++++++++----- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/extensions/versions-latest-prerelease/vlp.js b/extensions/versions-latest-prerelease/vlp.js index 795f037..9e1d13a 100644 --- a/extensions/versions-latest-prerelease/vlp.js +++ b/extensions/versions-latest-prerelease/vlp.js @@ -43,7 +43,10 @@ const LATEST_DEV_FILE = "latest_dev.txt"; const debugLabel = `[${require("node:path").basename(__filename)}]`; function dprint(...args) { if (debug) { - console.log(debugLabel, ...args); + console.log( + debugLabel, + ...args + ); } } @@ -54,7 +57,11 @@ function createSymlink(targetPath, symlinkPath) { fs.unlinkSync(symlinkPath); } else { // If it's a directory, do not touch it - dprint("Not writing", symlinkPath, "because it is a directory"); + dprint( + "Not writing", + symlinkPath, + "because it is a directory" + ); return; } } @@ -67,22 +74,29 @@ function isSafePath(base, target) { return !relative.startsWith("..") && !path.isAbsolute(relative); } -// Rewrite xrefs to latest/dev versions. +// Rewrite xrefs to be latest/dev versions. function modifyXrefsInText(fileText, file, latestVersionsList) { const regex = new RegExp( - `xref:(${LATEST_SYMLINK}|${DEV_SYMLINK})@([^:]+):([^\s[]+)`, + `xref:(${LATEST_SYMLINK}|${DEV_SYMLINK})@([^:]+):([^[]+)`, "g", ); let match = regex.exec(fileText); let newFileText = fileText; let xrefsModified = 0; while (match !== null) { + dprint( + `[XREF REGEX MATCH] Full match: '${match[0]}' | ` + + `Group 1 (version): '${match[1]}' | ` + + `Group 2 (component): '${match[2]}' | ` + + `Group 3 (file): '${match[3]}' | ` + + `File: ${file.src?.path}` + ); const versionType = match[1]; const targetComponent = match[2]; const targetFile = match[3]; dprint( `[MODIFIABLE XREF FOUND] Version: ${versionType}, Target Component: ` + - `${targetComponent}, Target File: ${targetFile}, File: ${file.src?.path}`, + `${targetComponent}, Target File: ${targetFile}, File: ${file.src?.path}` ); // Look up the correct version from latestVersionsList @@ -100,21 +114,25 @@ function modifyXrefsInText(fileText, file, latestVersionsList) { if (actualVersion) { // Build the new xref with full filename const newXref = `xref:${actualVersion}@${targetComponent}:${targetFile}`; - dprint(`[XREF MODIFIED] ${newXref}`); + dprint( + `[XREF MODIFIED] ${newXref}` + ); // Replace the original xref in newFileText const originalXref = match[0]; // Find the line containing the original xref const lines = newFileText.split(/\r?\n/); const modifiedLine = lines.find((line) => line.includes(originalXref)); if (modifiedLine) { - dprint(`[MODIFIED LINE] ${modifiedLine}`); + dprint( + `[MODIFIED LINE] ${modifiedLine}` + ); } newFileText = newFileText.replace(originalXref, newXref); xrefsModified++; } else { dprint( - `[XREF MODIFIED] No version found for ${versionType} in component ` + - `${targetComponent}`, + `[XREF MODIFIED] No replacement version found for ${versionType} ` + + `in component ${targetComponent}` ); } match = regex.exec(newFileText); @@ -123,7 +141,7 @@ function modifyXrefsInText(fileText, file, latestVersionsList) { if (xrefsModified > 0) { console.log( `${debugLabel} Modified ${xrefsModified} xref(s) in ` + - `file: ${file.src?.path}`, + `file: ${file.src?.path}` ); } return newFileText; @@ -135,7 +153,10 @@ function writeLatestDevFile(dir, content) { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, LATEST_DEV_FILE), content, "utf8"); } catch (err) { - console.error(`Failed to write ${LATEST_DEV_FILE} in ${dir}:`, err); + console.error( + `Failed to write ${LATEST_DEV_FILE} in ${dir}:`, + err + ); } } @@ -216,12 +237,13 @@ module.exports.register = function () { dprint( `In contentClassified:\n${component.name}/${LATEST_DEV_FILE} ` + - `will contain\n--------\n${fileContent}--------`, + `will contain\n--------\n${fileContent}--------` ); writeLatestDevFile(dirName, fileContent); }); contentCatalog.findBy({ mediaType: "text/asciidoc" }).forEach((file) => { + dprint("Entered contentClassified file processing loop"); try { const basename = file.src?.basename; const compName = file.component?.name || file.src?.component; @@ -233,17 +255,25 @@ module.exports.register = function () { return; } + dprint( + `[PROCESSING FILE] Component: ${compName}, File: ${filename}` + ); + // Output component name and version from file.src if available if (file.src?.component && file.src?.version) { dprint( `[SRC COMPONENT INFO] Component: ${file.src.component}, ` + - `Version: ${file.src.version}, File: ${file.src.path}`, + `Version: ${file.src.version}, File: ${file.src.path}` ); } // Scan file for 'xref:latest@' or 'xref:dev@' and modify them const fileText = file.contents?.toString(); if (fileText) { + dprint( + `[SCANNING FILE] Scanning for xref:(latest|dev)@ in ` + + `component: ${compName}, file: ${file.src?.path}` + ); const newFileText = modifyXrefsInText( fileText, file, @@ -276,7 +306,7 @@ module.exports.register = function () { "going to create symlink", linkName, "to", - obj.version, + obj.version ); // Symlink points to the version directory const symlinkPath = path.join(dirName, linkName); @@ -313,8 +343,8 @@ module.exports.register = function () { if (fs.existsSync(latestPath)) { dprint( `Updating index.html: ` + - `Replacing /${startPageComponent}/${startPageVersion}/ ` + - `with /${startPageComponent}/latest/`, + `Replacing /${startPageComponent}/${startPageVersion}/ ` + + `with /${startPageComponent}/latest/` ); // Build a regex to match URLs containing the version // for this component @@ -322,7 +352,9 @@ module.exports.register = function () { // Backup index.html before modifying const backupPath = path.join(outputDir, "index.html.bkp"); fs.copyFileSync(indexPath, backupPath); - dprint(`Backed up index.html to ${backupPath}`); + dprint( + `Backed up index.html to ${backupPath}` + ); const versionPattern = new RegExp( `(${startPageComponent})/${startPageVersion}(/|\b)`, "g", @@ -331,11 +363,16 @@ module.exports.register = function () { indexContent = indexContent.replace(versionPattern, "$1/latest$2"); } else { // If 'latest' does not exist, skip the replacement - dprint(`Skipping index.html update: '${latestPath}' does not exist.`); + dprint( + `Skipping index.html update: '${latestPath}' does not exist.` + ); } } fs.writeFileSync(indexPath, indexContent, "utf8"); - dprint("Updated index.html content:", indexContent); + dprint( + "Updated index.html content:", + indexContent + ); } }); };