@redpanda-data/docs-extensions-and-macros 5.22.1 → 5.22.3

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/bin/doc-tools.js CHANGED
@@ -901,7 +901,7 @@ automation
901
901
  * @description
902
902
  * One-time migration that rewires published connector reference pages onto the
903
903
  * regenerated description partial: the header `:description:` becomes
904
- * `include::...[tag=attrs]` and the frozen intro prose becomes
904
+ * `include::...[tag=meta]` and the frozen intro prose becomes
905
905
  * `include::...[tag=body]`. Afterwards `generate rpcn-connector-docs` keeps
906
906
  * both fresh on every run.
907
907
  *
@@ -64,7 +64,6 @@ IMPORTANT: Extensions must be registered under the `antora.extensions` key in yo
64
64
 
65
65
  === Navigation
66
66
  * **unlisted-pages** - Manage pages that appear in navigation but aren't listed
67
- * **produce-redirects** - Generate redirect configurations
68
67
  * **unpublish-pages** - Mark pages as unpublished
69
68
 
70
69
  === Integrations
@@ -125,13 +124,16 @@ antora:
125
124
  algolia_index_name: YOUR_INDEX
126
125
  ----
127
126
 
128
- === Generate redirects
127
+ === Redirects
128
+
129
+ No extension required: core Antora (>= 3.1.8) generates redirects for https://docs.antora.org/antora/latest/page/page-aliases/[page aliases] automatically on every build. Define aliases in your `antora.yml`:
129
130
 
130
131
  [,yaml]
131
132
  ----
132
- antora:
133
- extensions:
134
- - '@redpanda-data/docs-extensions-and-macros/extensions/produce-redirects.js'
133
+ name: redpanda
134
+ asciidoc:
135
+ attributes:
136
+ page-aliases: old-page.adoc,another-old-page.adoc
135
137
  ----
136
138
 
137
139
  == Support
@@ -706,23 +706,7 @@ In this example, the `attributespath` option points to a local YAML file (`./loc
706
706
 
707
707
  == Produce redirects (customization of core Antora)
708
708
 
709
- 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.
710
-
711
- === Environment variables
712
-
713
- This extension does not require any environment variables.
714
-
715
- === Configuration options
716
-
717
- There are no configurable options for this extension.
718
-
719
- === Registration
720
-
721
- ```yaml
722
- antora:
723
- extensions:
724
- - '@redpanda-data/docs-extensions-and-macros/extensions/modify-redirects'
725
- ```
709
+ Antora's built-in `@antora/redirect-producer` already produces redirect rules for https://docs.antora.org/antora/latest/page/page-aliases/[page aliases], including the redirect-loop case caused by https://docs.antora.org/antora/latest/playbook/urls-html-extension-style/#html-extension-style-key[`indexify`] that once required a workaround. Antora runs the real module automatically on every build with no extension required. This project previously shipped a hand-copied override of that module to work around the loop, but the override had drifted from the upstream fix and reintroduced a worse bug (a self-referential alias could overwrite the real page it pointed at). The override has been removed; core Antora handles redirects natively.
726
710
 
727
711
  == Replace attributes in attachments
728
712
 
@@ -85,7 +85,6 @@ antora:
85
85
  extensions:
86
86
  - '@redpanda-data/docs-extensions-and-macros/extensions/version-fetcher/set-latest-version'
87
87
  - '@redpanda-data/docs-extensions-and-macros/extensions/generate-index-data.js'
88
- - '@redpanda-data/docs-extensions-and-macros/extensions/produce-redirects.js'
89
88
  ----
90
89
 
91
90
  Extensions run in the order they're listed.
@@ -122,16 +121,7 @@ antora:
122
121
 
123
122
  === Redirect management
124
123
 
125
- Generate redirects from old URLs to new ones:
126
-
127
- [,yaml]
128
- ----
129
- antora:
130
- extensions:
131
- - '@redpanda-data/docs-extensions-and-macros/extensions/produce-redirects.js'
132
- ----
133
-
134
- Define redirects in your `antora.yml`:
124
+ No extension required: core Antora (>= 3.1.8) generates redirects for old URLs automatically. Define redirects in your `antora.yml`:
135
125
 
136
126
  [,yaml]
137
127
  ----
@@ -60,9 +60,9 @@
60
60
  */
61
61
 
62
62
  const yaml = require('js-yaml')
63
- const chalk = require('chalk')
64
63
  const semver = require('semver')
65
64
  const { buildBadgeHtml, DEFAULT_TOOLTIPS } = require('./badge')
65
+ const logger = require('@antora/logger')('enterprise-macro')
66
66
 
67
67
  const $enterpriseRegistry = Symbol('$enterpriseRegistry')
68
68
  const $enterpriseRegistryUnreadable = Symbol('$enterpriseRegistryUnreadable')
@@ -76,7 +76,7 @@ function warnOnce (contentCatalog, key, message) {
76
76
  const seen = contentCatalog[$warned] || (contentCatalog[$warned] = new Set())
77
77
  if (seen.has(key)) return false
78
78
  seen.add(key)
79
- console.warn(chalk.yellow(message))
79
+ logger.warn(message)
80
80
  return true
81
81
  }
82
82
 
@@ -143,7 +143,7 @@ function entryStatus (entry, report) {
143
143
  // One registry typo needs one fix, so report it once per build rather
144
144
  // than once per mention of the feature.
145
145
  if (report.contentCatalog) warnOnce(report.contentCatalog, `status:${entry.name}`, message)
146
- else console.warn(chalk.yellow(message))
146
+ else logger.warn(message)
147
147
  }
148
148
  return STATUS_UNRELEASED
149
149
  }
@@ -167,9 +167,9 @@ function isPrereleasePage (config, document) {
167
167
  // Anything else is a typo. Falling through to the catalog is safer than
168
168
  // guessing, and silently treating it as "released" produced a warning that
169
169
  // told the writer their prerelease page belonged to a released version.
170
- console.warn(chalk.yellow(
170
+ logger.warn(
171
171
  `enterprise macro: page-component-version-is-prerelease is '${attribute}', which is neither true nor false, so it is ignored and the component version's own prerelease flag is used instead.`
172
- ))
172
+ )
173
173
  }
