@redpanda-data/docs-extensions-and-macros 3.6.6 → 3.7.1

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
@@ -226,7 +226,7 @@ antora:
226
226
 
227
227
  === Global attributes
228
228
 
229
- This extension collects Asciidoc attributes from the {url-playbook}[`shared` component] and makes them available to all component versions. Having global attributes is useful for consistent configuration of local and production builds.
229
+ This extension collects Asciidoc attributes from the {url-playbook}[`shared` component] or a local YAML file and makes them available to all component versions. Having global attributes is useful for consistent configuration of local and production builds.
230
230
 
231
231
  ==== Environment variables
232
232
 
@@ -234,16 +234,21 @@ This extension does not require any environment variables.
234
234
 
235
235
  ==== Configuration options
236
236
 
237
- There are no configurable options for this extension.
237
+ The extension accepts the following configuration options:
238
+
239
+ attributespath (optional):: Specifies the path to a local YAML file that contains global attributes. If this is provided, the extension will load attributes from this file first. If this path is not provided or no valid attributes are found in the file, the extension will fall back to loading attributes from the `shared` component.
238
240
 
239
241
  ==== Registration example
240
242
 
241
- ```yaml
243
+ ```yml
242
244
  antora:
243
245
  extensions:
244
246
  - require: '@redpanda-data/docs-extensions-and-macros/extensions/add-global-attributes'
247
+ attributespath: './local-attributes.yml'
245
248
  ```
246
249
 
250
+ In this example, the `attributespath` option points to a local YAML file (`./local-attributes.yml`), which contains the global attributes. The extension will load attributes from this file first before falling back to the `shared` component.
251
+
247
252
  === Produce redirects (customization of core Antora)
248
253
 
249
254
  This extension replaces the default https://gitlab.com/antora/antora/-/tree/v3.1.x/packages/redirect-producer[`produceRedirects()` function] in Antora to handle redirect loops caused by https://docs.antora.org/antora/latest/page/page-aliases/[page aliases]. Normally, page aliases in Antora are used to resolve outdated links without causing issues. However, with https://docs.antora.org/antora/latest/playbook/urls-html-extension-style/#html-extension-style-key[`indexify`], the same URL may inadvertently be used for both the source and target of a redirect, leading to loops. This problem is https://antora.zulipchat.com/#narrow/stream/282400-users/topic/Redirect.20Loop.20Issue.20with.20Page.20Renaming.20and.20Indexify/near/433691700[recognized as a bug] in core Antora. For example, creating a page alias for `modules/manage/security/authorization.adoc` to point to `modules/manage/security/authorization/index.adoc' can lead to a redirect loop where `manage/security/authorization/` points to `manage/security/authorization/`. Furthermore, omitting the alias would lead to `xref not found` errors because Antora relies on the alias to resolve the old xrefs. This extension is necessary until such behaviors are natively supported or fixed in Antora core.
@@ -305,16 +310,33 @@ This extension does not require any environment variables.
305
310
 
306
311
  ==== Configuration options
307
312
 
308
- There are no configurable options for this extension.
313
+ The extension accepts the following configuration options:
314
+
315
+ termspath (optional):: Specifies the path to a local directory containing term files (in `.adoc` format). If this path is provided, the extension will attempt to load terms from this directory first. If this path is not provided or no valid terms are found in the specified directory, the extension will fall back to loading terms from the `shared` component.
316
+
317
+ Term files should follow the following structure:
318
+
319
+ ```asciidoc
320
+ :category: Documentation
321
+ :hover-text: This is a description of the term.
322
+ :link: https://example.com
323
+
324
+ == Term Title
325
+
326
+ This is the detailed description of the term.
327
+ ```
309
328
 
310
329
  ==== Registration example
311
330
 
312
- ```yaml
331
+ ```yml
313
332
  antora:
314
333
  extensions:
