@redpanda-data/docs-extensions-and-macros 3.11.1 → 4.0.0

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
@@ -165,6 +165,71 @@ antora:
165
165
  index-latest-only: true
166
166
  ```
167
167
 
168
+ === Archive attachments
169
+
170
+ The `archive-attachments` extension automates the packaging of specific attachment files into a compressed archive (`.tar.gz`) based on configurable patterns. This archive is then made available to the generated site, allowing users to easily download grouped resources such as Docker Compose configurations.
171
+
172
+ This extension enables you to define which files and directories to include in the archive, ensuring that only relevant content is packaged and accessible.
173
+
174
+ ==== Environment variables
175
+
176
+ This extension does not require any environment variables.
177
+
178
+ ==== Configuration options
179
+
180
+ The extension accepts the following options in the Antora playbook.
181
+
182
+ Configure the extension in your Antora playbook by defining an array of archive configurations under `data.archives`. Each archive configuration includes:
183
+
184
+ output_archive (string, required):: The name of the generated archive file.
185
+
186
+ component (string, required):: The name of the Antora component whose attachments should be archived.
187
+
188
+ file_patterns (array of strings, required):: Glob patterns specifying which attachment paths to include in the archive.
189
+
190
+ NOTE: Ensure that `file_patterns` accurately reflect the paths of the attachments you want to archive. Overly broad patterns may include unintended files, while overly restrictive patterns might exclude necessary resources.
191
+
192
+ ==== Example configuration
193
+
194
+ Here's an example configuration to enable the extension:
195
+
196
+ ```yaml
197
+ antora:
198
+ extensions:
199
+ - require: '../docs-extensions-and-macros/extensions/archive-creation-extension.js'
200
+ data:
201
+ archives:
202
+ - output_archive: 'redpanda-quickstart.tar.gz' <1>
203
+ component: 'ROOT' <2>
204
+ file_patterns:
205
+ - '**/test-resources/**/docker-compose/**' <3>
206
+ ```
207
+
208
+ <1> Defines the name of the generated archive placed at the site root.
209
+ <2> Defines the name of the component in which to search for attachments.
210
+ <3> Lists the glob patterns to match attachment paths for inclusion in the archive.
211
+ +
212
+ - `**`: Matches any number of directories.
213
+ - `/test-resources/`: Specifies that the matching should occur within the `test-resources/` directory.
214
+ - `/docker-compose/`: Targets the `docker-compose/` directory and all its subdirectories.
215
+ - `**:` Ensures that all files and nested directories within `docker-compose/` are included.
216
+
217
+ === Behavior with multiple components/versions
218
+
219
+ *Scenario*: Multiple components and/or multiple versions of the same component contain attachments that match the defined file_patterns.
220
+
221
+ *Outcome*: Separate archives for each component version.
222
+
223
+ For each matching (component, version) pair, the extension creates a distinct archive named `<version>-<output_archive>`. For example:
224
+ `24.3-redpanda-quickstart.tar.gz`.
225
+
226
+ These archives are placed at the site root, ensuring they are easily accessible and do not overwrite each other.
227
+
228
+ For the latest version of each component, the extension also adds the archive using the base `output_archive` name. As a result, the latest archives are accessible through a consistent filename, facilitating easy downloads without needing to reference version numbers.
229
+
230
+ Because each archive has a unique filename based on the component version, there is no risk of archives overwriting each other.
231
+ The only exception is the archive for the latest version, which consistently uses the `output_archive` name.
232
+
168
233
  === Component category aggregator
169
234
 
170
235
  This extension maps Redpanda Connect component data into a structured format:
@@ -463,11 +528,10 @@ antora:
463
528
 
464
529
  === Replace attributes in attachments
465
530
 
466
- This extension replaces AsciiDoc attribute placeholders with their respective values in attachment files, such as CSS, HTML, and YAML.
531
+ This extension automates the replacement of AsciiDoc attribute placeholders with their respective values within attachment files, such as CSS, HTML, and YAML.
467
532
 
468
- [IMPORTANT]
533
+ [NOTE]
469
534
  ====
470
- - This extension processes attachments only if the component version includes the attribute `replace-attributes-in-attachments: true`.
471
535
  - The `@` character is removed from attribute values to prevent potential issues with CSS or HTML syntax.
472
536
  - If the same attribute placeholder is used multiple times within a file, all instances will be replaced with the attribute's value.
473
537
  ====
@@ -478,14 +542,46 @@ This extension does not require any environment variables.
478
542
 
479
543
  ==== Configuration options
480
544
 
481
- There are no configurable options for this extension.
545
+ The extension accepts the following configuration options in the Antora playbook:
482
546
 
483
- ==== Registration example
547
+ data.replacements (required):: An array of replacement configurations. Each configuration can target multiple components and define specific file patterns and custom replacement rules.
548
+
549
+ * `components` (array of strings, required): Lists the names of the Antora components whose attachments should undergo attribute replacement.
550
+
551
+ * `file_patterns` (array of strings, required): Glob patterns specifying which attachment files to process. These patterns determine the files that will undergo attribute replacement based on their paths within the content catalog.
552
+
553
+ * `custom_replacements` (array of objects, optional): Defines custom search-and-replace rules to be applied to the matched files. Each rule consists of:
554
+ ** `search` (string, required): A regular expression pattern to search for within the file content.
555
+ ** `replace` (string, required): The string to replace each match found by the `search` pattern.
556
+
557
+ NOTE: Ensure that `file_patterns` accurately reflect the paths of the attachments you want to process. Overly broad patterns may include unintended files, while overly restrictive patterns might exclude necessary resources.
558
+
559
+ ==== Registration Example
560
+
561
+ This is an example of how to register and configure the `replace-attributes-in-attachments` extension in your Antora playbook. This example demonstrates defining multiple replacement configurations, each targeting different components and specifying their own file patterns and custom replacements.
484
562
 
485
563
  ```yaml
