@redpanda-data/docs-extensions-and-macros 5.11.1 → 5.12.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.
@@ -58,13 +58,110 @@ const chalk = require('chalk')
58
58
  const { buildBadgeHtml } = require('./badge')
59
59
 
60
60
  const $enterpriseRegistry = Symbol('$enterpriseRegistry')
61
+ const $warned = Symbol('$warned')
62
+
63
+ /**
64
+ * Report a message once per build. Keyed off the content catalog, which Antora
65
+ * rebuilds per build, so watch mode keeps reporting instead of going quiet.
66
+ */
67
+ function warnOnce (contentCatalog, key, message) {
68
+ const seen = contentCatalog[$warned] || (contentCatalog[$warned] = new Set())
69
+ if (seen.has(key)) return false
70
+ seen.add(key)
71
+ console.warn(chalk.yellow(message))
72
+ return true
73
+ }
61
74
 
62
75
  const DEFAULT_LICENSING_PAGE = 'get-started:licensing/overview.adoc'
63
76
  const DEFAULT_ROLE = 'enterprise-feature'
64
77
  const BETA_LABEL = 'beta'
78
+ const UNRELEASED_LABEL = 'unreleased'
79
+ const UNRELEASED_TOOLTIP =
80
+ 'This feature is not in a released version of Redpanda yet. It is documented here for the upcoming release.'
81
+
82
+ // Release status of a feature, which decides where it may be referenced.
83
+ //
84
+ // ga Shipped. Referenced anywhere. The default.
85
+ // beta Publicly available as a beta. Referenced anywhere, badged, so
86
+ // readers know the feature is not yet stable.
87
+ // unreleased In a release candidate only, not yet public. Referenced ONLY
88
+ // from prerelease docs (a beta branch). A mention on a released
89
+ // page describes something readers cannot get, so it is reported
90
+ // and rendered as plain text.
91
+ const STATUS_GA = 'ga'
92
+ const STATUS_BETA = 'beta'
93
+ const STATUS_UNRELEASED = 'unreleased'
94
+ const VALID_STATUSES = [STATUS_GA, STATUS_BETA, STATUS_UNRELEASED]
95
+
96
+ /**
97
+ * The release status of a registry entry.
98
+ *
99
+ * `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
+ * rather than quietly treated as shipped, which is the failure mode that lets a
102
+ * typo publish an unreleased feature.
103
+ *
104
+ * @param {object} entry - Registry entry.
105
+ * @param {object} [report] - {mode, filePath} to report an invalid status.
106
+ * @returns {string} One of VALID_STATUSES.
107
+ */
108
+ function entryStatus (entry, report) {
109
+ if (!entry) return STATUS_GA
110
+ if (entry.status !== undefined) {
111
+ const status = String(entry.status).trim().toLowerCase()
112
+ if (VALID_STATUSES.includes(status)) return status
113
+ if (report && report.mode !== 'off') {
114
+ const where = report.filePath ? ` in ${report.filePath}` : ''
115
+ const message =
116
+ `enterprise:${entry.name}[]${where}: registry status '${entry.status}' is not one of ${VALID_STATUSES.join(', ')}, so the feature is treated as released. ` +
117
+ 'Fix the status in enterprise-features.yml.'
118
+ // Honour error mode like every other registry diagnostic. A typo here is
119
+ // exactly what publishes an unreleased feature, so the strictest setting
120
+ // has to stop it.
121
+ if (report.mode === 'error') throw new Error(message)
122
+ // One registry typo needs one fix, so report it once per build rather
123
+ // than once per mention of the feature.
124
+ if (report.contentCatalog) warnOnce(report.contentCatalog, `status:${entry.name}`, message)
125
+ else console.warn(chalk.yellow(message))
126
+ }
127
+ return STATUS_GA
128
+ }
129
+ return entry.beta === true ? STATUS_BETA : STATUS_GA
130
+ }
131
+
132
+ /**
133
+ * Whether the page being converted belongs to a prerelease component version,
134
+ * which is where an unreleased feature may be referenced.
135
+ *
136
+ * Read from the component version's own prerelease flag (antora.yml) rather
137
+ * than inferred, with a document attribute as an escape hatch for playbooks
138
+ * that surface it themselves.
139
+ */
140
+ function isPrereleasePage (config, document) {
141
+ const attribute = document && document.getAttribute('page-component-version-is-prerelease')
142
+ if (attribute !== undefined) {
143
+ const value = String(attribute).trim().toLowerCase()
144
+ if (value === 'true' || value === '') return true
145
+ if (value === 'false') return false
146
+ // Anything else is a typo. Falling through to the catalog is safer than
147
+ // guessing, and silently treating it as "released" produced a warning that
148
+ // told the writer their prerelease page belonged to a released version.
149
+ console.warn(chalk.yellow(
150
+ `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.`
151
+ ))
152
+ }
153
+ const contentCatalog = config && config.contentCatalog
154
+ const src = config && config.file && config.file.src
155
+ if (!contentCatalog || !src || typeof contentCatalog.getComponent !== 'function') return false
156
+ const component = contentCatalog.getComponent(src.component)
157
+ if (!component) return false
158
+ const versions = Array.isArray(component.versions) ? component.versions : []
159
+ const componentVersion = versions.find((entry) => entry.version === src.version)
160
+ return Boolean(componentVersion && componentVersion.prerelease)
161
+ }
65
162
 