174
174
  const contentCatalog = config && config.contentCatalog
175
175
  const src = config && config.file && config.file.src
@@ -215,7 +215,7 @@ function reportUnreleasedFeature ({ feature, mode, filePath }) {
215
215
  'This page belongs to a released version, so the mention renders as plain text with no enterprise styling, tooltip, or link. ' +
216
216
  'Move the mention to the prerelease (beta) branch, or change the status once the feature ships.'
217
217
  if (mode === 'error') throw new Error(message)
218
- console.warn(chalk.yellow(message))
218
+ logger.warn(message)
219
219
  }
220
220
 
221
221
  /**
@@ -268,7 +268,7 @@ function isFeatureShippedOnPage (entry, config, report) {
268
268
  `enterprise:${entry.name}[]${where}: registry has since: '${since}', which is not a parsable version, so the since-gating check is skipped and the feature renders as already shipped. ` +
269
269
  'Fix the since value in enterprise-features.yml (e.g. "26.3"), or remove the key if the feature has always been available.'
270
270
  if (report.contentCatalog) warnOnce(report.contentCatalog, `since:${entry.name}`, message)
271
- else console.warn(chalk.yellow(message))
271
+ else logger.warn(message)
272
272
  }
273
273
  return true
274
274
  }
@@ -290,7 +290,7 @@ function reportFeatureNotYetShipped ({ feature, since, pageVersion, mode, filePa
290
290
  'The mention renders as plain text with no enterprise styling, tooltip, or link. ' +
291
291
  'Move the mention to a page whose version is at or after the since version, or remove since if the feature covers this version too.'
292
292
  if (mode === 'error') throw new Error(message)
293
- console.warn(chalk.yellow(message))
293
+ logger.warn(message)
294
294
  }
295
295
 
296
296
  /**
@@ -418,7 +418,7 @@ function warnNoRegistry (contentCatalog) {
418
418
  if (contentCatalog) return warnOnce(contentCatalog, 'noregistry', message)
419
419
  if (warnedNoRegistry) return
420
420
  warnedNoRegistry = true
421
- console.warn(chalk.yellow(message))
421
+ logger.warn(message)
422
422
  }
423
423
 
424
424
  /**
@@ -434,7 +434,7 @@ function reportUnknownFeature ({ feature, mode, registry, filePath }) {
434
434
  const where = filePath ? ` in ${filePath}` : ''
435
435
  const message = `enterprise:${feature}[] does not match any feature in the enterprise features registry${where}.${hint} Add the feature to ${REGISTRY_FILENAME} in the shared component first.`
436
436
  if (mode === 'error') throw new Error(message)
437
- console.warn(chalk.yellow(message))
437
+ logger.warn(message)
438
438
  }
439
439
 
440
440
  /**
@@ -468,7 +468,7 @@ function resolveTooltipAttribute (raw) {
468
468
  if (raw === undefined || raw === 'title') return 'title'
469
469
  if (raw === 'true') return 'data-enterprise-tooltip'
470
470
  if (raw.startsWith('data-')) return raw
471
- console.warn(`enterprise-tooltip attribute '${raw}' must be 'title', 'true', 'false', or start with 'data-'. Falling back to 'title'.`)
471
+ logger.warn(`enterprise-tooltip attribute '${raw}' must be 'title', 'true', 'false', or start with 'data-'. Falling back to 'title'.`)
472
472
  return 'title'
473
473
  }
474
474
 
@@ -690,7 +690,7 @@ function register (registry, config = {}) {
690
690
  registry.inlineMacro(enterpriseInlineMacro(config))
691
691
  if (typeof registry.blockMacro === 'function') registry.blockMacro(enterpriseFeaturesBlockMacro(config))
692
692
  } else {
693
- console.warn("no 'inlineMacro' method on alleged registry")
693
+ logger.warn("no 'inlineMacro' method on alleged registry")
694
694
  }
695
695
  return registry
696
696
  }
@@ -2,7 +2,62 @@
2
2
 
3
3
  const $glossaryContexts = Symbol('$glossaryContexts')
4
4
  const { posix: path } = require('path')
5
- const chalk = require('chalk')
5
+ const logger = require('@antora/logger')('glossary-macro')
6
+
7
+ // Backtick-monospace in hover-text. The unconstrained (double-backtick) form is
8
+ // matched first, otherwise it leaves a stray delimiter on each side.
9
+ const MONOSPACE_RX = /``([^`]+)``|`([^`]+)`/g
10
+
11
+ // Escape for interpolation into a double-quoted HTML attribute. Same four
12
+ // replacements, in the same order, as badge.js and enterprise.js; `&` has to go
13
+ // first or it re-escapes the entities the later passes introduce.
14
+ function escapeAttr (value) {
15
+ return String(value)
16
+ .replace(/&/g, '&')
17
+ .replace(/"/g, '"')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ }
21
+
22
+ // Only a data-* tooltip is read by a JS tooltip library that renders it as HTML;
23
+ // the native title attribute is always plain text. Shared with the
24
+ // glossary-tooltip validation below so the two cannot drift apart.
25
+ function tooltipRendersHtml (tooltipAttr) {
26
+ return typeof tooltipAttr === 'string' && tooltipAttr.startsWith('data-')
27
+ }
28
+
29
+ // hover-text is raw, unconverted source text, so it has to be escaped before it
30
+ // lands in an HTML attribute. How many passes depends on the consumer:
31
+ //
32
+ // title The browser decodes the attribute once and shows the result as
33
+ // text, so one pass is exactly right.
34
+ // data-* docs-ui initializes tippy on these with allowHTML: true
35
+ // (docs-ui/src/js/12-activate-tooltips.js), which means the value is
36
+ // decoded once by the HTML parser and then parsed AGAIN as HTML by
37
+ // innerHTML. A single pass is spent on that first decode and escaped
38
+ // markup comes back to life -- `<img onerror=...>` in hover-text
39
+ // became a live element that fired. Escape twice so one level
40
+ // survives the decode, and add <code> afterwards so it is the only
41
+ // live markup in the value.
42
+ //
43
+ // Set definitionIsHtml for a definition that Asciidoctor already converted --
44
+ // the inline glossterm:term[definition] form arrives as HTML, unlike a term
45
+ // file's raw :hover-text: value -- so it is escaped once rather than twice and
46
+ // its markup survives to the reader instead of being shown as literal tags.
47
+ function formatTooltipDefinition (text, tooltipAttr, definitionIsHtml) {
48
+ if (!tooltipRendersHtml(tooltipAttr)) return escapeAttr(text)
49
+ if (definitionIsHtml) {
50
+ // Already HTML, so one pass is right: the attribute decode hands the markup
51
+ // back intact for innerHTML to render, and a quote in the text still cannot
52
+ // terminate the attribute early.
53
+ return escapeAttr(text)
54
+ }
55
+ return escapeAttr(escapeAttr(text)).replace(
56
+ MONOSPACE_RX,
57
+ (match, unconstrained, constrained) =>
58
+ `<code>${unconstrained !== undefined ? unconstrained : constrained}</code>`
59
+ )
60
+ }
6
61
 
7
62
  module.exports.register = function (registry, config = {}) {
8
63
 
@@ -41,7 +96,7 @@ module.exports.register = function (registry, config = {}) {
41
96
  }
42
97
 
43
98
  if (!attributes['term-name'] || !attributes['hover-text']) {
44
- console.warn(`Skipping term ${file.path} due to missing 'term-name' and/or 'hover-text attributes'.`)
99
+ logger.warn(`Skipping term ${file.path} due to missing 'term-name' and/or 'hover-text attributes'.`)
45
100
  return null
46
101
  }
47
102
 
@@ -113,22 +168,27 @@ module.exports.register = function (registry, config = {}) {
113
168
  }
114
169
  var tooltip = document.getAttribute('glossary-tooltip')
115
170
  if (tooltip === 'true') tooltip = 'data-glossary-tooltip'
116
- if (tooltip && tooltip !== 'title' && !tooltip.startsWith('data-')) {
117
- console.log(`glossary-tooltip attribute '${tooltip}' must be 'true', 'title', or start with 'data-`)
171
+ if (tooltip && tooltip !== 'title' && !tooltipRendersHtml(tooltip)) {
172
+ logger.warn(`glossary-tooltip attribute '${tooltip}' must be 'true', 'title', or start with 'data-`)
118
173
  tooltip = undefined
119
174
  }
120
175
  const logTerms = document.hasAttribute('glossary-log-terms')
121
176
  var definition;
122
177
  var pageTitle;
178
+ // A term file's :hover-text: is raw source text, but the definition
179
+ // passed inline as glossterm:term[definition] has already been through
180
+ // Asciidoctor's inline substitutions, so the two need different escaping.
181
+ var definitionIsHtml = false;
123
182
  const index = context.gloss.findIndex((candidate) => candidate.term === term)
124
183
  if (index >= 0) {
125
184
  definition = context.gloss[index].def
126
185
  pageTitle = context.gloss[index].pageTitle
127
186
  } else {
128
187
  definition = attributes.definition;
188
+ definitionIsHtml = !!definition;
129
189
  }
130
190
  if (definition) {
131
- logTerms && console.log(`${term}:: ${definition}`)
191
+ logTerms && logger.info(`${term}:: ${definition}`)
132
192
  } else if (tooltip) {
133
193
  definition = `${term} not yet defined`
134
194
  }
@@ -154,12 +214,13 @@ module.exports.register = function (registry, config = {}) {
154
214
  inline = self.createInline(parent, 'quoted', customText, { attributes: attrs })
155
215
  }
156
216
  if (tooltip) {
217
+ const formattedDefinition = formatTooltipDefinition(definition, tooltip, definitionIsHtml)
157
218
  const a = inline.convert()
158
219
  const matches = a.match(TRX)
159
220
  if (matches) {
160
- return self.createInline(parent, 'quoted', `${matches[1]} ${tooltip}="${definition}"${matches[2]}`)
221
+ return self.createInline(parent, 'quoted', `${matches[1]} ${tooltip}="${formattedDefinition}"${matches[2]}`)
161
222
  } else {
162
- return self.createInline(parent, 'quoted', `<span ${tooltip}="${definition}">${a}</span>`)
223
+ return self.createInline(parent, 'quoted', `<span ${tooltip}="${formattedDefinition}">${a}</span>`)
163
224
  }
164
225
  }
165
226
  return inline
@@ -171,7 +232,7 @@ module.exports.register = function (registry, config = {}) {
171
232
  if (typeof registry.inlineMacro === 'function') {
172
233
  registry.inlineMacro(glossaryInlineMacro())
173
234
  } else {
174
- console.warn('no \'inlineMacro\' method on alleged registry')
235
+ logger.warn('no \'inlineMacro\' method on alleged registry')
175
236
  }
176
237
  }
177
238
 
@@ -186,3 +247,5 @@ module.exports.register = function (registry, config = {}) {
186
247
  }
187
248
  return registry
188
249
  }
250
+
251
+ module.exports.formatTooltipDefinition = formatTooltipDefinition
package/macros/prop.js CHANGED
@@ -90,8 +90,8 @@
90
90
  * - '@redpanda-data/docs-extensions-and-macros/macros/prop'
91
91
  */