486
564
  antora:
487
565
  extensions:
488
- - '@redpanda-data/docs-extensions-and-macros/extensions/replace-attributes-in-attachments'
566
+ - require: './extensions/replace-attributes-in-attachments'
567
+ data:
568
+ replacements:
569
+ - components:
570
+ - 'ROOT'
571
+ - 'redpanda-labs'
572
+ file_patterns:
573
+ - '**/docker-compose.yaml'
574
+ - '**/docker-compose.yml'
575
+ custom_replacements:
576
+ - search: ''\\$\\{CONFIG_FILE:[^}]*\\}''
577
+ replace: 'console.yaml'
578
+ - components:
579
+ - 'API'
580
+ file_patterns:
581
+ - '**/api-docs/**/resources/**'
582
+ custom_replacements:
583
+ - search: '\\$\\{API_ENDPOINT:[^}]*\\}'
584
+ replace: 'https://api.example.com'
489
585
  ```
490
586
 
491
587
  === Aggregate terms
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const tar = require("tar");
6
+ const micromatch = require("micromatch");
7
+ const { PassThrough } = require("stream");
8
+ const os = require("os"); // For accessing the system's temp directory
9
+
10
+ /**
11
+ * Create a tar.gz archive in memory.
12
+ * @param {string} tempDir - The temporary directory containing files to archive.
13
+ * @returns {Promise<Buffer>} - A promise that resolves to the tar.gz buffer.
14
+ */
15
+ async function createTarInMemory(tempDir) {
16
+ return new Promise((resolve, reject) => {
17
+ const pass = new PassThrough();
18
+ const chunks = [];
19
+
20
+ pass.on("data", (chunk) => chunks.push(chunk));
21
+ pass.on("error", (error) => reject(error));
22
+ pass.on("end", () => resolve(Buffer.concat(chunks)));
23
+
24
+ tar
25
+ .create(
26
+ {
27
+ gzip: true,
28
+ cwd: tempDir,
29
+ },
30
+ ["."]
31
+ )
32
+ .pipe(pass)
33
+ .on("error", (err) => reject(err));
34
+ });
35
+ }
36
+
37
+ module.exports.register = function ({ config }) {
38
+ const logger = this.getLogger("archive-attachments-extension");
39
+ const archives = config.data?.archives || [];
40
+
41
+ // Validate configuration
42
+ if (!archives.length) {
43
+ logger.info("No `archives` configurations provided. Archive creation skipped.");
44
+ return;
45
+ }
46
+
47
+ this.on("beforePublish", async ({ contentCatalog, siteCatalog }) => {
48
+ logger.info("Starting archive creation process");
49
+
50
+ const components = contentCatalog.getComponents();
51
+
52
+ for (const archiveConfig of archives) {
53
+ const { output_archive, component, file_patterns } = archiveConfig;
54
+
55
+ // Validate individual archive configuration
56
+ if (!output_archive) {
57
+ logger.warn("An `archive` configuration is missing `output_archive`. Skipping this archive.");
58
+ continue;
59
+ }
60
+ if (!component) {
61
+ logger.warn(`Archive "${output_archive}" is missing component config. Skipping this archive.`);
62
+ continue;
63
+ }
64
+ if (!file_patterns || !file_patterns.length) {
65
+ logger.warn(`Archive "${output_archive}" has no file_patterns config. Skipping this archive.`);
66
+ continue;
67
+ }
68
+
69
+ logger.debug(`Processing archive: ${output_archive} for component: ${component}`);
70
+
71
+ // Find the specified component
72
+ const comp = components.find((c) => c.name === component);
73
+ if (!comp) {
74
+ logger.warn(`Component "${component}" not found. Skipping archive "${output_archive}".`);
75
+ continue;
76
+ }
77
+
78
+ for (const compVer of comp.versions) {
79
+ const compName = comp.name;
80
+ const compVersion = compVer.version;
81
+ const latest = comp.latest?.version || "not latest";
82
+
83
+ const isLatest = latest === compVersion;
84
+
85
+ logger.debug(`Processing component version: ${compName}@${compVersion}`);
86
+
87
+ // Gather attachments for this component version
88
+ const attachments = contentCatalog.findBy({
89
+ component: compName,
90
+ version: compVersion,
91
+ family: "attachment",
92
+ });
93
+
94
+ logger.debug(`Found ${attachments.length} attachments for ${compName}@${compVersion}`);
95
+
96
+ if (!attachments.length) {
97
+ logger.debug(`No attachments found for ${compName}@${compVersion}, skipping.`);
98
+ continue;
99
+ }
100
+
101
+ // Filter attachments based on file_patterns
102
+ const attachmentsSegment = "_attachments/";
103
+ const matched = attachments.filter((attachment) =>
104
+ micromatch.isMatch(attachment.out.path, file_patterns)
105
+ );
106
+
107
+ logger.debug(`Matched ${matched.length} attachments for ${compName}@${compVersion}`);
108
+
109
+ if (!matched.length) {
110
+ logger.debug(`No attachments matched patterns for ${compName}@${compVersion}, skipping.`);
111
+ continue;
112
+ }
113
+
114
+ // Create a temporary directory and write matched attachments
115
+ let tempDir;
116
+ try {
117
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${compName}-${compVersion}-`));
118
+ logger.debug(`Created temporary directory: ${tempDir}`);
119
+
120
+ for (const attachment of matched) {
121
+ const relPath = attachment.out.path;
122
+ const attachmentsIndex = relPath.indexOf(attachmentsSegment);
123
+
124
+ if (attachmentsIndex === -1) {
125
+ logger.warn(`'${attachmentsSegment}' segment not found in path: ${relPath}. Skipping this file.`);
126
+ continue;
127
+ }
128
+
129
+ // Extract the path starting after '_attachments/'
130
+ const relativePath = relPath.substring(attachmentsIndex + attachmentsSegment.length);
131
+
132
+ const destPath = path.join(tempDir, relativePath);
133
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
134
+ fs.writeFileSync(destPath, attachment.contents);
135
+ logger.debug(`Written file to tempDir: ${destPath}`);
136
+ }
137
+
138
+ // Asynchronously create the tar.gz archive in memory
139
+ try {
140
+ logger.debug(`Starting tar creation for ${compName}@${compVersion}`);
141
+ const archiveBuffer = await createTarInMemory(tempDir);
142
+ logger.debug(`Tar creation completed for ${compName}@${compVersion}`);
143
+
144
+ // Define the output path for the archive in the site
145
+ const archiveOutPath = `${compVersion ? compVersion + "-" : ""}${output_archive}`.toLowerCase();
146
+
147
+ // Add the archive to siteCatalog
148
+ siteCatalog.addFile({
149
+ contents: archiveBuffer,
150
+ out: { path: archiveOutPath },
151
+ });
152
+
153
+ if (isLatest) {
154
+ siteCatalog.addFile({
155
+ contents: archiveBuffer,
156
+ out: { path: path.basename(output_archive) },
157
+ });
158
+ }
159
+
160
+ logger.info(`Archive "${archiveOutPath}" added to site.`);
161
+ } catch (error) {
162
+ logger.error(`Error creating tar archive for ${compName}@${compVersion}:`, error);
163
+ continue; // Skip further processing for this version
164
+ }
165
+ } catch (error) {
166
+ logger.error(`Error processing ${compName}@${compVersion}:`, error);
167
+ } finally {
168
+ // Clean up the temporary directory
169
+ if (tempDir) {
170
+ try {
171
+ fs.rmSync(tempDir, { recursive: true, force: true });
172
+ logger.debug(`Cleaned up temporary directory: ${tempDir}`);
173
+ } catch (cleanupError) {
174
+ logger.error(`Error cleaning up tempDir "${tempDir}":`, cleanupError);
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ logger.info("Archive creation process completed");
182
+ });
183
+ };
@@ -1,80 +1,228 @@
1
1
  'use strict';
