@redpanda-data/docs-extensions-and-macros 5.17.0 → 5.22.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.
package/bin/doc-tools.js CHANGED
@@ -2475,6 +2475,88 @@ validation
2475
2475
  }
2476
2476
  })
2477
2477
 
2478
+ /**
2479
+ * validate property-overrides
2480
+ *
2481
+ * @description
2482
+ * Validates docs-data/property-overrides.json against its JSON Schema:
2483
+ * unknown keys (a typo that would otherwise be silently dropped by the
2484
+ * extractor), and the see_also shape (a plain string, or an object naming
2485
+ * exactly one of cloud_only/self_hosted_only).
2486
+ *
2487
+ * @why
2488
+ * property-overrides.json has no catch-all pass-through when an override
2489
+ * targets an existing property — an unrecognized key like the typo
2490
+ * `acceptable_values` (the generated-only field is `acceptable_values`, but
2491
+ * the override key the extractor reads is `accepted_values`) is silently
2492
+ * ignored rather than erroring. This check catches that class of mistake
2493
+ * before generation, the same way `validate rpk-overrides` does for rpk.
2494
+ *
2495
+ * @example
2496
+ * # Validate overrides with the default path
2497
+ * npx doc-tools validate property-overrides
2498
+ *
2499
+ * # Validate a custom overrides file
2500
+ * npx doc-tools validate property-overrides --overrides my-overrides.json
2501
+ *
2502
+ * # Strict mode - exit with error code on validation failures
2503
+ * npx doc-tools validate property-overrides --strict
2504
+ *
2505
+ * @requirements
2506
+ * - property-overrides.schema.json in docs-data/
2507
+ */
2508
+ validation
2509
+ .command('property-overrides')
2510
+ .description('Validate property-overrides.json against its schema')
2511
+ .option('--overrides <path>', 'Path to overrides JSON file', 'docs-data/property-overrides.json')
2512
+ .option('--strict', 'Exit with error code on validation failures')
2513
+ .action((options) => {
2514
+ try {
2515
+ const { loadAndValidateOverrides } = require('../tools/property-extractor/validate-overrides.js')
2516
+ const repoRoot = findRepoRoot()
2517
+ const overridesPath = path.resolve(repoRoot, options.overrides)
2518
+
2519
+ console.log(`Validating: ${overridesPath}`)
2520
+ console.log('')
2521
+
2522
+ const { overrides, validation: result } = loadAndValidateOverrides(overridesPath)
2523
+
2524
+ if (!overrides) {
2525
+ console.error('Error: Could not load overrides file')
2526
+ process.exit(1)
2527
+ }
2528
+
2529
+ console.log('='.repeat(60))
2530
+ console.log('VALIDATION RESULTS')
2531
+ console.log('='.repeat(60))
2532
+
2533
+ if (result.errors.length === 0 && result.warnings.length === 0) {
2534
+ console.log('✓ No issues found')
2535
+ } else {
2536
+ console.log(result.format())
2537
+ }
2538
+
2539
+ console.log('='.repeat(60))
2540
+ console.log(`Errors: ${result.errors.length}`)
2541
+ console.log(`Warnings: ${result.warnings.length}`)
2542
+ console.log('='.repeat(60))
2543
+
2544
+ if (options.strict && !result.valid) {
2545
+ console.log('\n✗ Validation failed (strict mode)')
2546
+ process.exit(1)
2547
+ } else if (result.valid) {
2548
+ console.log('\n✓ Validation passed')
2549
+ process.exit(0)
2550
+ } else {
2551
+ console.log('\n⚠ Validation completed with errors (use --strict to fail)')
2552
+ process.exit(0)
2553
+ }
2554
+ } catch (err) {
2555
+ console.error(`Error: ${err.message}`)
2556
+ process.exit(1)
2557
+ }
2558
+ })
2559
+
2478
2560
  /**
2479
2561
  * lint-strings
2480
2562
  *
@@ -0,0 +1,53 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Accumulates errors and warnings across a validation pass, so a set of
5
+ * independent checks (schema shape, cross-references, semantic rules) can
6
+ * each report findings and have them merged into one result the caller
7
+ * prints and exits on. Shared by every overrides validator in this repo —
8
+ * originally written for tools/rpk-docs/validate-overrides.js, extracted
9
+ * here once tools/property-extractor needed the identical accumulator.
10
+ */
11
+ class ValidationResult {
12
+ constructor() {
13
+ this.valid = true
14
+ this.errors = []
15
+ this.warnings = []
16
+ }
17
+
18
+ addError(message, context = null) {
19
+ this.valid = false
20
+ this.errors.push({ message, context })
21
+ }
22
+
23
+ addWarning(message, context = null) {
24
+ this.warnings.push({ message, context })
25
+ }
26
+
27
+ merge(other) {
28
+ if (!other.valid) this.valid = false
29
+ this.errors.push(...other.errors)
30
+ this.warnings.push(...other.warnings)
31
+ }
32
+
33
+ format() {
34
+ const lines = []
35
+ if (this.errors.length > 0) {
36
+ lines.push('ERRORS:')
37
+ for (const err of this.errors) {
38
+ lines.push(` ✗ ${err.message}`)
39
+ if (err.context) lines.push(` at: ${err.context}`)
40
+ }
41
+ }
42
+ if (this.warnings.length > 0) {
43
+ lines.push('WARNINGS:')
44
+ for (const warn of this.warnings) {
45
+ lines.push(` ⚠ ${warn.message}`)
46
+ if (warn.context) lines.push(` at: ${warn.context}`)
47
+ }
48
+ }
49
+ return lines.join('\n')
50
+ }
51
+ }
52
+
53
+ module.exports = { ValidationResult }
@@ -71,10 +71,36 @@ function stripMarkdownMetadata(content) {
71
71
  return text.trim();
72
72
  }
