@redpanda-data/docs-extensions-and-macros 5.13.4 → 5.13.5

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.
@@ -31,6 +31,12 @@
31
31
  * registry is available (for example, outside an Antora build), the macros
32
32
  * fall back to unvalidated rendering with a single warning.
33
33
  *
34
+ * A registry entry's `since` field, when set, is checked against the page
35
+ * being converted, not the newest release: a mention or table row for a
36
+ * feature whose `since` postdates the page's own version renders unstyled,
37
+ * the same way an unreleased feature renders on released docs, because as
38
+ * of that page's version the feature had not shipped yet.
39
+ *
34
40
  * Document or site attributes:
35
41
  *
36
42
  * enterprise-validate 'warn' (default) to log unknown feature names,
@@ -55,6 +61,7 @@
55
61
 
56
62
  const yaml = require('js-yaml')
57
63
  const chalk = require('chalk')
64
+ const semver = require('semver')
58
65
  const { buildBadgeHtml, DEFAULT_TOOLTIPS } = require('./badge')
59
66
 
60
67
  const $enterpriseRegistry = Symbol('$enterpriseRegistry')
@@ -211,6 +218,81 @@ function reportUnreleasedFeature ({ feature, mode, filePath }) {
211
218
  console.warn(chalk.yellow(message))
212
219
  }
213
220
 
221
+ /**
222
+ * Parse a version string into a semver for comparison, or undefined when it
223
+ * cannot be parsed. Lenient the same way `semver.coerce` is: Redpanda docs
224
+ * versions are "MAJOR.MINOR", not full semver, so an exact-match parse would
225
+ * reject every one of them.
226
+ */
227
+ function coerceVersion (raw) {
228
+ if (raw === undefined || raw === null) return undefined
229
+ return semver.coerce(String(raw)) || undefined
230
+ }
231
+
232
+ /**
233
+ * Whether a registry entry's `since` version has shipped as of the page
234
+ * being converted.
235
+ *
236
+ * enterprise-features.yml lives in the non-versioned 'shared' component, so
237
+ * every version branch reads the exact same entry -- unlike property data,
238
+ * which has one JSON file per release and is checked against the page's
239
+ * own version (see PROPERTY_AND_ENTERPRISE_REFERENCES.adoc). Without this
240
+ * check, a feature that ships in 26.3 renders as fully available -- linked,
241
+ * badge-free, no warning -- on a 24.1 page that predates it by two years.
242
+ *
243
+ * Returns true (shipped) when `since` is absent, unparsable, or the page's
244
+ * own version cannot be determined. That default favors data quality over
245
+ * release-readiness: unlike an unrecognized `status`, which fails closed
246
+ * because it is the one mistake that could publish an unreleased feature,
247
+ * a `since` typo or a build with no page-version context has no such
248
+ * asymmetry to protect against, so it defaults to not gating rather than
249
+ * hiding a shipped feature over unrelated bad data. An unparsable `since`
250
+ * is still reported, the same way an unrecognized `status` is: the failure
251
+ * mode differs (not gated, instead of gated), but a silent typo is exactly
252
+ * as easy to miss either way.
253
+ *
254
+ * @param {object} entry - Registry entry.
255
+ * @param {object} [config] - Extension config, used to read the page's own
256
+ * version.
257
+ * @param {object} [report] - {mode, filePath, contentCatalog} to report an
258
+ * unparsable since value. Omit to check silently.
259
+ */
260
+ function isFeatureShippedOnPage (entry, config, report) {
261
+ const since = entry && entry.since
262
+ if (since === undefined || since === null || String(since).trim() === '') return true
263
+ const sinceVersion = coerceVersion(since)
264
+ if (!sinceVersion) {
265
+ if (report && report.mode !== 'off') {
266
+ const where = report.filePath ? ` in ${report.filePath}` : ''
267
+ const message =
268
+ `enterprise:${entry.name}[]${where}: registry has since: '${since}', which is not a parsable version, so the since-gating check is skipped and the feature renders as already shipped. ` +
269
+ 'Fix the since value in enterprise-features.yml (e.g. "26.3"), or remove the key if the feature has always been available.'
270
+ if (report.contentCatalog) warnOnce(report.contentCatalog, `since:${entry.name}`, message)
271
+ else console.warn(chalk.yellow(message))
272
+ }
273
+ return true
274
+ }
275
+ const src = config && config.file && config.file.src
276
+ const pageVersion = src && coerceVersion(src.version)
277
+ if (!pageVersion) return true
278
+ return semver.gte(pageVersion, sinceVersion)
279
+ }
280
+
281
+ /**
282
+ * Report an inline mention of a feature before its `since` version, on a
283
+ * page whose own version predates it.
284
+ */
285
+ function reportFeatureNotYetShipped ({ feature, since, pageVersion, mode, filePath }) {
286
+ if (mode === 'off') return
287
+ const where = filePath ? ` in ${filePath}` : ''
288
+ const message =
289
+ `enterprise:${feature}[]${where}: '${feature}' is marked since: ${since} in the enterprise features registry, but this page is version ${pageVersion}, which predates it. ` +
290
+ 'The mention renders as plain text with no enterprise styling, tooltip, or link. ' +
291
+ 'Move the mention to a page whose version is at or after the since version, or remove since if the feature covers this version too.'
292
+ if (mode === 'error') throw new Error(message)
293
+ console.warn(chalk.yellow(message))
294
+ }
295
+
214
296
  /**
215
297
  * The badge for a registry entry, wrapped for use in generated AsciiDoc.
216
298
  *
@@ -436,6 +518,8 @@ function buildEnterpriseContent ({ feature, text, xref, url, tooltip, licensingP
436
518
  * @param {object} [opts]
437
519
  * @param {string} [opts.title] - Table title override.
438
520
  * @param {string} [opts.heading] - Third column heading override.
521
+ * @param {object} [opts.config] - Extension config, used to read the page's
522
+ * own version for since-gating (see isFeatureShippedOnPage).
439
523
  * @returns {string}
440
524
  */
