@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 +102 -6
- package/extensions/archive-attachments.js +183 -0
- package/extensions/replace-attributes-in-attachments.js +210 -62
- package/extensions/util/format-version.js +7 -0
- package/extensions/util/sanitize-attributes.js +6 -0
- package/macros/rp-connect-components.js +1 -1
- package/package.json +5 -2
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
|
|
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
|
-
[
|
|
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
|
-
|
|
545
|
+
The extension accepts the following configuration options in the Antora playbook:
|
|
482
546
|
|
|
483
|
-
|
|
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
|
-
|
|
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,
|
|
8
|
-
versionMap[
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
85
|
+
// Iterate over each replacement configuration
|
|
86
|
+
replacements.forEach((replacementConfig, replacementIndex) => {
|
|
87
|
+
const { components, matchers, compiledCustomReplacements } = replacementConfig;
|
|
17
88
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
if (!
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
101
|
+
comp.versions.forEach((compVer) => {
|
|
102
|
+
const compName = comp.name;
|
|
103
|
+
const compVersion = compVer.version;
|
|
68
104
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|
|
@@ -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">
|
|
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
|
+
"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",
|