@redpanda-data/docs-extensions-and-macros 3.6.5 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.adoc CHANGED
@@ -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,21 @@
1
+ /* Example use in the playbook
2
+ * antora:
3
+ extensions:
4
+ * - require: ./extensions/generate-rp-connect-info.js
5
+ */
6
+
1
7
  'use strict'
2
- const https = require('https');
8
+ const fs = require('fs');
9
+ const path = require('path');
3
10
  const Papa = require('papaparse');
4
11
 
5
12
  const CSV_PATH = 'redpanda_connect.csv'
6
13
  const GITHUB_OWNER = 'redpanda-data'
7
14
  const GITHUB_REPO = 'rp-connect-docs'
8
15
  const GITHUB_REF = 'main'
9
- /* const csvUrl = 'https://localhost:3000/csv';
10
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; */
11
16
 
12
- module.exports.register = function ({ config,contentCatalog }) {
13
- const logger = this.getLogger('redpanda-connect-info-extension');
17
+ module.exports.register = function ({ config }) {
18
+ const logger = this.getLogger('redpanda-connect-info-extension');
14
19
 
15
20
  async function loadOctokit() {
16
21
  const { Octokit } = await import('@octokit/rest');
@@ -20,21 +25,25 @@ module.exports.register = function ({ config,contentCatalog }) {
20
25
  });
21
26
  }
22
27
 
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()
28
+ this.once('contentClassified', async ({ contentCatalog }) => {
29
+ const redpandaConnect = contentCatalog.getComponents().find(component => component.name === 'redpanda-connect');
30
+ const redpandaCloud = contentCatalog.getComponents().find(component => component.name === 'redpanda-cloud');
31
+ if (!redpandaConnect) return;
32
+ const pages = contentCatalog.getPages();
33
+
28
34
  try {
29
- // Fetch CSV data and parse it
30
- const csvData = await fetchCSV();
35
+ // Fetch CSV data (either from local file or GitHub)
36
+ const csvData = await fetchCSV(config.csvpath);
31
37
  const parsedData = Papa.parse(csvData, { header: true, skipEmptyLines: true });
32
38
  const enrichedData = enrichCsvDataWithUrls(parsedData, pages, logger);
33
- parsedData.data = enrichedData
34
- if(redpandaConnect)
39
+ parsedData.data = enrichedData;
40
+
41
+ if (redpandaConnect) {
35
42
  redpandaConnect.latest.asciidoc.attributes.csvData = parsedData;
36
- if(redpandaCloud)
43
+ }
44
+ if (redpandaCloud) {
37
45
  redpandaCloud.latest.asciidoc.attributes.csvData = parsedData;
46
+ }
38
47
 
39
48
  } catch (error) {
40
49
  logger.error('Error fetching or parsing CSV data:', error.message);
@@ -42,7 +51,22 @@ module.exports.register = function ({ config,contentCatalog }) {
42
51
  }
43
52
  });
44
53
 
45
- async function fetchCSV() {
54
+ // Check for local CSV file first. If not found, fetch from GitHub
55
+ async function fetchCSV(localCsvPath) {
56
+ if (localCsvPath && fs.existsSync(localCsvPath)) {
57
+ if (path.extname(localCsvPath).toLowerCase() !== '.csv') {
58
+ throw new Error(`Invalid file type: ${localCsvPath}. Expected a CSV file.`);
59
+ }
60
+ logger.info(`Loading CSV data from local file: ${localCsvPath}`);
61
+ return fs.readFileSync(localCsvPath, 'utf8');
62
+ } else {
63
+ logger.info('Local CSV file not found. Fetching from GitHub...');
64
+ return await fetchCsvFromGitHub();
65
+ }
66
+ }
67
+
68
+ // Fetch CSV data from GitHub
69
+ async function fetchCsvFromGitHub() {
46
70
  const octokit = await loadOctokit();
47
71
  try {
48
72
  const { data: fileContent } = await octokit.rest.repos.getContent({
@@ -54,7 +78,7 @@ module.exports.register = function ({ config,contentCatalog }) {
54
78
  return Buffer.from(fileContent.content, 'base64').toString('utf8');
55
79
  } catch (error) {
56
80
  console.error('Error fetching Redpanda Connect catalog from GitHub:', error);
57
- return [];
81
+ return '';
58
82
  }
59
83
  }
60
84
 
@@ -83,4 +107,4 @@ module.exports.register = function ({ config,contentCatalog }) {
83
107
  };
84
108
  });
85
109
  }
86
- }
110
+ }
@@ -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) => {
@@ -27,6 +27,11 @@ module.exports.register = function (registry, config = {}) {
27
27
 
28
28
  const terms = termFiles.map(file => {
29
29
  const content = file.contents.toString()
30
+ // Split content by lines and get the first non-empty line as the title
31
+ const lines = content.split('\n').map(line => line.trim())
32
+ const firstNonEmptyLine = lines.find(line => line.length > 0)
33
+ // Remove leading '=' characters (AsciiDoc syntax) and trim whitespace
34
+ const pageTitle = firstNonEmptyLine ? firstNonEmptyLine.replace(/^=+\s*/, '') : '#'
30
35
  const attributes = {}
31
36
 
32
37
  let match
@@ -50,6 +55,7 @@ module.exports.register = function (registry, config = {}) {
50
55
  term: attributes['term-name'],
51
56
  def: attributes['hover-text'],
52
57
  category: attributes['category'] || '',
58
+ pageTitle,
53
59
  content
54
60
  }
55
61
 
@@ -76,13 +82,16 @@ module.exports.register = function (registry, config = {}) {
76
82
  }
77
83
  }
78
84
 
79
- //characters to replace by '-' in generated idprefix
80
- const IDRX = /[/ _.-]+/g
85
+ // Characters to replace by '-' in generated idprefix
86
+ const IDRX = /[\/ _.-]+/g
81
87
 
82
- function termId (term) {
83
- return term.toLowerCase().replace(IDRX, '-')
88
+ function termId(term) {
89
+ // Remove brackets before replacing other characters
90
+ const noBracketsTerm = term.replace(/[\[\]\(\)]/g, '') // Remove brackets
91
+ return noBracketsTerm.toLowerCase().replace(IDRX, '-')
84
92
  }
85
93
 
94
+
86
95
  const TRX = /(<[a-z]+)([^>]*>.*)/
87
96
 
88
97
  function glossaryInlineMacro () {
@@ -91,7 +100,7 @@ module.exports.register = function (registry, config = {}) {
91
100
  self.named('glossterm')
92
101
  //Specifying the regexp allows spaces in the term.
93
102
  self.$option('regexp', /glossterm:([^[]+)\[(|.*?[^\\])\]/)
94
- self.positionalAttributes(['definition', 'customText']);
103
+ self.positionalAttributes(['definition', 'customText']); // Allows for specifying custom link text
95
104
  self.process(function (parent, target, attributes) {
96
105
  const term = attributes.term || target
97
106
  const customText = attributes.customText || term;
@@ -110,9 +119,11 @@ module.exports.register = function (registry, config = {}) {
110
119
  }
111
120
  const logTerms = document.hasAttribute('glossary-log-terms')
112
121
  var definition;
122
+ var pageTitle;
113
123
  const index = context.gloss.findIndex((candidate) => candidate.term === term)
114
124
  if (index >= 0) {
115
125
  definition = context.gloss[index].def
126
+ pageTitle = context.gloss[index].pageTitle
116
127
  } else {
117
128
  definition = attributes.definition;
118
129
  }
@@ -138,7 +149,7 @@ module.exports.register = function (registry, config = {}) {
138
149
  if ((termExistsInContext && links) || (links && customLink)) {
139
150
  inline = customLink
140
151
  ? self.createInline(parent, 'anchor', customText, { type: 'link', target: customLink, attributes: { ...attrs, window: '_blank', rel: 'noopener noreferrer' } })
141
- : self.createInline(parent, 'anchor', customText, { type: 'xref', target: `${glossaryPage}#${termId(term)}`, reftext: customText, attributes: attrs })
152
+ : self.createInline(parent, 'anchor', customText, { type: 'xref', target: `${glossaryPage}#${termId(pageTitle)}`, reftext: customText, attributes: attrs })
142
153
  } else {
143
154
  inline = self.createInline(parent, 'quoted', customText, { attributes: attrs })
144
155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "3.6.5",
3
+ "version": "3.7.0",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",