@redpanda-data/docs-extensions-and-macros 5.57.0 → 5.58.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.
package/macros/prop.js CHANGED
@@ -83,6 +83,24 @@
83
83
  * property-ref-role CSS class applied to the code element
84
84
  * (default: property-ref).
85
85
  *
86
+ * PROPERTY VALUES: propval:
87
+ *
88
+ * The same registration also provides propval:<name>[<field>], which renders
89
+ * one value from the property data as plain text, so prose such as "defaults
90
+ * to 128 MiB" comes from the data instead of being typed by hand:
91
+ *
92
+ * propval:log_segment_ms[default] -> 2 weeks
93
+ * propval:log_segment_ms[default,format=raw] -> 1209600000
94
+ * propval:log_segment_ms[min] -> 1 minute
95
+ * propval:cloud_storage_enabled[restart] -> yes
96
+ *
97
+ * It uses exactly prop:'s dataset selection, validation modes, Cloud gating
98
+ * and missing-data refusal (loadProperties, isAvailable, warnSeriesMissing),
99
+ * and formats values with the reference generator's own helpers (see
100
+ * extension-utils/property-values.js). Whenever no verified value exists, it
101
+ * renders a visible placeholder such as <unknown default> and reports why,
102
+ * because unlike a property name, a value has no safe fallback.
103
+ *
86
104
  * Example use in a playbook:
87
105
  *
88
106
  * asciidoc:
@@ -92,6 +110,7 @@
92
110
 
93
111
  const semver = require('semver')
94
112
  const { escapeHtml } = require('../extension-utils/html-utils')
113
+ const propertyValues = require('../extension-utils/property-values')
95
114
  const logger = require('@antora/logger')('prop-macro')
96
115
 
97
116
  const $propertyRegistry = Symbol('$propertyRegistry')
@@ -968,6 +987,116 @@ function propInlineMacro (config) {
968
987
  }
969
988
  }
970
989
 
