@redpanda-data/docs-extensions-and-macros 5.55.0 → 5.56.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/bin/doc-tools.js +315 -7
- package/cli-utils/docs-links-to-xrefs.js +174 -0
- package/cli-utils/generated-marker.js +43 -0
- package/cli-utils/helm-commented-values.js +37 -3
- package/extension-utils/llms-utils.js +6 -2
- package/extension-utils/metadata-candidates.js +304 -0
- package/extension-utils/metadata-check.js +174 -0
- package/extension-utils/metadata-suggest.js +573 -0
- package/extension-utils/taxonomy-categories.js +106 -0
- package/extension-utils/taxonomy.js +566 -0
- package/extensions/REFERENCE.adoc +90 -4
- package/extensions/algolia-indexer/generate-index.js +13 -1
- package/extensions/algolia-indexer/index.js +4 -2
- package/extensions/convert-sitemap-to-markdown.js +47 -12
- package/extensions/convert-to-markdown.js +176 -0
- package/extensions/generate-fields-only-pages.js +14 -0
- package/extensions/modify-connect-tag-playbook.js +2 -2
- package/extensions/url-to-xref.js +92 -22
- package/extensions/validate-attributes.js +10 -1
- package/package.json +2 -1
- package/tools/check-metadata/index.js +523 -0
- package/tools/cloud-regions/cloud-regions-table-adoc.hbs +1 -2
- package/tools/cloud-regions/cloud-regions-table-md.hbs +2 -2
- package/tools/cloud-regions/render-cloud-regions.js +3 -0
- package/tools/dispatch-graph/check-dispatch-graph.js +790 -0
- package/tools/dispatch-graph/classify-runs.js +128 -0
- package/tools/dispatch-graph/parse-workflows.js +499 -0
- package/tools/enterprise-features/detect-gating-changes.js +393 -0
- package/tools/enterprise-features/verify.js +2 -1
- package/tools/gen-rpk-ascii.py +7 -1
- package/tools/lint-strings/fixtures/connect/lint_connect.go +8 -0
- package/tools/lint-strings/fixtures/crd/lint_types.go +4 -1
- package/tools/lint-strings/fixtures/rpk-pr461/delete.base.go +42 -0
- package/tools/lint-strings/fixtures/rpk-pr461/delete.head.go +45 -0
- package/tools/lint-strings/fixtures/rpk-pr461/reviewer-comments.json +1 -0
- package/tools/lint-strings/index.js +133 -3
- package/tools/lint-strings/rules/verbatim-asciidoc.js +166 -1
- package/tools/lint-strings/surfaces/connect.js +72 -12
- package/tools/lint-strings/surfaces/crd.js +10 -6
- package/tools/metrics/metrics.py +11 -0
- package/tools/property-extractor/generate-handlebars-docs.js +9 -1
- package/tools/property-extractor/helpers/applyPropertyLinks.js +35 -0
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +29 -8
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +19 -24
- package/tools/redpanda-connect/templates/bloblang-function.hbs +1 -1
- package/tools/redpanda-connect/templates/descriptions-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/examples-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/fields-partials.hbs +2 -2
- package/tools/redpanda-connect/templates/metadata-partials.hbs +1 -1
- package/tools/rpk-docs/generate-rpk-docs.js +2 -0
- package/tools/rpk-docs/generate-x-env-partial.js +7 -4
- package/tools/rpk-docs/helpers/index.js +4 -1
- package/tools/rpk-docs/templates/command.hbs +1 -1
|
@@ -35,12 +35,23 @@ const { raiseListenerLimit } = require('./util/raise-listener-limit')
|
|
|
35
35
|
* generator wrapped across a line break. An unlabeled URL becomes xref:...[]
|
|
36
36
|
* so Antora fills in the target page title, except when it carries a
|
|
37
37
|
* fragment, where link text has to be supplied (see linkTextFor).
|
|
38
|
+
* - A URL with a fragment is converted only when the anchor exists on the
|
|
39
|
+
* target page: a section id, an explicit [[id]], [#id] or anchor:id[]
|
|
40
|
+
* anchor, or one of those in a partial the page includes (property
|
|
41
|
+
* headings live in generated partials). A URL whose page resolves but whose
|
|
42
|
+
* anchor does not stays a URL and is reported as a broken anchor, since an
|
|
43
|
+
* xref to it would pass Antora's page check and still land at the top of
|
|
44
|
+
* the page.
|
|
38
45
|
* - Targets in the latest version of a component emit an unversioned xref;
|
|
39
46
|
* targets pinned to an older version emit version@component:module:page.
|
|
40
47
|
* - URLs pointing at a page's former path resolve through the page-aliases the
|
|
41
48
|
* page declares (see buildUrlMap).
|
|
42
49
|
* - URLs carrying a query string are left raw, because an xref cannot express
|
|
43
50
|
* one; they are reported at info level rather than as warnings.
|
|
51
|
+
* - A resolver context may name a localComponent (see
|
|
52
|
+
* cli-utils/docs-links-to-xrefs.js, which converts generated pages before
|
|
53
|
+
* they are committed): targets in that component are written as
|
|
54
|
+
* module:page xrefs, the form the docs repos use for same-component links.
|
|
44
55
|
*/
|
|
45
56
|
|
|
46
57
|
// Rewrites for URL shapes that predate the current docs.redpanda.com site
|
|
@@ -80,6 +91,7 @@ module.exports.register = function ({ config = {} }) {
|
|
|
80
91
|
const unmapped = new Map()
|
|
81
92
|
const withQueryString = new Set()
|
|
82
93
|
const withoutLinkText = new Set()
|
|
94
|
+
const missingAnchor = new Map()
|
|
83
95
|
const files = contentCatalog
|
|
84
96
|
.getPages((page) => page.out)
|
|
85
97
|
.concat(contentCatalog.findBy({ family: 'partial' }))
|
|
@@ -96,11 +108,18 @@ module.exports.register = function ({ config = {} }) {
|
|
|
96
108
|
}
|
|
97
109
|
for (const url of result.withQueryString) withQueryString.add(url)
|
|
98
110
|
for (const url of result.withoutLinkText) withoutLinkText.add(url)
|
|
111
|
+
for (const url of result.missingAnchor) {
|
|
112
|
+
if (!missingAnchor.has(url)) missingAnchor.set(url, new Set())
|
|
113
|
+
missingAnchor.get(url).add(file.path)
|
|
114
|
+
}
|
|
99
115
|
}
|
|
100
116
|
if (logUnconverted) {
|
|
101
117
|
for (const [url, pages] of unmapped) {
|
|
102
118
|
logger.warn(`No published page matches ${url} (found in: ${[...pages].join(', ')})`)
|
|
103
119
|
}
|
|
120
|
+
for (const [url, pages] of missingAnchor) {
|
|
121
|
+
logger.warn(`Anchor not found on the target page of ${url}; left as a URL (found in: ${[...pages].join(', ')})`)
|
|
122
|
+
}
|
|
104
123
|
}
|
|
105
124
|
if (withQueryString.size) {
|
|
106
125
|
logger.info(
|
|
@@ -111,7 +130,7 @@ module.exports.register = function ({ config = {} }) {
|
|
|
111
130
|
if (withoutLinkText.size) {
|
|
112
131
|
logger.info(
|
|
113
132
|
`Left ${withoutLinkText.size} docs URL${withoutLinkText.size === 1 ? '' : 's'} with a fragment as raw ` +
|
|
114
|
-
`link${withoutLinkText.size === 1 ? '' : 's'} (
|
|
133
|
+
`link${withoutLinkText.size === 1 ? '' : 's'} (the target page could not be read to check the fragment): ` +
|
|
115
134
|
`${[...withoutLinkText].join(', ')}`
|
|
116
135
|
)
|
|
117
136
|
}
|
|
@@ -335,31 +354,49 @@ function resolveUrlPath (urlPath, resolverContext) {
|
|
|
335
354
|
return undefined
|
|
336
355
|
}
|
|
337
356
|
|
|
338
|
-
function entryToXref (entry, fragment, label) {
|
|
357
|
+
function entryToXref (entry, fragment, label, localComponent) {
|
|
339
358
|
const versionPrefix = entry.latest || !entry.version ? '' : `${entry.version}@`
|
|
340
|
-
|
|
359
|
+
// Content that is itself part of a component (a page a generator writes into
|
|
360
|
+
// the docs repo) names a target in that component by module and page only,
|
|
361
|
+
// the way the docs repos write xrefs by hand. A version-pinned xref still
|
|
362
|
+
// has to name its component.
|
|
363
|
+
const componentPrefix = !versionPrefix && localComponent && entry.component === localComponent ? '' : `${entry.component}:`
|
|
364
|
+
return `xref:${versionPrefix}${componentPrefix}${entry.module}:${entry.relative}${fragment || ''}[${label || ''}]`
|
|
341
365
|
}
|
|
342
366
|
|
|
343
367
|
// Parsed heading id -> heading text, cached per target file.
|
|
344
368
|
const headingsByFile = new WeakMap()
|
|
345
369
|
const EXPLICIT_ANCHOR_PATTERNS = [
|
|
346
370
|
/^\[\[([\w:.-]+)(?:,.*)?\]\]$/,
|
|
347
|
-
/^\[#([\w
|
|
371
|
+
/^\[#([\w:-]+)(?:[,.%].*)?\]$/,
|
|
348
372
|
/^\[id="?([\w:.-]+)"?(?:,.*)?\]$/,
|
|
349
373
|
]
|
|
350
374
|
const HEADING_RX = /^(={1,6})\s+(\S.*)$/
|
|
375
|
+
const INLINE_ANCHOR_RX = /\[\[([\w:.-]+)(?:,[^\]]*)?\]\]|\banchor:([\w:.-]+)\[/g
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The anchors a page publishes, id -> heading text (undefined for an anchor
|
|
379
|
+
* on something other than a heading), or undefined when the page has no
|
|
380
|
+
* contents to read. Cached per page.
|
|
381
|
+
*/
|
|
382
|
+
function pageAnchors (page, resolveInclude) {
|
|
383
|
+
const contents = page && page.contents && page.contents.toString()
|
|
384
|
+
if (!contents) return undefined
|
|
385
|
+
let anchors = headingsByFile.get(page)
|
|
386
|
+
if (!anchors) headingsByFile.set(page, (anchors = pageHeadingIds(contents, page, resolveInclude)))
|
|
387
|
+
return anchors
|
|
388
|
+
}
|
|
351
389
|
|
|
352
390
|
/**
|
|
353
391
|
* Returns the link text for a converted URL, or undefined when the URL should
|
|
354
|
-
* be left alone.
|
|
392
|
+
* be left alone. Callers check the fragment against pageAnchors first.
|
|
355
393
|
*
|
|
356
394
|
* A labeled URL keeps its label, and an unlabeled URL without a fragment gets
|
|
357
395
|
* empty text so that Antora fills in the target page title. An unlabeled URL
|
|
358
396
|
* *with* a fragment has to be given text explicitly: Antora cannot resolve a
|
|
359
397
|
* section title, so `xref:page.adoc#anchor[]` renders the raw resource id as
|
|
360
398
|
* the link text. Prefer the heading the fragment points at, then the page
|
|
361
|
-
* title
|
|
362
|
-
* which still displays sensibly.
|
|
399
|
+
* title (an anchor on a paragraph or table has no heading of its own).
|
|
363
400
|
*/
|
|
364
401
|
function linkTextFor (label, fragment, page, resolveInclude) {
|
|
365
402
|
// A trailing ^ opens a URL link in a new window; an xref would print it.
|
|
@@ -367,11 +404,19 @@ function linkTextFor (label, fragment, page, resolveInclude) {
|
|
|
367
404
|
label = (label || '').replace(/\^$/, '')
|
|
368
405
|
if (label) return label
|
|
369
406
|
if (!fragment) return ''
|
|
370
|
-
const
|
|
371
|
-
if (!
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
407
|
+
const anchors = pageAnchors(page, resolveInclude)
|
|
408
|
+
if (!anchors) return undefined
|
|
409
|
+
return anchors.get(fragmentId(fragment)) || documentTitle(page.contents.toString())
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** The anchor id of a URL fragment (`#a%2Fb` is `a/b`), without the `#`. */
|
|
413
|
+
function fragmentId (fragment) {
|
|
414
|
+
const raw = fragment.slice(1)
|
|
415
|
+
try {
|
|
416
|
+
return decodeURIComponent(raw)
|
|
417
|
+
} catch {
|
|
418
|
+
return raw
|
|
419
|
+
}
|
|
375
420
|
}
|
|
376
421
|
|
|
377
422
|
// Only the first level of includes is followed, and only this many per page:
|
|
@@ -380,7 +425,7 @@ function linkTextFor (label, fragment, page, resolveInclude) {
|
|
|
380
425
|
const MAX_INCLUDES_SEARCHED = 50
|
|
381
426
|
|
|
382
427
|
/**
|
|
383
|
-
* Collects the
|
|
428
|
+
* Collects the anchor ids of a page plus those of the partials it includes.
|
|
384
429
|
* The reference pages that carry most anchor targets are thin wrappers around
|
|
385
430
|
* generated partials — every property heading on the cluster and broker
|
|
386
431
|
* property pages lives in one — and Antora has not resolved includes yet at
|
|
@@ -407,10 +452,14 @@ function pageHeadingIds (contents, page, resolveInclude) {
|
|
|
407
452
|
}
|
|
408
453
|
|
|
409
454
|
/**
|
|
410
|
-
* Maps every
|
|
411
|
-
* ([[id]], [#id],
|
|
412
|
-
*
|
|
413
|
-
*
|
|
455
|
+
* Maps every anchor id in a page to its heading text: the ids Asciidoctor
|
|
456
|
+
* generates from heading text, explicit block anchors ([[id]], [#id],
|
|
457
|
+
* [id=...], on a heading or any other block) and inline anchors ([[id]] or
|
|
458
|
+
* anchor:id[] inside a line). An anchor that is not on a heading maps to
|
|
459
|
+
* undefined. Id generation is approximated with the docs sites' settings
|
|
460
|
+
* (idprefix empty, idseparator -); a heading whose id comes out differently
|
|
461
|
+
* is reported as a missing anchor and its URL is left as written, which
|
|
462
|
+
* still works.
|
|
414
463
|
*/
|
|
415
464
|
function parseHeadingIds (contents) {
|
|
416
465
|
const ids = new Map()
|
|
@@ -420,13 +469,18 @@ function parseHeadingIds (contents) {
|
|
|
420
469
|
const anchor = EXPLICIT_ANCHOR_PATTERNS.reduce((found, pattern) => found || pattern.exec(line), null)
|
|
421
470
|
if (anchor) {
|
|
422
471
|
pendingId = anchor[1]
|
|
472
|
+
if (!ids.has(pendingId)) ids.set(pendingId, undefined)
|
|
423
473
|
continue
|
|
424
474
|
}
|
|
475
|
+
for (const inline of line.matchAll(INLINE_ANCHOR_RX)) {
|
|
476
|
+
const id = inline[1] || inline[2]
|
|
477
|
+
if (!ids.has(id)) ids.set(id, undefined)
|
|
478
|
+
}
|
|
425
479
|
const heading = HEADING_RX.exec(line)
|
|
426
480
|
if (heading) {
|
|
427
481
|
const text = sanitizeLinkText(heading[2])
|
|
428
482
|
const id = pendingId || autoId(text)
|
|
429
|
-
if (id && text &&
|
|
483
|
+
if (id && text && ids.get(id) === undefined) ids.set(id, text)
|
|
430
484
|
}
|
|
431
485
|
if (line) pendingId = undefined
|
|
432
486
|
}
|
|
@@ -467,8 +521,9 @@ function autoId (text) {
|
|
|
467
521
|
* Rewrites every mappable docs URL in content. Returns the updated content,
|
|
468
522
|
* the number of conversions, the internal URLs that matched no published page,
|
|
469
523
|
* the internal URLs that carry a query string (which an xref cannot express),
|
|
470
|
-
*
|
|
471
|
-
*
|
|
524
|
+
* the internal URLs left alone because their target page could not be read to
|
|
525
|
+
* check the fragment, and the internal URLs whose page resolves but whose
|
|
526
|
+
* anchor is not on it.
|
|
472
527
|
*/
|
|
473
528
|
function convertContent (content, resolverContext) {
|
|
474
529
|
const { hostnames, ignore = [] } = resolverContext
|
|
@@ -478,6 +533,7 @@ function convertContent (content, resolverContext) {
|
|
|
478
533
|
const unmapped = []
|
|
479
534
|
const withQueryString = []
|
|
480
535
|
const withoutLinkText = []
|
|
536
|
+
const missingAnchor = []
|
|
481
537
|
for (const match of scanContentUrls(content)) {
|
|
482
538
|
let url
|
|
483
539
|
try {
|
|
@@ -505,17 +561,31 @@ function convertContent (content, resolverContext) {
|
|
|
505
561
|
unmapped.push(match.url)
|
|
506
562
|
continue
|
|
507
563
|
}
|
|
564
|
+
if (url.hash) {
|
|
565
|
+
const anchors = pageAnchors(entry.page, resolverContext.resolveInclude)
|
|
566
|
+
if (!anchors) {
|
|
567
|
+
withoutLinkText.push(match.url)
|
|
568
|
+
continue
|
|
569
|
+
}
|
|
570
|
+
// The page resolves but the anchor does not: an xref would pass
|
|
571
|
+
// Antora's check and still land at the top of the page. Keep the URL,
|
|
572
|
+
// which the site's broken-anchor checks can still see, and report it.
|
|
573
|
+
if (!anchors.has(fragmentId(url.hash))) {
|
|
574
|
+
missingAnchor.push(match.url)
|
|
575
|
+
continue
|
|
576
|
+
}
|
|
577
|
+
}
|
|
508
578
|
const label = linkTextFor(match.label, url.hash, entry.page, resolverContext.resolveInclude)
|
|
509
579
|
if (label === undefined) {
|
|
510
580
|
withoutLinkText.push(match.url)
|
|
511
581
|
continue
|
|
512
582
|
}
|
|
513
|
-
result += content.slice(cursor, match.start) + entryToXref(entry, url.hash, label)
|
|
583
|
+
result += content.slice(cursor, match.start) + entryToXref(entry, url.hash, label, resolverContext.localComponent)
|
|
514
584
|
cursor = match.end
|
|
515
585
|
converted++
|
|
516
586
|
}
|
|
517
587
|
result += content.slice(cursor)
|
|
518
|
-
return { content: result, converted, unmapped, withQueryString, withoutLinkText }
|
|
588
|
+
return { content: result, converted, unmapped, withQueryString, withoutLinkText, missingAnchor }
|
|
519
589
|
}
|
|
520
590
|
|
|
521
591
|
module.exports.buildUrlMap = buildUrlMap
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
9
|
const { raiseListenerLimit } = require('./util/raise-listener-limit')
|
|
10
|
+
const { getTaxonomy } = require('../extension-utils/taxonomy')
|
|
11
|
+
const { checkTaxonomy, createTaxonomyReport, logTaxonomyReport } = require('../extension-utils/taxonomy-categories')
|
|
10
12
|
|
|
11
13
|
module.exports.register = function ({ config }) {
|
|
12
14
|
raiseListenerLimit(this)
|
|
@@ -18,15 +20,22 @@ module.exports.register = function ({ config }) {
|
|
|
18
20
|
const validCategories = siteCatalog.attributeFile['page-valid-categories'];
|
|
19
21
|
if (!validCategories) return logger.warn('No page-valid-categories attribute found - skipping attribute validation')
|
|
20
22
|
const categoryMap = createCategoryMap(validCategories);
|
|
23
|
+
// The taxonomy (shared/modules/ROOT/attachments/taxonomy.yml) when the
|
|
24
|
+
// docs repo has it; null keeps the valid-categories-only behavior.
|
|
25
|
+
const taxonomy = getTaxonomy({ contentCatalog, siteCatalog, logger });
|
|
26
|
+
const report = createTaxonomyReport();
|
|
21
27
|
const pages = contentCatalog.findBy({ family: 'page' });
|
|
22
28
|
pages.forEach((page) => {
|
|
23
29
|
let pageCategories = page.asciidoc.attributes['page-categories'];
|
|
24
30
|
if (!pageCategories) return;
|
|
25
31
|
let pageCategoryList = pageCategories.split(',').map(c => c.trim());
|
|
26
|
-
const
|
|
32
|
+
const pageInfo = page.asciidoc.attributes['page-relative-src-path'];
|
|
33
|
+
if (taxonomy) pageCategoryList = checkTaxonomy(pageCategoryList, { taxonomy, categoryMap, page, report });
|
|
34
|
+
const validatedCategories = validateCategories(pageCategoryList, pageInfo, categoryMap, logger);
|
|
27
35
|
page.asciidoc.attributes['page-categories'] = validatedCategories
|
|
28
36
|
processEnvironmentAttributes(page, logger);
|
|
29
37
|
})
|
|
38
|
+
if (taxonomy) logTaxonomyReport(report, logger);
|
|
30
39
|
})
|
|
31
40
|
}
|
|
32
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redpanda-data/docs-extensions-and-macros",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.56.0",
|
|
4
4
|
"description": "Antora extensions and macros developed for Redpanda documentation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antora",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"test:cgo": "node __tests__/tools/cgo-detection.test.js",
|
|
34
34
|
"test:all": "npm run test && npm run test:python && npm run test:cgo",
|
|
35
35
|
"eval:doc-strings": "node evals/doc-strings/run-evals.js",
|
|
36
|
+
"eval:doc-impact": "node evals/doc-strings/run-evals.js --doc-impact",
|
|
36
37
|
"generate:cli-docs": "node tools/generate-cli-docs.js",
|
|
37
38
|
"bundle:admin": "doc-tools generate bundle-openapi --surface admin",
|
|
38
39
|
"bundle:connect": "doc-tools generate bundle-openapi --surface connect",
|