441
525
  function buildFeatureTable (features, scope, opts = {}) {
@@ -456,7 +540,13 @@ function buildFeatureTable (features, scope, opts = {}) {
456
540
  // did not fail there either.
457
541
  .filter((feature) => {
458
542
  const status = entryStatus(feature, report)
459
- return includeUnreleased || status !== STATUS_UNRELEASED
543
+ if (!includeUnreleased && status === STATUS_UNRELEASED) return false
544
+ // since only describes a shipped feature's first version, so it does
545
+ // not apply to one that has not released at all yet. Silent, like the
546
+ // unreleased filter above: an older version's table naturally does not
547
+ // list a feature that did not exist yet on that version, which is not
548
+ // a writer mistake worth warning about.
549
+ return status === STATUS_UNRELEASED || isFeatureShippedOnPage(feature, opts.config, report)
460
550
  })
461
551
  .sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }))
462
552
  const title = opts.title || TABLE_TITLES[scope]
@@ -517,6 +607,18 @@ function enterpriseInlineMacro (config) {
517
607
  reportUnreleasedFeature({ feature, mode, filePath })
518
608
  return self.createInline(parent, 'quoted', attributes.text || feature)
519
609
  }
610
+ if (!isFeatureShippedOnPage(entry, config, { mode, filePath, contentCatalog: config && config.contentCatalog })) {
611
+ // isFeatureShippedOnPage only returns false when entry.since parsed,
612
+ // so entry is defined here.
613
+ reportFeatureNotYetShipped({
614
+ feature,
615
+ since: entry.since,
616
+ pageVersion: config && config.file && config.file.src && config.file.src.version,
617
+ mode,
618
+ filePath,
619
+ })
620
+ return self.createInline(parent, 'quoted', attributes.text || feature)
621
+ }
520
622
  let content = buildEnterpriseContent({
521
623
  feature,
522
624
  text: attributes.text,
@@ -568,6 +670,7 @@ function enterpriseFeaturesBlockMacro (config) {
568
670
  // Prerelease docs describe the upcoming release, so they list features
569
671
  // that are still only in a release candidate.
570
672
  includeUnreleased: isPrereleasePage(config, parent.getDocument()),
673
+ config,
571
674
  })
572
675
  // A block anchor before the macro call ([[my-id]]) is consumed into the
573
676
  // macro's id attribute. Re-emit it so crossrefs to the table keep working.
@@ -598,3 +701,4 @@ module.exports.buildFeatureTable = buildFeatureTable
598
701
  module.exports.parseRegistry = parseRegistry
599
702
  module.exports.resolveEntryXref = resolveEntryXref
600
703
  module.exports.resolveTooltipAttribute = resolveTooltipAttribute
704
+ module.exports.isFeatureShippedOnPage = isFeatureShippedOnPage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.13.4",
3
+ "version": "5.13.5",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  const yaml = require('js-yaml')
15
+ const semver = require('semver')
15
16
 
16
17
  const VALID_SCOPES = ['redpanda', 'console', 'connect', 'operator', 'cloud']
17
18
  const SOURCE_KINDS = ['core-enum', 'core-property', 'connect-plugin', 'manual']
@@ -58,6 +59,14 @@ function lintRegistry (source) {
58
59
  } else if (!feature.source.value || !String(feature.source.value).trim()) {
59
60
  findings.push(finding('error', 'registry-lint', `'${name}' has an empty source value; manual entries need a justification.`))
60
61
  }
62
+ if (feature.since !== undefined && feature.since !== null && String(feature.since).trim() !== '') {
63
+ const since = String(feature.since).trim()
64
+ // Lenient the same way the enterprise macro's own coerceVersion is:
65
+ // docs versions are "MAJOR.MINOR", not full semver.
66
+ if (!semver.coerce(since)) {
67
+ findings.push(finding('error', 'registry-lint', `'${name}' has since '${since}', which is not a parsable version.`))
68
+ }
69
+ }
61
70
  }
62
71
  return { features: data.features, findings }
63
72
  }