@redpanda-data/docs-extensions-and-macros 5.11.1 → 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/docs-data/rpk-overrides.schema.json +744 -0
- package/extensions/set-available-attachment-versions.js +32 -10
- package/macros/DOCUMENTING_UNRELEASED_FEATURES.adoc +188 -0
- package/macros/PROPERTY_AND_ENTERPRISE_REFERENCES.adoc +23 -3
- package/macros/REFERENCE.adoc +98 -9
- package/macros/badge.js +33 -3
- package/macros/enterprise.js +232 -22
- package/macros/prop.js +687 -187
- package/package.json +5 -4
- package/tools/property-extractor/cloud_config.py +16 -3
- 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
- package/tools/redpanda-connect/report-delta.js +49 -3
- package/tools/rpk-docs/extract-conditional-content.js +3 -1
package/macros/enterprise.js
CHANGED
|
@@ -55,16 +55,127 @@
|
|
|
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')
|
|
62
|
+
const $warned = Symbol('$warned')
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Report a message once per build. Keyed off the content catalog, which Antora
|
|
66
|
+
* rebuilds per build, so watch mode keeps reporting instead of going quiet.
|
|
67
|
+
*/
|
|
68
|
+
function warnOnce (contentCatalog, key, message) {
|
|
69
|
+
const seen = contentCatalog[$warned] || (contentCatalog[$warned] = new Set())
|
|
70
|
+
if (seen.has(key)) return false
|
|
71
|
+
seen.add(key)
|
|
72
|
+
console.warn(chalk.yellow(message))
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
61
75
|
|
|
62
76
|
const DEFAULT_LICENSING_PAGE = 'get-started:licensing/overview.adoc'
|
|
63
77
|
const DEFAULT_ROLE = 'enterprise-feature'
|
|
64
78
|
const BETA_LABEL = 'beta'
|
|
79
|
+
const UNRELEASED_LABEL = 'unreleased'
|
|
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
|
|
84
|
+
|
|
85
|
+
// Release status of a feature, which decides where it may be referenced.
|
|
86
|
+
//
|
|
87
|
+
// ga Shipped. Referenced anywhere. The default.
|
|
88
|
+
// beta Publicly available as a beta. Referenced anywhere, badged, so
|
|
89
|
+
// readers know the feature is not yet stable.
|
|
90
|
+
// unreleased In a release candidate only, not yet public. Referenced ONLY
|
|
91
|
+
// from prerelease docs (a beta branch). A mention on a released
|
|
92
|
+
// page describes something readers cannot get, so it is reported
|
|
93
|
+
// and rendered as plain text.
|
|
94
|
+
const STATUS_GA = 'ga'
|
|
95
|
+
const STATUS_BETA = 'beta'
|
|
96
|
+
const STATUS_UNRELEASED = 'unreleased'
|
|
97
|
+
const VALID_STATUSES = [STATUS_GA, STATUS_BETA, STATUS_UNRELEASED]
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The release status of a registry entry.
|
|
101
|
+
*
|
|
102
|
+
* `beta: true` predates the status field and still means beta, so old entries
|
|
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.
|
|
109
|
+
*
|
|
110
|
+
* @param {object} entry - Registry entry.
|
|
111
|
+
* @param {object} [report] - {mode, filePath} to report an invalid status.
|
|
112
|
+
* @returns {string} One of VALID_STATUSES.
|
|
113
|
+
*/
|
|
114
|
+
function entryStatus (entry, report) {
|
|
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.
|
|
122
|
+
if (entry.status !== undefined) {
|
|
123
|
+
const raw = entry.status === null ? '' : String(entry.status).trim()
|
|
124
|
+
const status = raw.toLowerCase()
|
|
125
|
+
if (VALID_STATUSES.includes(status)) return status
|
|
126
|
+
if (report && report.mode !== 'off') {
|
|
127
|
+
const where = report.filePath ? ` in ${report.filePath}` : ''
|
|
128
|
+
const what = raw === '' ? 'an empty status' : `status '${entry.status}'`
|
|
129
|
+
const message =
|
|
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.'
|
|
132
|
+
// Honour error mode like every other registry diagnostic. A typo here is
|
|
133
|
+
// exactly what publishes an unreleased feature, so the strictest setting
|
|
134
|
+
// has to stop it.
|
|
135
|
+
if (report.mode === 'error') throw new Error(message)
|
|
136
|
+
// One registry typo needs one fix, so report it once per build rather
|
|
137
|
+
// than once per mention of the feature.
|
|
138
|
+
if (report.contentCatalog) warnOnce(report.contentCatalog, `status:${entry.name}`, message)
|
|
139
|
+
else console.warn(chalk.yellow(message))
|
|
140
|
+
}
|
|
141
|
+
return STATUS_UNRELEASED
|
|
142
|
+
}
|
|
143
|
+
return entry.beta === true ? STATUS_BETA : STATUS_GA
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Whether the page being converted belongs to a prerelease component version,
|
|
148
|
+
* which is where an unreleased feature may be referenced.
|
|
149
|
+
*
|
|
150
|
+
* Read from the component version's own prerelease flag (antora.yml) rather
|
|
151
|
+
* than inferred, with a document attribute as an escape hatch for playbooks
|
|
152
|
+
* that surface it themselves.
|
|
153
|
+
*/
|
|
154
|
+
function isPrereleasePage (config, document) {
|
|
155
|
+
const attribute = document && document.getAttribute('page-component-version-is-prerelease')
|
|
156
|
+
if (attribute !== undefined) {
|
|
157
|
+
const value = String(attribute).trim().toLowerCase()
|
|
158
|
+
if (value === 'true' || value === '') return true
|
|
159
|
+
if (value === 'false') return false
|
|
160
|
+
// Anything else is a typo. Falling through to the catalog is safer than
|
|
161
|
+
// guessing, and silently treating it as "released" produced a warning that
|
|
162
|
+
// told the writer their prerelease page belonged to a released version.
|
|
163
|
+
console.warn(chalk.yellow(
|
|
164
|
+
`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.`
|
|
165
|
+
))
|
|
166
|
+
}
|
|
167
|
+
const contentCatalog = config && config.contentCatalog
|
|
168
|
+
const src = config && config.file && config.file.src
|
|
169
|
+
if (!contentCatalog || !src || typeof contentCatalog.getComponent !== 'function') return false
|
|
170
|
+
const component = contentCatalog.getComponent(src.component)
|
|
171
|
+
if (!component) return false
|
|
172
|
+
const versions = Array.isArray(component.versions) ? component.versions : []
|
|
173
|
+
const componentVersion = versions.find((entry) => entry.version === src.version)
|
|
174
|
+
return Boolean(componentVersion && componentVersion.prerelease)
|
|
175
|
+
}
|
|
65
176
|
|
|
66
177
|
/**
|
|
67
|
-
* Render the
|
|
178
|
+
* Render the release-status badge for a registry entry.
|
|
68
179
|
*
|
|
69
180
|
* The badge HTML is built directly rather than emitted as `badge:[...]`
|
|
70
181
|
* AsciiDoc, so it renders whether or not the consuming playbook registers the
|
|
@@ -73,14 +184,31 @@ const BETA_LABEL = 'beta'
|
|
|
73
184
|
* `badge::[label=beta]` in any build without that macro registered.
|
|
74
185
|
*
|
|
75
186
|
* @param {object} entry - Registry entry.
|
|
76
|
-
* @returns {string} Badge HTML, or an empty string
|
|
187
|
+
* @returns {string} Badge HTML, or an empty string for a shipped feature.
|
|
77
188
|
*/
|
|
78
189
|
function buildBetaBadge (entry) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
label: BETA_LABEL,
|
|
82
|
-
|
|
83
|
-
|
|
190
|
+
const status = entryStatus(entry)
|
|
191
|
+
if (status === STATUS_BETA) {
|
|
192
|
+
return buildBadgeHtml({ label: BETA_LABEL, tooltip: entry['beta-tooltip'] || undefined })
|
|
193
|
+
}
|
|
194
|
+
if (status === STATUS_UNRELEASED) {
|
|
195
|
+
return buildBadgeHtml({ label: UNRELEASED_LABEL, tooltip: entry['status-tooltip'] || UNRELEASED_TOOLTIP })
|
|
196
|
+
}
|
|
197
|
+
return ''
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Report an unreleased feature referenced from released documentation.
|
|
202
|
+
*/
|
|
203
|
+
function reportUnreleasedFeature ({ feature, mode, filePath }) {
|
|
204
|
+
if (mode === 'off') return
|
|
205
|
+
const where = filePath ? ` in ${filePath}` : ''
|
|
206
|
+
const message =
|
|
207
|
+
`enterprise:${feature}[]${where}: '${feature}' is marked status: unreleased in the enterprise features registry, so it is only documented for an upcoming release. ` +
|
|
208
|
+
'This page belongs to a released version, so the mention renders as plain text with no enterprise styling, tooltip, or link. ' +
|
|
209
|
+
'Move the mention to the prerelease (beta) branch, or change the status once the feature ships.'
|
|
210
|
+
if (mode === 'error') throw new Error(message)
|
|
211
|
+
console.warn(chalk.yellow(message))
|
|
84
212
|
}
|
|
85
213
|
|
|
86
214
|
/**
|
|
@@ -125,7 +253,14 @@ let warnedNoRegistry = false
|
|
|
125
253
|
* @returns {{features: object[], lookup: Map<string, object>}}
|
|
126
254
|
*/
|
|
127
255
|
function parseRegistry (source, origin = REGISTRY_FILENAME) {
|
|
128
|
-
|
|
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
|
+
}
|
|
129
264
|
if (!data || !Array.isArray(data.features)) {
|
|
130
265
|
throw new Error(`Enterprise features registry ${origin} has no 'features' list.`)
|
|
131
266
|
}
|
|
@@ -164,17 +299,44 @@ function loadRegistry (config) {
|
|
|
164
299
|
const partials = contentCatalog.findBy({ component: 'shared', module: 'ROOT', family: 'partial' }) || []
|
|
165
300
|
const registryFile = partials.find((file) => file.path && file.path.endsWith(REGISTRY_FILENAME))
|
|
166
301
|
if (registryFile) {
|
|
167
|
-
|
|
302
|
+
try {
|
|
303
|
+
registry = parseRegistry(registryFile.contents.toString(), registryFile.path)
|
|
304
|
+
} catch (error) {
|
|
305
|
+
// A malformed registry must not take the whole build down. Unvalidated
|
|
306
|
+
// enterprise mentions are the same graceful degradation as no registry at
|
|
307
|
+
// all, and this matches how prop.js treats a corrupt properties JSON.
|
|
308
|
+
// Caching the failure also stops the error being re-raised per macro call
|
|
309
|
+
// and attributed to whichever page happened to convert first.
|
|
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.
|
|
320
|
+
warnOnce(contentCatalog, 'badregistry',
|
|
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.`)
|
|
322
|
+
}
|
|
168
323
|
}
|
|
169
324
|
// Cache null too, so a missing registry is only searched for once per build.
|
|
170
325
|
contentCatalog[$enterpriseRegistry] = registry
|
|
171
326
|
return registry || undefined
|
|
172
327
|
}
|
|
173
328
|
|
|
174
|
-
function warnNoRegistry () {
|
|
329
|
+
function warnNoRegistry (contentCatalog) {
|
|
330
|
+
// Already reported as unreadable: one problem, one diagnostic.
|
|
331
|
+
if (contentCatalog && contentCatalog[$enterpriseRegistryUnreadable]) return
|
|
332
|
+
const message = "Enterprise features registry (enterprise-features.yml in the 'shared' component) not found; enterprise: targets are not validated."
|
|
333
|
+
// Deduplicate per build, not per process: Antora's watch mode reuses one
|
|
334
|
+
// process across builds, and a module-level guard reported this only on a
|
|
335
|
+
// session's first build -- exactly when a writer is iterating.
|
|
336
|
+
if (contentCatalog) return warnOnce(contentCatalog, 'noregistry', message)
|
|
175
337
|
if (warnedNoRegistry) return
|
|
176
338
|
warnedNoRegistry = true
|
|
177
|
-
console.warn(chalk.yellow(
|
|
339
|
+
console.warn(chalk.yellow(message))
|
|
178
340
|
}
|
|
179
341
|
|
|
180
342
|
/**
|
|
@@ -247,7 +409,16 @@ function resolveTooltipAttribute (raw) {
|
|
|
247
409
|
function buildEnterpriseContent ({ feature, text, xref, url, tooltip, licensingPage, role, tooltipAttr, links }) {
|
|
248
410
|
const display = text || feature
|
|
249
411
|
const tooltipText = tooltip || `${feature} requires an Enterprise Edition license.`
|
|
250
|
-
|
|
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, '>')
|
|
251
422
|
const tooltipHtml = tooltipAttr ? ` ${tooltipAttr}="${escapedTooltip}"` : ''
|
|
252
423
|
let inner = display
|
|
253
424
|
if (links) {
|
|
@@ -268,8 +439,25 @@ function buildEnterpriseContent ({ feature, text, xref, url, tooltip, licensingP
|
|
|
268
439
|
* @returns {string}
|
|
269
440
|
*/
|
|
270
441
|
function buildFeatureTable (features, scope, opts = {}) {
|
|
442
|
+
// On released docs an unreleased feature is omitted entirely: the licensing
|
|
443
|
+
// table is a list of what a licence covers today, and listing something
|
|
444
|
+
// unavailable makes it wrong. Prerelease docs list it with its badge.
|
|
445
|
+
const includeUnreleased = opts.includeUnreleased === true
|
|
446
|
+
// Report a bad status from here too. Without a reporter a typo'd status was
|
|
447
|
+
// silently downgraded to released, so an unreleased feature that no page
|
|
448
|
+
// happens to reference inline was published in the table with no diagnostic.
|
|
449
|
+
const report = opts.report
|
|
271
450
|
const rows = features
|
|
272
451
|
.filter((feature) => feature.scope === scope)
|
|
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
|
+
})
|
|
273
461
|
.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }))
|
|
274
462
|
const title = opts.title || TABLE_TITLES[scope]
|
|
275
463
|
const heading = opts.heading || THIRD_COLUMN_HEADINGS[scope] || THIRD_COLUMN_HEADINGS.default
|
|
@@ -302,7 +490,7 @@ function enterpriseInlineMacro (config) {
|
|
|
302
490
|
let registry
|
|
303
491
|
if (config && config.contentCatalog) {
|
|
304
492
|
registry = loadRegistry(config)
|
|
305
|
-
if (!registry) warnNoRegistry()
|
|
493
|
+
if (!registry) warnNoRegistry(config && config.contentCatalog)
|
|
306
494
|
}
|
|
307
495
|
let feature = target
|
|
308
496
|
let entry
|
|
@@ -319,6 +507,16 @@ function enterpriseInlineMacro (config) {
|
|
|
319
507
|
})
|
|
320
508
|
}
|
|
321
509
|
}
|
|
510
|
+
const mode = document.getAttribute('enterprise-validate', 'warn')
|
|
511
|
+
const filePath = config && config.file && config.file.src && config.file.src.path
|
|
512
|
+
const status = entryStatus(entry, { mode, filePath, contentCatalog: config && config.contentCatalog })
|
|
513
|
+
if (status === STATUS_UNRELEASED && !isPrereleasePage(config, document)) {
|
|
514
|
+
// Documented for an upcoming release, referenced from released docs.
|
|
515
|
+
// Styling it as an available enterprise feature would promise readers
|
|
516
|
+
// something they cannot get, so render the name and say so.
|
|
517
|
+
reportUnreleasedFeature({ feature, mode, filePath })
|
|
518
|
+
return self.createInline(parent, 'quoted', attributes.text || feature)
|
|
519
|
+
}
|
|
322
520
|
let content = buildEnterpriseContent({
|
|
323
521
|
feature,
|
|
324
522
|
text: attributes.text,
|
|
@@ -330,12 +528,13 @@ function enterpriseInlineMacro (config) {
|
|
|
330
528
|
tooltipAttr: resolveTooltipAttribute(document.getAttribute('enterprise-tooltip')),
|
|
331
529
|
links: document.getAttribute('enterprise-links', 'true') === 'true',
|
|
332
530
|
})
|
|
333
|
-
// A feature
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
531
|
+
// A feature's status holds wherever it is referenced, so prose gets the
|
|
532
|
+
// same badge as the generated tables. enterprise-beta-badge=false
|
|
533
|
+
// suppresses the beta badge in prose; the unreleased badge is not
|
|
534
|
+
// suppressible, because "not in a release yet" is not decoration.
|
|
535
|
+
if (status === STATUS_UNRELEASED || document.getAttribute('enterprise-beta-badge', 'true') === 'true') {
|
|
536
|
+
const badge = buildBetaBadge(entry)
|
|
537
|
+
if (badge) content += ` ${badge}`
|
|
339
538
|
}
|
|
340
539
|
// The xref inside the span is resolved by the 'macros' substitution,
|
|
341
540
|
// the same mechanism the config_ref macro relies on.
|
|
@@ -355,10 +554,21 @@ function enterpriseFeaturesBlockMacro (config) {
|
|
|
355
554
|
}
|
|
356
555
|
const registry = config && config.contentCatalog ? loadRegistry(config) : undefined
|
|
357
556
|
if (!registry) {
|
|
358
|
-
warnNoRegistry()
|
|
557
|
+
warnNoRegistry(config && config.contentCatalog)
|
|
359
558
|
return self.parseContent(parent, `WARNING: The enterprise features registry is unavailable, so the ${scope} feature table cannot be rendered.`)
|
|
360
559
|
}
|
|
361
|
-
const table = buildFeatureTable(registry.features, scope, {
|
|
560
|
+
const table = buildFeatureTable(registry.features, scope, {
|
|
561
|
+
title: attributes.title,
|
|
562
|
+
heading: attributes.heading,
|
|
563
|
+
report: {
|
|
564
|
+
mode: parent.getDocument().getAttribute('enterprise-validate', 'warn'),
|
|
565
|
+
filePath: config && config.file && config.file.src && config.file.src.path,
|
|
566
|
+
contentCatalog: config && config.contentCatalog,
|
|
567
|
+
},
|
|
568
|
+
// Prerelease docs describe the upcoming release, so they list features
|
|
569
|
+
// that are still only in a release candidate.
|
|
570
|
+
includeUnreleased: isPrereleasePage(config, parent.getDocument()),
|
|
571
|
+
})
|
|
362
572
|
// A block anchor before the macro call ([[my-id]]) is consumed into the
|
|
363
573
|
// macro's id attribute. Re-emit it so crossrefs to the table keep working.
|
|
364
574
|
const source = attributes.id ? `[[${attributes.id}]]\n${table}` : table
|