73
73
 
74
+ /**
75
+ * Return the subset of `components` that have at least one page in `pages` —
76
+ * i.e. the components that actually get a generated `<name>-full.txt` export.
77
+ *
78
+ * A `<name>-full.txt` file is only written for components that have pages, so
79
+ * advertising an export for a corpus-less landing/utility component (e.g.
80
+ * `data-platform`, `self-managed`, `search`) links the AI index to a 404.
81
+ * Filtering the advertised list through this helper keeps it in sync with the
82
+ * files that are actually produced. Input order is preserved.
83
+ *
84
+ * @param {Array<{name: string}>} components - All components from the content catalog
85
+ * @param {Array<{src?: {component?: string}}>} pages - Pages that will be exported
86
+ * @returns {Array<{name: string}>} Components that have at least one page
87
+ */
88
+ function componentsWithExports(components, pages) {
89
+ const componentNames = new Set(
90
+ (pages || [])
91
+ .map((page) => page && page.src && page.src.component)
92
+ .filter(Boolean)
93
+ );
94
+ return (components || []).filter((component) =>
95
+ componentNames.has(component.name)
96
+ );
97
+ }
98
+
74
99
  module.exports = {
75
100
  LLMS_DIRECTIVE_BASE,
76
101
  LLMS_DIRECTIVE_REGEX,
77
102
  SOURCE_COMMENT_REGEX,
78
103
  formatLlmsDirective,
79
104
  stripMarkdownMetadata,
105
+ componentsWithExports,
80
106
  };
@@ -3,7 +3,7 @@
3
3
  const { raiseListenerLimit } = require('./util/raise-listener-limit')
4
4
 
5
5
  const { toMarkdownUrl } = require('../extension-utils/url-utils');
6
- const { stripMarkdownMetadata } = require('../extension-utils/llms-utils');
6
+ const { stripMarkdownMetadata, componentsWithExports } = require('../extension-utils/llms-utils');
7
7
 