92
92
 
93
- const chalk = require('chalk')
94
93
  const semver = require('semver')
94
+ const logger = require('@antora/logger')('prop-macro')
95
95
 
96
96
  const $propertyRegistry = Symbol('$propertyRegistry')
97
97
  // Release series a component version has no property data for, recorded at
@@ -236,7 +236,7 @@ function buildPageIndex (contentCatalog, component, properties, version) {
236
236
  return head.startsWith('<') && !head.startsWith('<<') && /^<[a-zA-Z!/]/.test(head)
237
237
  }
238
238
  if (pages.some(looksConverted)) {
239
- console.warn(chalk.yellow(`prop macro: building the property page index for ${component}@${version || 'any'} after conversion started; some pages are already HTML and their properties may not be indexed. Register '@redpanda-data/docs-extensions-and-macros/extensions/property-page-index' under antora.extensions to build the index up front.`))
239
+ logger.warn(`prop macro: building the property page index for ${component}@${version || 'any'} after conversion started; some pages are already HTML and their properties may not be indexed. Register '@redpanda-data/docs-extensions-and-macros/extensions/property-page-index' under antora.extensions to build the index up front.`)
240
240
  }
241
241
  // A page can declare that it is the reference for a named collection of
242
242
  // properties:
@@ -404,7 +404,7 @@ function warnOnce (contentCatalog, key, message) {
404
404
  const seen = contentCatalog[$warned] || (contentCatalog[$warned] = new Set())
405
405
  if (seen.has(key)) return false
406
406
  seen.add(key)
407
- console.warn(chalk.yellow(message))
407
+ logger.warn(message)
408
408
  return true
409
409
  }
