@redpanda-data/docs-extensions-and-macros 5.55.0 → 5.56.1

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.
Files changed (53) hide show
  1. package/bin/doc-tools.js +323 -7
  2. package/cli-utils/docs-links-to-xrefs.js +174 -0
  3. package/cli-utils/generated-marker.js +43 -0
  4. package/cli-utils/helm-commented-values.js +37 -3
  5. package/extension-utils/llms-utils.js +6 -2
  6. package/extension-utils/metadata-candidates.js +304 -0
  7. package/extension-utils/metadata-check.js +174 -0
  8. package/extension-utils/metadata-suggest.js +573 -0
  9. package/extension-utils/taxonomy-categories.js +106 -0
  10. package/extension-utils/taxonomy.js +566 -0
  11. package/extensions/REFERENCE.adoc +90 -4
  12. package/extensions/algolia-indexer/generate-index.js +13 -1
  13. package/extensions/algolia-indexer/index.js +4 -2
  14. package/extensions/convert-sitemap-to-markdown.js +47 -12
  15. package/extensions/convert-to-markdown.js +176 -0
  16. package/extensions/generate-fields-only-pages.js +14 -0
  17. package/extensions/modify-connect-tag-playbook.js +2 -2
  18. package/extensions/url-to-xref.js +92 -22
  19. package/extensions/validate-attributes.js +10 -1
  20. package/package.json +2 -1
  21. package/tools/check-metadata/index.js +523 -0
  22. package/tools/cloud-regions/cloud-regions-table-adoc.hbs +1 -2
  23. package/tools/cloud-regions/cloud-regions-table-md.hbs +2 -2
  24. package/tools/cloud-regions/render-cloud-regions.js +3 -0
  25. package/tools/dispatch-graph/check-dispatch-graph.js +790 -0
  26. package/tools/dispatch-graph/classify-runs.js +128 -0
  27. package/tools/dispatch-graph/parse-workflows.js +499 -0
  28. package/tools/enterprise-features/detect-gating-changes.js +440 -0
  29. package/tools/enterprise-features/verify.js +2 -1
  30. package/tools/gen-rpk-ascii.py +7 -1
  31. package/tools/lint-strings/fixtures/connect/lint_connect.go +8 -0
  32. package/tools/lint-strings/fixtures/crd/lint_types.go +4 -1
  33. package/tools/lint-strings/fixtures/rpk-pr461/delete.base.go +42 -0
  34. package/tools/lint-strings/fixtures/rpk-pr461/delete.head.go +45 -0
  35. package/tools/lint-strings/fixtures/rpk-pr461/reviewer-comments.json +1 -0
  36. package/tools/lint-strings/index.js +133 -3
  37. package/tools/lint-strings/rules/verbatim-asciidoc.js +166 -1
  38. package/tools/lint-strings/surfaces/connect.js +72 -12
  39. package/tools/lint-strings/surfaces/crd.js +10 -6
  40. package/tools/metrics/metrics.py +11 -0
  41. package/tools/property-extractor/generate-handlebars-docs.js +9 -1
  42. package/tools/property-extractor/helpers/applyPropertyLinks.js +35 -0
  43. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +29 -8
  44. package/tools/redpanda-connect/rpcn-connector-docs-handler.js +19 -24
  45. package/tools/redpanda-connect/templates/bloblang-function.hbs +1 -1
  46. package/tools/redpanda-connect/templates/descriptions-partials.hbs +1 -1
  47. package/tools/redpanda-connect/templates/examples-partials.hbs +1 -1
  48. package/tools/redpanda-connect/templates/fields-partials.hbs +2 -2
  49. package/tools/redpanda-connect/templates/metadata-partials.hbs +1 -1
  50. package/tools/rpk-docs/generate-rpk-docs.js +2 -0
  51. package/tools/rpk-docs/generate-x-env-partial.js +7 -4
  52. package/tools/rpk-docs/helpers/index.js +4 -1
  53. 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'} (no link text could be resolved from the target page): ` +
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
- return `xref:${versionPrefix}${entry.component}:${entry.module}:${entry.relative}${fragment || ''}[${label || ''}]`
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; if neither can be read, the caller leaves the URL as a raw link,
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 contents = page && page.contents && page.contents.toString()
371
- if (!contents) return undefined
372
- let headings = headingsByFile.get(page)
373
- if (!headings) headingsByFile.set(page, (headings = pageHeadingIds(contents, page, resolveInclude)))
374
- return headings.get(fragment.slice(1)) || documentTitle(contents)
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 heading ids of a page plus those of the partials it includes.
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 heading id in a page to its text, covering explicit anchors
411
- * ([[id]], [#id], [id=...]) and the ids Asciidoctor generates from heading
412
- * text. Id generation is approximated: a mismatch only costs the page-title
413
- * fallback.
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 && !ids.has(id)) ids.set(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
- * and the internal URLs left alone because no link text could be resolved for
471
- * their fragment.
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 validatedCategories = validateCategories(pageCategoryList, page.asciidoc.attributes['page-relative-src-path'], categoryMap, logger);
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.55.0",
3
+ "version": "5.56.1",
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",