8
8
  /**
9
9
  * Extracts markdown from llms.adoc page and generates AI-friendly documentation exports.
@@ -167,7 +167,11 @@ module.exports.register = function () {
167
167
  fullContent += `- **${siteUrl}/llms.txt**: Curated overview following the llms.txt standard - start here for a quick introduction\n`;
168
168
  fullContent += `- **${siteUrl}/llms-full.txt**: Complete documentation export (this file) - comprehensive reference with all pages\n`;
169
169
  fullContent += `- **Component-specific exports**: Focused documentation for individual products:\n`;
170
- components.forEach(component => {
170
+ // Only advertise exports for components that actually have pages — a
171
+ // `<name>-full.txt` is generated per component below, so listing corpus-less
172
+ // landing/utility components (e.g. data-platform, self-managed, search) would
173
+ // point the AI index at a 404.
174
+ componentsWithExports(components, pages).forEach(component => {
171
175
  fullContent += ` - \`${siteUrl}/${component.name}-full.txt\`: ${component.title}\n`;
172
176
  });
173
177
  fullContent += `- **Individual markdown pages**: Each HTML page has a corresponding .md file (e.g., \`/docs/page.html\` → \`/docs/page.md\`)\n\n`;
@@ -220,16 +220,17 @@ The macro checks that a property is actually available to the audience reading t
220
220
  prop:cloud_storage_enabled[]
221
221
  prop:iceberg_enabled[link=true]
222
222
  prop:retention.ms[link=true,page=properties/topic-properties]
223
+ prop:cloud_storage_enabled[link=true,page=cloud:reference:properties/cluster-properties]
223
224
  prop:write_caching_default[text=write caching]
224
225
  ----
225
226
 
226
227
  The macro target is the property name. Named attributes:
227
228
 
228
229
  link::
229
- Set to `true` to also link the property to its reference page. The page is discovered dynamically: the macro indexes which reference-module page in the current component documents each property, by scanning property headings in the partials each page includes (respecting include tag filters such as `tags=redpanda-cloud`). Links are component-relative, so a Cloud page links to the Cloud component's own property pages, and they keep working if properties are split across different pages in the future. See <<prop-availability>> for properties the current component does not document. This supersedes the `config_ref` macro's linking for most uses.
230
+ Set to `true` to also link the property to its reference page. The page is discovered dynamically: the macro indexes which reference-module page in the current component documents each property, by scanning property headings in the partials each page includes (respecting include tag filters such as `tags=redpanda-cloud`). Discovered links are component-relative, so a Cloud page links to the Cloud component's own property pages, and they keep working if properties are split across different pages in the future. See <<prop-availability>> for properties the current component does not document. This supersedes the `config_ref` macro's linking for most uses.
230
231
 
231
232
  page::
232
- Optional reference-page override (module-relative, without `.adoc`), for example `properties/object-storage-properties`. Overrides discovery entirely.
233
+ Optional reference-page override, without `.adoc`. Overrides discovery entirely, and is never build-verified either way, so it may name any page that documents the property. A bare name (`properties/object-storage-properties`) stays module-relative within `reference` in the current component; qualify it with a module (`guides:cloud/object-storage`) or a component and module (`cloud:reference:properties/cluster-properties`) to point elsewhere.
233
234
 
234
235
  helm-path::
235
236
  Set to `auto` to display the property as its Helm values path on pages rendered with the `env-kubernetes` attribute, so single-sourced content reads correctly for both Linux and Kubernetes audiences. The path is derived per property: `storage.tiered.config.<name>` for properties whose name begins `cloud_storage_`, `config.node.<name>` for broker properties, and `config.cluster.<name>` for cluster properties. Topic properties render as the plain name, because they are set per topic through the Kafka API, rpk, or a Topic resource and have no `values.yaml` key at all. The plain name also renders on any page where `env-kubernetes` is unset or explicitly `false`. This supersedes the deprecated `config_ref` macro's prefixing.
package/macros/prop.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * prop:cloud_storage_enabled[]
8
8
  * prop:iceberg_enabled[link=true]
9
9
  * prop:retention.ms[link=true,page=properties/topic-properties]
10
+ * prop:cloud_storage_enabled[link=true,page=cloud:reference:properties/cluster-properties]
10
11
  * prop:write_caching_default[text=write caching]
11
12
  *
12
13
  * The property renders as a marked <code> element that the docs UI decorates
@@ -54,11 +55,17 @@
54
55
  * a page in this component actually documents it. The page is discovered by
55
56
  * indexing which reference-module page documents each property, scanning
56
57
  * property headings in the partials each page includes and respecting include
57
- * tag filters such as tags=redpanda-cloud. Links are therefore
58
+ * tag filters such as tags=redpanda-cloud. Discovered links are therefore
58
59
  * 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.
60
+ * different pages later: nothing is ever discovered across components, and no
61
+ * page target is guessed from a property's scope, because a link the build
62
+ * cannot verify would be a broken link.
63
+ *
64
+ * Use page= to set the target explicitly instead of discovering it -- an
65
+ * override the build does not verify either way, so it may name any page that
66
+ * documents the property, in this component or another: a bare name
67
+ * (properties/topic-properties) stays module-relative within reference here,
68
+ * while module:page or component:module:page points anywhere else.
62
69
  *
63
70
  * With helm-path=auto, pages rendered with the env-kubernetes attribute
64
71
  * display the property as its Helm values path (storage.tiered.config.*
@@ -814,6 +821,23 @@ function reportUnpublishedProperty ({ name, registry, mode, filePath }) {
814
821
  ))
815
822
  }
816
823
 
824
+ /**
825
+ * Resolve a page= value into the resource ID prefix an xref needs, before the
826
+ * trailing .adoc. A bare name (no colon) is module-relative within reference
827
+ * in the page's own component, matching every page= value published today.
828
+ * Qualifying it with a module (module:page) or a component and module
829
+ * (component:module:page) points the link anywhere else the property is
830
+ * documented: page= is never build-verified (buildPageIndex only runs when
831
+ * page= is absent), so restricting its shape bought no safety, only
832
+ * inflexibility.
833
+ *
834
+ * @param {string} page
835
+ * @returns {string}
836
+ */
837
+ function resolvePageTarget (page) {
838
+ return page.includes(':') ? page : `reference:${page}`
839
+ }
840
+
817
841
  /**
818
842
  * Build the AsciiDoc content emitted for one macro instance. Exported for
819
843
  * unit testing.
@@ -823,7 +847,10 @@ function reportUnpublishedProperty ({ name, registry, mode, filePath }) {
823
847
  * @param {string} [opts.text] - Display text override.
824
848
  * @param {boolean} [opts.link] - Whether to link to the reference page. Ignored
825
849
  * without a page: the macro never guesses a target.
826
- * @param {string} [opts.page] - Reference page (module-relative, no .adoc).
850
+ * @param {string} [opts.page] - Reference page. A bare name (no colon) is
851
+ * module-relative within reference in the current component; qualify it
852
+ * with a module (module:page) or a component and module
853
+ * (component:module:page) to point elsewhere. No .adoc.
827
854
  * @param {string} [opts.scope] - config_scope, used only for the Helm path.
828
855
  * @param {string} opts.role - CSS class for the code element.
829
856
  * @param {boolean} [opts.plain] - Render unmarked code (no tooltip, no link)
@@ -834,7 +861,7 @@ function buildPropContent ({ name, text, link, page, scope, role, helmPath = fal
834
861
  const display = text || (helmPath && helmValuesPath(name, scope)) || name
835
862
  if (plain) return `<code>${display}</code>`
836
863
  const inner = link && page
837
- ? `xref:reference:${page}.adoc#${propertyAnchor(name)}[${display}]`
864
+ ? `xref:${resolvePageTarget(page)}.adoc#${propertyAnchor(name)}[${display}]`
838
865
  : display
839
866
  // Escape every interpolated attribute value. role comes from a site
840
867
  // attribute, docUrl from a page's published URL, and name from a dataset
@@ -980,6 +1007,7 @@ function register (registry, config = {}) {
980
1007
 
981
1008
  module.exports.register = register
982
1009
  module.exports.buildPropContent = buildPropContent
1010
+ module.exports.resolvePageTarget = resolvePageTarget
983
1011
  module.exports.propertyAnchor = propertyAnchor
984
1012
  module.exports.helmValuesPath = helmValuesPath
985
1013
  module.exports.loadPropertiesFor = loadPropertiesFor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.17.0",
3
+ "version": "5.22.1",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -142,8 +142,12 @@ Create a `property-overrides.json` file to customize extracted documentation:
142
142
  "description": "Network address and port for Kafka API clients to connect to Redpanda brokers.",
143
143
  "example": ".Example\n[,yaml]\n----\nredpanda:\n kafka_api:\n - name: internal\n address: 0.0.0.0\n port: 9092\n - name: external\n address: redpanda.example.com\n port: 19092\n----",
144
144
  "version": "v21.4.1",
145
- "related_topics": [
146
- "xref:manage:kubernetes/networking/k-networking-and-connectivity.adoc[]"
145
+ "see_also": [
146
+ "xref:manage:kubernetes/networking/k-networking-and-connectivity.adoc[]",
147
+ {
148
+ "content": "xref:manage:cluster-maintenance/config-cluster.adoc[]",
149
+ "cloud_only": true
150
+ }
147
151
  ]
148
152
  }
149
153
  }
@@ -156,7 +160,17 @@ Create a `property-overrides.json` file to customize extracted documentation:
156
160
  * `example` - Add usage example in AsciiDoc format
157
161
  * `example_file` - Load example from external file
158
162
  * `version` - Document when property was introduced
159
- * `related_topics` - Array of AsciiDoc xrefs to related content
163
+ * `see_also` - Array of related links. Each item is either a plain string (shown in every build) or an object `{content, cloud_only}` / `{content, self_hosted_only}` (shown only in that build, wrapped with `ifdef::env-cloud[]`/`ifndef::env-cloud[]` at generation time). Setting both `cloud_only` and `self_hosted_only` on the same item is rejected by the schema — it would wrap the item in both directives at once, so it renders in neither build.
164
+ * `related_topics` - Deprecated predecessor to `see_also`. An array of plain strings, optionally prefixed `cloud-only:` / `self-managed-only:` for the same conditional behavior — that prefix is free text with no validation, which is why `see_also` replaced it. Existing `related_topics` entries keep working; new entries should use `see_also`.
165
+
166
+ Validate an overrides file against its schema (`docs-data/property-overrides.schema.json`) before generating docs:
167
+
168
+ [,bash]
169
+ ----
170
+ npx doc-tools validate property-overrides --overrides docs-data/property-overrides.json
171
+ ----
172
+
173
+ This catches malformed `see_also` items and unrecognized override keys — including a typo that would otherwise be silently dropped, since applying an override to an *existing* property has no catch-all pass-through (only overrides that create a new property from scratch do).
160
174
 
161
175
  === Understanding property changes
162
176
 
@@ -16,8 +16,7 @@ module.exports = {
16
16
  anchorName: require('./anchorName.js'),
17
17
  normalizePropertyAnchors: require('./normalizePropertyAnchors.js'),
18
18
  canonicalizePropertyXrefs: require('./canonicalizePropertyXrefs.js'),
19
- parseRelatedTopic: require('./parseRelatedTopic.js'),
20
- allTopicsConditional: require('./allTopicsConditional.js'),
19
+ seeAlsoView: require('./seeAlsoView.js'),
21
20
  capitalize: require('./capitalize.js'),
22
21
  length: require('./length.js'),
23
22
  };
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Normalizes a property's related-topics data into one shape for the
5
+ * templates to render, regardless of which field or convention the override
6
+ * author used:
7
+ *
8
+ * - see_also (current): array of items, each either a plain string (shown
9
+ * everywhere) or {content, cloud_only, self_hosted_only} (shown only in
10
+ * the named build). Schema-validated — see docs-data/property-overrides.schema.json.
11
+ * - related_topics (deprecated, still read for back-compat with existing
12
+ * override files): array of strings, optionally prefixed with
13
+ * 'cloud-only:' / 'self-managed-only:' to get the same conditional
14
+ * behaviour. That prefix was free text with no validation; see_also
15
+ * replaces it with data a schema can check.
16
+ *
17
+ * @param {object} property - A property record, as extracted or overridden.
18
+ * @returns {Array<{content: string, cloudOnly: boolean, selfHostedOnly: boolean}>}
19
+ */
20
+ function normalizeSeeAlso(property) {
21
+ if (!property) return [];
22
+ const rawItems = Array.isArray(property.see_also)
23
+ ? property.see_also
24
+ : Array.isArray(property.related_topics)
25
+ ? property.related_topics
26
+ : [];
27
+
28
+ return rawItems
29
+ .map((item) => {
30
+ if (typeof item === 'string') {
31
+ const trimmed = item.trim();
32
+ if (trimmed.startsWith('cloud-only:')) {
33
+ return { content: trimmed.slice('cloud-only:'.length).trim(), cloudOnly: true, selfHostedOnly: false };
34
+ }
35
+ if (trimmed.startsWith('self-managed-only:')) {
36
+ return { content: trimmed.slice('self-managed-only:'.length).trim(), cloudOnly: false, selfHostedOnly: true };
37
+ }
38
+ return { content: trimmed, cloudOnly: false, selfHostedOnly: false };
39
+ }
40
+ if (item && typeof item === 'object' && typeof item.content === 'string') {
41
+ return {
42
+ content: item.content.trim(),
43
+ cloudOnly: item.cloud_only === true,
44
+ selfHostedOnly: item.self_hosted_only === true,
45
+ };
46
+ }
47
+ return null;
48
+ })
49
+ .filter((item) => item && item.content);
50
+ }
51
+
52
+ /**
53
+ * Builds the single view object property.hbs/topic-property.hbs render the
54
+ * "Related topics" row from: the normalized item list, plus which env-cloud
55
+ * branch (if any) every item shares, so the row's own header can be wrapped
56
+ * once instead of duplicated per item.
57
+ *
58
+ * sectionType is 'cloud' only when every item is cloud-only, 'self-managed'
59
+ * only when every item is self-hosted-only, and 'normal' otherwise (a mix of
60
+ * conditional and/or unconditional items, each wrapped individually).
61
+ *
62
+ * @param {object} property - A property record, as extracted or overridden.
63
+ * @returns {{items: Array, sectionType: ('cloud'|'self-managed'|'normal')}}
64
+ */
65
+ function seeAlsoView(property) {
66
+ const items = normalizeSeeAlso(property);
67
+ let sectionType = 'normal';
68
+ if (items.length > 0) {
69
+ if (items.every((item) => item.cloudOnly)) sectionType = 'cloud';
70
+ else if (items.every((item) => item.selfHostedOnly)) sectionType = 'self-managed';
71
+ }
72
+ return { items, sectionType };
73
+ }
74
+
75
+ module.exports = seeAlsoView;
76
+ module.exports.normalizeSeeAlso = normalizeSeeAlso;
@@ -1041,7 +1041,7 @@ def apply_property_overrides(properties, overrides, overrides_file_path=None):
1041
1041
  - Otherwise, searches existing properties for an entry whose `"name"` equals the override key and applies the override if found.