2
+
2
3
  const semver = require('semver');
4
+ const micromatch = require('micromatch');
5
+ const formatVersion = require('./util/format-version.js');
6
+ const sanitize = require('./util/sanitize-attributes.js');
7
+
8
+ /**
9
+ * Registers the replace attributes extension with support for multiple replacements.
10
+ * Each replacement configuration can target specific components and file patterns.
11
+ *
12
+ * Configuration Structure:
13
+ * data:
14
+ * replacements:
15
+ * - components:
16
+ * - 'ComponentName1'
17
+ * - 'ComponentName2'
18
+ * file_patterns:
19
+ * - 'path/to/attachments/**'
20
+ * - '/another/path/*.adoc'
21
+ * custom_replacements:
22
+ * - search: 'SEARCH_REGEX_PATTERN'
23
+ * replace: 'Replacement String'
24
+ * - ...
25
+ */
26
+ module.exports.register = function ({ config }) {
27
+ const logger = this.getLogger('replace-attributes-extension');
28
+ const replacements = config.data?.replacements || [];
29
+
30
+ // Validate configuration
31
+ if (!replacements.length) {
32
+ logger.info('No `replacements` configurations provided. Replacement process skipped.');
33
+ return;
34
+ }
35
+
36
+ // Precompile all glob matchers for performance
37
+ replacements.forEach((replacementConfig, index) => {
38
+ const { components, file_patterns } = replacementConfig;
39
+
40
+ if (!components || !Array.isArray(components) || !components.length) {
41
+ logger.warn(`Replacement configuration at index ${index} is missing 'components'. Skipping this replacement configuration.`);
42
+ replacementConfig.matchers = null;
43
+ return;
44
+ }
45
+
46
+ if (!file_patterns || !file_patterns.length) {
47
+ logger.warn(`Replacement configuration at index ${index} is missing 'file_patterns'. Skipping this replacement configuration.`);
48
+ replacementConfig.matchers = null;
49
+ return;
50
+ }
51
+
52
+ replacementConfig.matchers = micromatch.matcher(file_patterns, { dot: true });
53
+ });
54
+
55
+ // Precompile all user replacements for each replacement configuration
56
+ replacements.forEach((replacementConfig, index) => {
57
+ const { custom_replacements } = replacementConfig;
58
+ if (!custom_replacements || !custom_replacements.length) {
59
+ replacementConfig.compiledCustomReplacements = [];
60
+ return;
61
+ }
62
+ replacementConfig.compiledCustomReplacements = custom_replacements.map(({ search, replace }) => {
63
+ try {
64
+ return {
65
+ regex: new RegExp(search, 'g'),
66
+ replace,
67
+ };
68
+ } catch (err) {
69
+ logger.error(`Invalid regex pattern in custom_replacements for replacement configuration at index ${index}: "${search}"`, err);
70
+ return null;
71
+ }
72
+ }).filter(Boolean); // Remove any null entries due to invalid regex
73
+ });
3
74
 
4
- module.exports.register = function () {
5
75
  this.on('contentClassified', ({ contentCatalog }) => {
76
+ // Build a lookup table: [componentName][version] -> componentVersion
6
77
  const componentVersionTable = contentCatalog.getComponents().reduce((componentMap, component) => {
7
- componentMap[component.name] = component.versions.reduce((versionMap, componentVersion) => {
8
- versionMap[componentVersion.version] = componentVersion;
78
+ componentMap[component.name] = component.versions.reduce((versionMap, compVer) => {
79
+ versionMap[compVer.version] = compVer;
9
80
  return versionMap;
10
81
  }, {});
11
82
  return componentMap;
12
83
  }, {});
13
84
 
14
- contentCatalog.findBy({ family: 'attachment' }).forEach((attachment) => {
15
- const componentVersion = componentVersionTable[attachment.src.component]?.[attachment.src.version];
16
- if (!componentVersion?.asciidoc?.attributes) return;
85
+ // Iterate over each replacement configuration
86
+ replacements.forEach((replacementConfig, replacementIndex) => {
87
+ const { components, matchers, compiledCustomReplacements } = replacementConfig;
17
88
 
18
- const attributes = Object.entries(componentVersion.asciidoc.attributes).reduce((accum, [name, val]) => {
19
- const stringValue = String(val);
20
- accum[name] = stringValue.endsWith('@') ? sanitizeAttributeValue(stringValue) : stringValue;
21
- return accum;
22
- }, {});
23
-
24
- let contentString = attachment.contents.toString();
25
- let modified = false;
26
-
27
- // Determine if we're using the tag or version attributes
28
- // We introduced tag attributes in Self-Managed 24.3
29
- const isPrerelease = attributes['page-component-version-is-prerelease'];
30
- const componentVersionNumber = formatVersion(componentVersion.version || '');
31
- const useTagAttributes = isPrerelease || (componentVersionNumber && semver.gte(componentVersionNumber, '24.3.0') && componentVersion.title === 'Self-Managed');
32
-
33
- // Set replacements based on the condition
34
- const redpandaVersion = isPrerelease
35
- ? sanitizeAttributeValue(attributes['redpanda-beta-tag'] || '')
36
- : (useTagAttributes
37
- ? sanitizeAttributeValue(attributes['latest-redpanda-tag'] || '')
38
- : sanitizeAttributeValue(attributes['full-version'] || ''));
39
-
40
- const consoleVersion = isPrerelease
41
- ? sanitizeAttributeValue(attributes['console-beta-tag'] || '')
42
- : (useTagAttributes
43
- ? sanitizeAttributeValue(attributes['latest-console-tag'] || '')
44
- : sanitizeAttributeValue(attributes['latest-console-version'] || ''));
45
-
46
- const redpandaRepo = isPrerelease ? 'redpanda-unstable' : 'redpanda';
47
- const consoleRepo = 'console';
48
-
49
- // YAML-specific replacements
50
- if (attachment.out.path.endsWith('.yaml') || attachment.out.path.endsWith('.yml')) {
51
- contentString = replacePlaceholder(contentString, /\$\{REDPANDA_DOCKER_REPO:[^\}]*\}/g, redpandaRepo);
52
- contentString = replacePlaceholder(contentString, /\$\{CONSOLE_DOCKER_REPO:[^\}]*\}/g, consoleRepo);
53
- contentString = replacePlaceholder(contentString, /\$\{REDPANDA_VERSION[^\}]*\}/g, redpandaVersion);
54
- contentString = replacePlaceholder(contentString, /\$\{REDPANDA_CONSOLE_VERSION[^\}]*\}/g, consoleVersion);
55
- modified = true;
89
+ if (!components || !matchers) {
90
+ // Already logged and skipped in precompilation
91
+ return;
56
92
  }
57
93
 
58
- // General attribute replacements (excluding uppercase with underscores)
59
- const result = contentString.replace(/\{([a-z][\p{Alpha}\d_-]*)\}/gu, (match, name) => {
60
- if (!(name in attributes)) return match;
61
- modified = true;
62
- return attributes[name];
63
- });
94
+ components.forEach((componentName) => {
95
+ const comp = contentCatalog.getComponents().find(c => c.name === componentName);
96
+ if (!comp) {
97
+ logger.warn(`Component "${componentName}" not found. Skipping replacement configuration at index ${replacementIndex}.`);
98
+ return;
99
+ }
64
100
 
65
- if (modified) attachment.contents = Buffer.from(result);
66
- });
67
- });
101
+ comp.versions.forEach((compVer) => {
102
+ const compName = comp.name;
103
+ const compVersion = compVer.version;
68
104
 
69
- // Helper function to replace placeholders with attribute values
70
- function replacePlaceholder(content, regex, replacement) {
71
- return content.replace(regex, replacement);
72
- }
105
+ logger.debug(`Processing component version: ${compName}@${compVersion} for replacement configuration at index ${replacementIndex}`);
106
+
107
+ // Gather attachments for this component version
108
+ const attachments = contentCatalog.findBy({
109
+ component: compName,
110
+ version: compVersion,
111
+ family: 'attachment',
112
+ });
113
+
114
+ logger.debug(`Found ${attachments.length} attachments for ${compName}@${compVersion}`);
115
+
116
+ if (!attachments.length) {
117
+ logger.debug(`No attachments found for ${compName}@${compVersion}, skipping.`);
118
+ return;
119
+ }
120
+
121
+ // Filter attachments based on file_patterns
122
+ const matched = attachments.filter((attachment) => {
123
+ const filePath = attachment.out.path;
124
+ return matchers(filePath);
125
+ });
126
+
127
+ logger.debug(`Matched ${matched.length} attachments for ${compName}@${compVersion} in replacement configuration at index ${replacementIndex}`);
73
128
 
74
- const sanitizeAttributeValue = (value) => String(value).replace('@', '');
129
+ if (!matched.length) {
130
+ logger.debug(`No attachments matched patterns for ${compName}@${compVersion} in replacement configuration at index ${replacementIndex}, skipping.`);
131
+ return;
132
+ }
75
133
 
76
- const formatVersion = (version) => {
77
- if (!version) return null;
78
- return semver.valid(version) ? version : `${version}.0`;
79
- };
134
+ // Process each matched attachment
135
+ matched.forEach((attachment) => {
136
+ const { component: attComponent, version: attVersion } = attachment.src;
137
+ const componentVer = componentVersionTable[attComponent]?.[attVersion];
138
+
139
+ if (!componentVer?.asciidoc?.attributes) {
140
+ // Skip attachments without asciidoc attributes
141
+ return;
142
+ }
143
+
144
+ const filePath = attachment.out.path;
145
+
146
+ logger.debug(`Processing attachment: ${filePath} for replacement configuration at index ${replacementIndex}`);
147
+
148
+ // Compute dynamic replacements specific to this componentVersion
149
+ const dynamicReplacements = getDynamicReplacements(componentVer, logger);
150
+
151
+ // Precompile dynamic replacements for this attachment
152
+ const compiledDynamicReplacements = dynamicReplacements.map(({ search, replace }) => ({
153
+ regex: new RegExp(search, 'g'),
154
+ replace,
155
+ }));
156
+
157
+ // Combine dynamic and user replacements
158
+ const allReplacements = [...compiledDynamicReplacements, ...compiledCustomReplacements];
159
+
160
+ // Convert buffer to string once
161
+ let contentStr = attachment.contents.toString('utf8');
162
+
163
+ // Apply all replacements in a single pass
164
+ contentStr = applyAllReplacements(contentStr, allReplacements);
165
+
166
+ // Expand AsciiDoc attributes
167
+ contentStr = expandAsciiDocAttributes(contentStr, componentVer.asciidoc.attributes);
168
+
169
+ // Convert back to buffer
170
+ attachment.contents = Buffer.from(contentStr, 'utf8');
171
+ });
172
+ });
173
+ });
174
+ });
175
+ })
80
176
  };
