@redpanda-data/docs-extensions-and-macros 5.7.1 → 5.9.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.
@@ -20,6 +20,8 @@ antora:
20
20
 
21
21
  == config_ref
22
22
 
23
+ IMPORTANT: `config_ref` is deprecated. The <<prop>> macro supersedes all of its functionality: it validates names against the published property reference, discovers the target page dynamically instead of taking a manual path, powers hover tooltips, and its `helm-path=auto` attribute replaces `config_ref`'s Kubernetes prefixing with a per-property correct Helm values path (`config_ref` prefixed every property with `storage.tiered.config.`, mislabeling non-tiered properties). Migrate call sites with `doc-tools generate migrate-property-refs --config-refs`. The macro stays registered only because the generated property partials still emit it.
24
+
23
25
  This inline macro is used to generate a reference to a configuration value in the Redpanda documentation. The macro's parameters allow for control over the generated reference's format and the type of output produced.
24
26
 
25
27
  === Usage
@@ -62,6 +64,171 @@ asciidoc:
62
64
  - '@redpanda-data/docs-extensions-and-macros/macros/config-ref'
63
65
  ----
64
66
 
67
+ == enterprise
68
+
69
+ This inline macro marks a feature as an enterprise feature in prose. The feature name renders as a uniquely styled term with a tooltip explaining that the feature requires an Enterprise Edition license, and links to the feature's documentation or to the licensing page.
70
+
71
+ The macro validates its target against the enterprise features registry: `modules/ROOT/partials/enterprise-features.yml` in the `shared` component (the `shared/` folder of the docs repository). The registry is the canonical list of enterprise features and their approved external names. Targets are matched case-insensitively, including aliases, and the canonical name, feature page link, and tooltip come from the matching entry. A target that matches no entry is reported according to `enterprise-validate`. To mark a new feature as enterprise, add it to the registry first.
72
+
73
+ === Usage
74
+
75
+ [,asciidoc]
76
+ ----
77
+ enterprise:Continuous Data Balancing[]
78
+ enterprise:Tiered Storage[xref=manage:tiered-storage.adoc]
79
+ enterprise:Audit Logging[text=audit logging]
80
+ enterprise:Iceberg Topics[tooltip=Iceberg Topics requires an Enterprise Edition license and object storage.]
81
+ ----
82
+
83
+ The macro target is the feature name, which can contain spaces. Named attributes:
84
+
85
+ xref::
86
+ Optional Antora resource ID of the feature's documentation page. Overrides the registry entry's link. When neither is set, the term links to the licensing page.
87
+ +
88
+ Registry entries can carry environment-specific pages: on pages with the `env-cloud` attribute the entry's `xref-cloud` is used, on pages with `env-kubernetes` the entry's `xref-kubernetes` is used, and otherwise the entry's default `xref` (the Linux or generic self-managed page). This mirrors the `config_ref` macro's environment awareness.
89
+
90
+ text::
91
+ Optional display text override. Use it when the sentence needs different casing or phrasing than the feature name.
92
+
93
+ tooltip::
94
+ Optional tooltip text override. Overrides the registry entry's `tooltip`. The default is `<feature> requires an Enterprise Edition license.`
95
+
96
+ === Configuration options
97
+
98
+ enterprise-validate::
99
+ How to report a target that does not match any registry entry: `warn` (default) logs a build warning with the file path and close-match suggestions, `error` fails the build, and `off` disables validation. When no registry is available in the build, the macro renders without validation and logs a single warning.
100
+
101
+ enterprise-licensing-page::
102
+ Resource ID of the licensing page used when no `xref` attribute is given. The default is `get-started:licensing/overview.adoc`.
103
+
104
+ enterprise-feature-role::
105
+ CSS class applied to the wrapping span. The default is `enterprise-feature`. Style this class in the UI bundle to give enterprise terms a consistent visual treatment.
106
+
107
+ enterprise-tooltip::
108
+ Controls the tooltip attribute: `title` (default), `true` (renders `data-enterprise-tooltip` for UI-driven tooltips), any attribute name starting with `data-`, or `false` to disable tooltips.
109
+
110
+ enterprise-links::
111
+ Set to `false` to render the term without a link.
112
+
113
+ enterprise-beta-badge::
114
+ Set to `false` to suppress the beta badge in prose. Generated tables always show it. See <<beta-features>>.
115
+
116
+ [[beta-features]]
117
+ === Beta features
118
+
119
+ Add `beta: true` to a registry entry to mark the feature as beta. The macro then renders a beta badge after the feature name, both in prose and in the generated `enterprise_features` tables, so one registry field keeps every reference consistent:
120
+
121
+ [,yaml]
122
+ ----
123
+ - name: Stretch Clusters
124
+ scope: operator
125
+ beta: true
126
+ feature-suffix: (StretchCluster resource)
127
+ ----
128
+
129
+ Add `beta-tooltip` to give the badge hover text.
130
+
131
+ Do not write a `badge:` macro call into `feature-suffix` to achieve this. The badge is emitted as an inline passthrough, so it renders whether or not the consuming playbook registers the `badge` macro, and it degrades to the text `(beta)` in Markdown. A hand-written macro call renders as literal source text in any build that does not register that macro.
132
+
133
+ === Markdown output
134
+
135
+ In HTML, the enterprise signal is carried by the span's CSS class and its tooltip attribute. Both are attributes, so an HTML-to-Markdown conversion drops them and the term arrives as an ordinary feature name. To keep the signal for readers of the Markdown pages, including AI agents, the `convert-to-markdown` extension appends an explicit `(enterprise)` marker and preserves any link:
136
+
137
+ [,markdown]
138
+ ----
139
+ Enable [Tiered Storage](../manage/tiered-storage.md) (enterprise) today.
140
+ ----
141
+
142
+ A feature that is also marked `beta: true` gets one combined marker rather than two adjacent parentheticals:
143
+
144
+ [,markdown]
145
+ ----
146
+ Enable [Stretch Clusters](../deploy/stretch-clusters.md) (enterprise, beta) today.
147
+ ----
148
+
149
+ This applies to the inline macro. The tables from `enterprise_features` need no enterprise marker, because their expiration column already states the licensing behavior as text. A beta badge in a table converts to `(beta)` on its own.
150
+
151
+ === Registration
152
+
153
+ [,yaml]
154
+ ----
155
+ asciidoc:
156
+ extensions:
157
+ - '@redpanda-data/docs-extensions-and-macros/macros/enterprise'
158
+ ----
159
+
160
+ == enterprise_features
161
+
162
+ This block macro renders the licensing feature table for one scope from the enterprise features registry (see <<enterprise>>). Because the table is generated, it can never drift from the registry.
163
+
164
+ === Usage
165
+
166
+ [,asciidoc]
167
+ ----
168
+ enterprise_features::redpanda[]
169
+ enterprise_features::connect[]
170
+ enterprise_features::operator[title=Operator features,heading=What stops working]
171
+ ----
172
+
173
+ The target is the scope: `redpanda`, `console`, `connect`, `operator`, or `cloud`. Rows render alphabetically by feature name with three columns: the feature name (linked to its documentation page when the entry has an `xref` or `url`), the description, and the expiration or restriction behavior. Named attributes:
174
+
175
+ title::
176
+ Optional table title override. The default is scope-specific, for example `Enterprise features in Redpanda`.
177
+
178
+ heading::
179
+ Optional third-column heading override. The default is `Behavior Upon Expiration` for the `redpanda` scope and `Restrictions Without Valid License` for the other scopes.
180
+
181
+ === Registration
182
+
183
+ Registered together with the `enterprise` inline macro (see <<enterprise>>).
184
+
185
+ == prop
186
+
187
+ This inline macro references a Redpanda configuration property in prose. The property renders as a marked `<code>` element (`property-ref` class plus a `data-property-name` attribute) that the docs UI decorates with a hover-documentation tooltip. Marking is opt-in: only properties referenced through this macro get tooltips, so ambiguous words such as `admin` or `rack` in unrelated contexts (Helm values, feature settings) are never decorated by mistake.
188
+
189
+ The macro validates its target against the published property reference: the newest `redpanda-properties-<tag>.json` attachment in the `reference` module of the build, the same data the tooltips fetch at runtime. To migrate existing plain-backtick property mentions, use `doc-tools generate migrate-property-refs`.
190
+
191
+ === Usage
192
+
193
+ [,asciidoc]
194
+ ----
195
+ prop:cloud_storage_enabled[]
196
+ prop:iceberg_enabled[link=true]
197
+ prop:retention_ms[link=true,page=properties/topic-properties]
198
+ prop:write_caching_default[text=write caching]
199
+ ----
200
+
201
+ The macro target is the property name. Named attributes:
202
+
203
+ link::
204
+ Set to `true` to also link the property to its reference page. The page is discovered dynamically: the macro indexes which reference-module page in the current component documents each property, by scanning property headings in the partials each page includes (respecting include tag filters such as `tags=redpanda-cloud`). Links are component-relative, so a Cloud page links to the Cloud component's own property pages, and they keep working if properties are split across different pages in the future. When discovery finds nothing, the page falls back deterministically to the property's `config_scope` (`cluster`, `broker`, or `topic`). This supersedes the `config_ref` macro's linking for most uses.
205
+
206
+ page::
207
+ Optional reference-page override (module-relative, without `.adoc`), for example `properties/object-storage-properties`. Overrides discovery entirely.
208
+
209
+ helm-path::
210
+ Set to `auto` to display the property as its Helm values path on pages rendered with the `env-kubernetes` attribute, so single-sourced content reads correctly for both Linux and Kubernetes audiences. The path is derived per property: `storage.tiered.config.<name>` for tiered storage properties, `config.node.<name>` for broker properties, and `config.cluster.<name>` for other cluster properties. On pages without `env-kubernetes`, the plain property name renders. This supersedes the deprecated `config_ref` macro's prefixing.
211
+
212
+ text::
213
+ Optional display text override. Wins over the Helm path display.
214
+
215
+ === Configuration options
216
+
217
+ property-validate::
218
+ How to report a target that does not match any published property: `warn` (default) logs a build warning with the file path and close-match suggestions, `error` fails the build, and `off` disables validation. When no property JSON is available in the build, the macro renders without validation and logs a single warning.
219
+
220
+ property-ref-role::
221
+ CSS class applied to the code element. The default is `property-ref`.
222
+
223
+ === Registration
224
+
225
+ [,yaml]
226
+ ----
227
+ asciidoc:
228
+ extensions:
229
+ - '@redpanda-data/docs-extensions-and-macros/macros/prop'
230
+ ----
231
+
65
232
  == glossterm