66
163
  /**
67
- * Render the beta badge for a registry entry marked `beta: true`.
164
+ * Render the release-status badge for a registry entry.
68
165
  *
69
166
  * The badge HTML is built directly rather than emitted as `badge:[...]`
70
167
  * AsciiDoc, so it renders whether or not the consuming playbook registers the
@@ -73,14 +170,31 @@ const BETA_LABEL = 'beta'
73
170
  * `badge::[label=beta]` in any build without that macro registered.
74
171
  *
75
172
  * @param {object} entry - Registry entry.
76
- * @returns {string} Badge HTML, or an empty string when the entry is not beta.
173
+ * @returns {string} Badge HTML, or an empty string for a shipped feature.
77
174
  */
78
175
  function buildBetaBadge (entry) {
79
- if (!entry || entry.beta !== true) return ''
80
- return buildBadgeHtml({
81
- label: BETA_LABEL,
82
- tooltip: entry['beta-tooltip'] || undefined,
83
- })
176
+ const status = entryStatus(entry)
177
+ if (status === STATUS_BETA) {
178
+ return buildBadgeHtml({ label: BETA_LABEL, tooltip: entry['beta-tooltip'] || undefined })
179
+ }
180
+ if (status === STATUS_UNRELEASED) {
181
+ return buildBadgeHtml({ label: UNRELEASED_LABEL, tooltip: entry['status-tooltip'] || UNRELEASED_TOOLTIP })
182
+ }
183
+ return ''
184
+ }
185
+
186
+ /**
187
+ * Report an unreleased feature referenced from released documentation.
188
+ */
189
+ function reportUnreleasedFeature ({ feature, mode, filePath }) {
190
+ if (mode === 'off') return
191
+ const where = filePath ? ` in ${filePath}` : ''
192
+ const message =
193
+ `enterprise:${feature}[]${where}: '${feature}' is marked status: unreleased in the enterprise features registry, so it is only documented for an upcoming release. ` +
194
+ 'This page belongs to a released version, so the mention renders as plain text with no enterprise styling, tooltip, or link. ' +
195
+ 'Move the mention to the prerelease (beta) branch, or change the status once the feature ships.'
196
+ if (mode === 'error') throw new Error(message)
197
+ console.warn(chalk.yellow(message))
84
198
  }
85
199
 
86
200
  /**
@@ -164,17 +278,33 @@ function loadRegistry (config) {
164
278
  const partials = contentCatalog.findBy({ component: 'shared', module: 'ROOT', family: 'partial' }) || []
165
279
  const registryFile = partials.find((file) => file.path && file.path.endsWith(REGISTRY_FILENAME))
166
280
  if (registryFile) {
167
- registry = parseRegistry(registryFile.contents.toString(), registryFile.path)
281
+ try {
282
+ registry = parseRegistry(registryFile.contents.toString(), registryFile.path)
283
+ } catch (error) {
284
+ // A malformed registry must not take the whole build down. Unvalidated
285
+ // enterprise mentions are the same graceful degradation as no registry at
286
+ // all, and this matches how prop.js treats a corrupt properties JSON.
287
+ // Caching the failure also stops the error being re-raised per macro call
288
+ // and attributed to whichever page happened to convert first.
289
+ registry = null
290
+ warnOnce(contentCatalog, 'badregistry',
291
+ `Enterprise features registry ${registryFile.path} could not be read (${error.message}); enterprise: targets are not validated.`)
292
+ }
168
293
  }
169
294
  // Cache null too, so a missing registry is only searched for once per build.
170
295
  contentCatalog[$enterpriseRegistry] = registry
171
296
  return registry || undefined
172
297
  }
173
298
 
174
- function warnNoRegistry () {
299
+ function warnNoRegistry (contentCatalog) {
300
+ const message = "Enterprise features registry (enterprise-features.yml in the 'shared' component) not found; enterprise: targets are not validated."
301
+ // Deduplicate per build, not per process: Antora's watch mode reuses one
302
+ // process across builds, and a module-level guard reported this only on a
303
+ // session's first build -- exactly when a writer is iterating.
304
+ if (contentCatalog) return warnOnce(contentCatalog, 'noregistry', message)
175
305
  if (warnedNoRegistry) return
176
306
  warnedNoRegistry = true
177
- console.warn(chalk.yellow(`Enterprise features registry (${REGISTRY_FILENAME} in the shared component) not found; enterprise: targets are not validated.`))
307
+ console.warn(chalk.yellow(message))
178
308
  }
179
309
 
180
310
  /**
@@ -268,8 +398,17 @@ function buildEnterpriseContent ({ feature, text, xref, url, tooltip, licensingP
268
398
  * @returns {string}
269
399
  */
