@redpanda-data/docs-extensions-and-macros 4.6.10 → 4.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.adoc +68 -0
- package/extensions/process-context-switcher.js +236 -0
- package/package.json +3 -1
package/README.adoc
CHANGED
|
@@ -739,6 +739,74 @@ antora:
|
|
|
739
739
|
unlistedPagesHeading: 'Additional Resources'
|
|
740
740
|
```
|
|
741
741
|
|
|
742
|
+
=== Process context switcher
|
|
743
|
+
|
|
744
|
+
This extension processes the `page-context-switcher` attribute to enable cross-version navigation widgets in documentation pages. It automatically replaces "current" references with full resource IDs and injects the context switcher configuration to all referenced target pages, ensuring bidirectional navigation works correctly.
|
|
745
|
+
|
|
746
|
+
The extension finds pages with the `page-context-switcher` attribute, parses the JSON configuration, and:
|
|
747
|
+
|
|
748
|
+
1. Replaces any "current" values with the full resource ID of the current page
|
|
749
|
+
2. Finds all target pages referenced in the switcher configuration
|
|
750
|
+
3. Injects the same context switcher attribute to target pages (with appropriate resource ID mappings)
|
|
751
|
+
4. Builds resource IDs in the format: `version@component:module:relative-path`
|
|
752
|
+
|
|
753
|
+
This enables UI components to render version switchers that work across different versions of the same content.
|
|
754
|
+
|
|
755
|
+
==== Environment variables
|
|
756
|
+
|
|
757
|
+
This extension does not require any environment variables.
|
|
758
|
+
|
|
759
|
+
==== Configuration options
|
|
760
|
+
|
|
761
|
+
This extension does not require any configuration options.
|
|
762
|
+
|
|
763
|
+
==== Registration
|
|
764
|
+
|
|
765
|
+
```yaml
|
|
766
|
+
antora:
|
|
767
|
+
extensions:
|
|
768
|
+
- require: '@redpanda-data/docs-extensions-and-macros/extensions/process-context-switcher'
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
==== Usage
|
|
772
|
+
|
|
773
|
+
Add the `page-context-switcher` attribute to any page where you want cross-version navigation:
|
|
774
|
+
|
|
775
|
+
```asciidoc
|
|
776
|
+
:page-context-switcher: [{"name": "Version 2.x", "to": "24.3@ROOT:console:config/security/authentication.adoc" },{"name": "Version 3.x", "to": "current" }]
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
==== Processed output
|
|
780
|
+
|
|
781
|
+
After processing, the "current" reference is replaced with the full resource ID:
|
|
782
|
+
|
|
783
|
+
```json
|
|
784
|
+
[
|
|
785
|
+
{"name": "Version 2.x", "to": "24.3@ROOT:console:config/security/authentication.adoc"},
|
|
786
|
+
{"name": "Version 3.x", "to": "current@ROOT:console:config/security/authentication.adoc"}
|
|
787
|
+
]
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
The target page (`24.3@ROOT:console:config/security/authentication.adoc`) will also receive the same context switcher configuration with appropriate resource ID mappings.
|
|
791
|
+
|
|
792
|
+
==== UI integration
|
|
793
|
+
|
|
794
|
+
The processed attribute can be used in Handlebars templates:
|
|
795
|
+
|
|
796
|
+
```html
|
|
797
|
+
<div class="context-switcher">
|
|
798
|
+
{{#each (obj page.attributes.page-context-switcher)}}
|
|
799
|
+
<a
|
|
800
|
+
id="{{{this.name}}}"
|
|
801
|
+
href="{{{relativize (resolve-resource this.to)}}}"
|
|
802
|
+
class="context-link {{#if (eq @root.page.url (resolve-resource this.to))}}active{{/if}}"
|
|
803
|
+
>
|
|
804
|
+
<button type="button">{{{this.name}}}</button>
|
|
805
|
+
</a>
|
|
806
|
+
{{/each}}
|
|
807
|
+
</div>
|
|
808
|
+
```
|
|
809
|
+
|
|
742
810
|
== Asciidoc Extensions
|
|
743
811
|
|
|
744
812
|
This section documents the Asciidoc extensions that are provided by this library and how to configure them.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/* Example use in the playbook
|
|
2
|
+
* antora:
|
|
3
|
+
extensions:
|
|
4
|
+
* - require: ./extensions/process-context-switcher.js
|
|
5
|
+
*
|
|
6
|
+
* This extension processes the `page-context-switcher` attribute and:
|
|
7
|
+
* 1. Replaces "current" references with the full resource ID of the current page
|
|
8
|
+
* 2. Injects context switchers into target pages with proper bidirectional linking
|
|
9
|
+
* 3. Automatically adds the current page's version to resource IDs that don't specify one
|
|
10
|
+
*
|
|
11
|
+
* Example context switcher attribute:
|
|
12
|
+
* :page-context-switcher: [{"name": "Version 2.x", "to": "ROOT:console:config/security/authentication.adoc"}, {"name": "Version 3.x", "to": "current"}]
|
|
13
|
+
*
|
|
14
|
+
* Note: You can omit the version from resource IDs - the extension will automatically
|
|
15
|
+
* use the current page's version. So "ROOT:console:file.adoc" becomes "current@ROOT:console:file.adoc"
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
module.exports.register = function ({ config }) {
|
|
21
|
+
const logger = this.getLogger('context-switcher-extension');
|
|
22
|
+
|
|
23
|
+
this.on('documentsConverted', async ({ contentCatalog }) => {
|
|
24
|
+
// Find all pages with context-switcher attribute
|
|
25
|
+
const pages = contentCatalog.findBy({ family: 'page' });
|
|
26
|
+
const pagesToProcess = pages.filter(page =>
|
|
27
|
+
page.asciidoc.attributes['page-context-switcher']
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (pagesToProcess.length === 0) {
|
|
31
|
+
logger.debug('No pages found with page-context-switcher attribute');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
logger.info(`Processing context-switcher attribute for ${pagesToProcess.length} pages`);
|
|
36
|
+
|
|
37
|
+
// Process each page with context-switcher
|
|
38
|
+
for (const page of pagesToProcess) {
|
|
39
|
+
processContextSwitcher(page, contentCatalog, logger);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Process the context-switcher attribute for a page
|
|
45
|
+
* @param {Object} page - The page object
|
|
46
|
+
* @param {Object} contentCatalog - The content catalog
|
|
47
|
+
* @param {Object} logger - Logger instance
|
|
48
|
+
*/
|
|
49
|
+
function processContextSwitcher(page, contentCatalog, logger) {
|
|
50
|
+
const contextSwitcherAttr = page.asciidoc.attributes['page-context-switcher'];
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Parse the JSON attribute
|
|
54
|
+
const contextSwitcher = JSON.parse(contextSwitcherAttr);
|
|
55
|
+
|
|
56
|
+
if (!Array.isArray(contextSwitcher)) {
|
|
57
|
+
logger.warn(`Invalid context-switcher format in ${page.src.path}: expected array`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Get current page's full resource ID
|
|
62
|
+
const currentResourceId = buildResourceId(page);
|
|
63
|
+
logger.debug(`Processing context-switcher for page: ${currentResourceId}`);
|
|
64
|
+
|
|
65
|
+
// Make a copy for processing target pages (before modifying "current")
|
|
66
|
+
const originalContextSwitcher = JSON.parse(JSON.stringify(contextSwitcher));
|
|
67
|
+
|
|
68
|
+
// Track if we made any changes
|
|
69
|
+
let hasChanges = false;
|
|
70
|
+
|
|
71
|
+
// Process each context switcher item
|
|
72
|
+
for (let item of contextSwitcher) {
|
|
73
|
+
if (item.to === 'current') {
|
|
74
|
+
item.to = currentResourceId;
|
|
75
|
+
hasChanges = true;
|
|
76
|
+
logger.debug(`Replaced 'current' with '${currentResourceId}' in context-switcher`);
|
|
77
|
+
} else if (item.to !== currentResourceId) {
|
|
78
|
+
// For non-current items, find and update the target page
|
|
79
|
+
const targetPage = findPageByResourceId(item.to, contentCatalog, page);
|
|
80
|
+
if (targetPage) {
|
|
81
|
+
injectContextSwitcherToTargetPage(targetPage, originalContextSwitcher, currentResourceId, logger);
|
|
82
|
+
} else {
|
|
83
|
+
logger.warn(`Target page not found for resource ID: '${item.to}'. Check that the component, module, and path exist. Enable debug logging to see available pages.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Update the current page's attribute if we made changes
|
|
89
|
+
if (hasChanges) {
|
|
90
|
+
page.asciidoc.attributes['page-context-switcher'] = JSON.stringify(contextSwitcher);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logger.error(`Error parsing context-switcher attribute in ${page.src.path}: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a full resource ID for a page (component:module:relative-path)
|
|
100
|
+
* @param {Object} page - The page object
|
|
101
|
+
* @returns {string} The full resource ID
|
|
102
|
+
*/
|
|
103
|
+
function buildResourceId(page) {
|
|
104
|
+
const component = page.src.component;
|
|
105
|
+
const version = page.src.version;
|
|
106
|
+
const module = page.src.module || 'ROOT';
|
|
107
|
+
const relativePath = page.src.relative;
|
|
108
|
+
|
|
109
|
+
// Format: version@component:module:relative-path
|
|
110
|
+
return `${version}@${component}:${module}:${relativePath}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Normalize a resource ID by adding the current page's version if missing
|
|
115
|
+
* @param {string} resourceId - The resource ID to normalize
|
|
116
|
+
* @param {Object} currentPage - The current page (for context)
|
|
117
|
+
* @returns {string} The normalized resource ID
|
|
118
|
+
*/
|
|
119
|
+
function normalizeResourceId(resourceId, currentPage) {
|
|
120
|
+
// Sanitize input to avoid syntax errors
|
|
121
|
+
if (!resourceId || typeof resourceId !== 'string') {
|
|
122
|
+
throw new Error('Resource ID must be a non-empty string');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!currentPage || !currentPage.src || !currentPage.src.version) {
|
|
126
|
+
throw new Error('Current page must have a valid src.version property');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Trim whitespace and remove any dangerous characters
|
|
130
|
+
const sanitizedResourceId = resourceId.trim();
|
|
131
|
+
if (!sanitizedResourceId) {
|
|
132
|
+
throw new Error('Resource ID cannot be empty or whitespace-only');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Validate basic resource ID format (component:module:path or version@component:module:path)
|
|
136
|
+
if (!/^([^@]+@)?[^:]+:[^:]+:.+$/.test(sanitizedResourceId)) {
|
|
137
|
+
throw new Error(`Invalid resource ID format: '${sanitizedResourceId}'. Expected format: [version@]component:module:path`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// If the resource ID already contains a version (has @), return as-is
|
|
141
|
+
if (sanitizedResourceId.includes('@')) {
|
|
142
|
+
return sanitizedResourceId;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Add the current page's version to the resource ID
|
|
146
|
+
const currentVersion = currentPage.src.version;
|
|
147
|
+
return `${currentVersion}@${sanitizedResourceId}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Find a page by its resource ID using Antora's built-in resolution
|
|
152
|
+
* @param {string} resourceId - The resource ID to find
|
|
153
|
+
* @param {Object} contentCatalog - The content catalog
|
|
154
|
+
* @param {Object} currentPage - The current page (for context)
|
|
155
|
+
* @returns {Object|null} The found page or null
|
|
156
|
+
*/
|
|
157
|
+
function findPageByResourceId(resourceId, contentCatalog, currentPage) {
|
|
158
|
+
try {
|
|
159
|
+
// Normalize the resource ID by adding version if missing
|
|
160
|
+
const normalizedResourceId = normalizeResourceId(resourceId, currentPage);
|
|
161
|
+
|
|
162
|
+
if (normalizedResourceId !== resourceId) {
|
|
163
|
+
logger.debug(`Normalized resource ID '${resourceId}' to '${normalizedResourceId}' using current page version`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
// Use Antora's built-in resource resolution
|
|
168
|
+
const resource = contentCatalog.resolveResource(normalizedResourceId, currentPage.src);
|
|
169
|
+
|
|
170
|
+
if (resource) {
|
|
171
|
+
logger.debug(`Resolved resource ID '${normalizedResourceId}' to: ${buildResourceId(resource)}`);
|
|
172
|
+
return resource;
|
|
173
|
+
} else {
|
|
174
|
+
logger.warn(`Could not resolve resource ID: '${normalizedResourceId}'. Check that the component, module, and path exist.`);
|
|
175
|
+
|
|
176
|
+
// Provide some debugging help by showing available pages in the current component
|
|
177
|
+
const currentComponentPages = contentCatalog.findBy({
|
|
178
|
+
family: 'page',
|
|
179
|
+
component: currentPage.src.component
|
|
180
|
+
}).slice(0, 10);
|
|
181
|
+
|
|
182
|
+
logger.debug(`Available pages in current component '${currentPage.src.component}' (first 10):`,
|
|
183
|
+
currentComponentPages.map(p => `${p.src.version}@${p.src.component}:${p.src.module || 'ROOT'}:${p.src.relative}`));
|
|
184
|
+
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
logger.debug(`Error resolving resource ID '${normalizedResourceId}': ${error.message}`);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
// Handle normalization errors (invalid resource ID format)
|
|
193
|
+
logger.warn(`Invalid resource ID '${resourceId}': ${error.message}`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Inject context-switcher attribute to target page
|
|
200
|
+
* @param {Object} targetPage - The target page to inject to
|
|
201
|
+
* @param {Array} contextSwitcher - The context switcher configuration
|
|
202
|
+
* @param {string} currentPageResourceId - The current page's resource ID
|
|
203
|
+
* @param {Object} logger - Logger instance
|
|
204
|
+
*/
|
|
205
|
+
function injectContextSwitcherToTargetPage(targetPage, contextSwitcher, currentPageResourceId, logger) {
|
|
206
|
+
// Check if target page already has context-switcher attribute
|
|
207
|
+
if (targetPage.asciidoc.attributes['page-context-switcher']) {
|
|
208
|
+
logger.warn(`Target page ${buildResourceId(targetPage)} already has context-switcher attribute. Skipping injection to avoid overwriting existing configuration: ${targetPage.asciidoc.attributes['page-context-switcher']}`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const targetPageResourceId = buildResourceId(targetPage);
|
|
213
|
+
|
|
214
|
+
logger.debug(`Injecting context switcher to target page: ${targetPageResourceId}`);
|
|
215
|
+
|
|
216
|
+
// Create a copy of the context switcher for the target page
|
|
217
|
+
// Simply replace "current" with the original page's resource ID
|
|
218
|
+
const targetContextSwitcher = contextSwitcher.map(item => {
|
|
219
|
+
if (item.to === 'current') {
|
|
220
|
+
logger.debug(`Replacing 'current' with original page: ${currentPageResourceId}`);
|
|
221
|
+
return {
|
|
222
|
+
...item,
|
|
223
|
+
to: currentPageResourceId
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
// All other items stay the same
|
|
227
|
+
return { ...item };
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
logger.debug(`Target context switcher:`, JSON.stringify(targetContextSwitcher, null, 2));
|
|
231
|
+
|
|
232
|
+
// Inject the attribute
|
|
233
|
+
targetPage.asciidoc.attributes['page-context-switcher'] = JSON.stringify(targetContextSwitcher);
|
|
234
|
+
logger.debug(`Successfully injected context-switcher to target page: ${targetPageResourceId}`);
|
|
235
|
+
}
|
|
236
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redpanda-data/docs-extensions-and-macros",
|
|
3
|
-
"version": "4.6.
|
|
3
|
+
"version": "4.6.12",
|
|
4
4
|
"description": "Antora extensions and macros developed for Redpanda documentation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antora",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"require": "./extensions/unlisted-pages.js"
|
|
38
38
|
},
|
|
39
39
|
"./extensions/replace-attributes-in-attachments": "./extensions/replace-attributes-in-attachments.js",
|
|
40
|
+
"./extensions/process-context-switcher": "./extensions/process-context-switcher.js",
|
|
40
41
|
"./extensions/archive-attachments": "./extensions/archive-attachments.js",
|
|
41
42
|
"./extensions/add-pages-to-root": "./extensions/add-pages-to-root.js",
|
|
42
43
|
"./extensions/collect-bloblang-samples": "./extensions/collect-bloblang-samples.js",
|
|
@@ -82,6 +83,7 @@
|
|
|
82
83
|
"@octokit/rest": "^21.0.1",
|
|
83
84
|
"algoliasearch": "^4.17.0",
|
|
84
85
|
"chalk": "4.1.2",
|
|
86
|
+
"commander": "^14.0.0",
|
|
85
87
|
"gulp": "^4.0.2",
|
|
86
88
|
"gulp-connect": "^5.7.0",
|
|
87
89
|
"handlebars": "^4.7.8",
|