410
410
 
@@ -774,7 +774,7 @@ function reportUnknownProperty ({ name, mode, registry, filePath }) {
774
774
  const message = `prop:${name}[]${where}: '${name}' is not in the property data this page validates against (${describeDataset(registry)}). ` +
775
775
  `Either the name is misspelled or the property does not exist in that release.${hint}`
776
776
  if (mode === 'error') throw new Error(message)
777
- console.warn(chalk.yellow(message))
777
+ logger.warn(message)
778
778
  }
779
779
 
780
780
  /**
@@ -799,11 +799,11 @@ function reportUnavailableProperty ({ name, component, registry, mode, filePath
799
799
  if (mode === 'off') return
800
800
  const where = filePath ? ` in ${filePath}` : ''
801
801
  const which = component || 'this'
802
- console.warn(chalk.yellow(
802
+ logger.warn(
803
803
  `prop:${name}[]${where}: '${name}' is not available in the ${which} component, so it renders as plain code -- no link, and no tooltip. ` +
804
804
  `${describeDataset(registry)} marks it cloud_supported: false, meaning Redpanda has the property but this audience cannot set it. ` +
805
805
  'If that is wrong, fix cloud_supported in the property extractor; if it is right, reword the sentence and drop the macro.'
806
- ))
806
+ )
807
807
  }
808
808
 
809
809
  /**
@@ -814,11 +814,11 @@ function reportUnavailableProperty ({ name, component, registry, mode, filePath
814
814
  function reportUnpublishedProperty ({ name, registry, mode, filePath }) {
815
815
  if (mode === 'off') return
816
816
  const where = filePath ? ` in ${filePath}` : ''
817
- console.warn(chalk.yellow(
817
+ logger.warn(
818
818
  `prop:${name}[link=true]${where}: '${name}' is in ${describeDataset(registry)} but no reference page in this component renders a heading for it, so it keeps its tooltip and renders without a link. ` +
819
819
  'Every property reference page filters out the deprecated and exclude-from-docs include tags, and some include only selected category tags -- check which tags enclose this property in the generated partial. ' +
820
820
  'If it is meant to stay unpublished, drop link=true.'
821
- ))
821
+ )
822
822
  }
823
823
 
824
824
  /**
@@ -1000,7 +1000,7 @@ function register (registry, config = {}) {
1000
1000
  } else if (typeof registry.inlineMacro === 'function') {
1001
1001
  registry.inlineMacro(propInlineMacro(config))
1002
1002
  } else {
1003
- console.warn("no 'inlineMacro' method on alleged registry")
1003
+ logger.warn("no 'inlineMacro' method on alleged registry")
1004
1004
  }
1005
1005
  return registry
1006
1006
  }
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
  const { posix: path } = require('path')
3
+ const logger = require('@antora/logger')('rp-connect-components-macro')
3
4
 
4
5
  /**
5
6
  * Registers macros for use in Redpanda Connect contexts in the Redpanda documentation.
@@ -602,7 +603,7 @@ module.exports.register = function (registry, context) {
602
603
  const showAllInfo = attributes?.all
603
604
 
604
605
  const csvData = context.config?.attributes?.csvData || null;
605
- if (!csvData) return console.error(`CSV data is not available for ${parent.getDocument().getAttributes()['page-relative-src-path']}. Make sure your playbook includes the generate-rp-connect-info extension.`)
606
+ if (!csvData) return logger.error(`CSV data is not available for ${parent.getDocument().getAttributes()['page-relative-src-path']}. Make sure your playbook includes the generate-rp-connect-info extension.`)
606
607
 
607
608
  // Get the enriched commercial names map (includes CSV + AsciiDoc names)
608
609
  const commercialNamesMap = context.config?.attributes?.commercialNamesMap || {};
@@ -1022,11 +1023,11 @@ module.exports.register = function (registry, context) {
1022
1023
  return self.createBlock(parent, 'pass', '');
1023
1024
  }
1024
1025
  const csvData = context.config?.attributes?.csvData || null;
1025
- if (!csvData) return console.error(`CSV data is not available for ${attributes['page-relative-src-path']}. Make sure your playbook includes the generate-rp-connect-info extension.`);
1026
+ if (!csvData) return logger.error(`CSV data is not available for ${attributes['page-relative-src-path']}. Make sure your playbook includes the generate-rp-connect-info extension.`);
1026
1027
  // Filter for the specific connector by name
1027
1028
  const componentRows = csvData.data.filter(row => row.connector.trim().toLowerCase() === name.trim().toLowerCase());
1028
1029
  if (componentRows.length === 0) {
1029
- console.warn(`No CSV data found for connector: ${name}. The connector may have been removed or renamed.`);
1030
+ logger.warn(`No CSV data found for connector: ${name}. The connector may have been removed or renamed.`);
1030
1031
  // Build link to release notes using content catalog for proper URL resolution
1031
1032
  const isCloud = attributes['env-cloud'] !== undefined;
1032
1033
  let whatsNewUrl = isCloud
@@ -1092,7 +1093,7 @@ module.exports.register = function (registry, context) {
1092
1093
  self.process((parent, target, attrs) => {
1093
1094
  const type = attrs.type;
1094
1095
  const categoriesData = context.config?.attributes?.connectCategoriesData || null
1095
- if (!categoriesData) return console.error (`Category data is not available for ${parent.getDocument().getAttributes()['page-relative-src-path']}. Make sure your playbook includes the generate-rp-connect-categories extension.`)
1096
+ if (!categoriesData) return logger.error(`Category data is not available for ${parent.getDocument().getAttributes()['page-relative-src-path']}. Make sure your playbook includes the generate-rp-connect-categories extension.`)
1096
1097
  const categories = categoriesData[type] || null;
1097
1098
  const currentTabsId = `tabs-${tabsCounter++}`; // Unique ID for this set of tabs
1098
1099
  if (!categories) return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.22.1",
3
+ "version": "5.22.3",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -77,8 +77,6 @@
77
77
  "./extensions/find-related-labs": "./extensions/find-related-labs.js",
78
78
  "./extensions/convert-llms-to-txt": "./extensions/convert-llms-to-txt.js",
79
79
  "./extensions/convert-sitemap-to-markdown": "./extensions/convert-sitemap-to-markdown.js",
80
- "./extensions/modify-redirects": "./extensions/produce-redirects.js",
81
- "./extensions/produce-redirects": "./extensions/produce-redirects.js",
82
80
  "./extensions/algolia-indexer/index": "./extensions/algolia-indexer/index.js",
83
81
  "./extensions/aggregate-terms": "./extensions/aggregate-terms.js",
84
82
  "./extensions/resolve-xrefs-in-attachments": "./extensions/resolve-xrefs-in-attachments.js",
@@ -161,6 +159,7 @@
161
159
  "jest-junit": "^16.0.0"
162
160
  },
163
161
  "peerDependencies": {
164
- "@antora/asciidoc-loader": "^3.1.0"
162
+ "@antora/asciidoc-loader": "^3.1.0",
163
+ "@antora/logger": "^3.1.0"
165
164
  }
166
165
  }
@@ -43,8 +43,8 @@ const STALE_METADATA_PARTIAL =
43
43
  // deleting the file would break any published page that includes it, so blank
44
44
  // it instead and log so a human removes the include line.
45
45
  //
46
- // The empty attrs/body regions are load-bearing. Pages include this file with
47
- // `[tag=attrs]` and `[tag=body]`, and Asciidoctor logs `tag 'attrs' not found
46
+ // The empty meta/body regions are load-bearing. Pages include this file with
47
+ // `[tag=meta]` and `[tag=body]`, and Asciidoctor logs `tag 'meta' not found
48
48
  // in include file` for every tag a target does not define -- so a banner
49
49
  // without the regions turns one blanked partial into two build warnings on
50
50
  // every page that includes it, on every build, which is a build failure
@@ -54,7 +54,7 @@ const STALE_DESCRIPTION_PARTIAL =
54
54
  '// This content is autogenerated. Do not edit manually. ' +
55
55
  'The connector no longer has a description, so this partial is intentionally empty. ' +
56
56
  'You can remove the include directive that points at this file from the connector page.\n' +
57
- '\n// tag::attrs[]\n// end::attrs[]\n' +
57
+ '\n// tag::meta[]\n// end::meta[]\n' +
58
58
  '\n// tag::body[]\n// end::body[]\n';
59
59
 
60
60
  // Component families with their own reference pages, keyed by the canonical
@@ -606,12 +606,12 @@ async function generateRpcnConnectorDocs(options) {
606
606
  // connectors with neither summary nor description, keep the inline
607
607
  // text rather than getting an include that cannot resolve.
608
608
  item.hasDescriptionPartial = true;
609
- // The attrs region only sets :description: when the connector has a
609
+ // The meta region only sets :description: when the connector has a
610
610
  // summary, so a page may only replace its header attribute with the
611
611
  // include when there is one. Otherwise the page would lose its meta
612
612
  // description entirely and keep the inline attribute instead.
613
613
  if (typeof item.summary === 'string' && item.summary.trim()) {
614
- item.hasDescriptionAttrs = true;
614
+ item.hasDescriptionMeta = true;
615
615
  }
616
616
  if (!writeFullDrafts) {
617
617
  partialsWritten++;
@@ -988,7 +988,7 @@ function backfillPageDescriptions (connectorData, { pagesRoot, dryRun = false }
988
988
  const lines = content.split('\n');
989
989
  const headerEnd = lines.findIndex((l) => l.trim() === '');
990
990
  const header = lines.slice(0, headerEnd === -1 ? lines.length : headerEnd);
991
- // A page migrated to the description partial carries the attrs include
991
+ // A page migrated to the description partial carries the meta include
992
992
  // instead of a literal :description:. Without this arm the backfill
993
993
  // splices a competing meta block into every migrated page, so the page
994
994
  // ends up with a dead include plus a hand-inserted attribute that
@@ -997,7 +997,7 @@ function backfillPageDescriptions (connectorData, { pagesRoot, dryRun = false }
997
997
 
998
998
  // Meta descriptions are read as plain text by search results, link
999
999
  // previews, and assistants, so the markup comes out mechanically. Same
1000
- // flattener the description partial's attrs region uses, so a page
1000
+ // flattener the description partial's meta region uses, so a page
1001
1001
  // cannot publish one meta description before migrating to the partial
1002
1002
  // and a different one after.
1003
1003
  const summary = flattenToAttributeValue(item.summary);
@@ -6,7 +6,7 @@ const { descriptionIncludeLine } = require('../metadata-utils.js');
6
6
  * Handlebars helper: emit the include directive for a connector's description
7
7
  * partial, optionally for one of its tag regions.
8
8
  *
9
- * {{{descriptionInclude this 'attrs'}}}
9
+ * {{{descriptionInclude this 'meta'}}}
10
10
  * {{{descriptionInclude this 'body'}}}
11
11
  *
12
12
  * Drafted pages use this instead of freezing the summary and description into
@@ -16,7 +16,7 @@ const { descriptionIncludeLine } = require('../metadata-utils.js');
16
16
  * writes the file.
17
17
  *
18
18
  * @param {object} item connector data with `type`/`typeDir` and `name`
19
- * @param {string} [tag] `attrs` or `body`
19
+ * @param {string} [tag] `meta` or `body`
20
20
  * @returns {string}
21
21
  */