990
+ const VALUE_ROLE = 'property-value'
991
+ const UNKNOWN_VALUE_ROLE = 'property-value-unknown'
992
+
993
+ /**
994
+ * Build the HTML emitted for one propval: instance. Exported for unit testing.
995
+ *
996
+ * @param {object} opts
997
+ * @param {string} opts.name - Property name.
998
+ * @param {string} opts.field - Canonical (or as-written, for a placeholder) field.
999
+ * @param {string} [opts.text] - The value, as plain text.
1000
+ * @param {string} [opts.html] - Pre-escaped markup, used instead of text.
1001
+ * @param {boolean} [opts.unknown] - Render the placeholder instead.
1002
+ * @returns {string}
1003
+ */
1004
+ function buildPropvalContent ({ name, field, text, html, unknown = false }) {
1005
+ const attrs = `data-property="${escapeHtml(name)}" data-field="${escapeHtml(field)}"`
1006
+ if (unknown) {
1007
+ return `<span class="${VALUE_ROLE} ${UNKNOWN_VALUE_ROLE}" ${attrs}>${escapeHtml(`<unknown ${field}>`)}</span>`
1008
+ }
1009
+ return `<span class="${VALUE_ROLE}" ${attrs}>${html !== undefined ? html : escapeHtml(text)}</span>`
1010
+ }
1011
+
1012
+ /**
1013
+ * Resolve one propval: call to its value, or to the reason there is none.
1014
+ * Shares every decision with prop: -- the dataset (loadProperties), the
1015
+ * availability gate (isAvailable) -- so a value is shown exactly where prop:
1016
+ * would mark the name, and nowhere else. Exported for unit testing and for
1017
+ * the migrate-property-values report.
1018
+ *
1019
+ * @param {string} [opts.filePath] - Named in the message, when known.
1020
+ * @returns {{ok: true, text: string, html?: string, registry: object, field: string} |
1021
+ * {ok: false, kind: string, message: string, registry?: object, field: string}}
1022
+ */
1023
+ function resolvePropertyValue ({ registry, name, field: requested, format, component, filePath }) {
1024
+ const field = propertyValues.normalizeField(requested)
1025
+ const call = `propval:${name}[${requested}]${filePath ? ` in ${filePath}` : ''}`
1026
+ if (!field) {
1027
+ const supported = Object.keys(propertyValues.FIELDS).join(', ')
1028
+ return { ok: false, kind: 'unsupported-field', field: requested, message: `${call}: '${requested}' is not a field propval: supports. Supported fields: ${supported}.` }
1029
+ }
1030
+ if (!registry) {
1031
+ return { ok: false, kind: 'no-data', field, message: `${call}: this page has no property data to read '${field}' from (see the prop macro warning for this component version), so it renders <unknown ${field}> rather than a value from another release.` }
1032
+ }
1033
+ const entry = Object.prototype.hasOwnProperty.call(registry.properties, name) ? registry.properties[name] : undefined
1034
+ if (!entry || typeof entry !== 'object') {
1035
+ return { ok: false, kind: 'unknown-property', field, registry, message: `${call}: '${name}' is not in the property data this page validates against (${describeDataset(registry)}). Either the name is misspelled or the property does not exist in that release.` }
1036
+ }
1037
+ if (!isAvailable(registry, entry)) {
1038
+ return { ok: false, kind: 'unavailable', field, registry, message: `${call}: '${name}' is not available in the ${component || 'this'} component: ${describeDataset(registry)} marks it cloud_supported: false, so no value is shown to readers who cannot set it.` }
1039
+ }
1040
+ if (registry.surface === SURFACE_CLOUD && propertyValues.FIELDS[field].cloudHidden) {
1041
+ return { ok: false, kind: 'cloud-hidden', field, registry, message: `${call}: the property reference does not state '${field}' for Cloud (Cloud configures it, and the reference renders it only outside env-cloud), so this Cloud page renders <unknown ${field}>. Put the sentence in an ifndef::env-cloud[] block, or use a field the Cloud reference shows, such as cloud-access.` }
1042
+ }
1043
+ const value = propertyValues.fieldValue(entry, field, { format })
1044
+ if (!value.ok && value.reason === 'imprecise') {
1045
+ return { ok: false, kind: 'imprecise', field, registry, message: `${call}: the ${field} of '${name}' in ${describeDataset(registry)} is an integer larger than JavaScript represents exactly, so the build cannot render it without changing digits and renders <unknown ${field}>. State the value in prose by hand.` }
1046
+ }
1047
+ if (!value.ok) {
1048
+ return { ok: false, kind: 'missing-field', field, registry, message: `${call}: '${name}' has no ${field} in ${describeDataset(registry)}, so it renders <unknown ${field}>.` }
1049
+ }
1050
+ return { ok: true, field, registry, text: value.text, html: value.html }
1051
+ }
1052
+
1053
+ function propvalInlineMacro (config) {
1054
+ return function () {
1055
+ const self = this
1056
+ self.named('propval')
1057
+ self.positionalAttributes(['field'])
1058
+ self.process(function (parent, target, attributes) {
1059
+ const document = parent.getDocument()
1060
+ const name = target.trim()
1061
+ const mode = document.getAttribute('property-validate', 'warn')
1062
+ const filePath = config && config.file && config.file.src && config.file.src.path
1063
+ const component = (config && config.file && config.file.src && config.file.src.component) || ''
1064
+ const version = (config && config.file && config.file.src && config.file.src.version) || ''
1065
+ const requested = String(attributes.field || 'default').trim()
1066
+ let format = String(attributes.format || propertyValues.FORMAT_HUMAN).trim().toLowerCase()
1067
+ const where = filePath ? ` in ${filePath}` : ''
1068
+ if (format !== propertyValues.FORMAT_HUMAN && format !== propertyValues.FORMAT_RAW) {
1069
+ logger.warn(`propval:${name}[${requested}]${where}: format=${format} is not supported; use format=raw for the exact value. Rendering the human-readable value.`)
1070
+ format = propertyValues.FORMAT_HUMAN
1071
+ }
1072
+
1073
+ const contentCatalog = config && config.contentCatalog
1074
+ const registry = contentCatalog ? loadProperties(config) : undefined
1075
+ if (!registry && contentCatalog && mode !== 'off') {
1076
+ // The same once-per-component-version diagnostic prop: emits.
1077
+ if (!warnSeriesMissing(contentCatalog, component, version)) warnNoPropertyData(contentCatalog, component, version)
1078
+ }
1079
+ const result = resolvePropertyValue({ registry, name, field: requested, format, component, filePath })
1080
+ if (result.ok) {
1081
+ return self.createInline(parent, 'quoted', buildPropvalContent({ name, field: result.field, text: result.text, html: result.html }))
1082
+ }
1083
+ // Authoring errors fail the build in error mode, like an unknown prop:
1084
+ // name. So do cloud-hidden and unavailable: they appear only in the
1085
+ // Cloud build of single-sourced prose, where a warning alone would let
1086
+ // the placeholder ship. Everything else is reported and rendered as the
1087
+ // placeholder. A placeholder is reported even in off mode: off means
1088
+ // "do not check names", but a value cannot fall back to anything, and a
1089
+ // silent <unknown default> would reach readers unnoticed.
1090
+ const { message } = result
1091
+ if (mode === 'error' && ['unknown-property', 'missing-field', 'unsupported-field', 'cloud-hidden', 'unavailable'].includes(result.kind)) {
1092
+ throw new Error(message)
1093
+ }
1094
+ logger.warn(message)
1095
+ return self.createInline(parent, 'quoted', buildPropvalContent({ name, field: result.field, unknown: true }))
1096
+ })
1097
+ }
1098
+ }
1099
+
971
1100
  /**
972
1101
  * helm-path=auto displays the property as its Helm values path on pages
973
1102
  * rendered with env-kubernetes, so single-sourced content reads correctly for
@@ -986,9 +1115,11 @@ function register (registry, config = {}) {
986
1115
  if (typeof registry.register === 'function') {
987
1116
  registry.register(function () {
988
1117
  this.inlineMacro(propInlineMacro(config))
1118
+ this.inlineMacro(propvalInlineMacro(config))
989
1119
  })
990
1120
  } else if (typeof registry.inlineMacro === 'function') {
991
1121
  registry.inlineMacro(propInlineMacro(config))
1122
+ registry.inlineMacro(propvalInlineMacro(config))
992
1123
  } else {
993
1124
  logger.warn("no 'inlineMacro' method on alleged registry")
994
1125
  }
@@ -1008,3 +1139,7 @@ module.exports.releaseSeries = releaseSeries
1008
1139
  module.exports.extractHeadingsWithTags = extractHeadingsWithTags
1009
1140
  module.exports.evaluateTagExpression = evaluateTagExpression
1010
1141
  module.exports.buildPageIndex = buildPageIndex
1142
+ module.exports.isAvailable = isAvailable
1143
+ module.exports.describeDataset = describeDataset
1144
+ module.exports.buildPropvalContent = buildPropvalContent
1145
+ module.exports.resolvePropertyValue = resolvePropertyValue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.57.0",
3
+ "version": "5.58.0",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -1531,6 +1531,23 @@ const MARKUP_PATTERNS = [
1531
1531
  // link needs an explicit module to resolve in both places.
1532
1532
  const XREF_MODULES = new Set(['components', 'configuration', 'cookbooks', 'get-started', 'guides', 'install', 'reference'])
1533
1533
 
1534
+ // Component reference pages live in the components module, one directory
1535
+ // per registration kind (inputs/, outputs/, ...). Derived from the kinds
1536
+ // above so a new component kind needs no second list.
1537
+ const COMPONENT_PAGE_DIRS = new Set(Object.values(REGISTRATION_TYPES)
1538
+ .filter((t) => !t.startsWith('bloblang-'))
1539
+ .map((t) => `${t}s`))
1540
+
1541
+ /** The fix a module-less xref needs. Component pages and Bloblang pages have
1542
+ * a module the lint can know (the review prompt says the same); any other
1543
+ * page's module is in its docs URL. */
1544
+ function xrefModuleHint (target) {
1545
+ const dir = target.split('/')[0]
1546
+ if (COMPONENT_PAGE_DIRS.has(dir)) return `Name the module: xref:components:${target}[...].`
1547
+ if (dir === 'bloblang') return `Name the module: xref:guides:${target}[...].`
1548
+ return 'Name the module the page lives in. It is the path segment after /connect/ in the page\'s docs URL, so https://docs.redpanda.com/connect/configuration/interpolation/ is xref:configuration:interpolation.adoc[...].'
1549
+ }
1550
+
1534
1551
  const AVAILABILITY_PROSE = /\b(?:not (?:available|supported) (?:in|on) (?:Redpanda )?Cloud|only available (?:in|on|for) (?:self-managed|Redpanda Cloud|Cloud)|self-managed only|requires? (?:a )?cgo|CGO_ENABLED|x_benthos_extra)\b/i