1042
1042
  - If no matching property is found, creates a new property from the override and adds it under the override key.
1043
1043
 
1044
- The function supports overrides that add or replace description, version, example content, default, type, config_scope, related_topics, and other metadata. When examples reference external files, relative paths are resolved relative to overrides_file_path.
1044
+ The function supports overrides that add or replace description, version, example content, default, type, config_scope, see_also (related_topics is the deprecated predecessor, still read for existing overrides), and other metadata. When examples reference external files, relative paths are resolved relative to overrides_file_path.
1045
1045
 
1046
1046
  Parameters:
1047
1047
  properties (dict): Mapping of existing property entries (modified in-place).
@@ -1144,13 +1144,24 @@ def _apply_override_to_existing_property(property_dict, override, overrides_file
1144
1144
  if "config_scope" in override:
1145
1145
  property_dict["config_scope"] = override["config_scope"]
1146
1146
 
1147
- # Apply related_topics override
1147
+ # Apply related_topics override (deprecated, see see_also)
1148
1148
  if "related_topics" in override:
1149
1149
  if isinstance(override["related_topics"], list):
1150
1150
  property_dict["related_topics"] = override["related_topics"]
1151
1151
  else:
1152
1152
  logger.warning(f"related_topics for property must be an array")
1153
1153
 
1154
+ # Apply see_also override. Structural shape (plain string, or an object
1155
+ # naming exactly one of cloud_only/self_hosted_only) is enforced by
1156
+ # docs-data/property-overrides.schema.json via `doc-tools validate
1157
+ # property-overrides`, not here — this just passes the data through for
1158
+ # seeAlsoView.js to normalize at render time.
1159
+ if "see_also" in override:
1160
+ if isinstance(override["see_also"], list):
1161
+ property_dict["see_also"] = override["see_also"]
1162
+ else:
1163
+ logger.warning(f"see_also for property must be an array")
1164
+
1154
1165
  # Apply exclude_from_docs override
1155
1166
  if "exclude_from_docs" in override:
1156
1167
  property_dict["exclude_from_docs"] = override["exclude_from_docs"]
@@ -1192,19 +1203,26 @@ def _create_property_from_override(prop_name, override, overrides_file_path):
1192
1203
  if example_content:
1193
1204
  new_property["example"] = example_content
1194
1205
 
1195
- # Add related_topics if specified
1206
+ # Add related_topics if specified (deprecated, see see_also)
1196
1207
  if "related_topics" in override:
1197
1208
  if isinstance(override["related_topics"], list):
1198
1209
  new_property["related_topics"] = override["related_topics"]
1199
1210
  else:
1200
1211
  logger.warning(f"related_topics for property '{prop_name}' must be an array")
1201
-
1212
+
1213
+ # Add see_also if specified
1214
+ if "see_also" in override:
1215
+ if isinstance(override["see_also"], list):
1216
+ new_property["see_also"] = override["see_also"]
1217
+ else:
1218
+ logger.warning(f"see_also for property '{prop_name}' must be an array")
1219
+
1202
1220
  # Add any other custom fields from override
1203
1221
  for key, value in override.items():
1204
1222
  if key not in ["description", "type", "default", "config_scope", "version",
1205
1223
  "example", "example_file", "example_yaml", "related_topics",
1206
- "is_deprecated", "visibility", "exclude_from_docs", "category",
1207
- "accepted_values", "_comment"]:
1224
+ "see_also", "is_deprecated", "visibility", "exclude_from_docs",
1225
+ "category", "accepted_values", "_comment"]:
1208
1226
  new_property[key] = value