22
22
  module.exports = function descriptionInclude (item, tag) {
@@ -7,7 +7,7 @@ const { flattenToAttributeValue } = require('../metadata-utils.js');
7
7
  * value suitable for the `:description:` page attribute (and therefore for the
8
8
  * page's `<meta name="description">`).
9
9
  *
10
- * Delegates to the shared flattener so the description partial's attrs region
10
+ * Delegates to the shared flattener so the description partial's meta region
11
11
  * and `backfillPageDescriptions` publish the same text for the same summary.
12
12
  * Collapsing whitespace alone is not enough: summaries carry xrefs, link
13
13
  * macros and inline code, and an unflattened value ships raw
@@ -248,10 +248,10 @@ function descriptionWithMetadataInclude (item) {
248
248
  * auto-generated summary and description from a regenerated partial instead of
249
249
  * freezing them into the page body at first draft.
250
250
  *
251
- * The partial defines two tag regions, so pages address it twice: `attrs` in
251
+ * The partial defines two tag regions, so pages address it twice: `meta` in
252
252
  * the page header (sets `:description:`) and `body` where the prose renders.
253
253
  * @param {object} item connector data with `type`/`typeDir` and `name`
254
- * @param {string} [tag] tag region to include (`attrs` or `body`); omit for the whole file
254
+ * @param {string} [tag] tag region to include (`meta` or `body`); omit for the whole file
255
255
  * @returns {string}
256
256
  */
257
257
  function descriptionIncludeLine (item, tag) {
@@ -308,7 +308,7 @@ function lostMetadataSections (oldContent, newContent) {
308
308
  * edited.
309
309
  *
310
310
  * This is the one implementation: `summaryAttribute` (the description
311
- * partial's attrs region) and `backfillPageDescriptions` (the page-header
311
+ * partial's meta region) and `backfillPageDescriptions` (the page-header
312
312
  * self-heal) both call it, so the two paths cannot drift into publishing
313
313
  * different meta descriptions for the same summary.
314
314
  *
@@ -11,7 +11,7 @@
11
11
  * to `modules/components/partials/descriptions/<type>/<name>.adoc` on every
12
12
  * run, with two tag regions. This script rewires existing pages to it:
13
13
  *
14
- * header: include::...[tag=attrs] inside the page's `// tag::meta[]` region
14
+ * header: include::...[tag=meta] inside the page's `// tag::meta[]` region
15
15
  * body: include::...[tag=body] where the frozen prose is today
16
16
  *
17
17
  * The header rewire is mechanical and safe: it swaps one `:description:` line
@@ -36,7 +36,7 @@ const path = require('path');
36
36
  const { descriptionIncludeLine, normalizeTypeDir, flattenToAttributeValue } = require('./metadata-utils.js');
37
37
 
38
38
  // The page's meta tag region: the block single-source stubs inherit
39
- // :description: from, so the attrs include belongs inside it.
39
+ // :description: from, so the meta include belongs inside it.
40
40
  const META_TAG_OPEN = /^\/\/\s*tag::meta\[\]\s*$/;
41
41
  const META_TAG_CLOSE = /^\/\/\s*end::meta\[\]\s*$/;
42
42
 
@@ -123,34 +123,34 @@ function migrateDescriptionsToPartials ({
123
123
  continue;
124
124
  }
125
125
  const partial = fs.readFileSync(partialPath, 'utf8');
126
- const attrs = tagRegion(partial, 'attrs');
126
+ const meta = tagRegion(partial, 'meta');
127
127
  const body = tagRegion(partial, 'body');
128
- if (!attrs || !body || contentLines(body).length === 0) {
128
+ if (!meta || !body || contentLines(body).length === 0) {
129
129
  // A blanked partial (description removed upstream) has both regions
130
130
  // but no content. Wiring a page to it would publish an empty page.
131
131
  results.skipped.push({ page: key, reason: 'description partial is empty' });
132
132
  continue;
133
133
  }
134
134
 
135
- const includeAttrs = descriptionIncludeLine({ typeDir: partialTypeDir, name }, 'attrs');
135
+ const includeMeta = descriptionIncludeLine({ typeDir: partialTypeDir, name }, 'meta');
136
136
  const includeBody = descriptionIncludeLine({ typeDir: partialTypeDir, name }, 'body');
137
137
 
138
138
  let lines = fs.readFileSync(pagePath, 'utf8').split('\n');
139
139
  let changed = false;
140
140
 
141
141
  // --- header ---------------------------------------------------------
142
- if (!lines.some((l) => l.trim() === includeAttrs)) {
142
+ if (!lines.some((l) => l.trim() === includeMeta)) {
143
143
  const open = lines.findIndex((l) => META_TAG_OPEN.test(l));
144
144
  const close = lines.findIndex((l, i) => i > open && META_TAG_CLOSE.test(l));
145
- const attrValue = contentLines(attrs)
145
+ const metaValue = contentLines(meta)
146
146
  .find((l) => l.startsWith(':description:'));
147
147
  const pageAttr = lines.slice(0, introOf(lines)).find((l) => l.startsWith(':description:'));
148
- if (!attrValue) {
149
- // The connector has no summary, so the attrs region sets nothing.
148
+ if (!metaValue) {
149
+ // The connector has no summary, so the meta region sets nothing.
150
150
  // Replacing a hand-written :description: with it would drop the
151
151
  // page's meta description.
152
- results.skipped.push({ page: key, reason: 'attrs region sets no :description:' });
153
- } else if (pageAttr && flattenToAttributeValue(pageAttr) !== flattenToAttributeValue(attrValue)) {
152
+ results.skipped.push({ page: key, reason: 'meta region sets no :description:' });
153
+ } else if (pageAttr && flattenToAttributeValue(pageAttr) !== flattenToAttributeValue(metaValue)) {
154
154
  // The page's meta description is not the connector summary, so it
155
155
  // was authored by hand (often tuned for search) and swapping in the
156
156
  // include would silently discard that editorial work. 13 pages in
@@ -159,7 +159,7 @@ function migrateDescriptionsToPartials ({
159
159
  // header alone.
160
160
  results.skipped.push({ page: key, reason: 'hand-authored :description: differs from the summary (needs a human)' });
161
161
  } else if (open !== -1 && close > open) {
162
- lines.splice(open + 1, close - open - 1, includeAttrs);
162
+ lines.splice(open + 1, close - open - 1, includeMeta);
163
163
  results.headers.push(key);
164
164
  changed = true;
165
165
  } else {
@@ -169,7 +169,7 @@ function migrateDescriptionsToPartials ({
169
169
  // :description: lines in the first place.
170
170
  const attrLine = lines.findIndex((l) => l.startsWith(':description:'));
171
171
  if (attrLine !== -1 && attrLine < introOf(lines)) {
172
- lines.splice(attrLine, 1, '// tag::meta[]', includeAttrs, '// end::meta[]');
172
+ lines.splice(attrLine, 1, '// tag::meta[]', includeMeta, '// end::meta[]');
173
173
  results.headers.push(key);
174
174
  changed = true;
175
175
  } else {
@@ -183,7 +183,7 @@ function migrateDescriptionsToPartials ({
183
183
  // it could not see and the page ended up with two. AsciiDoc takes the
184
184
  // last assignment, so leaving the literal one there would make the
185
185
  // include a no-op and the page would keep publishing frozen text.
186
- if (lines.some((l) => l.trim() === includeAttrs)) {
186
+ if (lines.some((l) => l.trim() === includeMeta)) {
187
187
  for (let i = introOf(lines) - 1; i >= 0; i--) {
188
188
  if (lines[i].startsWith(':description:')) {
189
189
  lines.splice(i, 1);
@@ -3,9 +3,9 @@
3
3
  :type: {{type}}
4
4
  {{#if support}}:support: {{support}}{{/if}}
5
5
  :categories: [{{join categories ", "}}]
6
- {{#if hasDescriptionAttrs}}
6
+ {{#if hasDescriptionMeta}}
7
7
  // tag::meta[]
8
- {{{descriptionInclude this "attrs"}}}
8
+ {{{descriptionInclude this "meta"}}}
9
9
  // end::meta[]
10
10
  {{else if summary}}
11
11
  // tag::meta[]
@@ -16,7 +16,7 @@
16
16
  :description: {{{summaryAttribute description}}}
17
17
  // end::meta[]
18
18
  {{/if}}
19
- {{!-- The attrs include stays inside the meta tag region so single-source
19
+ {{!-- The meta include stays inside the meta tag region so single-source
20
20
  stubs keep inheriting :description: as a header attribute, and the
21
21
  value now refreshes with the connector instead of being frozen at
22
22
  first draft. Component families with no description partial keep the
@@ -2,12 +2,12 @@
2
2
  // This content is autogenerated. Do not edit manually. The summary and description come from the connector's definition in the Connect source. To change them, update the connector upstream, or use the doc-tools CLI with the --overrides option: https://redpandadata.atlassian.net/wiki/spaces/DOC/pages/1247543314/Generate+reference+docs+for+Redpanda+Connect
3
3
  //
4
4
  // Pages consume this partial in two places so both stay fresh:
5
- // header: include::...[tag=attrs] (sets :description: for search snippets and meta tags)
5
+ // header: include::...[tag=meta] (sets :description: for search snippets and meta tags)
6
6
  // body: include::...[tag=body] (renders the summary and description)
7
7
 
8
- // tag::attrs[]
8
+ // tag::meta[]
9
9
  {{#if summary}}:description: {{{summaryAttribute summary}}}
10
- {{/if}}// end::attrs[]
10
+ {{/if}}// end::meta[]
11
11
 
12
12
  // tag::body[]
13
13
  {{#if summary}}{{{summary}}}
@@ -1,166 +0,0 @@
1
- 'use strict';
2
-
3
- const { raiseListenerLimit } = require('./util/raise-listener-limit')
4
-
5
- const File = require('vinyl')
6
- const { posix: path } = require('path')
7
-
8
- const ENCODED_SPACE_RX = /%20/g
9
-
10
- module.exports.register = function () {
11
- raiseListenerLimit(this)
12
- this.once('contextStarted', () => {
13
- const { publishFiles: produceRedirectsDelegate } = this.getFunctions()
14
- this.replaceFunctions({
15
- produceRedirects (playbook, aliases) {
16
- if ('findBy' in aliases) aliases = aliases.findBy({ family: 'alias' }) // @deprecated remove in Antora 4
17
- if (!(aliases = aliases.filter((it) => it.pub.url !== it.rel.pub.url)).length) return []
18
- let siteUrl = playbook.site.url
19
- if (siteUrl) siteUrl = stripTrailingSlash(siteUrl, '')
20
- const directoryRedirects = (playbook.urls.htmlExtensionStyle || 'default') !== 'default'
21
- switch (playbook.urls.redirectFacility) {
22
- case 'gitlab':
23
- return createNetlifyRedirects(aliases, extractUrlPath(siteUrl), !directoryRedirects, false)
24
- case 'httpd':
25
- return createHttpdHtaccess(aliases, extractUrlPath(siteUrl), directoryRedirects)
26
- case 'netlify':
27
- return createNetlifyRedirects(aliases, extractUrlPath(siteUrl), !directoryRedirects)
28
- case 'nginx':
29
- return createNginxRewriteConf(aliases, extractUrlPath(siteUrl))
30
- case 'static':
31
- return populateStaticRedirectFiles(
32
- aliases.filter((it) => it.out),
33
- siteUrl
34
- )
35
- default:
36
- return unpublish(aliases)
37
- }
38
- }
39
- })
40
- })
41
- }
42
-
43
- function createStaticRedirectContents (file, siteUrl) {
44
- const targetUrl = file.rel.pub.url
45
- let linkTag
46
- let to = targetUrl.charAt() === '/' ? computeRelativeUrlPath(file.pub.url, targetUrl) : undefined
47
- let toText = to
48
- if (to) {
49
- if (siteUrl && siteUrl.charAt() !== '/') {
50
- linkTag = `<link rel="canonical" href="${(toText = siteUrl + targetUrl)}">\n`
51
- }
52
- } else {
53
- linkTag = `<link rel="canonical" href="${(toText = to = targetUrl)}">\n`
54
- }
55
- return `<!DOCTYPE html>
56
- <meta charset="utf-8">
57
- ${linkTag || ''}<script>location="${to}"</script>
58
- <meta http-equiv="refresh" content="0; url=${to}">
59
- <meta name="robots" content="noindex">
60
- <title>Redirect Notice</title>
61
- <h1>Redirect Notice</h1>
62
- <p>The page you requested has been relocated to <a href="${to}">${toText}</a>.</p>`
63
- }
64
-
65
- function extractUrlPath (url) {
66
- if (url) {
67
- if (url.charAt() === '/') return url
68
- const urlPath = new URL(url).pathname
69
- return urlPath === '/' ? '' : urlPath
70
- } else {
71
- return ''
72
- }
73
- }
74
-
75
- function createHttpdHtaccess (files, urlPath, directoryRedirects = false) {
76
- const rules = files.reduce((accum, file) => {
77
- delete file.out
78
- let fromUrl = file.pub.url
79
- fromUrl = ~fromUrl.indexOf('%20') ? `'${urlPath}${fromUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + fromUrl
80
- let toUrl = file.rel.pub.url
81
- toUrl = ~toUrl.indexOf('%20') ? `'${urlPath}${toUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + toUrl
82
- // see https://httpd.apache.org/docs/current/en/mod/mod_alias.html#redirect
83
- // NOTE: redirect rule for directory prefix does not require trailing slash
84
- if (file.pub.splat) {
85
- accum.push(`Redirect 302 ${fromUrl} ${stripTrailingSlash(toUrl)}`)
86
- } else if (directoryRedirects) {
87
- accum.push(`RedirectMatch 301 ^${regexpEscape(fromUrl)}$ ${stripTrailingSlash(toUrl)}`)
88
- } else {
89
- accum.push(`Redirect 301 ${fromUrl} ${toUrl}`)
90
- }
91
- return accum
92
- }, [])
93
- return [new File({ contents: Buffer.from(rules.join('\n') + '\n'), out: { path: '.htaccess' } })]
94
- }
95
-
96
- // NOTE: a trailing slash on the pathname will be ignored
97
- // see https://docs.netlify.com/routing/redirects/redirect-options/#trailing-slash
98
- // however, we keep it when generating the rules for clarity
99
- function createNetlifyRedirects (files, urlPath, addDirectoryRedirects = false, useForceFlag = true) {
100
- const rules = files.reduce((accum, file) => {
101
- delete file.out
102
- const fromUrl = urlPath + file.pub.url
103
- const toUrl = urlPath + file.rel.pub.url
104
- const forceFlag = useForceFlag ? '!' : ''
105
- if (file.pub.splat) {
106
- accum.push(`${fromUrl}/* ${ensureTrailingSlash(toUrl)}:splat 302${forceFlag}`)
107
- } else {
108
- accum.push(`${fromUrl} ${toUrl} 301${forceFlag}`)
109
- if (addDirectoryRedirects && fromUrl.endsWith('/index.html')) {
110
- accum.push(`${fromUrl.substr(0, fromUrl.length - 10)} ${toUrl} 301${forceFlag}`)
111
- }
112
- }
113
- return accum
114
- }, [])
115
- return [new File({ contents: Buffer.from(rules.join('\n') + '\n'), out: { path: '_redirects' } })]
116
- }
117
-
118
- function createNginxRewriteConf (files, urlPath) {
119
- const rules = files.map((file) => {
120
- delete file.out
121
- let fromUrl = file.pub.url
122
- fromUrl = ~fromUrl.indexOf('%20') ? `'${urlPath}${fromUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + fromUrl
123
- let toUrl = file.rel.pub.url
124
- toUrl = ~toUrl.indexOf('%20') ? `'${urlPath}${toUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + toUrl
125
- if (file.pub.splat) {
126
- const toUrlWithTrailingSlash = ensureTrailingSlash(toUrl)
127
- return `location ^~ ${fromUrl}/ { rewrite ^${regexpEscape(fromUrl)}/(.*)$ ${toUrlWithTrailingSlash}$1 redirect; }`
128
- } else {
129
- return `location = ${fromUrl} { return 301 ${toUrl}; }`
130
- }
131
- })
132
- return [new File({ contents: Buffer.from(rules.join('\n').trim() + '\n'), out: { path: '.etc/nginx/rewrite.conf' } })]
133
- }
134
-
135
- function populateStaticRedirectFiles(files, siteUrl) {
136
- for (const file of files) {
137
- const content = createStaticRedirectContents(file, siteUrl) + '\n';
138
- file.contents = Buffer.from(content);
139
- }
140
- return []
141
- }
142
-
143
- function unpublish (files) {
144
- files.forEach((file) => delete file.out)
145
- return []
146
- }
147
-
148
- function computeRelativeUrlPath (from, to) {
149
- if (to === from) return to.charAt(to.length - 1) === '/' ? './' : path.basename(to)
150
- return (path.relative(path.dirname(from + '.'), to) || '.') + (to.charAt(to.length - 1) === '/' ? '/' : '')
151
- }
152
-
153
- function ensureTrailingSlash (str) {
154
- return str.charAt(str.length - 1) === '/' ? str : str + '/'
155
- }
156
-
157
- function stripTrailingSlash (str, root = '/') {
158
- if (str === '/') return root
159
- const lastIdx = str.length - 1
160
- return str.charAt(lastIdx) === '/' ? str.substr(0, lastIdx) : str
161
- }
162
-
163
- function regexpEscape (str) {
164
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // don't escape "-" since it's meaningless in a literal
165
- }
166
-