@redpanda-data/docs-extensions-and-macros 3.11.2 → 4.1.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 +186 -6
- package/extensions/archive-attachments.js +183 -0
- package/extensions/compute-end-of-life.js +56 -0
- package/extensions/replace-attributes-in-attachments.js +210 -62
- package/extensions/util/calculate-eol.js +55 -0
- package/extensions/util/custom-stringify.js +19 -0
- package/extensions/util/format-version.js +7 -0
- package/extensions/util/sanitize-attributes.js +6 -0
- package/package.json +6 -2
- package/extensions/util/customStringify.js +0 -19
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:
|
|
@@ -189,6 +254,90 @@ antora:
|
|
|
189
254
|
- require: '@redpanda-data/docs-extensions-and-macros/extensions/generate-rp-connect-categories'
|
|
190
255
|
```
|
|
191
256
|
|
|
257
|
+
=== Compute end-of-life extension
|
|
258
|
+
|
|
259
|
+
This extension calculates and attaches metadata related to the end-of-life (EoL) status of docs pages, such as nearing EoL, past EoL, and associated EoL dates. This metadata can be used to display relevant banners or messages in docs to inform users about the lifecycle of each version.
|
|
260
|
+
|
|
261
|
+
The extension leverages configuration settings provided in the Antora playbook to apply EoL calculations, specify the warning period, and include links to upgrade documentation and EoL policies.
|
|
262
|
+
|
|
263
|
+
The extension computes whether a page is nearing EoL or past EoL based on the `page-release-date` attribute and configured settings.
|
|
264
|
+
It injects the following attributes into each page, making them available for use in UI templates:
|
|
265
|
+
|
|
266
|
+
- `page-is-nearing-eol`: Indicates if the page is within the warning period before EoL. Calculated using `(page-release-date + supported_months) - warning_weeks`.
|
|
267
|
+
- `page-is-past-eol`: Indicates if the page has passed its EoL. Calculated using `today > (page-release-date + supported_months)`.
|
|
268
|
+
- `page-eol-date`: The calculated EoL date in a human-readable format. Calculated using `page-release-date + supported_months`.
|
|
269
|
+
- `page-eol-doc`: The URL to the supported versions policy or EoL documentation.
|
|
270
|
+
- `page-upgrade-doc`: The Antora resource ID to a document containing upgrade instructions.
|
|
271
|
+
|
|
272
|
+
==== Environment variables
|
|
273
|
+
|
|
274
|
+
This extension does not require any environment variables.
|
|
275
|
+
|
|
276
|
+
==== Configuration options
|
|
277
|
+
|
|
278
|
+
To enable and configure the extension, add it to the `antora.extensions` section of your Antora playbook. Define the EoL settings under the `data.eol_settings` key with the following options:
|
|
279
|
+
|
|
280
|
+
`component` (required):: The component name to which the configuration applies.
|
|
281
|
+
`eol_doc` (required):: A link to the supported versions policy or EoL documentation.
|
|
282
|
+
`upgrade_doc` (required):: A link to the upgrade instructions.
|
|
283
|
+
`supported_months` (optional, default: 12):: The number of months after the publish date when the documentation reaches its EoL.
|
|
284
|
+
`warning_weeks` (optional, default: 6):: The number of weeks before EoL when the documentation is considered to be nearing EoL. Can be used to decide when to notify users of the upcoming EoL status.
|
|
285
|
+
|
|
286
|
+
[,yaml]
|
|
287
|
+
----
|
|
288
|
+
antora:
|
|
289
|
+
extensions:
|
|
290
|
+
- require: '@redpanda-data/docs-extensions-and-macros/extensions/end-of-life'
|
|
291
|
+
data:
|
|
292
|
+
eol_settings:
|
|
293
|
+
- component: 'ROOT'
|
|
294
|
+
supported_months: 18
|
|
295
|
+
warning_weeks: 8
|
|
296
|
+
eol_doc: https://support.redpanda.com/hc/en-us/articles/20617574366743-Redpanda-Supported-Versions
|
|
297
|
+
upgrade_doc: ROOT:upgrade:index.adoc
|
|
298
|
+
----
|
|
299
|
+
|
|
300
|
+
==== Registration example
|
|
301
|
+
|
|
302
|
+
You can register the extension with a customized configuration for different components in your playbook:
|
|
303
|
+
|
|
304
|
+
[,yaml]
|
|
305
|
+
----
|
|
306
|
+
antora:
|
|
307
|
+
extensions:
|
|
308
|
+
- require: '@redpanda-data/docs-extensions-and-macros/extensions/compute-end-of-life'
|
|
309
|
+
data:
|
|
310
|
+
eol_settings:
|
|
311
|
+
- component: 'ROOT'
|
|
312
|
+
supported_months: 12
|
|
313
|
+
warning_weeks: 6
|
|
314
|
+
eol_doc: https://example.com/supported-versions
|
|
315
|
+
upgrade_doc: ROOT:upgrade:index.adoc
|
|
316
|
+
- component: 'example-docs'
|
|
317
|
+
supported_months: 24
|
|
318
|
+
warning_weeks: 12
|
|
319
|
+
eol_doc: https://example.com/example-supported-versions
|
|
320
|
+
upgrade_doc: example-docs:upgrade:index.adoc
|
|
321
|
+
----
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
==== Example Handlebars template:
|
|
325
|
+
|
|
326
|
+
[,handlebars]
|
|
327
|
+
----
|
|
328
|
+
{{#if page.attributes.is-nearing-eol}}
|
|
329
|
+
<div class="banner-container nearing-eol">
|
|
330
|
+
This documentation will reach its end of life on {{page.attributes.eol-date}}.
|
|
331
|
+
Please <a href="{{resolve-resource page.attributes.upgrade-doc}}">upgrade to a supported version</a>.
|
|
332
|
+
</div>
|
|
333
|
+
{{else if page.attributes.is-past-eol}}
|
|
334
|
+
<div class="banner-container past-eol">
|
|
335
|
+
This documentation reached its end of life on {{page.attributes.eol-date}}.
|
|
336
|
+
See our <a href="{{page.attributes.eol-doc}}" target="_blank">supported versions policy</a>.
|
|
337
|
+
</div>
|
|
338
|
+
{{/if}}
|
|
339
|
+
----
|
|
340
|
+
|
|
192
341
|
=== Generate index data
|
|
193
342
|
|
|
194
343
|
The `generate-index-data` extension creates structured index data about doc pages based on configurable filters. The indexed data is saved to a specified attribute in all component versions, enabling the dynamic generation of categorized links and descriptions within your docs using UI templates.
|
|
@@ -463,11 +612,10 @@ antora:
|
|
|
463
612
|
|
|
464
613
|
=== Replace attributes in attachments
|
|
465
614
|
|
|
466
|
-
This extension
|
|
615
|
+
This extension automates the replacement of AsciiDoc attribute placeholders with their respective values within attachment files, such as CSS, HTML, and YAML.
|
|
467
616
|
|
|
468
|
-
[
|
|
617
|
+
[NOTE]
|
|
469
618
|
====
|
|
470
|
-
- This extension processes attachments only if the component version includes the attribute `replace-attributes-in-attachments: true`.
|
|
471
619
|
- The `@` character is removed from attribute values to prevent potential issues with CSS or HTML syntax.
|
|
472
620
|
- If the same attribute placeholder is used multiple times within a file, all instances will be replaced with the attribute's value.
|
|
473
621
|
====
|
|
@@ -478,14 +626,46 @@ This extension does not require any environment variables.
|
|
|
478
626
|
|
|
479
627
|
==== Configuration options
|
|
480
628
|
|
|
481
|
-
|
|
629
|
+
The extension accepts the following configuration options in the Antora playbook:
|
|
482
630
|
|
|
483
|
-
|
|
631
|
+
data.replacements (required):: An array of replacement configurations. Each configuration can target multiple components and define specific file patterns and custom replacement rules.
|
|
632
|
+
|
|
633
|
+
* `components` (array of strings, required): Lists the names of the Antora components whose attachments should undergo attribute replacement.
|
|
634
|
+
|
|
635
|
+
* `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.
|
|
636
|
+
|
|
637
|
+
* `custom_replacements` (array of objects, optional): Defines custom search-and-replace rules to be applied to the matched files. Each rule consists of:
|
|
638
|
+
** `search` (string, required): A regular expression pattern to search for within the file content.
|
|
639
|
+
** `replace` (string, required): The string to replace each match found by the `search` pattern.
|
|
640
|
+
|
|
641
|
+
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.
|
|
642
|
+
|
|
643
|
+
==== Registration Example
|
|
644
|
+
|
|
645
|
+
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
646
|
|
|
485
647
|
```yaml
|
|
486
648
|
antora:
|
|
487
649
|
extensions:
|
|
488
|
-
|
|
650
|
+
- require: './extensions/replace-attributes-in-attachments'
|
|
651
|
+
data:
|
|
652
|
+
replacements:
|
|
653
|
+
- components:
|
|
654
|
+
- 'ROOT'
|
|
655
|
+
- 'redpanda-labs'
|
|
656
|
+
file_patterns:
|
|
657
|
+
- '**/docker-compose.yaml'
|
|
658
|
+
- '**/docker-compose.yml'
|
|
659
|
+
custom_replacements:
|
|
660
|
+
- search: ''\\$\\{CONFIG_FILE:[^}]*\\}''
|
|
661
|
+
replace: 'console.yaml'
|
|
662
|
+
- components:
|
|
663
|
+
- 'API'
|
|
664
|
+
file_patterns:
|
|
665
|
+
- '**/api-docs/**/resources/**'
|
|
666
|
+
custom_replacements:
|
|
667
|
+
- search: '\\$\\{API_ENDPOINT:[^}]*\\}'
|
|
668
|
+
replace: 'https://api.example.com'
|
|
489
669
|
```
|
|
490
670
|
|
|
491
671
|
=== 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
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const calculateEOL = require('./util/calculate-eol.js');
|
|
4
|
+
|
|
5
|
+
module.exports.register = function ({ config }) {
|
|
6
|
+
this.on('contentClassified', ({ contentCatalog }) => {
|
|
7
|
+
const logger = this.getLogger("compute-end-of-life-extension");
|
|
8
|
+
|
|
9
|
+
// Extract EOL configuration from the config object
|
|
10
|
+
const eolConfigs = config.data?.eol_settings || [];
|
|
11
|
+
if (!Array.isArray(eolConfigs) || eolConfigs.length === 0) {
|
|
12
|
+
logger.warn('No end-of-life settings found in configuration.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
eolConfigs.forEach(({ component: componentName, supported_months, warning_weeks, eol_doc, upgrade_doc }) => {
|
|
17
|
+
if (!eol_doc || !upgrade_doc) {
|
|
18
|
+
logger.error(
|
|
19
|
+
`End-of-life configuration for component "${component}" is missing required attributes. ` +
|
|
20
|
+
`Ensure both "eol_doc" and "upgrade_doc" are specified.`
|
|
21
|
+
);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const resolvedEOLMonths = supported_months && supported_months > 0 ? supported_months : 12; // Default: 12 months
|
|
25
|
+
const resolvedWarningWeeks = warning_weeks && warning_weeks > 0 ? warning_weeks : 6; // Default: 6 weeks
|
|
26
|
+
|
|
27
|
+
logger.info(
|
|
28
|
+
`Processing component: ${componentName} with end-of-life months: ${resolvedEOLMonths}, Warning weeks: ${resolvedWarningWeeks}`
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const component = contentCatalog.getComponents().find((c) => c.name === componentName);
|
|
32
|
+
|
|
33
|
+
if (!component) {
|
|
34
|
+
logger.warn(`Component not found: ${componentName}`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
component.versions.forEach(({ asciidoc, version }) => {
|
|
39
|
+
const releaseDate = asciidoc.attributes['page-release-date'];
|
|
40
|
+
if (releaseDate) {
|
|
41
|
+
// Pass resolved configuration to calculateEOL
|
|
42
|
+
const eolInfo = calculateEOL(releaseDate, resolvedEOLMonths, resolvedWarningWeeks, logger);
|
|
43
|
+
Object.assign(asciidoc.attributes, {
|
|
44
|
+
'page-is-nearing-eol': eolInfo.isNearingEOL.toString(),
|
|
45
|
+
'page-is-past-eol': eolInfo.isPastEOL.toString(),
|
|
46
|
+
'page-eol-date': eolInfo.eolDate,
|
|
47
|
+
'page-eol-doc': eol_doc,
|
|
48
|
+
'page-upgrade-doc': upgrade_doc,
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
logger.warn(`No release date found for component: ${componentName}. Make sure to set {page-release-date} in the antora.yml of the component version ${version}.`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = (releaseDate, eolMonths, warningWeeks, logger) => {
|
|
4
|
+
if (!releaseDate) {
|
|
5
|
+
logger.warn(
|
|
6
|
+
'No release date provided. Make sure to set {page-release-date} in the antora.yml of the component.'
|
|
7
|
+
);
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Parse the input date string YYYY-MM-DD in UTC
|
|
12
|
+
const parseUTCDate = (dateString) => {
|
|
13
|
+
const [year, month, day] = dateString.split('-').map(Number);
|
|
14
|
+
// month - 1 because JS months are 0-based
|
|
15
|
+
// https://www.w3schools.com/jsref/jsref_getmonth.asp
|
|
16
|
+
return new Date(Date.UTC(year, month - 1, day));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const targetDate = parseUTCDate(releaseDate);
|
|
20
|
+
|
|
21
|
+
if (isNaN(targetDate.getTime())) {
|
|
22
|
+
logger.warn('Invalid release date format:', releaseDate);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Calculate EoL date in UTC
|
|
27
|
+
const eolDate = new Date(targetDate.getTime()); // clone
|
|
28
|
+
eolDate.setUTCMonth(eolDate.getUTCMonth() + eolMonths);
|
|
29
|
+
|
|
30
|
+
// Calculate the threshold for warning (X weeks before EoL) in UTC
|
|
31
|
+
const weeksBeforeEOL = new Date(eolDate.getTime());
|
|
32
|
+
weeksBeforeEOL.setUTCDate(weeksBeforeEOL.getUTCDate() - warningWeeks * 7);
|
|
33
|
+
|
|
34
|
+
// Compare times in milliseconds to avoid timezone confusion
|
|
35
|
+
const nowMs = Date.now();
|
|
36
|
+
const eolMs = eolDate.getTime();
|
|
37
|
+
const warningMs = weeksBeforeEOL.getTime();
|
|
38
|
+
|
|
39
|
+
const isNearingEOL = nowMs >= warningMs && nowMs < eolMs;
|
|
40
|
+
const isPastEOL = nowMs > eolMs;
|
|
41
|
+
|
|
42
|
+
// Format the EoL date in UTC
|
|
43
|
+
const humanReadableEOLDate = new Intl.DateTimeFormat('en-US', {
|
|
44
|
+
year: 'numeric',
|
|
45
|
+
month: 'long',
|
|
46
|
+
day: 'numeric',
|
|
47
|
+
timeZone: 'UTC', // Ensure UTC in output
|
|
48
|
+
}).format(eolDate);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
isNearingEOL,
|
|
52
|
+
isPastEOL,
|
|
53
|
+
eolDate: humanReadableEOLDate, // For example "March 1, 2025"
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function customStringify(obj) {
|
|
2
|
+
return JSON.stringify(obj, (key, value) => {
|
|
3
|
+
if (value instanceof Map) {
|
|
4
|
+
return {
|
|
5
|
+
type: 'Map',
|
|
6
|
+
value: Array.from(value.entries())
|
|
7
|
+
};
|
|
8
|
+
} else if (value instanceof Set) {
|
|
9
|
+
return {
|
|
10
|
+
type: 'Set',
|
|
11
|
+
value: Array.from(value)
|
|
12
|
+
};
|
|
13
|
+
} else if (typeof value === 'function') {
|
|
14
|
+
return value.toString();
|
|
15
|
+
} else {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
}, 2);
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redpanda-data/docs-extensions-and-macros",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Antora extensions and macros developed for Redpanda documentation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antora",
|
|
@@ -30,8 +30,10 @@
|
|
|
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",
|
|
36
|
+
"./extensions/compute-end-of-life": "./extensions/compute-end-of-life.js",
|
|
35
37
|
"./extensions/generate-rp-connect-categories": "./extensions/generate-rp-connect-categories.js",
|
|
36
38
|
"./extensions/generate-index-data": "./extensions/generate-index-data.js",
|
|
37
39
|
"./extensions/generate-rp-connect-info": "./extensions/generate-rp-connect-info.js",
|
|
@@ -72,9 +74,11 @@
|
|
|
72
74
|
"html-entities": "2.3",
|
|
73
75
|
"js-yaml": "^4.1.0",
|
|
74
76
|
"lodash": "^4.17.21",
|
|
77
|
+
"micromatch": "^4.0.8",
|
|
75
78
|
"node-html-parser": "5.4.2-0",
|
|
76
79
|
"papaparse": "^5.4.1",
|
|
77
|
-
"semver": "^7.6.0"
|
|
80
|
+
"semver": "^7.6.0",
|
|
81
|
+
"tar": "^7.4.3"
|
|
78
82
|
},
|
|
79
83
|
"devDependencies": {
|
|
80
84
|
"@antora/cli": "3.1.4",
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
function customStringify(obj) {
|
|
2
|
-
return JSON.stringify(obj, (key, value) => {
|
|
3
|
-
if (value instanceof Map) {
|
|
4
|
-
return {
|
|
5
|
-
type: 'Map',
|
|
6
|
-
value: Array.from(value.entries())
|
|
7
|
-
};
|
|
8
|
-
} else if (value instanceof Set) {
|
|
9
|
-
return {
|
|
10
|
-
type: 'Set',
|
|
11
|
-
value: Array.from(value)
|
|
12
|
-
};
|
|
13
|
-
} else if (typeof value === 'function') {
|
|
14
|
-
return value.toString();
|
|
15
|
-
} else {
|
|
16
|
-
return value;
|
|
17
|
-
}
|
|
18
|
-
}, 2);
|
|
19
|
-
}
|