66
233
 
67
234
  The `glossterm` inline macro provides a way to define and reference glossary terms in your AsciiDoc documents.
package/macros/badge.js CHANGED
@@ -1,21 +1,43 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * Build the HTML for a badge.
5
+ *
6
+ * Extracted from the macro body so other macros can render an identical badge
7
+ * without depending on the badge macro being registered in the consuming
8
+ * playbook. The enterprise macro uses it to render the beta badge for registry
9
+ * entries marked `beta: true`, which keeps one definition of the markup and
10
+ * the CSS class names.
11
+ *
12
+ * @param {object} opts
13
+ * @param {string} [opts.label] - Badge text.
14
+ * @param {string} [opts.size] - `large` renders without surrounding brackets.
15
+ * @param {string} [opts.tooltip] - Tooltip text, rendered as `data-tooltip`.
16
+ * @returns {string} Badge HTML.
17
+ */
18
+ function buildBadgeHtml ({ label, size, tooltip } = {}) {
19
+ const text = label || 'label';
20
+ const className = `badge--${text.toLowerCase().replace(/\s+/g, '-')}`;
21
+ const isLarge = size === 'large';
22
+ const sizeClass = isLarge ? 'badge--large' : '';
23
+ const tooltipAttr = tooltip ? ` data-tooltip="${tooltip}"` : '';
24
+
25
+ // Add brackets if not large. The brackets matter beyond styling: they are
26
+ // real text content, so the label survives an HTML-to-Markdown conversion as
27
+ // "(beta)" rather than vanishing with the CSS class.
28
+ const renderedLabel = isLarge ? text : `(${text})`;
29
+
30
+ return `<span class="badge ${className} ${sizeClass}"${tooltipAttr}>${renderedLabel}</span>`;
31
+ }
32
+
33
+ module.exports.buildBadgeHtml = buildBadgeHtml;
34
+
3
35
  module.exports.register = function (registry) {
4
36
  registry.inlineMacro(function () {
5
37
  const self = this;
6
38
  self.named('badge');
7
- self.process((parent, target, attrs) => {
8
- const label = attrs.label || 'label';
9
- const className = `badge--${label.toLowerCase().replace(/\s+/g, '-')}`;
10
- const isLarge = attrs.size === 'large';
11
- const sizeClass = isLarge ? 'badge--large' : '';
12
- const tooltip = attrs.tooltip;
13
- const tooltipAttr = tooltip ? ` data-tooltip="${tooltip}"` : '';
14
-
15
- // Add brackets if not large
16
- const renderedLabel = isLarge ? label : `(${label})`;
17
-
18
- return `<span class="badge ${className} ${sizeClass}"${tooltipAttr}>${renderedLabel}</span>`;
19
- });
39
+ self.process((parent, target, attrs) =>
40
+ buildBadgeHtml({ label: attrs.label, size: attrs.size, tooltip: attrs.tooltip })
41
+ );
20
42
  });
21
43
  };