177
+
178
+ // Build dynamic placeholder replacements
179
+ function getDynamicReplacements(componentVersion, logger) {
180
+ const attrs = componentVersion.asciidoc.attributes;
181
+ const isPrerelease = attrs['page-component-version-is-prerelease'];
182
+ const versionNum = formatVersion(componentVersion.version || '', semver);
183
+ const is24_3plus =
184
+ versionNum && semver.gte(versionNum, '24.3.0') && componentVersion.title === 'Self-Managed';
185
+ const useTagAttributes = isPrerelease || is24_3plus;
186
+
187
+ // Derive Redpanda / Console versions
188
+ const redpandaVersion = isPrerelease
189
+ ? sanitize(attrs['redpanda-beta-tag'] || '')
190
+ : useTagAttributes
191
+ ? sanitize(attrs['latest-redpanda-tag'] || '')
192
+ : sanitize(attrs['full-version'] || '');
193
+ const consoleVersion = isPrerelease
194
+ ? sanitize(attrs['console-beta-tag'] || '')
195
+ : useTagAttributes
196
+ ? sanitize(attrs['latest-console-tag'] || '')
197
+ : sanitize(attrs['latest-console-version'] || '');
198
+
199
+ const redpandaRepo = isPrerelease ? 'redpanda-unstable' : 'redpanda';
200
+ const consoleRepo = 'console';
201
+
202
+ return [
203
+ { search: '\\$\\{REDPANDA_DOCKER_REPO:[^}]*\\}', replace: redpandaRepo },
204
+ { search: '\\$\\{CONSOLE_DOCKER_REPO:[^}]*\\}', replace: consoleRepo },
205
+ { search: '\\$\\{REDPANDA_VERSION[^}]*\\}', replace: redpandaVersion },
206
+ { search: '\\$\\{REDPANDA_CONSOLE_VERSION[^}]*\\}', replace: consoleVersion },
207
+ ];
208
+ }
209
+
210
+ // Apply an array of { regex, replace } to a string in a single pass
211
+ function applyAllReplacements(content, replacements) {
212
+ // Sort replacements by descending length of regex source to handle overlapping patterns
213
+ replacements.sort((a, b) => b.regex.source.length - a.regex.source.length);
214
+
215
+ replacements.forEach(({ regex, replace }) => {
216
+ content = content.replace(regex, replace);
217
+ });
218
+
219
+ return content;
220
+ }
221
+
222
+ // Expand all existing attributes
223
+ function expandAsciiDocAttributes(content, attributes) {
224
+ return content.replace(/\{([a-z][\p{Alpha}\d_-]*)\}/gu, (match, name) => {
225
+ if (!(name in attributes)) return match;
226
+ return sanitize(attributes[name]);
227
+ });
228
+ }
@@ -0,0 +1,7 @@
1
+ /* -----------------------------
2
+ Utility: ensure valid semver or fallback
3
+ ----------------------------- */
4
+ module.exports = (version, semver) => {
5
+ if (!version) return null;
6
+ return semver.valid(version) ? version : `${version}.0`;
7
+ }
@@ -0,0 +1,6 @@
1
+ /* -----------------------------
2
+ Utility: remove trailing '@'
3
+ ----------------------------- */
4
+ module.exports = (val) => {
5
+ return String(val).replace('@', '');
6
+ }
@@ -704,7 +704,7 @@ module.exports.register = function (registry, context) {
704
704
  const requiresEnterprise = componentRows.some(row => row.is_licensed.toLowerCase() === 'yes');
705
705
  if (requiresEnterprise) {
706
706
  enterpriseLicenseInfo = `
707
- <p><strong>License</strong>: This component requires an <a href="https://docs.redpanda.com/redpanda-connect/get-started/licensing/" target="_blank">Enterprise license</a>. To upgrade, go to the <a href="https://www.redpanda.com/upgrade" target="_blank" rel="noopener">Redpanda website</a>.</p>`;
707
+ <p><strong>License</strong>: This component requires an <a href="https://docs.redpanda.com/redpanda-connect/get-started/licensing/" target="_blank">enterprise license</a>. You can either <a href="https://www.redpanda.com/upgrade" target="_blank">upgrade to an Enterprise Edition license</a>, or <a href="http://redpanda.com/try-enterprise" target="_blank" rel="noopener">generate a trial license key</a> that's valid for 30 days.</p>`;
708
708
  }
709
709
  }