1535
1552
 
1536
1553
  /** Surface-specific convention rules. */
@@ -1575,7 +1592,7 @@ const RULES = [
1575
1592
  if (parts.length === 2 && XREF_MODULES.has(parts[0])) continue
1576
1593
  issues.push({
1577
1594
  message: parts.length === 1
1578
- ? `"xref:${m[1]}[" has no module. It resolves on the self-managed page, but Cloud includes the same partial into its own module, where the link breaks. Name the module, for example xref:components:${m[1]}[...].`
1595
+ ? `"xref:${m[1]}[" has no module. It resolves on the self-managed page, but Cloud includes the same partial into its own module, where the link breaks. ${xrefModuleHint(m[1])}`
1579
1596
  : `"xref:${m[1]}[" names the ${parts[0]} module, which the Redpanda Connect docs do not have, so the link breaks on the rendered page. Use one of ${[...XREF_MODULES].join(', ')}, a component:module:page xref, or a full https:// link.`
1580
1597
  })
1581
1598
  }
@@ -150,6 +150,19 @@ function convertDocument (content, convertible, options = {}) {
150
150
  return { content: converted.join('\n'), count: total, skippedConfigRefs }
151
151
  }
152
152
 
153
+ // Paths no migration touches: generated content, whose hand edits revert on
154
+ // regeneration, and non-page assets. Shared with migrate-property-values.
155
+ const DEFAULT_EXCLUDE = [
156
+ 'modules/reference/partials/properties/',
157
+ 'modules/reference/pages/rpk/',
158
+ 'modules/reference/partials/rpk',
159
+ 'modules/reference/pages/k-crd.adoc',
160
+ '-helm-spec.adoc',
161
+ '/attachments/',
162
+ '/examples/',
163
+ '/test-resources/',
164
+ ]
165
+
153
166
  /**
154
167
  * Walk a modules tree and migrate every .adoc file.
155
168
  *
@@ -160,17 +173,7 @@ function convertDocument (content, convertible, options = {}) {
160
173
  * @param {string[]} [opts.exclude] - Path substrings to skip.
161
174
  * @returns {{files: number, conversions: number, changed: {file: string, count: number}[], ambiguous: string[]}}
162
175
  */
