@redpanda-data/docs-extensions-and-macros 4.6.9 → 4.6.11

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 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.
package/bin/doc-tools.js CHANGED
@@ -273,13 +273,14 @@ After installation, verify with:
273
273
 
274
274
  // Check for C++ standard library headers (critical for tree-sitter compilation)
275
275
  let tempDir = null;
276
+ let compileCmd = null;
276
277
  try {
277
278
  const testProgram = '#include <functional>\nint main() { return 0; }';
278
279
  tempDir = require('fs').mkdtempSync(require('path').join(require('os').tmpdir(), 'cpp-test-'));
279
280
  const tempFile = require('path').join(tempDir, 'test.cpp');
280
281
  require('fs').writeFileSync(tempFile, testProgram);
281
282
 
282
- const compileCmd = cppCompiler === 'gcc' ? 'gcc' : 'clang++';
283
+ compileCmd = cppCompiler === 'gcc' ? 'gcc' : 'clang++';
283
284
  execSync(`${compileCmd} -x c++ -fsyntax-only "${tempFile}"`, { stdio: 'ignore' });
284
285
  require('fs').rmSync(tempDir, { recursive: true, force: true });
285
286
  } catch {
@@ -293,22 +294,22 @@ After installation, verify with:
293
294
  }
294
295
  fail(`C++ standard library headers are missing or incomplete.
295
296
 
296
- This error typically means:
297
- 1. No C++ compiler is installed, OR
298
- 2. Xcode Command Line Tools are missing/incomplete
297
+ 1. **Test if the issue exists**:
298
+ echo '#include <functional>' | ${compileCmd} -x c++ -fsyntax-only -
299
299
 
300
- To fix this on macOS:
301
- 1. Install Xcode Command Line Tools:
302
- xcode-select --install
303
-
304
- 2. If already installed, reset the developer path:
300
+ 2. **If the test fails, try these fixes in order**:
301
+ **Fix 1**: Reset developer path
305
302
  sudo xcode-select --reset
306
303
 
307
- 3. For Xcode users, ensure correct path:
308
- sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
304
+ **Fix 2**: Force reinstall Command Line Tools
305
+ sudo rm -rf /Library/Developer/CommandLineTools
306
+ xcode-select --install
307
+
308
+ Complete the GUI installation dialog that appears.
309
309
 
310
- 4. Verify the fix:
311
- echo '#include <functional>' | \${cppCompiler || 'clang++'} -x c++ -fsyntax-only -
310
+ 3. **Verify the fix**:
311
+ echo '#include <functional>' | ${compileCmd} -x c++ -fsyntax-only -
312
+ If successful, you should see no output and the command should exit with code 0.
312
313
  `);
313
314
  }
314
315
  }
@@ -31,7 +31,7 @@ function findRelated(labPage, sourceCategoryList, sourceDeploymentType, logger)
31
31
  const targetCategoryList = pageCategories.split(',').map(c => c.trim());
32
32
  const targetDeploymentType = getDeploymentType(targetAttributes)
33
33
  const categoryMatch = hasMatchingCategory(sourceCategoryList, targetCategoryList)
34
- if (categoryMatch && (!targetDeploymentType ||sourceDeploymentType === targetDeploymentType || (targetDeploymentType === 'Docker' && !sourceDeploymentType))) {
34
+ if (categoryMatch && isCompatibleDeployment(sourceDeploymentType, targetDeploymentType)) {
35
35
  return {
36
36
  title: labPage.asciidoc.doctitle,
37
37
  url: labPage.pub.url,
@@ -51,4 +51,21 @@ function getDeploymentType (attributes) {
51
51
 
52
52
  function hasMatchingCategory (sourcePageCategories, targetPageCategories) {
53
53
  return sourcePageCategories.every((category) => targetPageCategories.includes(category))
54
+ }
55
+
56
+ function isCompatibleDeployment (sourceDeploymentType, targetDeploymentType) {
57
+ // If no target deployment type specified, it's compatible with everything
58
+ if (!targetDeploymentType) return true
59
+
60
+ // Cloud pages show only cloud labs
61
+ if (sourceDeploymentType === 'Redpanda Cloud') {
62
+ return targetDeploymentType === 'Redpanda Cloud'
63
+ }
64
+
65
+ // All other cases (Kubernetes, Docker, Linux, or no deployment type) show Docker and Kubernetes
66
+ if (targetDeploymentType === 'Docker' || targetDeploymentType === 'Kubernetes') {
67
+ return true
68
+ }
69
+
70
+ return false
54
71
  }
@@ -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.9",
3
+ "version": "4.6.11",
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",