710
710
  const isCloudSupported = componentRows.some(row => row.is_cloud_supported === 'y');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "3.11.1",
3
+ "version": "4.0.0",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -30,6 +30,7 @@
30
30
  "require": "./extensions/unlisted-pages.js"
31
31
  },
32
32
  "./extensions/replace-attributes-in-attachments": "./extensions/replace-attributes-in-attachments.js",
33
+ "./extensions/archive-attachments": "./extensions/archive-attachments.js",
33
34
  "./extensions/add-pages-to-root": "./extensions/add-pages-to-root.js",
34
35
  "./extensions/collect-bloblang-samples": "./extensions/collect-bloblang-samples.js",
35
36
  "./extensions/generate-rp-connect-categories": "./extensions/generate-rp-connect-categories.js",
@@ -72,9 +73,11 @@
72
73
  "html-entities": "2.3",
73
74
  "js-yaml": "^4.1.0",
74
75
  "lodash": "^4.17.21",
76
+ "micromatch": "^4.0.8",
75
77
  "node-html-parser": "5.4.2-0",
76
78
  "papaparse": "^5.4.1",
77
- "semver": "^7.6.0"
79
+ "semver": "^7.6.0",
80
+ "tar": "^7.4.3"
78
81
  },
79
82
  "devDependencies": {
80
83
  "@antora/cli": "3.1.4",