1209
1227
 
1210
1228
  # Add exclude_from_docs if specified
@@ -167,78 +167,66 @@ endif::[]
167
167
  |
168
168
  {{{example}}}
169
169
  {{/if}}
170
- {{#if related_topics}}
171
- {{#with (allTopicsConditional related_topics) as |sectionType|}}
170
+ {{#with (seeAlsoView .) as |seeAlso|}}
171
+ {{#if seeAlso.items.length}}
172
172
 
173
- {{#if (eq sectionType "cloud")}}
173
+ {{#if (eq seeAlso.sectionType "cloud")}}
174
174
  ifdef::env-cloud[]
175
175
  | Related topics
176
- |{{#if (gt (length ../related_topics) 1)}}{{#each ../related_topics}}
177
- {{#with (parseRelatedTopic this)}}
178
- * {{{value}}}
179
- {{/with}}
176
+ |{{#if (gt seeAlso.items.length 1)}}{{#each seeAlso.items}}
177
+ * {{{content}}}
180
178
  {{/each}}
181
179
  {{else}}
182
- {{#each ../related_topics}}
183
- {{#with (parseRelatedTopic this)}}
184
- {{{value}}}
185
- {{/with}}
180
+ {{#each seeAlso.items}}
181
+ {{{content}}}
186
182
  {{/each}}
187
183
  {{/if}}
188
184
  endif::[]
189
- {{else if (eq sectionType "self-managed")}}
185
+ {{else if (eq seeAlso.sectionType "self-managed")}}
190
186
  ifndef::env-cloud[]
191
187
  | Related topics
192
- |{{#if (gt (length ../related_topics) 1)}}{{#each ../related_topics}}
193
- {{#with (parseRelatedTopic this)}}
194
- * {{{value}}}
195
- {{/with}}
188
+ |{{#if (gt seeAlso.items.length 1)}}{{#each seeAlso.items}}
189
+ * {{{content}}}
196
190
  {{/each}}
197
191
  {{else}}
198
- {{#each ../related_topics}}
199
- {{#with (parseRelatedTopic this)}}
200
- {{{value}}}
201
- {{/with}}
192
+ {{#each seeAlso.items}}
193
+ {{{content}}}
202
194
  {{/each}}
203
195
  {{/if}}
204
196
  endif::[]
205
197
  {{else}}
206
198
  | Related topics
207
- |{{#if (gt (length ../related_topics) 1)}}{{#each ../related_topics}}
208
- {{#with (parseRelatedTopic this)}}
209
- {{#if (eq type "cloud")}}
199
+ |{{#if (gt seeAlso.items.length 1)}}{{#each seeAlso.items}}
200
+ {{#if cloudOnly}}
210
201
  ifdef::env-cloud[]
211
- * {{{value}}}
202
+ * {{{content}}}
212
203
  endif::[]
213
- {{else if (eq type "self-managed")}}
204
+ {{else if selfHostedOnly}}
214
205
  ifndef::env-cloud[]
215
- * {{{value}}}
206
+ * {{{content}}}
216
207
  endif::[]
217
208
  {{else}}
218
- * {{{value}}}
209
+ * {{{content}}}
219
210
  {{/if}}
220
- {{/with}}
221
211
  {{/each}}
222
212
  {{else}}
223
- {{#each ../related_topics}}
224
- {{#with (parseRelatedTopic this)}}
225
- {{#if (eq type "cloud")}}
213
+ {{#each seeAlso.items}}
214
+ {{#if cloudOnly}}
226
215
  ifdef::env-cloud[]
227
- {{{value}}}
216
+ {{{content}}}
228
217
  endif::[]
229
- {{else if (eq type "self-managed")}}
218
+ {{else if selfHostedOnly}}
230
219
  ifndef::env-cloud[]
231
- {{{value}}}
220
+ {{{content}}}
232
221
  endif::[]
233
222
  {{else}}
234
- {{{value}}}
223
+ {{{content}}}
235
224
  {{/if}}
236
- {{/with}}
237
225
  {{/each}}
238
226
  {{/if}}
239
227
  {{/if}}
240
- {{/with}}
241
228
  {{/if}}
229
+ {{/with}}
242
230
 
243
231
  |===
244
232
 
@@ -130,78 +130,66 @@ endif::[]
130
130
  |
131
131
  {{{example}}}
132
132
  {{/if}}
133
- {{#if related_topics}}
134
- {{#with (allTopicsConditional related_topics) as |sectionType|}}
133
+ {{#with (seeAlsoView .) as |seeAlso|}}
134
+ {{#if seeAlso.items.length}}
135
135
 
136
- {{#if (eq sectionType "cloud")}}
136
+ {{#if (eq seeAlso.sectionType "cloud")}}
137
137
  ifdef::env-cloud[]
138
138
  | Related topics
139
- |{{#if (gt (length ../related_topics) 1)}}{{#each ../related_topics}}
140
- {{#with (parseRelatedTopic this)}}
141
- * {{{value}}}
142
- {{/with}}
139
+ |{{#if (gt seeAlso.items.length 1)}}{{#each seeAlso.items}}
140
+ * {{{content}}}
143
141
  {{/each}}
144
142
  {{else}}
145
- {{#each ../related_topics}}
146
- {{#with (parseRelatedTopic this)}}
147
- {{{value}}}
148
- {{/with}}
143
+ {{#each seeAlso.items}}
144
+ {{{content}}}
149
145
  {{/each}}
150
146
  {{/if}}
151
147
  endif::[]
152
- {{else if (eq sectionType "self-managed")}}
148
+ {{else if (eq seeAlso.sectionType "self-managed")}}
153
149
  ifndef::env-cloud[]
154
150
  | Related topics
155
- |{{#if (gt (length ../related_topics) 1)}}{{#each ../related_topics}}
156
- {{#with (parseRelatedTopic this)}}
157
- * {{{value}}}
158
- {{/with}}
151
+ |{{#if (gt seeAlso.items.length 1)}}{{#each seeAlso.items}}
152
+ * {{{content}}}
159
153
  {{/each}}
160
154
  {{else}}
161
- {{#each ../related_topics}}
162
- {{#with (parseRelatedTopic this)}}
163
- {{{value}}}
164
- {{/with}}
155
+ {{#each seeAlso.items}}
156
+ {{{content}}}
165
157
  {{/each}}
166
158
  {{/if}}
167
159
  endif::[]
168
160
  {{else}}
169
161
  | Related topics
170
- |{{#if (gt (length ../related_topics) 1)}}{{#each ../related_topics}}
171
- {{#with (parseRelatedTopic this)}}
172
- {{#if (eq type "cloud")}}
162
+ |{{#if (gt seeAlso.items.length 1)}}{{#each seeAlso.items}}
163
+ {{#if cloudOnly}}
173
164
  ifdef::env-cloud[]
174
- * {{{value}}}
165
+ * {{{content}}}
175
166
  endif::[]
176
- {{else if (eq type "self-managed")}}
167
+ {{else if selfHostedOnly}}
177
168
  ifndef::env-cloud[]
178
- * {{{value}}}
169
+ * {{{content}}}
179
170
  endif::[]
180
171
  {{else}}
181
- * {{{value}}}
172
+ * {{{content}}}
182
173
  {{/if}}
183
- {{/with}}
184
174
  {{/each}}
185
175
  {{else}}
186
- {{#each ../related_topics}}
187
- {{#with (parseRelatedTopic this)}}
188
- {{#if (eq type "cloud")}}
176
+ {{#each seeAlso.items}}
177
+ {{#if cloudOnly}}
189
178
  ifdef::env-cloud[]
190
- {{{value}}}
179
+ {{{content}}}
191
180
  endif::[]
192
- {{else if (eq type "self-managed")}}
181
+ {{else if selfHostedOnly}}
193
182
  ifndef::env-cloud[]
194
- {{{value}}}
183
+ {{{content}}}
195
184
  endif::[]
196
185
  {{else}}
197
- {{{value}}}
186
+ {{{content}}}
198
187
  {{/if}}
199
- {{/with}}
200
188
  {{/each}}
201
189
  {{/if}}
202
190
  {{/if}}
203
- {{/with}}
204
191
  {{/if}}
192
+ {{/with}}
205
193
  {{#if aliases}}
206
194
 
207
195
  ifndef::env-cloud[]
@@ -0,0 +1,120 @@
1
+ 'use strict'
2
+
3
+ const Ajv2020 = require('ajv/dist/2020')
4
+ const fs = require('fs')
5
+ const path = require('path')
6
+ const { ValidationResult } = require('../../cli-utils/validation-result')
7
+
8
+ /**
9
+ * Load and compile the JSON Schema for validation
10
+ * @param {string} [overridesPath] - Path to the overrides file; the schema is
11
+ * looked up as a sibling (same directory) before falling back to the
12
+ * package-relative path.
13
+ * @returns {Function} Compiled schema validator
14
+ */
15
+ function loadSchemaValidator(overridesPath) {
16
+ // Primary: look for the schema next to the overrides file (docs repo's docs-data/).
17
+ if (overridesPath) {
18
+ const siblingPath = path.join(path.dirname(path.resolve(overridesPath)), 'property-overrides.schema.json')
19
+ if (fs.existsSync(siblingPath)) {
20
+ const schema = JSON.parse(fs.readFileSync(siblingPath, 'utf8'))
21
+ const ajv = new Ajv2020({ allErrors: true, verbose: true })
22
+ return ajv.compile(schema)
23
+ }
24
+ }
25
+
26
+ // Fallback: package-relative path (legacy / monorepo usage).
27
+ const schemaPath = path.resolve(__dirname, '../../docs-data/property-overrides.schema.json')
28
+
29
+ if (!fs.existsSync(schemaPath)) {
30
+ throw new Error(`Schema file not found: ${schemaPath}`)
31
+ }
32
+
33
+ const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
34
+ const ajv = new Ajv2020({ allErrors: true, verbose: true })
35
+ return ajv.compile(schema)
36
+ }
37
+
38
+ /**
39
+ * Validate overrides against the JSON Schema
40
+ * @param {Object} overrides - Overrides object to validate
41
+ * @param {string} [overridesPath] - Path to the overrides file (used to locate the schema)
42
+ * @returns {ValidationResult}
43
+ */
44
+ function validateSchema(overrides, overridesPath) {
45
+ const result = new ValidationResult()
46
+
47
+ try {
48
+ const validate = loadSchemaValidator(overridesPath)
49
+ const valid = validate(overrides)
50
+
51
+ if (!valid) {
52
+ for (const error of validate.errors || []) {
53
+ const instancePath = error.instancePath || '/'
54
+ result.addError(error.message, instancePath)
55
+ }
56
+ }
57
+ } catch (err) {
58
+ result.addError(`Schema validation failed: ${err.message}`)
59
+ }
60
+
61
+ return result
62
+ }
63
+
64
+ /**
65
+ * Run all validations on a property-overrides object.
66
+ * @param {Object} overrides - Overrides object to validate
67
+ * @param {string} [overridesPath] - Path to the overrides file (used to locate the schema)
68
+ * @returns {ValidationResult}
69
+ */
70
+ function validateOverrides(overrides, overridesPath = null) {
71
+ const result = new ValidationResult()
72
+
73
+ if (!overrides || typeof overrides !== 'object') {
74
+ result.addError('Overrides must be a non-null object')
75
+ return result
76
+ }
77
+
78
+ result.merge(validateSchema(overrides, overridesPath))
79
+
80
+ return result
81
+ }
82
+
83
+ /**
84
+ * Load and validate a property-overrides.json file.
85
+ * @param {string} overridesPath - Path to overrides JSON file
86
+ * @returns {{ overrides: Object|null, validation: ValidationResult }}
87
+ */
88
+ function loadAndValidateOverrides(overridesPath) {
89
+ const validation = new ValidationResult()
90
+
91
+ if (!overridesPath) {
92
+ return { overrides: null, validation }
93
+ }
94
+
95
+ if (!fs.existsSync(overridesPath)) {
96
+ validation.addWarning(`Overrides file not found: ${overridesPath}`)
97
+ return { overrides: null, validation }
98
+ }
99
+
100
+ let overrides
101
+ try {
102
+ const content = fs.readFileSync(overridesPath, 'utf8')
103
+ overrides = JSON.parse(content)
104
+ } catch (err) {
105
+ validation.addError(`Failed to parse overrides JSON: ${err.message}`, overridesPath)
106
+ return { overrides: null, validation }
107
+ }
108
+
109
+ validation.merge(validateOverrides(overrides, overridesPath))
110
+
111
+ return { overrides, validation }
112
+ }
113
+
114
+ module.exports = {
115
+ ValidationResult,
116
+ loadSchemaValidator,
117
+ validateSchema,
118
+ validateOverrides,
119
+ loadAndValidateOverrides,
120
+ }
@@ -3,6 +3,7 @@
3
3
  const Ajv2020 = require('ajv/dist/2020')
4
4
  const fs = require('fs')
5
5
  const path = require('path')
6
+ const { ValidationResult } = require('../../cli-utils/validation-result')
6
7
 
7
8
  /**
8
9
  * Valid admonition locations for notes, warnings, tips, cautions, importants
@@ -50,51 +51,6 @@ const VALID_SECTION_POSITIONS = [
50
51
  'end'
51
52
  ]
52
53
 
53
- /**
54
- * Validation result object
55
- */
56
- class ValidationResult {
57
- constructor() {
58
- this.valid = true
59
- this.errors = []
60
- this.warnings = []
61
- }
62
-
63
- addError(message, context = null) {
64
- this.valid = false
65
- this.errors.push({ message, context })
66
- }
67
-
68
- addWarning(message, context = null) {
69
- this.warnings.push({ message, context })
70
- }
71
-
72
- merge(other) {
73
- if (!other.valid) this.valid = false
74
- this.errors.push(...other.errors)
75
- this.warnings.push(...other.warnings)
76
- }
77
-
78
- format() {
79
- const lines = []
80
- if (this.errors.length > 0) {
81
- lines.push('ERRORS:')
82
- for (const err of this.errors) {
83
- lines.push(` ✗ ${err.message}`)
84
- if (err.context) lines.push(` at: ${err.context}`)
85
- }
86
- }
87
- if (this.warnings.length > 0) {
88
- lines.push('WARNINGS:')
89
- for (const warn of this.warnings) {
90
- lines.push(` ⚠ ${warn.message}`)
91
- if (warn.context) lines.push(` at: ${warn.context}`)
92
- }
93
- }
94
- return lines.join('\n')
95
- }
96
- }
97
-
98
54
  /**
99
55
  * Normalize a command path (trim whitespace, collapse multiple spaces)
100
56
  * @param {string} cmdPath - Command path to normalize
@@ -1,19 +0,0 @@
1
- // Returns 'cloud' if all topics are cloud-only, 'self-managed' if all are self-managed-only, else 'normal'
2
- module.exports = function allTopicsConditional(related_topics) {
3
- if (!Array.isArray(related_topics) || related_topics.length === 0) return null;
4
- let allCloud = true;
5
- let allSelfManaged = true;
6
- for (const t of related_topics) {
7
- if (typeof t !== 'string') {
8
- allCloud = false;
9
- allSelfManaged = false;
10
- break;
11
- }
12
- const trimmed = t.trim();
13
- if (!trimmed.startsWith('cloud-only:')) allCloud = false;
14
- if (!trimmed.startsWith('self-managed-only:')) allSelfManaged = false;
15
- }
16
- if (allCloud) return 'cloud';
17
- if (allSelfManaged) return 'self-managed';
18
- return 'normal';
19
- };
@@ -1,13 +0,0 @@
1
- // Returns an object with type and value for a related topic
2
- // type: 'cloud', 'self-managed', or 'normal'
3
- module.exports = function parseRelatedTopic(topic) {
4
- if (typeof topic !== 'string') return { type: 'normal', value: topic };
5
- const trimmed = topic.trim();
6
- if (trimmed.startsWith('cloud-only:')) {
7
- return { type: 'cloud', value: trimmed.replace(/^cloud-only:/, '').trim() };
8
- }
9
- if (trimmed.startsWith('self-managed-only:')) {
10
- return { type: 'self-managed', value: trimmed.replace(/^self-managed-only:/, '').trim() };
11
- }
12
- return { type: 'normal', value: trimmed };
13
- };