@redpanda-data/docs-extensions-and-macros 5.12.0 → 5.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/set-available-attachment-versions.js +23 -9
- package/macros/PROPERTY_AND_ENTERPRISE_REFERENCES.adoc +5 -3
- package/macros/REFERENCE.adoc +8 -8
- package/macros/badge.js +28 -6
- package/macros/enterprise.js +63 -14
- package/macros/prop.js +59 -12
- package/package.json +1 -1
- package/tools/property-extractor/generate-handlebars-docs.js +100 -0
- package/tools/property-extractor/helpers/anchorName.js +14 -1
- package/tools/property-extractor/helpers/canonicalizePropertyXrefs.js +54 -0
- package/tools/property-extractor/helpers/index.js +2 -0
- package/tools/property-extractor/helpers/normalizePropertyAnchors.js +86 -0
- package/tools/property-extractor/templates/topic-property-mappings.hbs +13 -4
- package/tools/property-extractor/templates/topic-property.hbs +11 -1
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { raiseListenerLimit } = require('./util/raise-listener-limit')
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// The same comparator the prop macro uses to pick a dataset. semver.coerce
|
|
6
|
+
// keeps 'rc10' as one alphanumeric identifier and string-compares it, so it
|
|
7
|
+
// ranked v26.3.1-rc2 above v26.3.1-rc10 while the macro ranked them correctly:
|
|
8
|
+
// build-time validation and the runtime tooltip fetch would then read different
|
|
9
|
+
// files. Sharing the comparator is the only way they cannot disagree.
|
|
10
|
+
const { compareTags } = require('../macros/prop')
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* Points UI templates at generated JSON attachments that actually exist.
|
|
@@ -19,7 +24,9 @@ const semver = require('semver')
|
|
|
19
24
|
* component version:
|
|
20
25
|
*
|
|
21
26
|
* - available-properties-tag: newest redpanda-properties-<tag>.json attachment
|
|
22
|
-
* in the reference module of
|
|
27
|
+
* in the reference module of a component version that publishes property
|
|
28
|
+
* data (the ROOT and streaming components, plus any component shipping such
|
|
29
|
+
* an attachment of its own)
|
|
23
30
|
* - available-connect-version: newest connect-<version>.json attachment in the
|
|
24
31
|
* components module of the connect component
|
|
25
32
|
* - page-disable-property-tooltips: 'true' on streaming versions that have no
|
|
@@ -54,8 +61,19 @@ module.exports.register = function () {
|
|
|
54
61
|
if (!isProperties && !isConnect) return
|
|
55
62
|
|
|
56
63
|
component.versions.forEach((compVer) => {
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
// Copy before writing. Antora hands every component version whose
|
|
65
|
+
// antora.yml declares no `asciidoc:` block THE SAME siteAsciiDocConfig
|
|
66
|
+
// object -- see the NOTE at content-classifier/lib/content-catalog.js
|
|
67
|
+
// ("if no AsciiDoc attributes are defined in the component descriptor,
|
|
68
|
+
// asciidoc is the siteAsciiDocConfig object"). Mutating it in place
|
|
69
|
+
// wrote these attributes into the shared site config, so a component
|
|
70
|
+
// publishing no property data still advertised a dataset, and because
|
|
71
|
+
// the write is guarded on the attribute being unset, the first version
|
|
72
|
+
// processed won and every later version silently kept that value --
|
|
73
|
+
// no log line, and the wrong tag. Cloning both levels makes the write
|
|
74
|
+
// per-version, which is what the attribute means.
|
|
75
|
+
const asciidoc = (compVer.asciidoc = { ...compVer.asciidoc })
|
|
76
|
+
const attributes = (asciidoc.attributes = { ...asciidoc.attributes })
|
|
59
77
|
const attachments = contentCatalog.findBy({
|
|
60
78
|
component: component.name,
|
|
61
79
|
version: compVer.version,
|
|
@@ -98,17 +116,13 @@ module.exports.register = function () {
|
|
|
98
116
|
|
|
99
117
|
function findNewestVersion (attachments, module, rx) {
|
|
100
118
|
let newestRaw = null
|
|
101
|
-
let newestSemver = null
|
|
102
119
|
attachments.forEach((attachment) => {
|
|
103
120
|
if (attachment.src.module !== module) return
|
|
104
121
|
const basename = attachment.src.relative.split('/').pop()
|
|
105
122
|
const match = basename.match(rx)
|
|
106
123
|
if (!match) return
|
|
107
124
|
const raw = match[1]
|
|
108
|
-
|
|
109
|
-
if (!parsed) return
|
|
110
|
-
if (!newestSemver || semver.gt(parsed, newestSemver)) {
|
|
111
|
-
newestSemver = parsed
|
|
125
|
+
if (!newestRaw || compareTags(raw, newestRaw) > 0) {
|
|
112
126
|
newestRaw = raw
|
|
113
127
|
}
|
|
114
128
|
})
|
|
@@ -31,7 +31,7 @@ Every release series has its own copy of that data, and the build picks the copy
|
|
|
31
31
|
|
|
32
32
|
*It can link itself.* Add `link=true` and the property links to its reference page. You don't say which page -- the build works that out by looking at which reference page actually documents that property (details below). If our property reference is ever reorganized into different pages, every link follows automatically.
|
|
33
33
|
|
|
34
|
-
*It adapts to Kubernetes pages.* Add `helm-path=auto` and, on pages rendered for Kubernetes, the property displays as the Helm values path readers actually type -- `config.cluster.log_segment_size`, `config.node.fips_mode`, or `storage.tiered.config.cloud_storage_enabled` for tiered storage settings. On Linux pages the same call shows the plain name. One sentence, correct for both audiences.
|
|
34
|
+
*It adapts to Kubernetes pages.* Add `helm-path=auto` and, on pages rendered for Kubernetes, the property displays as the Helm values path readers actually type -- `config.cluster.log_segment_size`, `config.node.fips_mode`, or `storage.tiered.config.cloud_storage_enabled` for tiered storage settings. On Linux pages the same call shows the plain name, as does a topic property anywhere: topic properties are set per topic rather than in `values.yaml`, so there is no path to show. One sentence, correct for both audiences.
|
|
35
35
|
|
|
36
36
|
*Once per paragraph is enough.* Mark the first mention of a property in a paragraph and leave later mentions as plain backticks -- a paragraph doesn't need three identical tooltips. The migration tool follows this rule, and the UI enforces it as a backstop: even if every mention is marked, only the first one per paragraph gets the tooltip.
|
|
37
37
|
|
|
@@ -41,9 +41,11 @@ Rather than hardcoding "cluster properties live on the cluster-properties page,"
|
|
|
41
41
|
|
|
42
42
|
Because the lookup is done separately for each part of the site, a Cloud page links to Cloud's own property pages and a streaming page to streaming's, and a link is only ever emitted when a page in that same doc set actually documents the property. Nothing is linked across doc sets, and no page is guessed from a property's scope -- a link the build can't verify is a broken link.
|
|
43
43
|
|
|
44
|
-
Availability is a separate question from linking, and it comes from the data rather than from which pages happen to include what.
|
|
44
|
+
Availability is a separate question from linking, and it comes from the data rather than from which pages happen to include what. Cloud is managed Redpanda, so it has no property release of its own: a Cloud page reads the newest *released* self-managed file, ignoring release candidates, because Cloud runs released Redpanda.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
What that file can rule out is narrower than it looks. `cloud_supported` is derived from the Cloud control plane's install pack, and that source describes *cluster* configuration and nothing else. So the gate applies to cluster properties only: a cluster property the control plane forbids renders as plain code -- no link and no tooltip -- plus a build warning naming the property and the file, because a tooltip there would tell a Cloud reader they can change something they can't. For any other scope the file has no opinion, and a mention is allowed. Topic properties in particular are set per topic through the Kafka API, rpk, or Console -- a surface the control plane doesn't gate -- and Cloud publishes a topic properties reference of its own.
|
|
47
|
+
|
|
48
|
+
An absent `cloud_supported` therefore means "not asserted", not "unsupported". Only an explicit `false` on a cluster property blocks a mention. (Where the flag is written, it is self-consistent: no published property is editable or read-only in Cloud without also being supported.)
|
|
47
49
|
|
|
48
50
|
Doc sets with no property data -- Connect, the agentic data plane -- get nothing, and nothing is inferred for them. Their version numbers are their own product's, and say nothing about which Redpanda properties exist. Adding a third doc set is a deliberate change to the macro, not something it guesses.
|
|
49
51
|
|
package/macros/REFERENCE.adoc
CHANGED
|
@@ -20,7 +20,7 @@ antora:
|
|
|
20
20
|
|
|
21
21
|
== config_ref
|
|
22
22
|
|
|
23
|
-
IMPORTANT: `config_ref` is deprecated. The
|
|
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 older versioned docs, which are frozen, still contain it; generated content emits `prop` (the extractor rewrites any `config_ref` it finds).
|
|
24
24
|
|
|
25
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.
|
|
26
26
|
|
|
@@ -46,7 +46,7 @@ This is the path to the document where the configuration value is defined. This
|
|
|
46
46
|
|
|
47
47
|
IMPORTANT: The path must be the name of a document at the root of the `reference` module.
|
|
48
48
|
|
|
49
|
-
NOTE: The `config_ref` macro is environment-aware. It checks if the document it is being used in is part of a Kubernetes environment by checking if the `env-kubernetes` attribute is set in the document's attributes. Depending on this check, it either prepends `storage.
|
|
49
|
+
NOTE: The `config_ref` macro is environment-aware. It checks if the document it is being used in is part of a Kubernetes environment by checking if the `env-kubernetes` attribute is set in the document's attributes. Depending on this check, it either prepends `storage.tiered.config.` to the `configRef` or just uses the `configRef` as is.
|
|
50
50
|
|
|
51
51
|
For example:
|
|
52
52
|
|
|
@@ -184,7 +184,7 @@ asciidoc:
|
|
|
184
184
|
|
|
185
185
|
== enterprise_features
|
|
186
186
|
|
|
187
|
-
This block macro renders the licensing feature table for one scope from the enterprise features registry (see
|
|
187
|
+
This block macro renders the licensing feature table for one scope from the enterprise features registry (see the `enterprise` macro above). Because the table is generated, it can never drift from the registry.
|
|
188
188
|
|
|
189
189
|
=== Usage
|
|
190
190
|
|
|
@@ -205,7 +205,7 @@ Optional third-column heading override. The default is `Behavior Upon Expiration
|
|
|
205
205
|
|
|
206
206
|
=== Registration
|
|
207
207
|
|
|
208
|
-
Registered together with the `enterprise` inline macro
|
|
208
|
+
Registered together with the `enterprise` inline macro.
|
|
209
209
|
|
|
210
210
|
== prop
|
|
211
211
|
|
|
@@ -219,20 +219,20 @@ The macro checks that a property is actually available to the audience reading t
|
|
|
219
219
|
----
|
|
220
220
|
prop:cloud_storage_enabled[]
|
|
221
221
|
prop:iceberg_enabled[link=true]
|
|
222
|
-
prop:
|
|
222
|
+
prop:retention.ms[link=true,page=properties/topic-properties]
|
|
223
223
|
prop:write_caching_default[text=write caching]
|
|
224
224
|
----
|
|
225
225
|
|
|
226
226
|
The macro target is the property name. Named attributes:
|
|
227
227
|
|
|
228
228
|
link::
|
|
229
|
-
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. See <<prop-
|
|
229
|
+
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. See <<prop-availability>> for properties the current component does not document. This supersedes the `config_ref` macro's linking for most uses.
|
|
230
230
|
|
|
231
231
|
page::
|
|
232
232
|
Optional reference-page override (module-relative, without `.adoc`), for example `properties/object-storage-properties`. Overrides discovery entirely.
|
|
233
233
|
|
|
234
234
|
helm-path::
|
|
235
|
-
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
|
|
235
|
+
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 properties whose name begins `cloud_storage_`, `config.node.<name>` for broker properties, and `config.cluster.<name>` for cluster properties. Topic properties render as the plain name, because they are set per topic through the Kafka API, rpk, or a Topic resource and have no `values.yaml` key at all. The plain name also renders on any page where `env-kubernetes` is unset or explicitly `false`. This supersedes the deprecated `config_ref` macro's prefixing.
|
|
236
236
|
|
|
237
237
|
text::
|
|
238
238
|
Optional display text override. Wins over the Helm path display.
|
|
@@ -240,7 +240,7 @@ Optional display text override. Wins over the Helm path display.
|
|
|
240
240
|
=== Configuration options
|
|
241
241
|
|
|
242
242
|
property-validate::
|
|
243
|
-
How to report a target that does not match any published property: `warn` (default) logs a build warning with the file path, the dataset the page was checked against, and close-match suggestions; `error` fails the build; and `off` disables validation and silences
|
|
243
|
+
How to report a target that does not match any published property: `warn` (default) logs a build warning with the file path, the dataset the page was checked against, and close-match suggestions; `error` fails the build; and `off` disables validation and silences the unknown-name, unavailable-property, unpublished-property and missing-data warnings. Diagnostics about the build itself are not silenced by `off`, because they report a broken input rather than a questionable mention: unusable property JSON, a property claimed by two declared sources, and an index built after conversion started. When no property JSON is available in the build, the macro renders without validation and logs one warning per component version.
|
|
244
244
|
|
|
245
245
|
[[prop-version-selection]]
|
|
246
246
|
=== Which property data a page validates against
|
package/macros/badge.js
CHANGED
|
@@ -15,30 +15,52 @@
|
|
|
15
15
|
* @param {string} [opts.tooltip] - Tooltip text, rendered as `data-tooltip`.
|
|
16
16
|
* @returns {string} Badge HTML.
|
|
17
17
|
*/
|
|
18
|
+
// Hover text for the labels this macro is asked for by name. The badge is
|
|
19
|
+
// styled with a help cursor, so a badge with nothing to say promises an
|
|
20
|
+
// explanation and then withholds it. Callers with something more specific pass
|
|
21
|
+
// their own tooltip, which wins.
|
|
22
|
+
const DEFAULT_TOOLTIPS = {
|
|
23
|
+
beta: 'This feature is in public beta. You can enable it, but it may change before it is generally available.',
|
|
24
|
+
unreleased: 'This feature is not in a released version of Redpanda yet. It is documented here for the upcoming release.',
|
|
25
|
+
};
|
|
26
|
+
|
|
18
27
|
function buildBadgeHtml ({ label, size, tooltip } = {}) {
|
|
19
28
|
const text = label || 'label';
|
|
20
|
-
const className = `badge--${text.toLowerCase().replace(/\s+/g, '-')}`;
|
|
21
29
|
const isLarge = size === 'large';
|
|
22
30
|
const sizeClass = isLarge ? 'badge--large' : '';
|
|
23
|
-
// Escape before interpolating:
|
|
24
|
-
//
|
|
25
|
-
// text into stray attributes (including event
|
|
31
|
+
// Escape before interpolating: these values reach the macro from .adoc source
|
|
32
|
+
// and from registry fields, and an unescaped quote terminates the attribute
|
|
33
|
+
// early, turning the rest of the text into stray attributes (including event
|
|
34
|
+
// handlers). The label needs it as much as the tooltip -- it lands in the
|
|
35
|
+
// class attribute and in the text content, and escaping only the tooltip left
|
|
36
|
+
// `badge:x[label=x" onmouseover="...]` free to break out of the class.
|
|
26
37
|
const escapeAttr = (value) => String(value)
|
|
27
38
|
.replace(/&/g, '&')
|
|
28
39
|
.replace(/"/g, '"')
|
|
29
40
|
.replace(/</g, '<')
|
|
30
41
|
.replace(/>/g, '>');
|
|
31
|
-
const
|
|
42
|
+
const className = `badge--${escapeAttr(text.toLowerCase().replace(/\s+/g, '-'))}`;
|
|
43
|
+
// hasOwnProperty, not a bare index: `label=constructor` and `label=__proto__`
|
|
44
|
+
// are already lowercase, so a bare lookup shipped
|
|
45
|
+
// `function Object() { [native code] }` to readers as hover text. prop.js
|
|
46
|
+
// guards its property lookups the same way.
|
|
47
|
+
const labelKey = text.trim().toLowerCase();
|
|
48
|
+
const defaultTooltip = Object.prototype.hasOwnProperty.call(DEFAULT_TOOLTIPS, labelKey)
|
|
49
|
+
? DEFAULT_TOOLTIPS[labelKey]
|
|
50
|
+
: undefined;
|
|
51
|
+
const hoverText = tooltip || defaultTooltip;
|
|
52
|
+
const tooltipAttr = hoverText ? ` data-tooltip="${escapeAttr(hoverText)}"` : '';
|
|
32
53
|
|
|
33
54
|
// Add brackets if not large. The brackets matter beyond styling: they are
|
|
34
55
|
// real text content, so the label survives an HTML-to-Markdown conversion as
|
|
35
56
|
// "(beta)" rather than vanishing with the CSS class.
|
|
36
|
-
const renderedLabel = isLarge ? text : `(${text})`;
|
|
57
|
+
const renderedLabel = isLarge ? escapeAttr(text) : `(${escapeAttr(text)})`;
|
|
37
58
|
|
|
38
59
|
return `<span class="badge ${className} ${sizeClass}"${tooltipAttr}>${renderedLabel}</span>`;
|
|
39
60
|
}
|
|
40
61
|
|
|
41
62
|
module.exports.buildBadgeHtml = buildBadgeHtml;
|
|
63
|
+
module.exports.DEFAULT_TOOLTIPS = DEFAULT_TOOLTIPS;
|
|
42
64
|
|
|
43
65
|
module.exports.register = function (registry) {
|
|
44
66
|
registry.inlineMacro(function () {
|
package/macros/enterprise.js
CHANGED
|
@@ -55,9 +55,10 @@
|
|
|
55
55
|
|
|
56
56
|
const yaml = require('js-yaml')
|
|
57
57
|
const chalk = require('chalk')
|
|
58
|
-
const { buildBadgeHtml } = require('./badge')
|
|
58
|
+
const { buildBadgeHtml, DEFAULT_TOOLTIPS } = require('./badge')
|
|
59
59
|
|
|
60
60
|
const $enterpriseRegistry = Symbol('$enterpriseRegistry')
|
|
61
|
+
const $enterpriseRegistryUnreadable = Symbol('$enterpriseRegistryUnreadable')
|
|
61
62
|
const $warned = Symbol('$warned')
|
|
62
63
|
|
|
63
64
|
/**
|
|
@@ -76,8 +77,10 @@ const DEFAULT_LICENSING_PAGE = 'get-started:licensing/overview.adoc'
|
|
|
76
77
|
const DEFAULT_ROLE = 'enterprise-feature'
|
|
77
78
|
const BETA_LABEL = 'beta'
|
|
78
79
|
const UNRELEASED_LABEL = 'unreleased'
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
// The badge macro supplies the default hover text for these labels, so the same
|
|
81
|
+
// wording appears whether a badge comes from the registry or from a
|
|
82
|
+
// badge::[label=beta] call in a page.
|
|
83
|
+
const UNRELEASED_TOOLTIP = DEFAULT_TOOLTIPS.unreleased
|
|
81
84
|
|
|
82
85
|
// Release status of a feature, which decides where it may be referenced.
|
|
83
86
|
//
|
|
@@ -97,9 +100,12 @@ const VALID_STATUSES = [STATUS_GA, STATUS_BETA, STATUS_UNRELEASED]
|
|
|
97
100
|
* The release status of a registry entry.
|
|
98
101
|
*
|
|
99
102
|
* `beta: true` predates the status field and still means beta, so old entries
|
|
100
|
-
* keep working. An explicit status wins. An unrecognized status is reported
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
+
* keep working. An explicit status wins. An unrecognized status is reported AND
|
|
104
|
+
* treated as unreleased: a typo must not be the thing that publishes an
|
|
105
|
+
* unreleased feature. The two failure directions are not symmetric -- gating a
|
|
106
|
+
* shipped feature shows a writer a warning and an unstyled mention they will
|
|
107
|
+
* notice, while publishing an unreleased one promises readers a feature they
|
|
108
|
+
* cannot get, and nothing on the page looks wrong.
|
|
103
109
|
*
|
|
104
110
|
* @param {object} entry - Registry entry.
|
|
105
111
|
* @param {object} [report] - {mode, filePath} to report an invalid status.
|
|
@@ -107,14 +113,22 @@ const VALID_STATUSES = [STATUS_GA, STATUS_BETA, STATUS_UNRELEASED]
|
|
|
107
113
|
*/
|
|
108
114
|
function entryStatus (entry, report) {
|
|
109
115
|
if (!entry) return STATUS_GA
|
|
116
|
+
// Any present key goes through validation, including a blank value. An absent
|
|
117
|
+
// key means no status was intended, so GA is the right default; `status:` with
|
|
118
|
+
// nothing after it means the writer meant to say something and did not, which
|
|
119
|
+
// is the same mistake as a typo and must fail the same way. Treating blank as
|
|
120
|
+
// absent reopened the hole this function exists to close: a forgotten value
|
|
121
|
+
// published an unreleased feature with no warning at all.
|
|
110
122
|
if (entry.status !== undefined) {
|
|
111
|
-
const
|
|
123
|
+
const raw = entry.status === null ? '' : String(entry.status).trim()
|
|
124
|
+
const status = raw.toLowerCase()
|
|
112
125
|
if (VALID_STATUSES.includes(status)) return status
|
|
113
126
|
if (report && report.mode !== 'off') {
|
|
114
127
|
const where = report.filePath ? ` in ${report.filePath}` : ''
|
|
128
|
+
const what = raw === '' ? 'an empty status' : `status '${entry.status}'`
|
|
115
129
|
const message =
|
|
116
|
-
`enterprise:${entry.name}[]${where}: registry
|
|
117
|
-
'Fix the status in enterprise-features.yml.'
|
|
130
|
+
`enterprise:${entry.name}[]${where}: registry has ${what}, which is not one of ${VALID_STATUSES.join(', ')}, so the feature is treated as unreleased and is left unpublished on released pages. ` +
|
|
131
|
+
'Fix the status in enterprise-features.yml, or remove the key entirely if the feature has shipped.'
|
|
118
132
|
// Honour error mode like every other registry diagnostic. A typo here is
|
|
119
133
|
// exactly what publishes an unreleased feature, so the strictest setting
|
|
120
134
|
// has to stop it.
|
|
@@ -124,7 +138,7 @@ function entryStatus (entry, report) {
|
|
|
124
138
|
if (report.contentCatalog) warnOnce(report.contentCatalog, `status:${entry.name}`, message)
|
|
125
139
|
else console.warn(chalk.yellow(message))
|
|
126
140
|
}
|
|
127
|
-
return
|
|
141
|
+
return STATUS_UNRELEASED
|
|
128
142
|
}
|
|
129
143
|
return entry.beta === true ? STATUS_BETA : STATUS_GA
|
|
130
144
|
}
|
|
@@ -239,7 +253,14 @@ let warnedNoRegistry = false
|
|
|
239
253
|
* @returns {{features: object[], lookup: Map<string, object>}}
|
|
240
254
|
*/
|
|
241
255
|
function parseRegistry (source, origin = REGISTRY_FILENAME) {
|
|
242
|
-
|
|
256
|
+
let data
|
|
257
|
+
try {
|
|
258
|
+
data = yaml.load(source)
|
|
259
|
+
} catch (error) {
|
|
260
|
+
// Every other throw below names the file, so this one does too: the caller
|
|
261
|
+
// then reports whatever it catches verbatim, and the path appears once.
|
|
262
|
+
throw new Error(`Enterprise features registry ${origin} is not valid YAML (${error.message}).`)
|
|
263
|
+
}
|
|
243
264
|
if (!data || !Array.isArray(data.features)) {
|
|
244
265
|
throw new Error(`Enterprise features registry ${origin} has no 'features' list.`)
|
|
245
266
|
}
|
|
@@ -287,8 +308,17 @@ function loadRegistry (config) {
|
|
|
287
308
|
// Caching the failure also stops the error being re-raised per macro call
|
|
288
309
|
// and attributed to whichever page happened to convert first.
|
|
289
310
|
registry = null
|
|
311
|
+
// Remember that the file was found and unreadable, so the caller does not
|
|
312
|
+
// then also report it missing. Saying "not found" about a file we just
|
|
313
|
+
// read and failed to parse sends the writer looking for the wrong problem.
|
|
314
|
+
contentCatalog[$enterpriseRegistryUnreadable] = true
|
|
315
|
+
// Report the error as thrown. It already names the file and says exactly
|
|
316
|
+
// what is wrong -- invalid YAML, a missing features list, an entry with no
|
|
317
|
+
// name, an unknown scope, or a duplicate name or alias. Wrapping it in
|
|
318
|
+
// "could not be read" was inaccurate for all but the first (the file read
|
|
319
|
+
// fine; its contents are invalid) and printed the path twice.
|
|
290
320
|
warnOnce(contentCatalog, 'badregistry',
|
|
291
|
-
|
|
321
|
+
`${error.message} No enterprise: target is validated, no feature is gated by release status, and the licensing tables are empty until this is fixed.`)
|
|
292
322
|
}
|
|
293
323
|
}
|
|
294
324
|
// Cache null too, so a missing registry is only searched for once per build.
|
|
@@ -297,6 +327,8 @@ function loadRegistry (config) {
|
|
|
297
327
|
}
|
|
298
328
|
|
|
299
329
|
function warnNoRegistry (contentCatalog) {
|
|
330
|
+
// Already reported as unreadable: one problem, one diagnostic.
|
|
331
|
+
if (contentCatalog && contentCatalog[$enterpriseRegistryUnreadable]) return
|
|
300
332
|
const message = "Enterprise features registry (enterprise-features.yml in the 'shared' component) not found; enterprise: targets are not validated."
|
|
301
333
|
// Deduplicate per build, not per process: Antora's watch mode reuses one
|
|
302
334
|
// process across builds, and a module-level guard reported this only on a
|
|
@@ -377,7 +409,16 @@ function resolveTooltipAttribute (raw) {
|
|
|
377
409
|
function buildEnterpriseContent ({ feature, text, xref, url, tooltip, licensingPage, role, tooltipAttr, links }) {
|
|
378
410
|
const display = text || feature
|
|
379
411
|
const tooltipText = tooltip || `${feature} requires an Enterprise Edition license.`
|
|
380
|
-
|
|
412
|
+
// All four, matching badge.js. Escaping only the quote kept the attribute
|
|
413
|
+
// intact but let < and > through from a registry tooltip field, which any
|
|
414
|
+
// consumer that re-parses the page then reads as markup -- the docs UI
|
|
415
|
+
// promotes this attribute into a tooltip, the Markdown converter and the
|
|
416
|
+
// search indexer both re-read it.
|
|
417
|
+
const escapedTooltip = tooltipText
|
|
418
|
+
.replace(/&/g, '&')
|
|
419
|
+
.replace(/"/g, '"')
|
|
420
|
+
.replace(/</g, '<')
|
|
421
|
+
.replace(/>/g, '>')
|
|
381
422
|
const tooltipHtml = tooltipAttr ? ` ${tooltipAttr}="${escapedTooltip}"` : ''
|
|
382
423
|
let inner = display
|
|
383
424
|
if (links) {
|
|
@@ -408,7 +449,15 @@ function buildFeatureTable (features, scope, opts = {}) {
|
|
|
408
449
|
const report = opts.report
|
|
409
450
|
const rows = features
|
|
410
451
|
.filter((feature) => feature.scope === scope)
|
|
411
|
-
|
|
452
|
+
// Evaluate the status first: `includeUnreleased || entryStatus(...)` skipped
|
|
453
|
+
// the call entirely on a prerelease page, so a typo'd status went unreported
|
|
454
|
+
// on the beta branch -- the one branch where unreleased features are
|
|
455
|
+
// actually authored and the table renders them -- and enterprise-validate=error
|
|
456
|
+
// did not fail there either.
|
|
457
|
+
.filter((feature) => {
|
|
458
|
+
const status = entryStatus(feature, report)
|
|
459
|
+
return includeUnreleased || status !== STATUS_UNRELEASED
|
|
460
|
+
})
|
|
412
461
|
.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }))
|
|
413
462
|
const title = opts.title || TABLE_TITLES[scope]
|
|
414
463
|
const heading = opts.heading || THIRD_COLUMN_HEADINGS[scope] || THIRD_COLUMN_HEADINGS.default
|
package/macros/prop.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* prop:cloud_storage_enabled[]
|
|
8
8
|
* prop:iceberg_enabled[link=true]
|
|
9
|
-
* prop:
|
|
9
|
+
* prop:retention.ms[link=true,page=properties/topic-properties]
|
|
10
10
|
* prop:write_caching_default[text=write caching]
|
|
11
11
|
*
|
|
12
12
|
* The property renders as a marked <code> element that the docs UI decorates
|
|
@@ -98,7 +98,10 @@ const SURFACE_CLOUD = 'cloud'
|
|
|
98
98
|
const PROPERTIES_JSON_RX = /^redpanda-properties-(v\d+\.\d+\.\d+(?:-[\w.]+)?)\.json$/
|
|
99
99
|
|
|
100
100
|
const $propertyPageIndex = Symbol('$propertyPageIndex')
|
|
101
|
-
|
|
101
|
+
// Two or more '=': a level-0 heading is the page title, which generates no
|
|
102
|
+
// anchor, so treating it as a property heading produced a link and a tooltip
|
|
103
|
+
// URL pointing at a fragment that does not exist on the page.
|
|
104
|
+
const HEADING_RX = /^={2,}\s+(\S+)\s*$/
|
|
102
105
|
const TAG_MARKER_RX = /^\/\/\s*(tag|end)::([\w-]+)\[\]\s*$/
|
|
103
106
|
const INCLUDE_RX = /^include::([^[]+)\[([^\]]*)\]/
|
|
104
107
|
// A reference page declaring the property collection it publishes.
|
|
@@ -314,7 +317,14 @@ function buildPageIndex (contentCatalog, component, properties, version) {
|
|
|
314
317
|
if (!include) continue
|
|
315
318
|
const partialRef = parsePartialTarget(include[1].trim(), page.src)
|
|
316
319
|
if (!partialRef) continue
|
|
317
|
-
|
|
320
|
+
// Match a quoted value first. An unquoted value ends at the next comma
|
|
321
|
+
// because the attrlist is comma-separated -- but a quoted one may contain
|
|
322
|
+
// commas, and that is AsciiDoc's documented way to write a comma-separated
|
|
323
|
+
// tag list. Capturing up to the first comma regardless truncated
|
|
324
|
+
// tags="!deprecated,!exclude-from-docs" to '"!deprecated', which matches
|
|
325
|
+
// nothing, so the page indexed no properties and every link on it silently
|
|
326
|
+
// disappeared. Asciidoctor honours the quoted form, so the index has to.
|
|
327
|
+
const tagsMatch = include[2].match(/tags?=("[^"]*"|'[^']*'|[^,\]]*)/)
|
|
318
328
|
const expression = tagsMatch && tagsMatch[1]
|
|
319
329
|
const partials = contentCatalog.findBy({ component: partialRef.component, module: partialRef.module, family: 'partial' }) || []
|
|
320
330
|
const matching = partials.filter((candidate) => candidate.src.relative === partialRef.relative)
|
|
@@ -364,6 +374,13 @@ function propertyAnchor (name) {
|
|
|
364
374
|
// config.node, and every other cluster property maps to config.cluster --
|
|
365
375
|
// the path the docs recommend for all cluster properties.
|
|
366
376
|
function helmValuesPath (name, scope) {
|
|
377
|
+
// Topic properties are not set in values.yaml at all -- they are set per topic
|
|
378
|
+
// through the Kafka API, rpk, or a Topic resource -- so there is no Helm path
|
|
379
|
+
// to show. Returning config.cluster.<name> for them produced a confident,
|
|
380
|
+
// copy-pasteable key that does not exist, which is the exact failure the
|
|
381
|
+
// config_ref macro's blanket prefixing had. Undefined means "no Helm path",
|
|
382
|
+
// and the caller falls back to the plain property name.
|
|
383
|
+
if (scope === 'topic') return undefined
|
|
367
384
|
if (name.startsWith('cloud_storage_')) return `storage.tiered.config.${name}`
|
|
368
385
|
if (scope === 'broker') return `config.node.${name}`
|
|
369
386
|
return `config.cluster.${name}`
|
|
@@ -523,7 +540,7 @@ function loadProperties (config) {
|
|
|
523
540
|
* property-page-index extension can warm the caches before conversion.
|
|
524
541
|
*/
|
|
525
542
|
function loadPropertiesFor (contentCatalog, pageComponent, pageVersion) {
|
|
526
|
-
const cacheKey = `${pageComponent}@${pageVersion}`
|
|
543
|
+
const cacheKey = `${pageComponent}@${pageVersion}`
|
|
527
544
|
const cache = contentCatalog[$propertyRegistry] || (contentCatalog[$propertyRegistry] = {})
|
|
528
545
|
if (cache[cacheKey] !== undefined) return cache[cacheKey] || undefined
|
|
529
546
|
|
|
@@ -531,6 +548,12 @@ function loadPropertiesFor (contentCatalog, pageComponent, pageVersion) {
|
|
|
531
548
|
const own = datasets.filter((d) => d.component === pageComponent)
|
|
532
549
|
let pick
|
|
533
550
|
let surface
|
|
551
|
+
// The candidates the corrupt-file fallback may fall back TO. It has to be the
|
|
552
|
+
// same filtered set `pick` was chosen from, not everything the component
|
|
553
|
+
// publishes: falling back across release series would validate a 25.3 page
|
|
554
|
+
// against 25.2 data, which is exactly the borrowing this function refuses to
|
|
555
|
+
// do on the primary path.
|
|
556
|
+
let pool
|
|
534
557
|
|
|
535
558
|
// Cloud is decided first, and deliberately outranks the component's own
|
|
536
559
|
// attachments. The property extractor writes
|
|
@@ -546,6 +569,7 @@ function loadPropertiesFor (contentCatalog, pageComponent, pageVersion) {
|
|
|
546
569
|
// version is not a Redpanda release candidate.
|
|
547
570
|
const lenders = datasets.filter((d) => d.component === 'streaming' || d.component === 'ROOT')
|
|
548
571
|
const released = lenders.filter((d) => !isPrerelease(d.tag))
|
|
572
|
+
pool = released
|
|
549
573
|
pick =
|
|
550
574
|
newestDataset(released.filter((d) => d.component === 'streaming')) ||
|
|
551
575
|
newestDataset(released.filter((d) => d.component === 'ROOT'))
|
|
@@ -567,6 +591,7 @@ function loadPropertiesFor (contentCatalog, pageComponent, pageVersion) {
|
|
|
567
591
|
const series = releaseSeries(pageVersion)
|
|
568
592
|
if (series) {
|
|
569
593
|
const inSeries = own.filter((d) => releaseSeries(d.tag) === series)
|
|
594
|
+
pool = inSeries
|
|
570
595
|
pick = newestDataset(inSeries.filter((d) => d.version === pageVersion)) || newestDataset(inSeries)
|
|
571
596
|
if (!pick) {
|
|
572
597
|
// Record the gap rather than reporting it here: this also runs from the
|
|
@@ -585,6 +610,7 @@ function loadPropertiesFor (contentCatalog, pageComponent, pageVersion) {
|
|
|
585
610
|
return undefined
|
|
586
611
|
}
|
|
587
612
|
} else {
|
|
613
|
+
pool = own
|
|
588
614
|
pick = newestDataset(own.filter((d) => d.version === pageVersion)) || newestDataset(own)
|
|
589
615
|
}
|
|
590
616
|
}
|
|
@@ -593,12 +619,26 @@ function loadPropertiesFor (contentCatalog, pageComponent, pageVersion) {
|
|
|
593
619
|
// and must not mask a good file: try candidates newest-first and use the
|
|
594
620
|
// first one that actually carries properties, reporting each one skipped.
|
|
595
621
|
let registry = null
|
|
596
|
-
|
|
622
|
+
const candidates = orderedCandidates(pick, pool)
|
|
623
|
+
for (const candidate of candidates) {
|
|
597
624
|
const properties = readProperties(candidate, contentCatalog)
|
|
598
625
|
if (!properties) continue
|
|
599
626
|
registry = { tag: candidate.tag, properties, component: candidate.component, version: candidate.version, surface }
|
|
600
627
|
break
|
|
601
628
|
}
|
|
629
|
+
if (!registry && candidates.length) {
|
|
630
|
+
// Every candidate was unusable. Record that, because the caller's fallback
|
|
631
|
+
// diagnostic says no attachment exists anywhere -- which contradicts the
|
|
632
|
+
// per-file warning readProperties just emitted and sends the writer looking
|
|
633
|
+
// for a file that is sitting right there.
|
|
634
|
+
const gaps = contentCatalog[$propertySeriesGap] || (contentCatalog[$propertySeriesGap] = {})
|
|
635
|
+
gaps[cacheKey] = {
|
|
636
|
+
kind: 'unusable',
|
|
637
|
+
component: pageComponent,
|
|
638
|
+
version: pageVersion,
|
|
639
|
+
have: [...new Set(candidates.map((d) => d.tag))].sort(compareTags),
|
|
640
|
+
}
|
|
641
|
+
}
|
|
602
642
|
// Cache null too, so a missing dataset is only searched for once per build.
|
|
603
643
|
cache[cacheKey] = registry
|
|
604
644
|
return registry || undefined
|
|
@@ -609,12 +649,9 @@ function loadPropertiesFor (contentCatalog, pageComponent, pageVersion) {
|
|
|
609
649
|
* descending release order, so an unusable file falls through to the next best
|
|
610
650
|
* rather than voiding the component's data entirely.
|
|
611
651
|
*/
|
|
612
|
-
function orderedCandidates (pick,
|
|
652
|
+
function orderedCandidates (pick, pool) {
|
|
613
653
|
if (!pick) return []
|
|
614
|
-
const
|
|
615
|
-
? datasets.filter((d) => (d.component === 'streaming' || d.component === 'ROOT') && !isPrerelease(d.tag))
|
|
616
|
-
: own
|
|
617
|
-
const rest = pool
|
|
654
|
+
const rest = (pool || [])
|
|
618
655
|
.filter((d) => d !== pick)
|
|
619
656
|
.sort((a, b) => compareTags(b.tag, a.tag) || stableKey(a).localeCompare(stableKey(b)))
|
|
620
657
|
return [pick, ...rest]
|
|
@@ -683,6 +720,12 @@ function warnSeriesMissing (contentCatalog, pageComponent, pageVersion) {
|
|
|
683
720
|
const gap = gaps && gaps[`${pageComponent}@${pageVersion}`]
|
|
684
721
|
if (!gap) return false
|
|
685
722
|
const have = gap.have.join(', ')
|
|
723
|
+
if (gap.kind === 'unusable') {
|
|
724
|
+
warnOnce(contentCatalog, `unusable:${gap.component}@${gap.version}`,
|
|
725
|
+
`prop macro: every property attachment for ${gap.component}@${gap.version} is unusable (${have}); see the per-file warning above for why. ` +
|
|
726
|
+
'prop: targets are therefore not validated and their tooltips carry no data. Regenerate the file with \'npx doc-tools generate property-docs\' rather than treating this as missing data.')
|
|
727
|
+
return true
|
|
728
|
+
}
|
|
686
729
|
if (gap.kind === 'cloud-prerelease-only') {
|
|
687
730
|
warnOnce(contentCatalog, `cloudpre:${gap.component}@${gap.version}`,
|
|
688
731
|
`prop macro: ${gap.component} found only prerelease property data (${have}). Cloud runs released Redpanda, so it uses GA property data and will not validate against a release candidate. ` +
|
|
@@ -788,7 +831,7 @@ function reportUnpublishedProperty ({ name, registry, mode, filePath }) {
|
|
|
788
831
|
* @returns {string}
|
|
789
832
|
*/
|
|
790
833
|
function buildPropContent ({ name, text, link, page, scope, role, helmPath = false, docUrl, plain = false }) {
|
|
791
|
-
const display = text || (helmPath
|
|
834
|
+
const display = text || (helmPath && helmValuesPath(name, scope)) || name
|
|
792
835
|
if (plain) return `<code>${display}</code>`
|
|
793
836
|
const inner = link && page
|
|
794
837
|
? `xref:reference:${page}.adoc#${propertyAnchor(name)}[${display}]`
|
|
@@ -915,7 +958,11 @@ function propInlineMacro (config) {
|
|
|
915
958
|
* hardcoded storage.tiered.config prefixing).
|
|
916
959
|
*/
|
|
917
960
|
function helmPathRequested (attributes, document) {
|
|
918
|
-
|
|
961
|
+
// isAttributeSet, not a bare !== undefined: Antora yields the string 'false'
|
|
962
|
+
// for `env-kubernetes: false` in antora.yml, and a bare presence check read
|
|
963
|
+
// that as "Kubernetes page", so a Linux page in a component that explicitly
|
|
964
|
+
// turned the attribute off still told readers to set a values.yaml key.
|
|
965
|
+
return attributes['helm-path'] === 'auto' && isAttributeSet(document.getAttribute('env-kubernetes'))
|
|
919
966
|
}
|
|
920
967
|
|
|
921
968
|
function register (registry, config = {}) {
|
package/package.json
CHANGED
|
@@ -104,6 +104,103 @@ function registerPartials() {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Rewrite property anchors across every description and related-topics entry so
|
|
109
|
+
* they match the heading IDs Asciidoctor will actually emit.
|
|
110
|
+
*
|
|
111
|
+
* Runs once over the whole property map before rendering, because a property's
|
|
112
|
+
* description routinely links to a sibling property and the anchor shapes differ
|
|
113
|
+
* by where the text came from. Reports what it corrected: a steady stream of
|
|
114
|
+
* rewrites is a signal to fix the upstream doc string, not to lean on this pass.
|
|
115
|
+
*
|
|
116
|
+
* @param {Object<string, Object>} properties - Property map, mutated in place.
|
|
117
|
+
* @returns {number} Number of anchors rewritten.
|
|
118
|
+
*/
|
|
119
|
+
function normalizeAnchorsInProperties(properties) {
|
|
120
|
+
const seen = new Map();
|
|
121
|
+
let total = 0;
|
|
122
|
+
|
|
123
|
+
let xrefTotal = 0;
|
|
124
|
+
const fix = (value, scope) => {
|
|
125
|
+
const { text, rewrites } = helpers.normalizePropertyAnchors(value, properties, scope);
|
|
126
|
+
for (const r of rewrites) {
|
|
127
|
+
total += 1;
|
|
128
|
+
seen.set(r.from, r.to);
|
|
129
|
+
}
|
|
130
|
+
const canonical = helpers.canonicalizePropertyXrefs(text);
|
|
131
|
+
xrefTotal += canonical.rewrites;
|
|
132
|
+
return canonical.text;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
Object.values(properties).forEach((prop) => {
|
|
136
|
+
if (!prop || typeof prop !== 'object') return;
|
|
137
|
+
const scope = prop.config_scope;
|
|
138
|
+
if (typeof prop.description === 'string') prop.description = fix(prop.description, scope);
|
|
139
|
+
if (typeof prop.acceptable_values === 'string') prop.acceptable_values = fix(prop.acceptable_values, scope);
|
|
140
|
+
if (Array.isArray(prop.related_topics)) {
|
|
141
|
+
// Canonicalizing collapses forms that differed only by prefix, which turns
|
|
142
|
+
// some entries into exact duplicates of each other. Drop the repeats.
|
|
143
|
+
const seenTopics = new Set();
|
|
144
|
+
prop.related_topics = prop.related_topics
|
|
145
|
+
.map((t) => (typeof t === 'string' ? fix(t, scope) : t))
|
|
146
|
+
.filter((t) => {
|
|
147
|
+
if (typeof t !== 'string') return true;
|
|
148
|
+
const key = t.trim();
|
|
149
|
+
if (seenTopics.has(key)) return false;
|
|
150
|
+
seenTopics.add(key);
|
|
151
|
+
return true;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (total > 0) {
|
|
157
|
+
console.log(`Fixed ${total} property anchor(s) that would have rendered as dead links:`);
|
|
158
|
+
for (const [from, to] of seen) console.log(` <<${from}>> -> <<${to}>>`);
|
|
159
|
+
}
|
|
160
|
+
if (xrefTotal > 0) {
|
|
161
|
+
console.log(`Canonicalized ${xrefTotal} property-page xref(s) to reference:properties/…`);
|
|
162
|
+
}
|
|
163
|
+
return total;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Record whether a topic property's corresponding cluster property is one a Cloud
|
|
168
|
+
* customer can actually reach.
|
|
169
|
+
*
|
|
170
|
+
* Cloud publishes a subset of cluster properties, so linking a topic property to
|
|
171
|
+
* its cluster counterpart unconditionally sends Cloud readers to an anchor that
|
|
172
|
+
* does not exist on their reference page. `cloud_supported` on the cluster
|
|
173
|
+
* property is the authority here: it is derived from the install pack's
|
|
174
|
+
* customer_managed_configs and readonly_cluster_config, so true means the Cloud
|
|
175
|
+
* page publishes it.
|
|
176
|
+
*
|
|
177
|
+
* Note what this flag does NOT say. False means the control plane does not expose
|
|
178
|
+
* the property to customers; it does not distinguish "Redpanda manages the value"
|
|
179
|
+
* from "the property does not apply to Cloud at all". So the template drops the
|
|
180
|
+
* link and keeps the name, and asserts nothing about who sets it.
|
|
181
|
+
*
|
|
182
|
+
* Both the primary and the alternate mapping are resolved, because a row that
|
|
183
|
+
* links to two cluster properties needs both to be reachable before it is worth
|
|
184
|
+
* linking either.
|
|
185
|
+
*
|
|
186
|
+
* @param {Object} prop - Topic property to annotate in place.
|
|
187
|
+
* @param {Object<string, Object>} properties - Full property map, used to look up counterparts.
|
|
188
|
+
*/
|
|
189
|
+
function annotateCorrespondingClusterSupport(prop, properties) {
|
|
190
|
+
const isSupported = (name) => {
|
|
191
|
+
if (!name) return false;
|
|
192
|
+
const target = properties[name];
|
|
193
|
+
return Boolean(target && target.cloud_supported);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const primary = isSupported(prop.corresponding_cluster_property);
|
|
197
|
+
prop.corresponding_cluster_property_cloud_supported = primary;
|
|
198
|
+
prop.alternate_cluster_property_cloud_supported = isSupported(prop.alternate_cluster_property);
|
|
199
|
+
prop.cluster_property_mapping_cloud_supported = prop.alternate_cluster_property
|
|
200
|
+
? primary && prop.alternate_cluster_property_cloud_supported
|
|
201
|
+
: primary;
|
|
202
|
+
}
|
|
203
|
+
|
|
107
204
|
/**
|
|
108
205
|
* Generate AsciiDoc partial files grouping input properties by scope (cluster, topic, broker, object-storage).
|
|
109
206
|
*
|
|
@@ -120,6 +217,8 @@ function registerPartials() {
|
|
|
120
217
|
function generatePropertyPartials(properties, partialsDir, onRender) {
|
|
121
218
|
console.log(`📝 Generating consolidated property partials in ${partialsDir}…`);
|
|
122
219
|
|
|
220
|
+
normalizeAnchorsInProperties(properties);
|
|
221
|
+
|
|
123
222
|
const propertyTemplate = handlebars.compile(
|
|
124
223
|
fs.readFileSync(getTemplatePath(path.join(__dirname, 'templates', 'property.hbs'), 'TEMPLATE_PROPERTY'), 'utf8')
|
|
125
224
|
);
|
|
@@ -151,6 +250,7 @@ function generatePropertyPartials(properties, partialsDir, onRender) {
|
|
|
151
250
|
|
|
152
251
|
switch (prop.config_scope) {
|
|
153
252
|
case 'topic':
|
|
253
|
+
annotateCorrespondingClusterSupport(prop, properties);
|
|
154
254
|
propertyGroups.topic.push(prop);
|
|
155
255
|
break;
|
|
156
256
|
case 'broker':
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
// Builds the anchor for a property's generated section heading.
|
|
2
|
+
//
|
|
3
|
+
// The headings are plain AsciiDoc (`=== cleanup.policy`), so Asciidoctor derives
|
|
4
|
+
// their IDs itself: lowercase, every run of characters outside [a-z0-9_] replaced
|
|
5
|
+
// with the id separator, which Antora sets to `-`. Underscores are word
|
|
6
|
+
// characters and survive, so `redpanda.cloud_topic.enabled` becomes
|
|
7
|
+
// `redpanda-cloud_topic-enabled`.
|
|
8
|
+
//
|
|
9
|
+
// Reproduce that here rather than inventing a separate scheme: anything else
|
|
10
|
+
// renders as a link to an anchor that does not exist on the page.
|
|
1
11
|
module.exports = function anchorName(name) {
|
|
2
|
-
const anchor = String(name)
|
|
12
|
+
const anchor = String(name)
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9_]+/g, '-')
|
|
15
|
+
.replace(/^-+|-+$/g, '');
|
|
3
16
|
if (!anchor) {
|
|
4
17
|
throw new Error(`Invalid property name for anchor generation: "${name}"`);
|
|
5
18
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The reference pages these xrefs target.
|
|
4
|
+
const PROPERTY_PAGES = [
|
|
5
|
+
'cluster-properties',
|
|
6
|
+
'object-storage-properties',
|
|
7
|
+
'topic-properties',
|
|
8
|
+
'broker-properties',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
// Every prefix seen in the wild for the same target:
|
|
12
|
+
// xref:cluster-properties.adoc#x bare filename
|
|
13
|
+
// xref:./cluster-properties.adoc#x relative to the current directory
|
|
14
|
+
// xref:reference:cluster-properties.adoc#x legacy, when the pages sat at the module root
|
|
15
|
+
// xref:reference:properties/...#x canonical
|
|
16
|
+
const PREFIXES = '(?:\\./|reference:properties/|reference:)?';
|
|
17
|
+
const PATTERN = new RegExp(
|
|
18
|
+
`xref:${PREFIXES}(${PROPERTY_PAGES.join('|')})\\.adoc(?=[#\\[])`,
|
|
19
|
+
'g'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Rewrite every reference to a property reference page to one canonical resource ID.
|
|
24
|
+
*
|
|
25
|
+
* The bare and `reference:`-prefixed forms both resolve to the module root, where
|
|
26
|
+
* these pages no longer live. Self-managed papers over that with a
|
|
27
|
+
* `reference:cluster-properties.adoc` page alias, so the broken form renders as a
|
|
28
|
+
* working link there and as an unresolved xref in cloud-docs, which has no such
|
|
29
|
+
* alias. The `./` form works but depends on the including page sitting in
|
|
30
|
+
* `reference/properties/`, which is a constraint on every page that includes a
|
|
31
|
+
* partial rather than a property of the partial.
|
|
32
|
+
*
|
|
33
|
+
* The fully qualified module-and-path form has neither problem: it resolves
|
|
34
|
+
* inside whichever component includes the partial, from any directory.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} text - AsciiDoc source that may contain property-page xrefs.
|
|
37
|
+
* @returns {{text: string, rewrites: number}}
|
|
38
|
+
*/
|
|
39
|
+
function canonicalizePropertyXrefs(text) {
|
|
40
|
+
if (typeof text !== 'string' || !text.includes('xref:')) {
|
|
41
|
+
return { text, rewrites: 0 };
|
|
42
|
+
}
|
|
43
|
+
let rewrites = 0;
|
|
44
|
+
const out = text.replace(PATTERN, (match, page) => {
|
|
45
|
+
const canonical = `xref:reference:properties/${page}.adoc`;
|
|
46
|
+
if (match === canonical) return match;
|
|
47
|
+
rewrites += 1;
|
|
48
|
+
return canonical;
|
|
49
|
+
});
|
|
50
|
+
return { text: out, rewrites };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = canonicalizePropertyXrefs;
|
|
54
|
+
module.exports.PROPERTY_PAGES = PROPERTY_PAGES;
|
|
@@ -14,6 +14,8 @@ module.exports = {
|
|
|
14
14
|
renderPropertyExample: require('./renderPropertyExample.js'),
|
|
15
15
|
formatUnits: require('./formatUnits.js'),
|
|
16
16
|
anchorName: require('./anchorName.js'),
|
|
17
|
+
normalizePropertyAnchors: require('./normalizePropertyAnchors.js'),
|
|
18
|
+
canonicalizePropertyXrefs: require('./canonicalizePropertyXrefs.js'),
|
|
17
19
|
parseRelatedTopic: require('./parseRelatedTopic.js'),
|
|
18
20
|
allTopicsConditional: require('./allTopicsConditional.js'),
|
|
19
21
|
capitalize: require('./capitalize.js'),
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const anchorName = require('./anchorName.js');
|
|
4
|
+
|
|
5
|
+
// Strip every separator so `redpanda.storage.mode.impl`, `redpanda-storage-mode-impl`
|
|
6
|
+
// and `redpandastoragemodeimpl` all collapse to one key.
|
|
7
|
+
function squash(value) {
|
|
8
|
+
return String(value).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Repair intra-page anchors (`<<anchor,text>>`) that name a property but use the
|
|
13
|
+
* wrong ID shape.
|
|
14
|
+
*
|
|
15
|
+
* Descriptions come from three places -- Redpanda's own doc strings, the
|
|
16
|
+
* overrides file, and hand-edits -- and each has at some point written a
|
|
17
|
+
* property anchor by deleting the dots (`<<redpandastoragemodeimpl>>`) or by
|
|
18
|
+
* hyphenating only part of the name (`<<redpandastorage-mode>>`). Asciidoctor
|
|
19
|
+
* derives the real heading ID from the heading text, so those render as links to
|
|
20
|
+
* nothing. Rather than hand-correct each one and wait for the next author to
|
|
21
|
+
* reintroduce it, resolve any anchor that unambiguously names a known property
|
|
22
|
+
* back to that property's real anchor.
|
|
23
|
+
*
|
|
24
|
+
* Only rewrites when the anchor is not already correct and the match is
|
|
25
|
+
* unambiguous, so an anchor pointing at a genuine non-property section is left
|
|
26
|
+
* alone. Names from different scopes can collide once separators are gone --
|
|
27
|
+
* `retention.bytes` and `retention_bytes` both squash to `retentionbytes` -- so
|
|
28
|
+
* a collision is resolved in favour of the scope the referring property is in,
|
|
29
|
+
* which is where a cross-reference nearly always points, and refused outright if
|
|
30
|
+
* that still leaves two candidates.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} text - AsciiDoc source that may contain `<<anchor,label>>` references.
|
|
33
|
+
* @param {Iterable<string>|Object<string, Object>} properties - Known property names, or the property map.
|
|
34
|
+
* @param {string} [scope] - config_scope of the property that owns this text.
|
|
35
|
+
* @returns {{text: string, rewrites: Array<{from: string, to: string}>}}
|
|
36
|
+
*/
|
|
37
|
+
function normalizePropertyAnchors(text, properties, scope) {
|
|
38
|
+
if (typeof text !== 'string' || !text.includes('<<')) {
|
|
39
|
+
return { text, rewrites: [] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isMap = properties && !Array.isArray(properties) && typeof properties === 'object'
|
|
43
|
+
&& !(typeof properties[Symbol.iterator] === 'function');
|
|
44
|
+
const names = isMap ? Object.keys(properties) : Array.from(properties || []);
|
|
45
|
+
const scopeOf = (name) => (isMap && properties[name] ? properties[name].config_scope : undefined);
|
|
46
|
+
|
|
47
|
+
const bySquashed = new Map();
|
|
48
|
+
const validAnchors = new Set();
|
|
49
|
+
for (const name of names) {
|
|
50
|
+
if (typeof name !== 'string' || !name) continue;
|
|
51
|
+
let anchor;
|
|
52
|
+
try {
|
|
53
|
+
anchor = anchorName(name);
|
|
54
|
+
} catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
validAnchors.add(anchor);
|
|
58
|
+
const key = squash(name);
|
|
59
|
+
const entry = bySquashed.get(key);
|
|
60
|
+
if (entry) entry.push({ anchor, scope: scopeOf(name) });
|
|
61
|
+
else bySquashed.set(key, [{ anchor, scope: scopeOf(name) }]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const resolve = (key) => {
|
|
65
|
+
const candidates = bySquashed.get(key);
|
|
66
|
+
if (!candidates || candidates.length === 0) return null;
|
|
67
|
+
if (candidates.length === 1) return candidates[0].anchor;
|
|
68
|
+
const sameScope = scope ? candidates.filter((c) => c.scope === scope) : [];
|
|
69
|
+
return sameScope.length === 1 ? sameScope[0].anchor : null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const rewrites = [];
|
|
73
|
+
const rewritten = text.replace(/<<([^<>,\]]+?)(,|>>)/g, (match, anchor, tail) => {
|
|
74
|
+
const trimmed = anchor.trim();
|
|
75
|
+
if (validAnchors.has(trimmed)) return match;
|
|
76
|
+
const target = resolve(squash(trimmed));
|
|
77
|
+
if (!target || target === trimmed) return match;
|
|
78
|
+
rewrites.push({ from: trimmed, to: target });
|
|
79
|
+
return `<<${target}${tail}`;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return { text: rewritten, rewrites };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = normalizePropertyAnchors;
|
|
86
|
+
module.exports.squash = squash;
|
|
@@ -4,12 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
{{#each topicProperties}}
|
|
6
6
|
| <<{{anchorName name}},`{{name}}`>>
|
|
7
|
-
|
|
|
7
|
+
|
|
|
8
|
+
ifdef::env-cloud[]
|
|
9
|
+
{{#if cluster_property_mapping_cloud_supported}}
|
|
10
|
+
{{#if alternate_cluster_property}}xref:reference:properties/{{cluster_property_doc_file}}#{{corresponding_cluster_property}}[`{{corresponding_cluster_property}}`] or xref:reference:properties/{{alternate_cluster_property_doc_file}}#{{alternate_cluster_property}}[`{{alternate_cluster_property}}`]{{else}}xref:reference:properties/{{cluster_property_doc_file}}#{{corresponding_cluster_property}}[`{{corresponding_cluster_property}}`]{{/if}}
|
|
11
|
+
{{else}}
|
|
12
|
+
{{#if alternate_cluster_property}}`{{corresponding_cluster_property}}` or `{{alternate_cluster_property}}`{{else}}`{{corresponding_cluster_property}}`{{/if}}
|
|
13
|
+
{{/if}}
|
|
14
|
+
endif::[]
|
|
15
|
+
ifndef::env-cloud[]
|
|
16
|
+
{{#if alternate_cluster_property}}xref:reference:properties/{{cluster_property_doc_file}}#{{corresponding_cluster_property}}[`{{corresponding_cluster_property}}`] or xref:reference:properties/{{alternate_cluster_property_doc_file}}#{{alternate_cluster_property}}[`{{alternate_cluster_property}}`]{{else}}xref:reference:properties/{{cluster_property_doc_file}}#{{corresponding_cluster_property}}[`{{corresponding_cluster_property}}`]{{/if}}
|
|
17
|
+
endif::[]
|
|
8
18
|
|
|
9
19
|
{{/each}}
|
|
10
20
|
|
|
11
21
|
|===
|
|
12
22
|
|
|
13
|
-
{{!--
|
|
14
|
-
{{!--
|
|
15
|
-
{{!-- Example: cleanup.policy -> cleanuppolicy --}}
|
|
23
|
+
{{!-- Anchors come from the anchorName helper, which reproduces Asciidoctor's --}}
|
|
24
|
+
{{!-- own auto-generated heading ID: cleanup.policy -> cleanup-policy. --}}
|
|
@@ -77,7 +77,17 @@ endif::[]
|
|
|
77
77
|
{{#if corresponding_cluster_property}}
|
|
78
78
|
|
|
79
79
|
| Corresponding cluster property
|
|
80
|
-
|
|
|
80
|
+
|
|
|
81
|
+
ifdef::env-cloud[]
|
|
82
|
+
{{#if corresponding_cluster_property_cloud_supported}}
|
|
83
|
+
xref:reference:properties/{{#if cluster_property_doc_file}}{{cluster_property_doc_file}}{{else}}cluster-properties.adoc{{/if}}#{{corresponding_cluster_property}}[`{{corresponding_cluster_property}}`]
|
|
84
|
+
{{else}}
|
|
85
|
+
`{{corresponding_cluster_property}}`
|
|
86
|
+
{{/if}}
|
|
87
|
+
endif::[]
|
|
88
|
+
ifndef::env-cloud[]
|
|
89
|
+
xref:reference:properties/{{#if cluster_property_doc_file}}{{cluster_property_doc_file}}{{else}}cluster-properties.adoc{{/if}}#{{corresponding_cluster_property}}[`{{corresponding_cluster_property}}`]
|
|
90
|
+
endif::[]
|
|
81
91
|
{{/if}}
|
|
82
92
|
{{#if (and minimum maximum)}}
|
|
83
93
|
|