@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.
- package/docs-data/rpk-overrides.schema.json +744 -0
- package/extensions/set-available-attachment-versions.js +9 -1
- package/macros/DOCUMENTING_UNRELEASED_FEATURES.adoc +188 -0
- package/macros/PROPERTY_AND_ENTERPRISE_REFERENCES.adoc +20 -2
- package/macros/REFERENCE.adoc +92 -3
- package/macros/badge.js +9 -1
- package/macros/enterprise.js +180 -19
- package/macros/prop.js +636 -183
- package/package.json +5 -4
- package/tools/property-extractor/cloud_config.py +16 -3
- package/tools/redpanda-connect/report-delta.js +49 -3
- package/tools/rpk-docs/extract-conditional-content.js +3 -1
package/macros/prop.js
CHANGED
|
@@ -15,25 +15,50 @@
|
|
|
15
15
|
* `admin` or `rack` in unrelated contexts (Helm values, feature settings)
|
|
16
16
|
* are never decorated by mistake.
|
|
17
17
|
*
|
|
18
|
-
* The macro
|
|
19
|
-
*
|
|
20
|
-
* module, the same data the tooltips fetch at runtime). Unknown names are
|
|
21
|
-
* reported according to the property-validate attribute. When no property
|
|
22
|
-
* JSON is available in the build, the macro renders unvalidated with a
|
|
23
|
-
* single warning.
|
|
18
|
+
* The macro's job is to check that a property is actually available to the
|
|
19
|
+
* audience reading the page, and to link it only where a page documents it.
|
|
24
20
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
21
|
+
* WHICH PROPERTIES A COMPONENT MAY REFERENCE
|
|
22
|
+
*
|
|
23
|
+
* Property data exists for two doc sets today:
|
|
24
|
+
*
|
|
25
|
+
* Self-managed (streaming) publishes its own redpanda-properties-<tag>.json
|
|
26
|
+
* per release, as a reference-module attachment. Every property in that
|
|
27
|
+
* release's file is available, and a page validates against the file for
|
|
28
|
+
* its OWN release series: a 25.3 page against 25.3 data, never against a
|
|
29
|
+
* neighbouring release, which would accept properties that release never
|
|
30
|
+
* had and flag removed ones as typos.
|
|
31
|
+
*
|
|
32
|
+
* Cloud is managed streaming, so it has no property data of its own and
|
|
33
|
+
* always tracks the newest streaming file. A cluster property is available
|
|
34
|
+
* only when its cloud_supported field is not false; topic and broker
|
|
35
|
+
* properties are always available, because the Cloud control plane config
|
|
36
|
+
* that flag derives from describes cluster configuration only and has no
|
|
37
|
+
* opinion on the other scopes. A Cloud component is recognized by its
|
|
38
|
+
* env-cloud attribute.
|
|
39
|
+
*
|
|
40
|
+
* Any other component (connect, the agentic data plane, a preview site) has
|
|
41
|
+
* no property data, and none is inferred for it: the macro warns once and
|
|
42
|
+
* renders the property as plain inline code. Supporting a further doc set is
|
|
43
|
+
* a deliberate addition here, not something guessed from a version number --
|
|
44
|
+
* those components carry their own product versions, which say nothing about
|
|
45
|
+
* which Redpanda properties exist.
|
|
46
|
+
*
|
|
47
|
+
* Unavailable and unknown names are reported according to property-validate.
|
|
48
|
+
* Every warning names the dataset the page was checked against, so a typo is
|
|
49
|
+
* distinguishable from an availability or release mismatch.
|
|
50
|
+
*
|
|
51
|
+
* LINKING
|
|
52
|
+
*
|
|
53
|
+
* With link=true the property also links to its reference page, but only when
|
|
54
|
+
* a page in this component actually documents it. The page is discovered by
|
|
55
|
+
* indexing which reference-module page documents each property, scanning
|
|
56
|
+
* property headings in the partials each page includes and respecting include
|
|
57
|
+
* tag filters such as tags=redpanda-cloud. Links are therefore
|
|
58
|
+
* component-relative and stay correct if properties are split across
|
|
59
|
+
* different pages later. Nothing is ever linked across components, and no
|
|
60
|
+
* page target is guessed from a property's scope: a link the build cannot
|
|
61
|
+
* verify would be a broken link. Use page= to set the target explicitly.
|
|
37
62
|
*
|
|
38
63
|
* With helm-path=auto, pages rendered with the env-kubernetes attribute
|
|
39
64
|
* display the property as its Helm values path (storage.tiered.config.*
|
|
@@ -45,9 +70,9 @@
|
|
|
45
70
|
*
|
|
46
71
|
* Document or site attributes:
|
|
47
72
|
*
|
|
48
|
-
* property-validate 'warn' (default) to log
|
|
49
|
-
*
|
|
50
|
-
* validation.
|
|
73
|
+
* property-validate 'warn' (default) to log problems, 'error' to fail
|
|
74
|
+
* the build on an unknown name, 'off' to disable
|
|
75
|
+
* validation and silence these warnings.
|
|
51
76
|
* property-ref-role CSS class applied to the code element
|
|
52
77
|
* (default: property-ref).
|
|
53
78
|
*
|
|
@@ -59,27 +84,32 @@
|
|
|
59
84
|
*/
|
|
60
85
|
|
|
61
86
|
const chalk = require('chalk')
|
|
87
|
+
const semver = require('semver')
|
|
62
88
|
|
|
63
89
|
const $propertyRegistry = Symbol('$propertyRegistry')
|
|
90
|
+
// Release series a component version has no property data for, recorded at
|
|
91
|
+
// load time and reported by the macro on first actual use.
|
|
92
|
+
const $propertySeriesGap = Symbol('$propertySeriesGap')
|
|
64
93
|
|
|
65
94
|
const DEFAULT_ROLE = 'property-ref'
|
|
95
|
+
// Which slice of a dataset a component may reference.
|
|
96
|
+
const SURFACE_ALL = 'all'
|
|
97
|
+
const SURFACE_CLOUD = 'cloud'
|
|
66
98
|
const PROPERTIES_JSON_RX = /^redpanda-properties-(v\d+\.\d+\.\d+(?:-[\w.]+)?)\.json$/
|
|
67
99
|
|
|
68
|
-
// Deterministic fallback only. The primary mechanism is dynamic discovery:
|
|
69
|
-
// the macro indexes which reference-module page actually documents each
|
|
70
|
-
// property (by scanning property headings in the partials each page
|
|
71
|
-
// includes, respecting include tag filters), so links keep working if
|
|
72
|
-
// properties are ever split across different pages.
|
|
73
|
-
const SCOPE_PAGES = {
|
|
74
|
-
cluster: 'properties/cluster-properties',
|
|
75
|
-
broker: 'properties/broker-properties',
|
|
76
|
-
topic: 'properties/topic-properties',
|
|
77
|
-
}
|
|
78
|
-
|
|
79
100
|
const $propertyPageIndex = Symbol('$propertyPageIndex')
|
|
80
101
|
const HEADING_RX = /^=+\s+(\S+)\s*$/
|
|
81
102
|
const TAG_MARKER_RX = /^\/\/\s*(tag|end)::([\w-]+)\[\]\s*$/
|
|
82
103
|
const INCLUDE_RX = /^include::([^[]+)\[([^\]]*)\]/
|
|
104
|
+
// A reference page declaring the property collection it publishes.
|
|
105
|
+
const PROPERTY_SOURCE_RX = /^:page-property-source:[ \t]*(\S.*)$/m
|
|
106
|
+
// Claim strength: an explicit declaration beats a generated partial, which
|
|
107
|
+
// beats a bare heading that happens to match a property name.
|
|
108
|
+
const CLAIM_HEADING = 1
|
|
109
|
+
const CLAIM_PARTIAL = 2
|
|
110
|
+
const CLAIM_DECLARED = 3
|
|
111
|
+
// Delimited block fences: listing, literal, example, sidebar, quote, comment.
|
|
112
|
+
const DELIMITER_RX = /^(-{4,}|\.{4,}|={4,}|\*{4,}|_{4,}|\/{4,})$/
|
|
83
113
|
|
|
84
114
|
/**
|
|
85
115
|
* Extract property-style headings from AsciiDoc source together with the
|
|
@@ -118,7 +148,19 @@ function extractHeadingsWithTags (source) {
|
|
|
118
148
|
*/
|
|
119
149
|
function evaluateTagExpression (headingTags, expression) {
|
|
120
150
|
if (!expression) return true
|
|
121
|
-
|
|
151
|
+
// Asciidoctor accepts a quoted value, which is its documented way to write a
|
|
152
|
+
// comma-separated list. Keeping the quotes made the first item look like a
|
|
153
|
+
// positive tag named '"!deprecated', which matches nothing, so the page
|
|
154
|
+
// indexed zero properties and every link on it silently disappeared.
|
|
155
|
+
const unquoted = expression.trim().replace(/^(['"])([\s\S]*)\1$/, '$2')
|
|
156
|
+
const items = unquoted.split(/[;,]/).map((item) => item.trim()).filter(Boolean)
|
|
157
|
+
// '*' selects all tagged regions, '**' all content. Both mean "do not filter
|
|
158
|
+
// on positive tags"; negations still apply.
|
|
159
|
+
const wildcards = items.filter((item) => item === '*' || item === '**')
|
|
160
|
+
if (wildcards.length) {
|
|
161
|
+
const negated = items.filter((item) => item.startsWith('!')).map((item) => item.slice(1))
|
|
162
|
+
return !negated.some((tag) => headingTags.includes(tag))
|
|
163
|
+
}
|
|
122
164
|
const positives = items.filter((item) => !item.startsWith('!'))
|
|
123
165
|
const negatives = items.filter((item) => item.startsWith('!')).map((item) => item.slice(1))
|
|
124
166
|
if (negatives.some((tag) => headingTags.includes(tag))) return false
|
|
@@ -158,8 +200,12 @@ function buildPageIndex (contentCatalog, component, properties, version) {
|
|
|
158
200
|
if (cache[cacheKey]) return cache[cacheKey]
|
|
159
201
|
const index = new Map()
|
|
160
202
|
const partialHeadings = new Map()
|
|
203
|
+
// Antora replaces contents with converted HTML as it goes, but backs the
|
|
204
|
+
// AsciiDoc up at src.contents first, so prefer that: it makes an index built
|
|
205
|
+
// mid-conversion correct instead of merely reporting that it might be wrong.
|
|
206
|
+
const sourceOf = (file) => ((file.src && file.src.contents) || file.contents || '').toString()
|
|
161
207
|
const headingsFor = (file) => {
|
|
162
|
-
if (!partialHeadings.has(file)) partialHeadings.set(file, extractHeadingsWithTags(file
|
|
208
|
+
if (!partialHeadings.has(file)) partialHeadings.set(file, extractHeadingsWithTags(sourceOf(file)))
|
|
163
209
|
return partialHeadings.get(file)
|
|
164
210
|
}
|
|
165
211
|
// Restrict the index to the page's own component version, so a page on an
|
|
@@ -171,17 +217,97 @@ function buildPageIndex (contentCatalog, component, properties, version) {
|
|
|
171
217
|
// an index built lazily mid-conversion can miss includes on pages that
|
|
172
218
|
// already converted. The property-page-index extension warms this cache
|
|
173
219
|
// before conversion starts; warn when that safety net is absent.
|
|
174
|
-
|
|
220
|
+
// Converted HTML starts with a tag. AsciiDoc that starts with '<<<' (a page
|
|
221
|
+
// break) or '<<anchor>>' also starts with '<', and tripping on those told the
|
|
222
|
+
// maintainer to register an extension that was often already running.
|
|
223
|
+
const looksConverted = (candidate) => {
|
|
224
|
+
if (candidate.src && candidate.src.contents) return false
|
|
225
|
+
const head = candidate.contents.toString('utf8', 0, 200).trimStart()
|
|
226
|
+
return head.startsWith('<') && !head.startsWith('<<') && /^<[a-zA-Z!/]/.test(head)
|
|
227
|
+
}
|
|
228
|
+
if (pages.some(looksConverted)) {
|
|
175
229
|
console.warn(chalk.yellow(`prop macro: building the property page index for ${component}@${version || 'any'} after conversion started; some pages are already HTML and their properties may not be indexed. Register '@redpanda-data/docs-extensions-and-macros/extensions/property-page-index' under antora.extensions to build the index up front.`))
|
|
176
230
|
}
|
|
177
|
-
|
|
178
|
-
|
|
231
|
+
// A page can declare that it is the reference for a named collection of
|
|
232
|
+
// properties:
|
|
233
|
+
//
|
|
234
|
+
// = Cluster Configuration Properties
|
|
235
|
+
// :page-property-source: cluster-properties
|
|
236
|
+
//
|
|
237
|
+
// Declared pages are authoritative and are visited first, so an ordinary page
|
|
238
|
+
// that merely happens to carry a heading matching a property name -- 'admin',
|
|
239
|
+
// 'rack' -- can never claim it from the reference page that documents it.
|
|
240
|
+
// Sorting also removes the last of the catalog-order dependence: two pages
|
|
241
|
+
// that both surface a property used to resolve by aggregation order, which is
|
|
242
|
+
// stable for one checkout but not meaningful.
|
|
243
|
+
const declarationOf = (page) => {
|
|
244
|
+
const match = sourceOf(page).match(PROPERTY_SOURCE_RX)
|
|
245
|
+
return match ? match[1].trim() : undefined
|
|
246
|
+
}
|
|
247
|
+
const ordered = pages
|
|
248
|
+
.map((page) => ({ page, declared: declarationOf(page) }))
|
|
249
|
+
.sort((a, b) =>
|
|
250
|
+
Number(Boolean(b.declared)) - Number(Boolean(a.declared)) ||
|
|
251
|
+
String(a.page.src.relative).localeCompare(String(b.page.src.relative)))
|
|
252
|
+
// Which page claimed each property, so a conflict between two *declared*
|
|
253
|
+
// sources can be reported rather than silently resolved.
|
|
254
|
+
const claims = new Map()
|
|
255
|
+
for (const { page, declared } of ordered) {
|
|
256
|
+
const source = sourceOf(page)
|
|
179
257
|
const pagePath = page.src.relative.replace(/\.adoc$/, '')
|
|
180
258
|
const pageUrl = (page.pub && page.pub.url) || undefined
|
|
259
|
+
// First claim wins. Declared pages come first, so a declaration always
|
|
260
|
+
// beats inference; two declared pages claiming one property is an authoring
|
|
261
|
+
// conflict worth reporting rather than resolving quietly.
|
|
262
|
+
// Claims are ranked, not first-come. A declared page outranks everything; a
|
|
263
|
+
// property heading pulled in from a generated partial outranks a bare
|
|
264
|
+
// heading that merely happens to match a property name, which is how a page
|
|
265
|
+
// titled '= admin' used to claim the admin broker property from the
|
|
266
|
+
// reference page that documents it.
|
|
267
|
+
const claim = (name, entry, strength) => {
|
|
268
|
+
// Declaring a page raises its claims above every undeclared page, but
|
|
269
|
+
// keeps the heading-versus-partial ordering within the page: a reference
|
|
270
|
+
// page can both hand-write a section for a property and include the
|
|
271
|
+
// generated partial that documents it.
|
|
272
|
+
const rank = (declared ? CLAIM_DECLARED : 0) + strength
|
|
273
|
+
const existing = claims.get(name)
|
|
274
|
+
if (existing) {
|
|
275
|
+
if (existing.rank > rank) return
|
|
276
|
+
// A page claiming the same property twice is not a conflict between
|
|
277
|
+
// sources, it is one page documenting it in two places.
|
|
278
|
+
if (existing.page === pagePath) return
|
|
279
|
+
if (existing.rank === rank) {
|
|
280
|
+
// Same standing. Keep the first, and say so when both pages
|
|
281
|
+
// explicitly declared themselves the source: that is an authoring
|
|
282
|
+
// conflict, not something to resolve quietly.
|
|
283
|
+
if (declared && existing.declared) {
|
|
284
|
+
warnOnce(contentCatalog, `claim:${component}:${name}`,
|
|
285
|
+
`prop macro: '${name}' is claimed by two declared property sources in ${component}: ${existing.page} (:page-property-source: ${existing.declared}) and ${pagePath} (:page-property-source: ${declared}). Links use ${existing.page}. Remove the declaration from whichever page is not the reference for it.`)
|
|
286
|
+
}
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
claims.set(name, { page: pagePath, declared, rank })
|
|
291
|
+
index.set(name, entry)
|
|
292
|
+
}
|
|
293
|
+
let inDelimitedBlock = false
|
|
181
294
|
for (const line of source.split('\n')) {
|
|
295
|
+
// A property name inside a listing, literal, or comment block is sample
|
|
296
|
+
// text, not documentation of that property. Indexing it handed the
|
|
297
|
+
// property to a page that renders no anchor for it.
|
|
298
|
+
if (DELIMITER_RX.test(line.trim())) {
|
|
299
|
+
inDelimitedBlock = !inDelimitedBlock
|
|
300
|
+
continue
|
|
301
|
+
}
|
|
302
|
+
if (inDelimitedBlock) continue
|
|
182
303
|
const heading = line.match(HEADING_RX)
|
|
183
304
|
if (heading && Object.prototype.hasOwnProperty.call(properties, heading[1])) {
|
|
184
|
-
|
|
305
|
+
// First writer wins, as for partial-derived entries. Setting
|
|
306
|
+
// unconditionally let any reference page whose title happens to be a
|
|
307
|
+
// property name -- 'admin', 'rack' -- steal it from the reference page
|
|
308
|
+
// that documents it, and a doctitle produces no anchor at all, so both
|
|
309
|
+
// the link and the tooltip URL pointed at nothing.
|
|
310
|
+
claim(heading[1], { page: pagePath, url: pageUrl }, CLAIM_HEADING)
|
|
185
311
|
continue
|
|
186
312
|
}
|
|
187
313
|
const include = line.match(INCLUDE_RX)
|
|
@@ -194,12 +320,26 @@ function buildPageIndex (contentCatalog, component, properties, version) {
|
|
|
194
320
|
const matching = partials.filter((candidate) => candidate.src.relative === partialRef.relative)
|
|
195
321
|
// Same-component includes resolve within the page's version; for
|
|
196
322
|
// cross-component includes take whichever version the catalog has.
|
|
197
|
-
|
|
323
|
+
// Resolve the partial the way Antora does. A same-component include is
|
|
324
|
+
// pinned to the including page's version, so falling back to another
|
|
325
|
+
// version's copy indexed foreign-release content against this page. A
|
|
326
|
+
// version-less cross-component include resolves to that component's
|
|
327
|
+
// latest version, not to whichever copy the catalog yielded first.
|
|
328
|
+
let partial
|
|
329
|
+
if (partialRef.component === page.src.component) {
|
|
330
|
+
partial = matching.find((candidate) => candidate.src.version === page.src.version)
|
|
331
|
+
} else {
|
|
332
|
+
const lender = typeof contentCatalog.getComponent === 'function' &&
|
|
333
|
+
contentCatalog.getComponent(partialRef.component)
|
|
334
|
+
const latest = lender && lender.latest && lender.latest.version
|
|
335
|
+
partial = matching.find((candidate) => candidate.src.version === latest) ||
|
|
336
|
+
[...matching].sort((a, b) => String(b.src.version).localeCompare(String(a.src.version)))[0]
|
|
337
|
+
}
|
|
198
338
|
if (!partial) continue
|
|
199
339
|
for (const entry of headingsFor(partial)) {
|
|
200
340
|
if (!Object.prototype.hasOwnProperty.call(properties, entry.name)) continue
|
|
201
341
|
if (!evaluateTagExpression(entry.tags, expression)) continue
|
|
202
|
-
|
|
342
|
+
claim(entry.name, { page: pagePath, url: pageUrl }, CLAIM_PARTIAL)
|
|
203
343
|
}
|
|
204
344
|
}
|
|
205
345
|
}
|
|
@@ -229,26 +369,146 @@ function helmValuesPath (name, scope) {
|
|
|
229
369
|
return `config.cluster.${name}`
|
|
230
370
|
}
|
|
231
371
|
|
|
232
|
-
|
|
372
|
+
// Warnings are deduplicated per build. Antora's watch mode (gulp/npm start)
|
|
373
|
+
// reuses one process across many builds, so a module-level guard would report
|
|
374
|
+
// each problem only on a session's first build and stay silent afterwards --
|
|
375
|
+
// exactly when a writer is iterating and most wants to see it. The content
|
|
376
|
+
// catalog is rebuilt per build, so keying off it scopes the guard correctly.
|
|
377
|
+
const $warned = Symbol('$warned')
|
|
378
|
+
|
|
379
|
+
function warnOnce (contentCatalog, key, message) {
|
|
380
|
+
const seen = contentCatalog[$warned] || (contentCatalog[$warned] = new Set())
|
|
381
|
+
if (seen.has(key)) return false
|
|
382
|
+
seen.add(key)
|
|
383
|
+
console.warn(chalk.yellow(message))
|
|
384
|
+
return true
|
|
385
|
+
}
|
|
233
386
|
|
|
234
387
|
/**
|
|
235
|
-
* Compare two
|
|
236
|
-
*
|
|
388
|
+
* Compare two property tags by release precedence, newest last.
|
|
389
|
+
*
|
|
390
|
+
* Prereleases matter here: a release cycle publishes v26.3.1-rc1, then rc2,
|
|
391
|
+
* then the GA v26.3.1, and the beta branch has to land on the newest of them
|
|
392
|
+
* deterministically. A numeric-only comparison rates all three equal, leaving
|
|
393
|
+
* the winner to catalog iteration order -- so rc1 could beat rc2, or an RC
|
|
394
|
+
* could beat its own GA.
|
|
395
|
+
*
|
|
396
|
+
* Redpanda tags spell prereleases as one identifier (-rc2, not -rc.2), which
|
|
397
|
+
* the semver spec compares as a string: 'rc10' would sort below 'rc2'. Split
|
|
398
|
+
* the trailing digits into their own identifier first so they compare
|
|
399
|
+
* numerically.
|
|
237
400
|
*/
|
|
238
401
|
function compareTags (a, b) {
|
|
239
|
-
const
|
|
402
|
+
const normalize = (tag) => String(tag).replace(/^v/, '').replace(/-([a-z]+)(\d+)/gi, '-$1.$2')
|
|
403
|
+
const left = semver.valid(normalize(a))
|
|
404
|
+
const right = semver.valid(normalize(b))
|
|
405
|
+
if (left && right) return semver.compare(left, right)
|
|
406
|
+
// Not semver (a hand-named file, say): fall back to numeric release order.
|
|
407
|
+
const parse = (tag) => String(tag).replace(/^v/, '').split('-')[0].split('.').map(Number)
|
|
240
408
|
const [a1, a2, a3] = parse(a)
|
|
241
409
|
const [b1, b2, b3] = parse(b)
|
|
242
|
-
return a1 - b1 || a2 - b2 || a3 - b3
|
|
410
|
+
return (a1 - b1) || (a2 - b2) || (a3 - b3) || 0
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Whether a tag names a prerelease (an RC or beta build).
|
|
415
|
+
*/
|
|
416
|
+
function isPrerelease (tag) {
|
|
417
|
+
const normalized = String(tag).replace(/^v/, '')
|
|
418
|
+
const valid = semver.valid(normalized)
|
|
419
|
+
return valid ? Boolean(semver.prerelease(valid)) : /-/.test(normalized)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The release series ('26.2') of a component version ('26.2', '25.3.1') or a
|
|
424
|
+
* properties tag ('v26.2.1', 'v26.3.1-rc1'). Undefined when the value names no
|
|
425
|
+
* numeric series.
|
|
426
|
+
*/
|
|
427
|
+
function releaseSeries (value) {
|
|
428
|
+
const match = String(value == null ? '' : value).replace(/^v/, '').match(/^(\d+)\.(\d+)/)
|
|
429
|
+
return match ? `${match[1]}.${match[2]}` : undefined
|
|
243
430
|
}
|
|
244
431
|
|
|
245
432
|
/**
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
433
|
+
* Whether an Antora attribute counts as declared.
|
|
434
|
+
*
|
|
435
|
+
* Antora keeps a component version's antora.yml attributes verbatim, and
|
|
436
|
+
* setting one to false is how a maintainer turns it off: the key stays present.
|
|
437
|
+
* A bare `!== undefined` test therefore reads `env-cloud: false` as "this is
|
|
438
|
+
* Cloud", the opposite of what was written. Sibling readers of this attribute
|
|
439
|
+
* in this repo use truthiness, so match them, while still honouring AsciiDoc's
|
|
440
|
+
* set-but-empty convention.
|
|
441
|
+
*/
|
|
442
|
+
function isAttributeSet (value) {
|
|
443
|
+
if (value === undefined || value === null || value === false) return false
|
|
444
|
+
return String(value).toLowerCase() !== 'false'
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The asciidoc attributes declared for one component version, which is where
|
|
449
|
+
* env-cloud lives. Read from the catalog rather than the converting document
|
|
450
|
+
* so the property-page-index extension resolves surfaces identically.
|
|
451
|
+
*/
|
|
452
|
+
function componentAttributes (contentCatalog, component, version) {
|
|
453
|
+
if (typeof contentCatalog.getComponent !== 'function') return {}
|
|
454
|
+
const found = contentCatalog.getComponent(component)
|
|
455
|
+
if (!found) return {}
|
|
456
|
+
const versions = found.versions || []
|
|
457
|
+
const match = versions.find((entry) => entry.version === version) || found.latest || versions[0]
|
|
458
|
+
return (match && match.asciidoc && match.asciidoc.attributes) || {}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Every properties JSON in the build, as {tag, file, component, version}.
|
|
463
|
+
*/
|
|
464
|
+
function propertyDatasets (contentCatalog) {
|
|
465
|
+
const found = []
|
|
466
|
+
for (const attachment of contentCatalog.findBy({ family: 'attachment' }) || []) {
|
|
467
|
+
if (attachment.src.module !== 'reference') continue
|
|
468
|
+
const match = attachment.src.relative.split('/').pop().match(PROPERTIES_JSON_RX)
|
|
469
|
+
if (!match) continue
|
|
470
|
+
found.push({ tag: match[1], file: attachment, component: attachment.src.component, version: attachment.src.version })
|
|
471
|
+
}
|
|
472
|
+
return found
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* The newest dataset in a list, with ties broken on a stable identity rather
|
|
477
|
+
* than on catalog iteration order: two attachments can carry the same tag, and
|
|
478
|
+
* picking whichever the catalog happened to yield first made the chosen
|
|
479
|
+
* property data differ between builds of identical content.
|
|
480
|
+
*/
|
|
481
|
+
const newestDataset = (list) =>
|
|
482
|
+
list.reduce((best, entry) => {
|
|
483
|
+
if (!best) return entry
|
|
484
|
+
const byTag = compareTags(entry.tag, best.tag)
|
|
485
|
+
if (byTag > 0) return entry
|
|
486
|
+
if (byTag < 0) return best
|
|
487
|
+
return stableKey(entry) < stableKey(best) ? entry : best
|
|
488
|
+
}, null)
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Load and cache the property data for one page's component and version, plus
|
|
492
|
+
* which slice of it that component may reference. Returns undefined when the
|
|
493
|
+
* component has no property data, which is the signal to render plain and warn.
|
|
494
|
+
*
|
|
495
|
+
* Two doc sets have property data:
|
|
496
|
+
*
|
|
497
|
+
* A component that publishes its own redpanda-properties-<tag>.json (the
|
|
498
|
+
* self-managed docs) uses the file for its own release series, newest patch
|
|
499
|
+
* within it, and may reference every property in it. Matching by series
|
|
500
|
+
* rather than by newest tag matters because the main branch carries the
|
|
501
|
+
* previous series' file alongside its own, and a prerelease could land there
|
|
502
|
+
* too. A versioned page whose series has no file goes unvalidated with an
|
|
503
|
+
* explicit warning rather than borrowing a neighbouring release's data.
|
|
504
|
+
*
|
|
505
|
+
* A Cloud component (env-cloud) is managed streaming: no data of its own, so
|
|
506
|
+
* it tracks the newest streaming (or ROOT) file, and may reference only the
|
|
507
|
+
* properties marked cloud_supported.
|
|
508
|
+
*
|
|
509
|
+
* Anything else gets nothing. Component versions elsewhere in the site carry
|
|
510
|
+
* their own product versions, which say nothing about Redpanda releases, so no
|
|
511
|
+
* dataset is inferred for them.
|
|
252
512
|
*/
|
|
253
513
|
function loadProperties (config) {
|
|
254
514
|
const contentCatalog = config && config.contentCatalog
|
|
@@ -267,44 +527,179 @@ function loadPropertiesFor (contentCatalog, pageComponent, pageVersion) {
|
|
|
267
527
|
const cache = contentCatalog[$propertyRegistry] || (contentCatalog[$propertyRegistry] = {})
|
|
268
528
|
if (cache[cacheKey] !== undefined) return cache[cacheKey] || undefined
|
|
269
529
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
530
|
+
const datasets = propertyDatasets(contentCatalog)
|
|
531
|
+
const own = datasets.filter((d) => d.component === pageComponent)
|
|
532
|
+
let pick
|
|
533
|
+
let surface
|
|
534
|
+
|
|
535
|
+
// Cloud is decided first, and deliberately outranks the component's own
|
|
536
|
+
// attachments. The property extractor writes
|
|
537
|
+
// modules/reference/attachments/redpanda-properties-<tag>.json into whatever
|
|
538
|
+
// repo it runs in, so running it inside cloud-docs would otherwise flip that
|
|
539
|
+
// component to the unfiltered surface and switch off cloud_supported gating
|
|
540
|
+
// site-wide, with no warning anywhere.
|
|
541
|
+
if (isAttributeSet(componentAttributes(contentCatalog, pageComponent, pageVersion)['env-cloud'])) {
|
|
542
|
+
surface = SURFACE_CLOUD
|
|
543
|
+
// GA only: Cloud runs released Redpanda, so an RC published on the beta
|
|
544
|
+
// branch must not become the dataset Cloud pages validate against.
|
|
545
|
+
// Only streaming/ROOT count as lenders -- another component's product
|
|
546
|
+
// version is not a Redpanda release candidate.
|
|
547
|
+
const lenders = datasets.filter((d) => d.component === 'streaming' || d.component === 'ROOT')
|
|
548
|
+
const released = lenders.filter((d) => !isPrerelease(d.tag))
|
|
549
|
+
pick =
|
|
550
|
+
newestDataset(released.filter((d) => d.component === 'streaming')) ||
|
|
551
|
+
newestDataset(released.filter((d) => d.component === 'ROOT'))
|
|
552
|
+
if (!pick && lenders.length) {
|
|
553
|
+
// Self-managed data exists but all of it is a prerelease. Say that
|
|
554
|
+
// specifically rather than claiming no property data was found.
|
|
555
|
+
const gaps = contentCatalog[$propertySeriesGap] || (contentCatalog[$propertySeriesGap] = {})
|
|
556
|
+
gaps[cacheKey] = {
|
|
557
|
+
kind: 'cloud-prerelease-only',
|
|
558
|
+
component: pageComponent,
|
|
559
|
+
version: pageVersion,
|
|
560
|
+
have: [...new Set(lenders.map((d) => d.tag))].sort(compareTags),
|
|
561
|
+
}
|
|
562
|
+
cache[cacheKey] = null
|
|
563
|
+
return undefined
|
|
564
|
+
}
|
|
565
|
+
} else if (own.length) {
|
|
566
|
+
surface = SURFACE_ALL
|
|
567
|
+
const series = releaseSeries(pageVersion)
|
|
568
|
+
if (series) {
|
|
569
|
+
const inSeries = own.filter((d) => releaseSeries(d.tag) === series)
|
|
570
|
+
pick = newestDataset(inSeries.filter((d) => d.version === pageVersion)) || newestDataset(inSeries)
|
|
571
|
+
if (!pick) {
|
|
572
|
+
// Record the gap rather than reporting it here: this also runs from the
|
|
573
|
+
// property-page-index extension, once per component version in the
|
|
574
|
+
// build, so warning now would report versions no page of which uses the
|
|
575
|
+
// macro. The macro reports it on first actual use.
|
|
576
|
+
const gaps = contentCatalog[$propertySeriesGap] || (contentCatalog[$propertySeriesGap] = {})
|
|
577
|
+
gaps[cacheKey] = {
|
|
578
|
+
kind: 'series',
|
|
579
|
+
component: pageComponent,
|
|
580
|
+
version: pageVersion,
|
|
581
|
+
series,
|
|
582
|
+
have: [...new Set(own.map((d) => d.tag))].sort(compareTags),
|
|
583
|
+
}
|
|
584
|
+
cache[cacheKey] = null
|
|
585
|
+
return undefined
|
|
586
|
+
}
|
|
587
|
+
} else {
|
|
588
|
+
pick = newestDataset(own.filter((d) => d.version === pageVersion)) || newestDataset(own)
|
|
589
|
+
}
|
|
283
590
|
}
|
|
284
|
-
const newest = (list) => list.reduce((best, entry) => (!best || compareTags(entry.tag, best.tag) > 0 ? entry : best), null)
|
|
285
|
-
const pick =
|
|
286
|
-
newest(candidates.filter((c) => c.component === pageComponent && c.version === pageVersion)) ||
|
|
287
|
-
newest(candidates.filter((c) => c.component === pageComponent)) ||
|
|
288
|
-
newest(candidates.filter((c) => c.component === 'streaming')) ||
|
|
289
|
-
newest(candidates.filter((c) => c.component === 'ROOT')) ||
|
|
290
|
-
newest(candidates)
|
|
291
591
|
|
|
592
|
+
// A corrupt or half-written attachment must not take the whole build down,
|
|
593
|
+
// and must not mask a good file: try candidates newest-first and use the
|
|
594
|
+
// first one that actually carries properties, reporting each one skipped.
|
|
292
595
|
let registry = null
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
596
|
+
for (const candidate of orderedCandidates(pick, own, datasets, surface)) {
|
|
597
|
+
const properties = readProperties(candidate, contentCatalog)
|
|
598
|
+
if (!properties) continue
|
|
599
|
+
registry = { tag: candidate.tag, properties, component: candidate.component, version: candidate.version, surface }
|
|
600
|
+
break
|
|
298
601
|
}
|
|
299
|
-
// Cache null too, so a missing
|
|
602
|
+
// Cache null too, so a missing dataset is only searched for once per build.
|
|
300
603
|
cache[cacheKey] = registry
|
|
301
604
|
return registry || undefined
|
|
302
605
|
}
|
|
303
606
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
607
|
+
/**
|
|
608
|
+
* The chosen dataset first, then the remaining candidates from the same pool in
|
|
609
|
+
* descending release order, so an unusable file falls through to the next best
|
|
610
|
+
* rather than voiding the component's data entirely.
|
|
611
|
+
*/
|
|
612
|
+
function orderedCandidates (pick, own, datasets, surface) {
|
|
613
|
+
if (!pick) return []
|
|
614
|
+
const pool = surface === SURFACE_CLOUD
|
|
615
|
+
? datasets.filter((d) => (d.component === 'streaming' || d.component === 'ROOT') && !isPrerelease(d.tag))
|
|
616
|
+
: own
|
|
617
|
+
const rest = pool
|
|
618
|
+
.filter((d) => d !== pick)
|
|
619
|
+
.sort((a, b) => compareTags(b.tag, a.tag) || stableKey(a).localeCompare(stableKey(b)))
|
|
620
|
+
return [pick, ...rest]
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Parse one properties attachment, or report why it is unusable and return
|
|
625
|
+
* undefined. A JSON syntax error used to escape as an unhandled exception that
|
|
626
|
+
* named no file, so a single bad attachment failed the build with no way to tell
|
|
627
|
+
* which of them was at fault.
|
|
628
|
+
*/
|
|
629
|
+
function readProperties (candidate, contentCatalog) {
|
|
630
|
+
const where = `${candidate.component}@${candidate.version || 'unversioned'}`
|
|
631
|
+
let data
|
|
632
|
+
try {
|
|
633
|
+
data = JSON.parse(candidate.file.contents.toString())
|
|
634
|
+
} catch (error) {
|
|
635
|
+
warnOnce(contentCatalog, `badjson:${where}:${candidate.tag}`,
|
|
636
|
+
`prop macro: redpanda-properties-${candidate.tag}.json in ${where} is not valid JSON (${error.message}), so it is ignored. Regenerate it with 'npx doc-tools generate property-docs'.`)
|
|
637
|
+
return undefined
|
|
638
|
+
}
|
|
639
|
+
const properties = data && data.properties
|
|
640
|
+
// Must be a plain object of name -> entry. A string, number, or array parses
|
|
641
|
+
// fine and would then make every prop: target look like a typo.
|
|
642
|
+
if (!properties || typeof properties !== 'object' || Array.isArray(properties)) {
|
|
643
|
+
warnOnce(contentCatalog, `noprops:${where}:${candidate.tag}`,
|
|
644
|
+
`prop macro: redpanda-properties-${candidate.tag}.json in ${where} has no usable 'properties' object, so it is ignored. Regenerate it with 'npx doc-tools generate property-docs'.`)
|
|
645
|
+
return undefined
|
|
646
|
+
}
|
|
647
|
+
return properties
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* A stable identity for an attachment, so two files carrying the same tag are
|
|
652
|
+
* resolved the same way on every build instead of by catalog iteration order.
|
|
653
|
+
*/
|
|
654
|
+
function stableKey (candidate) {
|
|
655
|
+
return `${candidate.component}\u0000${candidate.version}\u0000${candidate.file.src.relative}`
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Whether a property is available to the audience this dataset was loaded for.
|
|
660
|
+
*
|
|
661
|
+
* On Cloud the answer comes from cloud_supported, but only where that field
|
|
662
|
+
* means anything. It is derived from the Cloud control plane's install pack,
|
|
663
|
+
* whose two sections describe CLUSTER configuration and contain no topic or
|
|
664
|
+
* broker property at all -- so for those scopes a false value asserted
|
|
665
|
+
* something its source cannot know. Cloud users set topic properties routinely
|
|
666
|
+
* through the Kafka API and Console, a surface the control plane does not gate,
|
|
667
|
+
* and Cloud publishes a topic properties reference of its own.
|
|
668
|
+
*
|
|
669
|
+
* So: gate only when the flag is explicitly false AND the property is cluster
|
|
670
|
+
* scoped. Anything else counts as available, because nothing authoritative says
|
|
671
|
+
* otherwise. The extractor no longer writes the field for other scopes, and
|
|
672
|
+
* this also keeps already-published datasets correct, which assert false on
|
|
673
|
+
* every topic and broker property.
|
|
674
|
+
*/
|
|
675
|
+
function isAvailable (registry, entry) {
|
|
676
|
+
if (registry.surface !== SURFACE_CLOUD) return true
|
|
677
|
+
if (entry.config_scope !== 'cluster') return true
|
|
678
|
+
return entry.cloud_supported !== false
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function warnSeriesMissing (contentCatalog, pageComponent, pageVersion) {
|
|
682
|
+
const gaps = contentCatalog[$propertySeriesGap]
|
|
683
|
+
const gap = gaps && gaps[`${pageComponent}@${pageVersion}`]
|
|
684
|
+
if (!gap) return false
|
|
685
|
+
const have = gap.have.join(', ')
|
|
686
|
+
if (gap.kind === 'cloud-prerelease-only') {
|
|
687
|
+
warnOnce(contentCatalog, `cloudpre:${gap.component}@${gap.version}`,
|
|
688
|
+
`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. ` +
|
|
689
|
+
'prop: targets on Cloud pages are therefore not validated and their tooltips carry no data. Include a GA self-managed branch in the build, or leave prop: out of Cloud content until the release ships.')
|
|
690
|
+
return true
|
|
691
|
+
}
|
|
692
|
+
warnOnce(contentCatalog, `series:${gap.component}@${gap.version}`,
|
|
693
|
+
`prop macro: ${gap.component}@${gap.version} publishes no redpanda-properties-v${gap.series}.*.json attachment in its reference module (found for other series: ${have}). ` +
|
|
694
|
+
`prop: targets on ${gap.version} pages are therefore not validated and their tooltips carry no data, rather than being checked against another release's properties. ` +
|
|
695
|
+
`Generate it with 'npx doc-tools generate property-docs --tag v${gap.series}.<patch>' on that branch, or leave prop: out of ${gap.version} content.`)
|
|
696
|
+
return true
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function warnNoPropertyData (contentCatalog, component, version) {
|
|
700
|
+
warnOnce(contentCatalog, `nodata:${component}@${version}`,
|
|
701
|
+
`prop macro: no redpanda-properties-<tag>.json attachment found in any component's reference module for ${component || 'unknown component'}@${version || 'unversioned'}; ` +
|
|
702
|
+
'prop: targets are not validated and their tooltips carry no data. Publish the property JSON as a reference-module attachment in this component or in streaming.')
|
|
308
703
|
}
|
|
309
704
|
|
|
310
705
|
/**
|
|
@@ -326,11 +721,56 @@ function reportUnknownProperty ({ name, mode, registry, filePath }) {
|
|
|
326
721
|
.map((scored) => scored.candidate)
|
|
327
722
|
const hint = candidates.length ? ` Did you mean: ${candidates.join(', ')}?` : ''
|
|
328
723
|
const where = filePath ? ` in ${filePath}` : ''
|
|
329
|
-
const message = `prop:${name}[]
|
|
724
|
+
const message = `prop:${name}[]${where}: '${name}' is not in the property data this page validates against (${describeDataset(registry)}). ` +
|
|
725
|
+
`Either the name is misspelled or the property does not exist in that release.${hint}`
|
|
330
726
|
if (mode === 'error') throw new Error(message)
|
|
331
727
|
console.warn(chalk.yellow(message))
|
|
332
728
|
}
|
|
333
729
|
|
|
730
|
+
/**
|
|
731
|
+
* Which properties JSON a page was checked against. Named in every warning
|
|
732
|
+
* so a reader can tell a typo apart from a release or component mismatch --
|
|
733
|
+
* the same name can be valid in one series and absent from another.
|
|
734
|
+
*/
|
|
735
|
+
function describeDataset (registry) {
|
|
736
|
+
const from = registry.component
|
|
737
|
+
? `${registry.component}@${registry.version || 'unversioned'}`
|
|
738
|
+
: 'unknown component'
|
|
739
|
+
return `redpanda-properties-${registry.tag}.json from ${from}`
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Report a property that exists in Redpanda but is not available to this
|
|
744
|
+
* component's audience. On Cloud that means cloud_supported is false: a real
|
|
745
|
+
* property a Cloud reader cannot set, so it renders as plain code rather than
|
|
746
|
+
* as a tooltip implying otherwise.
|
|
747
|
+
*/
|
|
748
|
+
function reportUnavailableProperty ({ name, component, registry, mode, filePath }) {
|
|
749
|
+
if (mode === 'off') return
|
|
750
|
+
const where = filePath ? ` in ${filePath}` : ''
|
|
751
|
+
const which = component || 'this'
|
|
752
|
+
console.warn(chalk.yellow(
|
|
753
|
+
`prop:${name}[]${where}: '${name}' is not available in the ${which} component, so it renders as plain code -- no link, and no tooltip. ` +
|
|
754
|
+
`${describeDataset(registry)} marks it cloud_supported: false, meaning Redpanda has the property but this audience cannot set it. ` +
|
|
755
|
+
'If that is wrong, fix cloud_supported in the property extractor; if it is right, reword the sentence and drop the macro.'
|
|
756
|
+
))
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Report an available property that no page in this component documents. The
|
|
761
|
+
* tooltip still works, so this only matters when a link was explicitly asked
|
|
762
|
+
* for -- there is nothing to link to.
|
|
763
|
+
*/
|
|
764
|
+
function reportUnpublishedProperty ({ name, registry, mode, filePath }) {
|
|
765
|
+
if (mode === 'off') return
|
|
766
|
+
const where = filePath ? ` in ${filePath}` : ''
|
|
767
|
+
console.warn(chalk.yellow(
|
|
768
|
+
`prop:${name}[link=true]${where}: '${name}' is in ${describeDataset(registry)} but no reference page in this component renders a heading for it, so it keeps its tooltip and renders without a link. ` +
|
|
769
|
+
'Every property reference page filters out the deprecated and exclude-from-docs include tags, and some include only selected category tags -- check which tags enclose this property in the generated partial. ' +
|
|
770
|
+
'If it is meant to stay unpublished, drop link=true.'
|
|
771
|
+
))
|
|
772
|
+
}
|
|
773
|
+
|
|
334
774
|
/**
|
|
335
775
|
* Build the AsciiDoc content emitted for one macro instance. Exported for
|
|
336
776
|
* unit testing.
|
|
@@ -338,27 +778,38 @@ function reportUnknownProperty ({ name, mode, registry, filePath }) {
|
|
|
338
778
|
* @param {object} opts
|
|
339
779
|
* @param {string} opts.name - Property name.
|
|
340
780
|
* @param {string} [opts.text] - Display text override.
|
|
341
|
-
* @param {boolean} opts.link - Whether to link to the reference page.
|
|
342
|
-
*
|
|
343
|
-
* @param {string} [opts.
|
|
781
|
+
* @param {boolean} [opts.link] - Whether to link to the reference page. Ignored
|
|
782
|
+
* without a page: the macro never guesses a target.
|
|
783
|
+
* @param {string} [opts.page] - Reference page (module-relative, no .adoc).
|
|
784
|
+
* @param {string} [opts.scope] - config_scope, used only for the Helm path.
|
|
344
785
|
* @param {string} opts.role - CSS class for the code element.
|
|
345
|
-
* @param {
|
|
346
|
-
*
|
|
786
|
+
* @param {boolean} [opts.plain] - Render unmarked code (no tooltip, no link)
|
|
787
|
+
* because the build could not verify the property for this component.
|
|
347
788
|
* @returns {string}
|
|
348
789
|
*/
|
|
349
|
-
function buildPropContent ({ name, text, link, page, scope, role,
|
|
790
|
+
function buildPropContent ({ name, text, link, page, scope, role, helmPath = false, docUrl, plain = false }) {
|
|
350
791
|
const display = text || (helmPath ? helmValuesPath(name, scope) : name)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
792
|
+
if (plain) return `<code>${display}</code>`
|
|
793
|
+
const inner = link && page
|
|
794
|
+
? `xref:reference:${page}.adoc#${propertyAnchor(name)}[${display}]`
|
|
795
|
+
: display
|
|
796
|
+
// Escape every interpolated attribute value. role comes from a site
|
|
797
|
+
// attribute, docUrl from a page's published URL, and name from a dataset
|
|
798
|
+
// key: an unescaped quote in any of them ends the attribute early and the
|
|
799
|
+
// remainder becomes stray attributes on every marked property in the build.
|
|
800
|
+
const docAttr = docUrl ? ` data-doc-url="${escapeAttribute(docUrl)}"` : ''
|
|
801
|
+
return `<code class="${escapeAttribute(role)}" data-property-name="${escapeAttribute(name)}"${docAttr}>${inner}</code>`
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Escape a value for use inside a double-quoted HTML attribute.
|
|
806
|
+
*/
|
|
807
|
+
function escapeAttribute (value) {
|
|
808
|
+
return String(value === undefined || value === null ? '' : value)
|
|
809
|
+
.replace(/&/g, '&')
|
|
810
|
+
.replace(/"/g, '"')
|
|
811
|
+
.replace(/</g, '<')
|
|
812
|
+
.replace(/>/g, '>')
|
|
362
813
|
}
|
|
363
814
|
|
|
364
815
|
function propInlineMacro (config) {
|
|
@@ -368,105 +819,105 @@ function propInlineMacro (config) {
|
|
|
368
819
|
self.process(function (parent, target, attributes) {
|
|
369
820
|
const document = parent.getDocument()
|
|
370
821
|
const name = target.trim()
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
822
|
+
const mode = document.getAttribute('property-validate', 'warn')
|
|
823
|
+
const filePath = config && config.file && config.file.src && config.file.src.path
|
|
824
|
+
const component = (config && config.file && config.file.src && config.file.src.component) || ''
|
|
825
|
+
const version = (config && config.file && config.file.src && config.file.src.version) || ''
|
|
826
|
+
const linkRequested = attributes.link === 'true' || attributes.link === true
|
|
827
|
+
|
|
828
|
+
// Anything that leaves the property unverified renders as plain inline
|
|
829
|
+
// code: no marker, so the docs UI adds no tooltip. A tooltip the build
|
|
830
|
+
// could not check is worse than none, and a marker with nothing behind it
|
|
831
|
+
// would leave the UI guessing at a documentation link.
|
|
832
|
+
// No Helm path here, deliberately: helmValuesPath needs the property's
|
|
833
|
+
// config_scope to choose between config.node.* and config.cluster.*, and
|
|
834
|
+
// on this path there is no verified entry to take it from. Emitting one
|
|
835
|
+
// anyway printed a confident, copy-pasteable values.yaml path -- and the
|
|
836
|
+
// wrong one for every broker property -- for a property the build could
|
|
837
|
+
// not verify. Show the name instead.
|
|
838
|
+
//
|
|
839
|
+
// No macro substitution either: the content is a hand-built tag pair, so
|
|
840
|
+
// a text= value containing xref:/image:/pass:[] would otherwise be
|
|
841
|
+
// expanded inside it and could close or nest past the </code>.
|
|
842
|
+
const plain = () => self.createInline(parent, 'quoted', buildPropContent({
|
|
843
|
+
name,
|
|
844
|
+
text: attributes.text,
|
|
845
|
+
role: document.getAttribute('property-ref-role', DEFAULT_ROLE),
|
|
846
|
+
plain: true,
|
|
847
|
+
}))
|
|
848
|
+
|
|
849
|
+
const registry = config && config.contentCatalog ? loadProperties(config) : undefined
|
|
850
|
+
if (!registry) {
|
|
851
|
+
// No dataset covers this component. Say so once and render plain.
|
|
852
|
+
if (mode !== 'off' && config && config.contentCatalog) {
|
|
853
|
+
if (!warnSeriesMissing(config.contentCatalog, component, version)) {
|
|
854
|
+
warnNoPropertyData(config.contentCatalog, component, version)
|
|
855
|
+
}
|
|
386
856
|
}
|
|
857
|
+
return plain()
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Own properties only: a bare index lookup makes toString, constructor
|
|
861
|
+
// and valueOf validate as real properties and render marked.
|
|
862
|
+
const entry = Object.prototype.hasOwnProperty.call(registry.properties, name)
|
|
863
|
+
? registry.properties[name]
|
|
864
|
+
: undefined
|
|
865
|
+
if (!entry) {
|
|
866
|
+
reportUnknownProperty({ name, mode, registry, filePath })
|
|
867
|
+
return plain()
|
|
868
|
+
}
|
|
869
|
+
if (!isAvailable(registry, entry)) {
|
|
870
|
+
reportUnavailableProperty({ name, component, registry, mode, filePath })
|
|
871
|
+
return plain()
|
|
387
872
|
}
|
|
388
|
-
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
let discoveredPage
|
|
873
|
+
|
|
874
|
+
// Discover the page in THIS component that documents the property. A page
|
|
875
|
+
// is never borrowed from another component, and never guessed from the
|
|
876
|
+
// property's scope, because a link the build cannot verify is a broken
|
|
877
|
+
// link. page= overrides discovery for the rare case a writer knows better.
|
|
878
|
+
let discoveredPage = attributes.page
|
|
395
879
|
let discoveredUrl
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (ownEntry) {
|
|
405
|
-
discoveredPage = ownEntry.page
|
|
406
|
-
discoveredUrl = ownEntry.url
|
|
407
|
-
} else {
|
|
408
|
-
let fallbackWithPages
|
|
409
|
-
for (const fallbackComponent of ['streaming', 'ROOT']) {
|
|
410
|
-
if (fallbackComponent === component) continue
|
|
411
|
-
// A component-qualified xref without a version resolves to the
|
|
412
|
-
// fallback component's latest version, so consult exactly that
|
|
413
|
-
// version's index (which the property-page-index extension warms).
|
|
414
|
-
const latest = typeof config.contentCatalog.getComponent === 'function' &&
|
|
415
|
-
config.contentCatalog.getComponent(fallbackComponent)
|
|
416
|
-
const fallbackVersion = (latest && latest.latest && latest.latest.version) || undefined
|
|
417
|
-
const fallbackIndex = buildPageIndex(config.contentCatalog, fallbackComponent, registry.properties, fallbackVersion)
|
|
418
|
-
if (fallbackIndex.size === 0) continue
|
|
419
|
-
if (!fallbackWithPages) fallbackWithPages = fallbackComponent
|
|
420
|
-
const fallbackEntry = fallbackIndex.get(name)
|
|
421
|
-
if (fallbackEntry) {
|
|
422
|
-
discoveredPage = fallbackEntry.page
|
|
423
|
-
discoveredUrl = fallbackEntry.url
|
|
424
|
-
componentPrefix = `${fallbackComponent}:`
|
|
425
|
-
break
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
// Nothing documents the property anywhere: point the scope-derived
|
|
429
|
-
// fallback at a component that at least publishes property pages,
|
|
430
|
-
// unless the current component does.
|
|
431
|
-
if (!discoveredPage && ownIndex.size === 0 && fallbackWithPages) {
|
|
432
|
-
componentPrefix = `${fallbackWithPages}:`
|
|
433
|
-
}
|
|
434
|
-
// Property pages exist but none of them documents this property
|
|
435
|
-
// (for example, a cloud-only topic property when no site publishes
|
|
436
|
-
// a cloud topic-properties page). Linking would produce a broken
|
|
437
|
-
// xref, so render the marker without a link and say why. A plain
|
|
438
|
-
// tooltip marker for an undocumented property is fine -- only an
|
|
439
|
-
// explicit link request warrants the warning.
|
|
440
|
-
if (linkRequested && !discoveredPage && (ownIndex.size > 0 || fallbackWithPages)) {
|
|
441
|
-
const where = (config.file && config.file.src && config.file.src.path) || 'unknown file'
|
|
442
|
-
console.warn(chalk.yellow(`prop:${name}[link=true] in ${where}: no property reference page documents '${name}' in this build, so it renders without a link. Check the property's include tags on the reference pages.`))
|
|
443
|
-
suppressLink = true
|
|
444
|
-
}
|
|
880
|
+
if (!discoveredPage) {
|
|
881
|
+
const index = buildPageIndex(config.contentCatalog, component, registry.properties, version)
|
|
882
|
+
const found = index.get(name)
|
|
883
|
+
if (found) {
|
|
884
|
+
discoveredPage = found.page
|
|
885
|
+
discoveredUrl = found.url
|
|
886
|
+
} else if (linkRequested) {
|
|
887
|
+
reportUnpublishedProperty({ name, registry, mode, filePath })
|
|
445
888
|
}
|
|
446
889
|
}
|
|
447
|
-
|
|
448
|
-
// pages rendered with env-kubernetes, so single-sourced content reads
|
|
449
|
-
// correctly for both Linux and Kubernetes audiences (the successor to
|
|
450
|
-
// the config_ref macro's hardcoded storage.tiered.config prefixing).
|
|
451
|
-
const helmPath = attributes['helm-path'] === 'auto' && document.getAttribute('env-kubernetes') !== undefined
|
|
890
|
+
|
|
452
891
|
const content = buildPropContent({
|
|
453
892
|
name,
|
|
454
893
|
text: attributes.text,
|
|
455
|
-
link: linkRequested &&
|
|
456
|
-
page:
|
|
457
|
-
scope: entry
|
|
894
|
+
link: linkRequested && Boolean(discoveredPage),
|
|
895
|
+
page: discoveredPage,
|
|
896
|
+
scope: entry.config_scope,
|
|
458
897
|
role: document.getAttribute('property-ref-role', DEFAULT_ROLE),
|
|
459
|
-
|
|
460
|
-
helmPath,
|
|
898
|
+
helmPath: helmPathRequested(attributes, document),
|
|
461
899
|
docUrl: discoveredUrl ? `${discoveredUrl}#${propertyAnchor(name)}` : undefined,
|
|
462
900
|
})
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
|
|
901
|
+
// Only request macro substitution when an xref actually needs resolving.
|
|
902
|
+
// Applying it unconditionally let a text= value containing another macro
|
|
903
|
+
// expand inside the hand-built <code> element, producing mis-nested or
|
|
904
|
+
// unclosed markup.
|
|
905
|
+
const options = linkRequested && discoveredPage ? { attributes: { subs: 'macros' } } : {}
|
|
906
|
+
return self.createInline(parent, 'quoted', content, options)
|
|
466
907
|
})
|
|
467
908
|
}
|
|
468
909
|
}
|
|
469
910
|
|
|
911
|
+
/**
|
|
912
|
+
* helm-path=auto displays the property as its Helm values path on pages
|
|
913
|
+
* rendered with env-kubernetes, so single-sourced content reads correctly for
|
|
914
|
+
* both Linux and Kubernetes audiences (the successor to the config_ref macro's
|
|
915
|
+
* hardcoded storage.tiered.config prefixing).
|
|
916
|
+
*/
|
|
917
|
+
function helmPathRequested (attributes, document) {
|
|
918
|
+
return attributes['helm-path'] === 'auto' && document.getAttribute('env-kubernetes') !== undefined
|
|
919
|
+
}
|
|
920
|
+
|
|
470
921
|
function register (registry, config = {}) {
|
|
471
922
|
if (typeof registry.register === 'function') {
|
|
472
923
|
registry.register(function () {
|
|
@@ -486,6 +937,8 @@ module.exports.propertyAnchor = propertyAnchor
|
|
|
486
937
|
module.exports.helmValuesPath = helmValuesPath
|
|
487
938
|
module.exports.loadPropertiesFor = loadPropertiesFor
|
|
488
939
|
module.exports.compareTags = compareTags
|
|
940
|
+
module.exports.isPrerelease = isPrerelease
|
|
941
|
+
module.exports.releaseSeries = releaseSeries
|
|
489
942
|
module.exports.extractHeadingsWithTags = extractHeadingsWithTags
|
|
490
943
|
module.exports.evaluateTagExpression = evaluateTagExpression
|
|
491
944
|
module.exports.buildPageIndex = buildPageIndex
|