315
- - '@redpanda-data/docs-extensions-and-macros/extensions/aggregate-terms'
334
+ - require: '@redpanda-data/docs-extensions-and-macros/extensions/aggregate-terms'
335
+ termspath: './local-terms/'
316
336
  ```
317
337
 
338
+ In this example, the `termspath` option points to a local directory (./local-terms/), where the term files are stored. The extension will load terms from this directory first before falling back to the `shared` component.
339
+
318
340
  === Unlisted pages
319
341
 
320
342
  This extension identifies and logs any pages that aren't listed in the navigation (nav) file of each version of each component. It then optionally adds these unlisted pages to the end of the navigation tree under a configurable heading.
@@ -4,33 +4,83 @@
4
4
  * - require: ./extensions/add-global-attributes.js
5
5
  */
6
6
 
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const yaml = require('js-yaml');
10
+ const _ = require('lodash');
11
+
12
+ const ATTRIBUTES_PATH = 'modules/ROOT/partials/'; // Default path within the 'shared' component
13
+
7
14
  module.exports.register = function ({ config }) {
8
- const yaml = require('js-yaml');
9
15
  const logger = this.getLogger('global-attributes-extension');
10
- const chalk = require('chalk')
11
- const _ = require('lodash');
16
+ const chalk = require('chalk');
17
+
18
+ /**
19
+ * Load global attributes from a specified local file if provided.
20
+ */
21
+ function loadLocalAttributes(siteCatalog, localAttributesFile) {
22
+ try {
23
+ const resolvedPath = path.resolve(localAttributesFile);
24
+ if (!fs.existsSync(resolvedPath)) {
25
+ logger.warn(`Local attributes file "${localAttributesFile}" does not exist.`);
26
+ return false; // Return false if the local file doesn't exist
27
+ }
28
+
29
+ const fileContents = fs.readFileSync(resolvedPath, 'utf8');
30
+ const fileAttributes = yaml.load(fileContents);
31
+
32
+ siteCatalog.attributeFile = _.merge({}, fileAttributes);
33
+ console.log(chalk.green(`Loaded global attributes from local file "${localAttributesFile}".`));
34
+ return true; // Return true if the local attributes were successfully loaded
35
+
36
+ } catch (error) {
37
+ logger.error(`Error loading local attributes from "${localAttributesFile}": ${error.message}`);
38
+ return false; // Return false if an error occurs
39
+ }
40
+ }
12
41
 
13
42
  this.on('contentAggregated', ({ siteCatalog, contentAggregate }) => {
14
43
  try {
15
- for (const component of contentAggregate) {
16
- if (component.name === 'shared') {
17
- const attributeFiles = component.files.filter(file => file.path.startsWith('modules/ROOT/partials/') && file.path.endsWith('.yml'));
18
- if (!attributeFiles.length) {
19
- logger.warn("No YAML attributes files found in 'shared' component.");
20
- } else {
21
- siteCatalog.attributeFile = attributeFiles.reduce((acc, file) => {
22
- const fileAttributes = yaml.load(file.contents.toString('utf8'));
23
- return _.merge(acc, fileAttributes);
24
- }, {});
25
- console.log(chalk.green('Loaded global attributes from shared component.'));
44
+ let attributesLoaded = false;
45
+
46
+ // Try to load attributes from the local file if provided
47
+ if (config.attributespath) {
48
+ attributesLoaded = loadLocalAttributes(siteCatalog, config.attributespath);
49
+ }
50
+
51
+ // If no local attributes were loaded, fallback to the 'shared' component
52
+ if (!attributesLoaded) {
53
+ let sharedComponentFound = false;
54
+
55
+ for (const component of contentAggregate) {
56
+ if (component.name === 'shared') {
57
+ sharedComponentFound = true;
58
+ const attributeFiles = component.files.filter(file => (file.path.startsWith(ATTRIBUTES_PATH) && (file.path.endsWith('.yml') || file.path.endsWith('.yaml'))));
59
+
60
+ if (!attributeFiles.length) {
61
+ logger.warn(`No YAML attributes files found in 'shared' component in ${ATTRIBUTES_PATH}.`);
62
+ } else {
63
+ siteCatalog.attributeFile = attributeFiles.reduce((acc, file) => {
64
+ const fileAttributes = yaml.load(file.contents.toString('utf8'));
65
+ return _.merge(acc, fileAttributes);
66
+ }, {});
67
+ console.log(chalk.green('Loaded global attributes from shared component.'));
68
+ }
69
+ break;
26
70
  }
27
- break;
71
+ }
72
+
73
+ // If no 'shared' component is found, log a warning
74
+ if (!sharedComponentFound) {
75
+ logger.warn("No component named 'shared' found in the content and no valid local attributes file provided. Global attributes will not be available. You may see Asciidoc warnings about missing attributes and some behavior may not work as expected.");
28
76
  }
29
77
  }
78
+
30
79
  } catch (error) {
31
80
  logger.error(`Error loading attributes: ${error.message}`);
32
81
  }
33
82
  })
83
+
34
84
  .on('contentClassified', async ({ siteCatalog, contentCatalog }) => {
35
85
  const components = await contentCatalog.getComponents();
36
86
  for (let i = 0; i < components.length; i++) {
@@ -39,7 +89,7 @@ module.exports.register = function ({ config }) {
39
89
  if (siteCatalog.attributeFile) {
40
90
  asciidoc.attributes = _.merge({}, siteCatalog.attributeFile, asciidoc.attributes);
41
91
  }
42
- })
92
+ });
43
93
  }
44
- })
45
- }
94
+ });
95
+ };
@@ -4,10 +4,18 @@
4
4
  * - require: ./extensions/aggregate-terms.js
5
5
  */
6
6
 
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const TERMS_PATH = 'modules/terms/partials/'; // Default path within the 'shared' component
11
+
7
12
  module.exports.register = function ({ config }) {
8
13
  const logger = this.getLogger('term-aggregation-extension');
9
14
  const chalk = require('chalk');
10
15
 
16
+ /**
17
+ * Function to process term content, extracting hover text and links.
18
+ */
11
19
  function processTermContent(termContent) {
12
20
  const hoverTextMatch = termContent.match(/:hover-text: (.*)/);
13
21
  const hoverText = hoverTextMatch ? hoverTextMatch[1] : '';
@@ -27,32 +35,91 @@ module.exports.register = function ({ config }) {
27
35
  return termContent;
28
36
  }
29
37
 
38
+ /**
39
+ * Load terms from a specified local path if provided.
40
+ */
41
+ function loadLocalTerms(siteCatalog, termsPath) {
42
+ try {
43
+ const resolvedPath = path.resolve(termsPath);
44
+ if (!fs.existsSync(resolvedPath)) {
45
+ logger.warn(`Local terms path "${termsPath}" does not exist.`);
46
+ return false;
47
+ }
48
+
49
+ const termFiles = fs.readdirSync(resolvedPath).filter(file => file.endsWith('.adoc'));
50
+ if (!termFiles.length) {
51
+ logger.warn(`No term files found in local path "${termsPath}".`);
52
+ return false;
53
+ }
54
+
55
+ termFiles.forEach(file => {
56
+ const filePath = path.join(resolvedPath, file);
57
+ const termContent = fs.readFileSync(filePath, 'utf8');
58
+ const categoryMatch = /:category: (.*)/.exec(termContent);
59
+ const category = categoryMatch ? categoryMatch[1] : 'Miscellaneous';
60
+
61
+ const formattedCategory = category.charAt(0).toUpperCase() + category.slice(1);
62
+
63
+ if (!siteCatalog.termsByCategory[formattedCategory]) {
64
+ siteCatalog.termsByCategory[formattedCategory] = [];
65
+ }
66
+
67
+ siteCatalog.termsByCategory[formattedCategory].push({ name: file, content: termContent });
68
+ });
69
+
70
+ console.log(chalk.green(`Categorized terms from local terms path "${termsPath}".`));
71
+ return true;
72
+
73
+ } catch (error) {
74
+ logger.error(`Error loading local terms from "${termsPath}": ${error.message}`);
75
+ return false;
76
+ }
77
+ }
78
+
30
79
  this.on('contentAggregated', ({ siteCatalog, contentAggregate }) => {
31
80
  try {
32
81
  siteCatalog.termsByCategory = {};
82
+ let termsLoaded = false;
83
+
84
+ // Try to load terms from the local path if provided
85
+ if (config.termspath) {
86
+ termsLoaded = loadLocalTerms(siteCatalog, config.termspath);
87
+ }
33
88
 
34
- for (const component of contentAggregate) {
35
- if (component.name === 'shared') {
36
- const termFiles = component.files.filter(file => file.path.includes('modules/terms/partials/'));
89
+ // If no local terms were loaded, fallback to the 'shared' component
90
+ if (!termsLoaded) {
91
+ let sharedComponentFound = false;
37
92
 
38
- termFiles.forEach(file => {
39
- const termContent = file.contents.toString('utf8');
40
- const categoryMatch = /:category: (.*)/.exec(termContent);
41
- var category = categoryMatch ? categoryMatch[1] : 'Miscellaneous'; // Default category
93
+ for (const component of contentAggregate) {
94
+ if (component.name === 'shared') {
95
+ sharedComponentFound = true;
96
+ const termFiles = component.files.filter(file => file.path.includes(TERMS_PATH));
42
97
 
43
- category = category.charAt(0).toUpperCase() + category.slice(1);
98
+ termFiles.forEach(file => {
99
+ const termContent = file.contents.toString('utf8');
100
+ const categoryMatch = /:category: (.*)/.exec(termContent);
101
+ const category = categoryMatch ? categoryMatch[1] : 'Miscellaneous';
44
102
 
45
- if (!siteCatalog.termsByCategory[category]) {
46
- siteCatalog.termsByCategory[category] = [];
47
- }
103
+ const formattedCategory = category.charAt(0).toUpperCase() + category.slice(1);
48
104
 
49
- siteCatalog.termsByCategory[category].push({ name: file.basename, content: termContent });
50
- });
105
+ if (!siteCatalog.termsByCategory[formattedCategory]) {
106
+ siteCatalog.termsByCategory[formattedCategory] = [];
107
+ }
51
108
 
52
- console.log(chalk.green('Categorized terms from shared component.'));
53
- break;
109
+ siteCatalog.termsByCategory[formattedCategory].push({ name: file.basename, content: termContent });
110
+ });
111
+
112
+ console.log(chalk.green('Categorized terms from shared component.'));
113
+ break;
114
+ }
115
+ }
116
+
117
+ // If no 'shared' component is found, log a warning
118
+ if (!sharedComponentFound) {
119
+ logger.warn(`No component named 'shared' found in the content and no valid local terms path provided. Terms will not be added to the glossary.`);
54
120
  }
55
121
  }
122
+
56
123
  } catch (error) {
57
124
  logger.error(`Error categorizing terms: ${error.message}`);
58
125
  }
@@ -1,16 +1,15 @@
1
1
  'use strict'
2
- const https = require('https');
2
+ const fs = require('fs');
3
+ const path = require('path');
3
4
  const Papa = require('papaparse');
4
5
 
5
- const CSV_PATH = 'redpanda_connect.csv'
6
+ const CSV_PATH = 'internal/plugins/info.csv'
6
7
  const GITHUB_OWNER = 'redpanda-data'
7
- const GITHUB_REPO = 'rp-connect-docs'
8
+ const GITHUB_REPO = 'connect'
8
9
  const GITHUB_REF = 'main'
9
- /* const csvUrl = 'https://localhost:3000/csv';
10
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; */
11
10
 
12
- module.exports.register = function ({ config,contentCatalog }) {
13
- const logger = this.getLogger('redpanda-connect-info-extension');
11
+ module.exports.register = function ({ config }) {
12
+ const logger = this.getLogger('redpanda-connect-info-extension');
14
13
 
15
14
  async function loadOctokit() {
16
15
  const { Octokit } = await import('@octokit/rest');
@@ -20,21 +19,30 @@ module.exports.register = function ({ config,contentCatalog }) {
20
19
  });
21
20
  }
22
21
 
23
- this.once('contentClassified', async ({ siteCatalog, contentCatalog }) => {
24
- const redpandaConnect = contentCatalog.getComponents().find(component => component.name === 'redpanda-connect')
25
- const redpandaCloud = contentCatalog.getComponents().find(component => component.name === 'redpanda-cloud')
26
- if (!redpandaConnect) return
27
- const pages = contentCatalog.getPages()
22
+ this.once('contentClassified', async ({ contentCatalog }) => {
23
+ const redpandaConnect = contentCatalog.getComponents().find(component => component.name === 'redpanda-connect');
24
+ const redpandaCloud = contentCatalog.getComponents().find(component => component.name === 'redpanda-cloud');
25
+ const preview = contentCatalog.getComponents().find(component => component.name === 'preview');
26
+ if (!redpandaConnect) return;
27
+ const pages = contentCatalog.getPages();
28
+
28
29
  try {
29
- // Fetch CSV data and parse it
30
- const csvData = await fetchCSV();
30
+ // Fetch CSV data (either from local file or GitHub)
31
+ const csvData = await fetchCSV(config.csvpath);
31
32
  const parsedData = Papa.parse(csvData, { header: true, skipEmptyLines: true });
32
- const enrichedData = enrichCsvDataWithUrls(parsedData, pages, logger);
33
- parsedData.data = enrichedData
34
- if(redpandaConnect)
33
+ const enrichedData = translateCsvData(parsedData, pages, logger);
34
+ parsedData.data = enrichedData;
35
+
36
+ if (redpandaConnect) {
35
37
  redpandaConnect.latest.asciidoc.attributes.csvData = parsedData;
36
- if(redpandaCloud)
38
+ }
39
+ if (redpandaCloud) {
37
40
  redpandaCloud.latest.asciidoc.attributes.csvData = parsedData;
41
+ }
42
+ // For previewing the data on our extensions site
43
+ if (preview) {
44
+ preview.latest.asciidoc.attributes.csvData = parsedData;
45
+ }
38
46
 
39
47
  } catch (error) {
40
48
  logger.error('Error fetching or parsing CSV data:', error.message);
@@ -42,7 +50,22 @@ module.exports.register = function ({ config,contentCatalog }) {
42
50
  }
43
51
  });
44
52
 
45
- async function fetchCSV() {
53
+ // Check for local CSV file first. If not found, fetch from GitHub
54
+ async function fetchCSV(localCsvPath) {
55
+ if (localCsvPath && fs.existsSync(localCsvPath)) {
56
+ if (path.extname(localCsvPath).toLowerCase() !== '.csv') {
57
+ throw new Error(`Invalid file type: ${localCsvPath}. Expected a CSV file.`);
58
+ }
59
+ logger.info(`Loading CSV data from local file: ${localCsvPath}`);
60
+ return fs.readFileSync(localCsvPath, 'utf8');
61
+ } else {
62
+ logger.info('Local CSV file not found. Fetching from GitHub...');
63
+ return await fetchCsvFromGitHub();
64
+ }
65
+ }
66
+
67
+ // Fetch CSV data from GitHub
68
+ async function fetchCsvFromGitHub() {
46
69
  const octokit = await loadOctokit();
47
70
  try {
48
71
  const { data: fileContent } = await octokit.rest.repos.getContent({
@@ -54,33 +77,87 @@ module.exports.register = function ({ config,contentCatalog }) {
54
77
  return Buffer.from(fileContent.content, 'base64').toString('utf8');
55
78
  } catch (error) {
56
79
  console.error('Error fetching Redpanda Connect catalog from GitHub:', error);
57
- return [];
80
+ return '';
58
81
  }
59
82
  }
60
83
 
61
- function enrichCsvDataWithUrls(parsedData, connectPages, logger) {
84
+ /**
85
+ * Translates the parsed CSV data into our expected format.
86
+ * If "enterprise" is found in the `support` column, it is replaced with "certified" in the output.
87
+ *
88
+ * @param {object} parsedData - The CSV data parsed into an object.
89
+ * @param {array} pages - The list of pages to map the URLs (used for enrichment with URLs).
90
+ * @param {object} logger - The logger used for error handling.
91
+ *
92
+ * @returns {array} - The translated and enriched data.
93
+ */
94
+ function translateCsvData(parsedData, pages, logger) {
62
95
  return parsedData.data.map(row => {
63
96
  // Create a new object with trimmed keys and values
64
97
  const trimmedRow = Object.fromEntries(
65
98
  Object.entries(row).map(([key, value]) => [key.trim(), value.trim()])
66
99
  );
67
- const connector = trimmedRow.connector;
100
+
101
+ // Map fields from the trimmed row to the desired output
102
+ const connector = trimmedRow.name;
68
103
  const type = trimmedRow.type;
69
- let url = '';
70
- for (const file of connectPages) {
104
+ const commercial_name = trimmedRow.commercial_name;
105
+ const available_connect_version = trimmedRow.version;
106
+ const deprecated = trimmedRow.deprecated.toLowerCase() === 'y' ? 'y' : 'n';
107
+ const is_cloud_supported = trimmedRow.cloud.toLowerCase() === 'y' ? 'y' : 'n';
108
+ const cloud_ai = trimmedRow.cloud_with_gpu.toLowerCase() === 'y' ? 'y' : 'n';
109
+ // Handle enterprise to certified conversion and set enterprise license flag
110
+ const originalSupport = trimmedRow.support.toLowerCase();
111
+ const support_level = originalSupport === 'enterprise' ? 'certified' : originalSupport;
112
+ const is_licensed = originalSupport === 'enterprise' ? 'Yes' : 'No';
113
+
114
+ // Redpanda Connect and Cloud enrichment URLs
115
+ let redpandaConnectUrl = '';
116
+ let redpandaCloudUrl = '';
117
+
118
+ // Look for both Redpanda Connect and Cloud URLs
119
+ for (const file of pages) {
120
+ const component = file.src.component;
71
121
  const filePath = file.path;
72
- if (filePath.endsWith(`${connector}.adoc`) && filePath.includes(`pages/${type}s/`)) {
73
- url = `../${type}s/${connector}`;
74
- break;
122
+
123
+ if (
124
+ component === 'redpanda-connect' &&
125
+ filePath.endsWith(`/${connector}.adoc`) &&
126
+ filePath.includes(`pages/${type}s/`)
127
+ ) {
128
+ redpandaConnectUrl = file.pub.url;
129
+ }
130
+
131
+ // Only check for Redpanda Cloud URLs if cloud is supported
132
+ if (
133
+ is_cloud_supported === 'y' &&
134
+ component === 'redpanda-cloud' &&
135
+ filePath.endsWith(`/${connector}.adoc`) &&
136
+ filePath.includes(`${type}s/`)
137
+ ) {
138
+ redpandaCloudUrl = file.pub.url;
75
139
  }
76
140
  }
77
- if (!url) {
78
- logger.warn(`No matching URL found for connector: ${connector} of type: ${type}`);
141
+
142
+ // Log a warning if neither URL was found (only warn for missing cloud if it should support cloud)
143
+ if (!redpandaConnectUrl && (!redpandaCloudUrl && is_cloud_supported === 'y')) {
144
+ logger.info(`Docs missing for: ${connector} of type: ${type}`);
79
145
  }
146
+
147
+ // Return the translated and enriched row
80
148
  return {
81
- ...trimmedRow,
82
- url: url
149
+ connector,
150
+ type,
151
+ commercial_name,
152
+ available_connect_version,
153
+ support_level, // "enterprise" is replaced with "certified"
154
+ deprecated,
155
+ is_cloud_supported,
156
+ cloud_ai,
157
+ is_licensed, // "Yes" if the original support level was "enterprise"
158
+ redpandaConnectUrl,
159
+ redpandaCloudUrl,
83
160
  };
84
161
  });
85
162
  }
86
- }
163
+ }
@@ -11,7 +11,9 @@ module.exports.register = function ({ config }) {
11
11
 
12
12
  this.on('documentsConverted', async ({ contentCatalog, siteCatalog }) => {
13
13
  // Retrieve valid categories and subcategories from site attributes defined in add-global-attributes.js.
14
+ if (!siteCatalog.attributeFile) return logger.warn('No global attributes file available - skipping attribute validation. Check global-attributes-extension for errors')
14
15
  const validCategories = siteCatalog.attributeFile['page-valid-categories'];
16
+ if (!validCategories) return logger.warn('No page-valid-categories attribute found - skipping attribute validation')
15
17
  const categoryMap = createCategoryMap(validCategories);
16
18
  const pages = contentCatalog.findBy({ family: 'page' });
17
19
  pages.forEach((page) => {