163
- function migrate ({ docsDir, propertiesJson, write, configRefs = false, exclude = [
164
- 'modules/reference/partials/properties/',
165
- // Autogenerated; hand edits revert on regeneration.
166
- 'modules/reference/pages/rpk/',
167
- 'modules/reference/partials/rpk',
168
- 'modules/reference/pages/k-crd.adoc',
169
- '-helm-spec.adoc',
170
- '/attachments/',
171
- '/examples/',
172
- '/test-resources/',
173
- ] }) {
176
+ function migrate ({ docsDir, propertiesJson, write, configRefs = false, exclude = DEFAULT_EXCLUDE }) {
174
177
  const { convertible, ambiguous } = classifyNames(propertiesJson)
175
178
  const knownNames = new Set(Object.keys(propertiesJson.properties || {}))
176
179
  const changed = []
@@ -203,4 +206,4 @@ function migrate ({ docsDir, propertiesJson, write, configRefs = false, exclude
203
206
  return { files, conversions, changed, ambiguous, skippedConfigRefs: [...skippedConfigRefs] }
204
207
  }
205
208
 
206
- module.exports = { classifyNames, convertLine, convertConfigRefLine, convertDocument, migrate }
209
+ module.exports = { classifyNames, convertLine, convertConfigRefLine, convertDocument, migrate, DEFAULT_EXCLUDE }