@@ -0,0 +1,390 @@
1
+ 'use strict'
2
+
3
+ /* Macros for marking and listing enterprise features.
4
+ *
5
+ * Inline macro — mark a feature in prose:
6
+ *
7
+ * enterprise:Continuous Data Balancing[]
8
+ * enterprise:Tiered Storage[xref=manage:tiered-storage.adoc]
9
+ * enterprise:Audit Logging[text=audit logging]
10
+ * enterprise:Iceberg Topics[tooltip=Iceberg Topics requires an Enterprise Edition license and object storage.]
11
+ *
12
+ * The feature name renders as a uniquely styled term with a tooltip that
13
+ * explains the feature requires an Enterprise Edition license. The term
14
+ * links to the feature's documentation when an xref attribute is given,
15
+ * and otherwise to the licensing page, so readers can always reach an
16
+ * explanation of what having (or not having) a license means.
17
+ *
18
+ * Block macro — render the licensing feature table for one scope:
19
+ *
20
+ * enterprise_features::redpanda[]
21
+ * enterprise_features::connect[title=Enterprise features in Redpanda Connect]
22
+ *
23
+ * Registry validation:
24
+ *
25
+ * Both macros read the canonical enterprise features registry from the
26
+ * 'shared' component (modules/ROOT/partials/enterprise-features.yml in the
27
+ * shared folder of the docs repo). Inline targets are resolved against the
28
+ * registry case-insensitively, including aliases, and the canonical name,
29
+ * feature xref, and tooltip come from the matching entry. Unknown targets
30
+ * are reported according to the enterprise-validate attribute. When no
31
+ * registry is available (for example, outside an Antora build), the macros
32
+ * fall back to unvalidated rendering with a single warning.
33
+ *
34
+ * Document or site attributes:
35
+ *
36
+ * enterprise-validate 'warn' (default) to log unknown feature names,
37
+ * 'error' to fail the build, 'off' to disable
38
+ * validation.
39
+ * enterprise-licensing-page Resource ID of the licensing page used when
40
+ * no feature page is known
41
+ * (default: get-started:licensing/overview.adoc).
42
+ * enterprise-feature-role CSS class applied to the wrapping span
43
+ * (default: enterprise-feature).
44
+ * enterprise-tooltip 'title' (default), 'true' (renders
45
+ * data-enterprise-tooltip), any attribute name
46
+ * starting with 'data-', or 'false' to disable.
47
+ * enterprise-links 'true' (default) to render the term as a link.
48
+ *
49
+ * Example use in a playbook:
50
+ *
51
+ * asciidoc:
52
+ * extensions:
53
+ * - '@redpanda-data/docs-extensions-and-macros/macros/enterprise'
54
+ */
55
+
56
+ const yaml = require('js-yaml')
57
+ const chalk = require('chalk')
58
+ const { buildBadgeHtml } = require('./badge')
59
+
60
+ const $enterpriseRegistry = Symbol('$enterpriseRegistry')
61
+
62
+ const DEFAULT_LICENSING_PAGE = 'get-started:licensing/overview.adoc'
63
+ const DEFAULT_ROLE = 'enterprise-feature'
64
+ const BETA_LABEL = 'beta'
65
+
66
+ /**
67
+ * Render the beta badge for a registry entry marked `beta: true`.
68
+ *
69
+ * The badge HTML is built directly rather than emitted as `badge:[...]`
70
+ * AsciiDoc, so it renders whether or not the consuming playbook registers the
71
+ * badge macro. Writing the macro call into a registry field used to be the only
72
+ * way to flag a beta feature, and it silently produced the literal text
73
+ * `badge::[label=beta]` in any build without that macro registered.
74
+ *
75
+ * @param {object} entry - Registry entry.
76
+ * @returns {string} Badge HTML, or an empty string when the entry is not beta.
77
+ */
78
+ function buildBetaBadge (entry) {
79
+ if (!entry || entry.beta !== true) return ''
80
+ return buildBadgeHtml({
81
+ label: BETA_LABEL,
82
+ tooltip: entry['beta-tooltip'] || undefined,
83
+ })
84
+ }
85
+
86
+ /**
87
+ * The badge for a registry entry, wrapped for use in generated AsciiDoc.
88
+ *
89
+ * The block macro returns AsciiDoc source that Asciidoctor then parses, so raw
90
+ * HTML placed in a table cell is escaped and the reader sees the markup as
91
+ * text. An inline passthrough emits it verbatim instead, and unlike a
92
+ * `badge:[...]` macro call it does not require the badge macro to be registered
93
+ * in the consuming playbook.
94
+ *
95
+ * @param {object} entry - Registry entry.
96
+ * @returns {string} Passthrough-wrapped badge, or an empty string.
97
+ */
98
+ function buildBetaBadgeAsciiDoc (entry) {
99
+ const html = buildBetaBadge(entry)
100
+ return html ? `pass:[${html}]` : ''
101
+ }
102
+ const REGISTRY_FILENAME = 'enterprise-features.yml'
103
+ const VALID_SCOPES = ['redpanda', 'console', 'connect', 'operator', 'cloud']
104
+
105
+ const TABLE_TITLES = {
106
+ redpanda: 'Enterprise features in Redpanda',
107
+ console: 'Enterprise features in Redpanda Console',
108
+ connect: 'Enterprise features in Redpanda Connect',
109
+ operator: 'Enterprise features in the Redpanda Operator',
110
+ cloud: 'Enterprise features in Redpanda Cloud',
111
+ }
112
+
113
+ const THIRD_COLUMN_HEADINGS = {
114
+ redpanda: 'Behavior Upon Expiration',
115
+ default: 'Restrictions Without Valid License',
116
+ }
117
+
118
+ let warnedNoRegistry = false
119
+
120
+ /**
121
+ * Parse the registry YAML into a lookup structure. Exported for unit testing.
122
+ *
123
+ * @param {string} source - Raw YAML from enterprise-features.yml.
124
+ * @param {string} origin - Human-readable location for error messages.
125
+ * @returns {{features: object[], lookup: Map<string, object>}}
126
+ */
127
+ function parseRegistry (source, origin = REGISTRY_FILENAME) {
128
+ const data = yaml.load(source)
129
+ if (!data || !Array.isArray(data.features)) {
130
+ throw new Error(`Enterprise features registry ${origin} has no 'features' list.`)
131
+ }
132
+ const lookup = new Map()
133
+ const claim = (rawKey, feature, kind) => {
134
+ const key = rawKey.trim().toLowerCase()
135
+ if (!key) return
136
+ const existing = lookup.get(key)
137
+ if (existing && existing !== feature) {
138
+ throw new Error(`Duplicate enterprise feature ${kind} '${rawKey}' in ${origin}: used by both '${existing.name}' and '${feature.name}'.`)
139
+ }
140
+ lookup.set(key, feature)
141
+ }
142
+ for (const feature of data.features) {
143
+ if (!feature || !feature.name) {
144
+ throw new Error(`Enterprise features registry ${origin} has an entry without a name.`)
145
+ }
146
+ if (feature.scope && !VALID_SCOPES.includes(feature.scope)) {
147
+ throw new Error(`Enterprise feature '${feature.name}' in ${origin} has unknown scope '${feature.scope}'.`)
148
+ }
149
+ claim(feature.name, feature, 'name')
150
+ for (const alias of feature.aliases || []) claim(String(alias), feature, 'alias')
151
+ }
152
+ return { features: data.features, lookup }
153
+ }
154
+
155
+ /**
156
+ * Load and cache the registry from the shared component in the Antora
157
+ * content catalog. Returns undefined when no registry is available.
158
+ */
159
+ function loadRegistry (config) {
160
+ const contentCatalog = config && config.contentCatalog
161
+ if (!contentCatalog) return undefined
162
+ if (contentCatalog[$enterpriseRegistry] !== undefined) return contentCatalog[$enterpriseRegistry] || undefined
163
+ let registry = null
164
+ const partials = contentCatalog.findBy({ component: 'shared', module: 'ROOT', family: 'partial' }) || []
165
+ const registryFile = partials.find((file) => file.path && file.path.endsWith(REGISTRY_FILENAME))
166
+ if (registryFile) {
167
+ registry = parseRegistry(registryFile.contents.toString(), registryFile.path)
168
+ }
169
+ // Cache null too, so a missing registry is only searched for once per build.
170
+ contentCatalog[$enterpriseRegistry] = registry
171
+ return registry || undefined
172
+ }
173
+
174
+ function warnNoRegistry () {
175
+ if (warnedNoRegistry) return
176
+ warnedNoRegistry = true
177
+ console.warn(chalk.yellow(`Enterprise features registry (${REGISTRY_FILENAME} in the shared component) not found; enterprise: targets are not validated.`))
178
+ }
179
+
180
+ /**
181
+ * Report an unknown feature name according to the enterprise-validate mode.
182
+ */
183
+ function reportUnknownFeature ({ feature, mode, registry, filePath }) {
184
+ if (mode === 'off') return
185
+ const candidates = registry.features
186
+ .map((entry) => entry.name)
187
+ .filter((name) => name.toLowerCase().includes(feature.trim().toLowerCase().split(/\s+/)[0] || ''))
188
+ .slice(0, 3)
189
+ const hint = candidates.length ? ` Did you mean: ${candidates.join(', ')}?` : ''
190
+ const where = filePath ? ` in ${filePath}` : ''
191
+ 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.`
192
+ if (mode === 'error') throw new Error(message)
193
+ console.warn(chalk.yellow(message))
194
+ }
195
+
196
+ /**
197
+ * Pick the environment-appropriate feature page from a registry entry.
198
+ * Some features have separate documentation for Kubernetes, Linux
199
+ * (self-managed), and Redpanda Cloud. Mirrors the config_ref macro's
200
+ * environment-awareness: the env-cloud and env-kubernetes page attributes
201
+ * select the xref-cloud and xref-kubernetes registry fields, falling back
202
+ * to the default xref.
203
+ *
204
+ * @param {object} entry - Registry entry.
205
+ * @param {object} document - Asciidoctor document (for env attributes).
206
+ * @returns {string|undefined}
207
+ */
208
+ function resolveEntryXref (entry, document) {
209
+ if (!entry) return undefined
210
+ if (document.getAttribute('env-cloud') !== undefined && entry['xref-cloud']) return entry['xref-cloud']
211
+ if (document.getAttribute('env-kubernetes') !== undefined && entry['xref-kubernetes']) return entry['xref-kubernetes']
212
+ return entry.xref
213
+ }
214
+
215
+ /**
216
+ * Resolve the tooltip attribute name from the enterprise-tooltip document
217
+ * attribute. Mirrors the glossary macro's contract.
218
+ *
219
+ * @param {string|undefined} raw - Raw attribute value.
220
+ * @returns {string|undefined} Attribute name to emit, or undefined when disabled.
221
+ */
222
+ function resolveTooltipAttribute (raw) {
223
+ if (raw === 'false') return undefined
224
+ if (raw === undefined || raw === 'title') return 'title'
225
+ if (raw === 'true') return 'data-enterprise-tooltip'
226
+ if (raw.startsWith('data-')) return raw
227
+ console.warn(`enterprise-tooltip attribute '${raw}' must be 'title', 'true', 'false', or start with 'data-'. Falling back to 'title'.`)
228
+ return 'title'
229
+ }
230
+
231
+ /**
232
+ * Build the AsciiDoc content emitted for one inline macro instance. Exported
233
+ * for unit testing.
234
+ *
235
+ * @param {object} opts
236
+ * @param {string} opts.feature - Canonical feature name.
237
+ * @param {string} [opts.text] - Display text override.
238
+ * @param {string} [opts.xref] - Resource ID of the feature documentation.
239
+ * @param {string} [opts.url] - Absolute link used when no xref exists.
240
+ * @param {string} [opts.tooltip] - Tooltip text override.
241
+ * @param {string} opts.licensingPage - Resource ID of the licensing page.
242
+ * @param {string} opts.role - CSS class for the wrapping span.
243
+ * @param {string|undefined} opts.tooltipAttr - Tooltip attribute name, or undefined to omit.
244
+ * @param {boolean} opts.links - Whether to render a link.
245
+ * @returns {string}
246
+ */
247
+ function buildEnterpriseContent ({ feature, text, xref, url, tooltip, licensingPage, role, tooltipAttr, links }) {
248
+ const display = text || feature
249
+ const tooltipText = tooltip || `${feature} requires an Enterprise Edition license.`
250
+ const escapedTooltip = tooltipText.replace(/"/g, '&quot;')
251
+ const tooltipHtml = tooltipAttr ? ` ${tooltipAttr}="${escapedTooltip}"` : ''
252
+ let inner = display
253
+ if (links) {
254
+ inner = xref ? `xref:${xref}[${display}]` : (url ? `link:${url}[${display}]` : `xref:${licensingPage}[${display}]`)
255
+ }
256
+ return `<span class="${role}"${tooltipHtml}>${inner}</span>`
257
+ }
258
+
259
+ /**
260
+ * Build the AsciiDoc source of the licensing feature table for one scope.
261
+ * Exported for unit testing.
262
+ *
263
+ * @param {object[]} features - All registry entries.
264
+ * @param {string} scope - Scope to render.
265
+ * @param {object} [opts]
266
+ * @param {string} [opts.title] - Table title override.
267
+ * @param {string} [opts.heading] - Third column heading override.
268
+ * @returns {string}
269
+ */
270
+ function buildFeatureTable (features, scope, opts = {}) {
271
+ const rows = features
272
+ .filter((feature) => feature.scope === scope)
273
+ .sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }))
274
+ const title = opts.title || TABLE_TITLES[scope]
275
+ const heading = opts.heading || THIRD_COLUMN_HEADINGS[scope] || THIRD_COLUMN_HEADINGS.default
276
+ const lines = [`.${title}`, '[cols="1a,2a,2a"]', '|===', `| Feature | Description | ${heading}`, '']
277
+ for (const feature of rows) {
278
+ let cell = feature.xref
279
+ ? `xref:${feature.xref}[${feature.name}]`
280
+ : (feature.url ? `link:${feature.url}[${feature.name}]` : feature.name)
281
+ if (feature['feature-suffix']) cell += ` ${feature['feature-suffix']}`
282
+ const betaBadge = buildBetaBadgeAsciiDoc(feature)
283
+ if (betaBadge) cell += ` ${betaBadge}`
284
+ if (feature['show-gating-property'] && feature['gating-property']) cell += `\n(\`${feature['gating-property']}\`)`
285
+ lines.push(`| ${cell}`)
286
+ lines.push(`| ${(feature.description || '').trim()}`)
287
+ lines.push(`| ${(feature.expiration || '').trim()}`)
288
+ lines.push('')
289
+ }
290
+ lines.push('|===')
291
+ return lines.join('\n')
292
+ }
293
+
294
+ function enterpriseInlineMacro (config) {
295
+ return function () {
296
+ const self = this
297
+ self.named('enterprise')
298
+ // Specifying the regexp allows spaces in the feature name.
299
+ self.$option('regexp', /enterprise:([^[]+)\[(|.*?[^\\])\]/)
300
+ self.process(function (parent, target, attributes) {
301
+ const document = parent.getDocument()
302
+ let registry
303
+ if (config && config.contentCatalog) {
304
+ registry = loadRegistry(config)
305
+ if (!registry) warnNoRegistry()
306
+ }
307
+ let feature = target
308
+ let entry
309
+ if (registry) {
310
+ entry = registry.lookup.get(target.trim().toLowerCase())
311
+ if (entry) {
312
+ feature = entry.name
313
+ } else {
314
+ reportUnknownFeature({
315
+ feature: target,
316
+ mode: document.getAttribute('enterprise-validate', 'warn'),
317
+ registry,
318
+ filePath: config && config.file && config.file.src && config.file.src.path,
319
+ })
320
+ }
321
+ }
322
+ let content = buildEnterpriseContent({
323
+ feature,
324
+ text: attributes.text,
325
+ xref: attributes.xref || resolveEntryXref(entry, document),
326
+ url: entry && entry.url,
327
+ tooltip: attributes.tooltip || (entry && entry.tooltip) || undefined,
328
+ licensingPage: document.getAttribute('enterprise-licensing-page', DEFAULT_LICENSING_PAGE),
329
+ role: document.getAttribute('enterprise-feature-role', DEFAULT_ROLE),
330
+ tooltipAttr: resolveTooltipAttribute(document.getAttribute('enterprise-tooltip')),
331
+ links: document.getAttribute('enterprise-links', 'true') === 'true',
332
+ })
333
+ // A feature marked beta in the registry is beta wherever it is
334
+ // referenced, so prose gets the same badge as the generated tables.
335
+ // Set enterprise-beta-badge to false to suppress it in prose only.
336
+ if (document.getAttribute('enterprise-beta-badge', 'true') === 'true') {
337
+ const betaBadge = buildBetaBadge(entry)
338
+ if (betaBadge) content += ` ${betaBadge}`
339
+ }
340
+ // The xref inside the span is resolved by the 'macros' substitution,
341
+ // the same mechanism the config_ref macro relies on.
342
+ return self.createInline(parent, 'quoted', content, { attributes: { subs: 'macros' } })
343
+ })
344
+ }
345
+ }
346
+
347
+ function enterpriseFeaturesBlockMacro (config) {
348
+ return function () {
349
+ const self = this
350
+ self.named('enterprise_features')
351
+ self.process(function (parent, target, attributes) {
352
+ const scope = (target || attributes.scope || '').trim()
353
+ if (!VALID_SCOPES.includes(scope)) {
354
+ throw new Error(`enterprise_features::[] needs a scope of ${VALID_SCOPES.join(', ')} as its target, got '${scope}'.`)
355
+ }
356
+ const registry = config && config.contentCatalog ? loadRegistry(config) : undefined
357
+ if (!registry) {
358
+ warnNoRegistry()
359
+ return self.parseContent(parent, `WARNING: The enterprise features registry is unavailable, so the ${scope} feature table cannot be rendered.`)
360
+ }
361
+ const table = buildFeatureTable(registry.features, scope, { title: attributes.title, heading: attributes.heading })
362
+ // A block anchor before the macro call ([[my-id]]) is consumed into the
363
+ // macro's id attribute. Re-emit it so crossrefs to the table keep working.
364
+ const source = attributes.id ? `[[${attributes.id}]]\n${table}` : table
365
+ return self.parseContent(parent, source)
366
+ })
367
+ }
368
+ }
369
+
370
+ function register (registry, config = {}) {
371
+ if (typeof registry.register === 'function') {
372
+ registry.register(function () {
373
+ this.inlineMacro(enterpriseInlineMacro(config))
374
+ this.blockMacro(enterpriseFeaturesBlockMacro(config))
375
+ })
376
+ } else if (typeof registry.inlineMacro === 'function') {
377
+ registry.inlineMacro(enterpriseInlineMacro(config))
378
+ if (typeof registry.blockMacro === 'function') registry.blockMacro(enterpriseFeaturesBlockMacro(config))
379
+ } else {
380
+ console.warn("no 'inlineMacro' method on alleged registry")
381
+ }
382
+ return registry
383
+ }
384
+
385
+ module.exports.register = register
386
+ module.exports.buildEnterpriseContent = buildEnterpriseContent
387
+ module.exports.buildFeatureTable = buildFeatureTable
388
+ module.exports.parseRegistry = parseRegistry
389
+ module.exports.resolveEntryXref = resolveEntryXref
390
+ module.exports.resolveTooltipAttribute = resolveTooltipAttribute