270
400
  function buildFeatureTable (features, scope, opts = {}) {
401
+ // On released docs an unreleased feature is omitted entirely: the licensing
402
+ // table is a list of what a licence covers today, and listing something
403
+ // unavailable makes it wrong. Prerelease docs list it with its badge.
404
+ const includeUnreleased = opts.includeUnreleased === true
405
+ // Report a bad status from here too. Without a reporter a typo'd status was
406
+ // silently downgraded to released, so an unreleased feature that no page
407
+ // happens to reference inline was published in the table with no diagnostic.
408
+ const report = opts.report
271
409
  const rows = features
272
410
  .filter((feature) => feature.scope === scope)
411
+ .filter((feature) => includeUnreleased || entryStatus(feature, report) !== STATUS_UNRELEASED)
273
412
  .sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }))
274
413
  const title = opts.title || TABLE_TITLES[scope]
275
414
  const heading = opts.heading || THIRD_COLUMN_HEADINGS[scope] || THIRD_COLUMN_HEADINGS.default
@@ -302,7 +441,7 @@ function enterpriseInlineMacro (config) {
302
441
  let registry
303
442
  if (config && config.contentCatalog) {
304
443
  registry = loadRegistry(config)
305
- if (!registry) warnNoRegistry()
444
+ if (!registry) warnNoRegistry(config && config.contentCatalog)
306
445
  }
307
446
  let feature = target
308
447
  let entry
@@ -319,6 +458,16 @@ function enterpriseInlineMacro (config) {
319
458
  })
320
459
  }
321
460
  }
461
+ const mode = document.getAttribute('enterprise-validate', 'warn')
462
+ const filePath = config && config.file && config.file.src && config.file.src.path
463
+ const status = entryStatus(entry, { mode, filePath, contentCatalog: config && config.contentCatalog })
464
+ if (status === STATUS_UNRELEASED && !isPrereleasePage(config, document)) {
465
+ // Documented for an upcoming release, referenced from released docs.
466
+ // Styling it as an available enterprise feature would promise readers
467
+ // something they cannot get, so render the name and say so.
468
+ reportUnreleasedFeature({ feature, mode, filePath })
469
+ return self.createInline(parent, 'quoted', attributes.text || feature)
470
+ }
322
471
  let content = buildEnterpriseContent({
323
472
  feature,
324
473
  text: attributes.text,
@@ -330,12 +479,13 @@ function enterpriseInlineMacro (config) {
330
479
  tooltipAttr: resolveTooltipAttribute(document.getAttribute('enterprise-tooltip')),
331
480
  links: document.getAttribute('enterprise-links', 'true') === 'true',
332
481
  })
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}`
482
+ // A feature's status holds wherever it is referenced, so prose gets the
483
+ // same badge as the generated tables. enterprise-beta-badge=false
484
+ // suppresses the beta badge in prose; the unreleased badge is not
485
+ // suppressible, because "not in a release yet" is not decoration.
486
+ if (status === STATUS_UNRELEASED || document.getAttribute('enterprise-beta-badge', 'true') === 'true') {
487
+ const badge = buildBetaBadge(entry)
488
+ if (badge) content += ` ${badge}`
339
489
  }
340
490
  // The xref inside the span is resolved by the 'macros' substitution,
341
491
  // the same mechanism the config_ref macro relies on.
@@ -355,10 +505,21 @@ function enterpriseFeaturesBlockMacro (config) {
355
505
  }
356
506
  const registry = config && config.contentCatalog ? loadRegistry(config) : undefined
357
507
  if (!registry) {
358
- warnNoRegistry()
508
+ warnNoRegistry(config && config.contentCatalog)
359
509
  return self.parseContent(parent, `WARNING: The enterprise features registry is unavailable, so the ${scope} feature table cannot be rendered.`)
360
510
  }
361
- const table = buildFeatureTable(registry.features, scope, { title: attributes.title, heading: attributes.heading })
511
+ const table = buildFeatureTable(registry.features, scope, {
512
+ title: attributes.title,
513
+ heading: attributes.heading,
514
+ report: {
515
+ mode: parent.getDocument().getAttribute('enterprise-validate', 'warn'),
516
+ filePath: config && config.file && config.file.src && config.file.src.path,
517
+ contentCatalog: config && config.contentCatalog,
518
+ },
519
+ // Prerelease docs describe the upcoming release, so they list features
520
+ // that are still only in a release candidate.
521
+ includeUnreleased: isPrereleasePage(config, parent.getDocument()),
522
+ })
362
523
  // A block anchor before the macro call ([[my-id]]) is consumed into the
363
524
  // macro's id attribute. Re-emit it so crossrefs to the table keep working.
364
525
  const source = attributes.id ? `[[${attributes.id}]]\n${table}` : table