@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 +82 -0
- package/cli-utils/validation-result.js +53 -0
- package/extension-utils/llms-utils.js +26 -0
- package/extensions/convert-llms-to-txt.js +6 -2
- package/macros/REFERENCE.adoc +3 -2
- package/macros/prop.js +34 -6
- package/package.json +1 -1
- package/tools/property-extractor/README.adoc +17 -3
- package/tools/property-extractor/helpers/index.js +1 -2
- package/tools/property-extractor/helpers/seeAlsoView.js +76 -0
- package/tools/property-extractor/property_extractor.py +24 -6
- package/tools/property-extractor/templates/property.hbs +25 -37
- package/tools/property-extractor/templates/topic-property.hbs +25 -37
- package/tools/property-extractor/validate-overrides.js +120 -0
- package/tools/rpk-docs/validate-overrides.js +1 -45
- package/tools/property-extractor/helpers/allTopicsConditional.js +0 -19
- package/tools/property-extractor/helpers/parseRelatedTopic.js +0 -13
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
|
|
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`;
|
package/macros/REFERENCE.adoc
CHANGED
|
@@ -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`).
|
|
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
|
|
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.
|
|
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
|
|
60
|
-
* page target is guessed from a property's scope
|
|
61
|
-
* verify would be a broken link.
|
|
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 (
|
|
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
|
|
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
|
@@ -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
|
-
"
|
|
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
|
-
* `
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
{{#
|
|
171
|
-
{{#
|
|
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
|
|
177
|
-
{{
|
|
178
|
-
* {{{value}}}
|
|
179
|
-
{{/with}}
|
|
176
|
+
|{{#if (gt seeAlso.items.length 1)}}{{#each seeAlso.items}}
|
|
177
|
+
* {{{content}}}
|
|
180
178
|
{{/each}}
|
|
181
179
|
{{else}}
|
|
182
|
-
{{#each
|
|
183
|
-
{{
|
|
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
|
|
193
|
-
{{
|
|
194
|
-
* {{{value}}}
|
|
195
|
-
{{/with}}
|
|
188
|
+
|{{#if (gt seeAlso.items.length 1)}}{{#each seeAlso.items}}
|
|
189
|
+
* {{{content}}}
|
|
196
190
|
{{/each}}
|
|
197
191
|
{{else}}
|
|
198
|
-
{{#each
|
|
199
|
-
{{
|
|
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
|
|
208
|
-
{{#
|
|
209
|
-
{{#if (eq type "cloud")}}
|
|
199
|
+
|{{#if (gt seeAlso.items.length 1)}}{{#each seeAlso.items}}
|
|
200
|
+
{{#if cloudOnly}}
|
|
210
201
|
ifdef::env-cloud[]
|
|
211
|
-
* {{{
|
|
202
|
+
* {{{content}}}
|
|
212
203
|
endif::[]
|
|
213
|
-
{{else if
|
|
204
|
+
{{else if selfHostedOnly}}
|
|
214
205
|
ifndef::env-cloud[]
|
|
215
|
-
* {{{
|
|
206
|
+
* {{{content}}}
|
|
216
207
|
endif::[]
|
|
217
208
|
{{else}}
|
|
218
|
-
* {{{
|
|
209
|
+
* {{{content}}}
|
|
219
210
|
{{/if}}
|
|
220
|
-
{{/with}}
|
|
221
211
|
{{/each}}
|
|
222
212
|
{{else}}
|
|
223
|
-
{{#each
|
|
224
|
-
{{#
|
|
225
|
-
{{#if (eq type "cloud")}}
|
|
213
|
+
{{#each seeAlso.items}}
|
|
214
|
+
{{#if cloudOnly}}
|
|
226
215
|
ifdef::env-cloud[]
|
|
227
|
-
{{{
|
|
216
|
+
{{{content}}}
|
|
228
217
|
endif::[]
|
|
229
|
-
{{else if
|
|
218
|
+
{{else if selfHostedOnly}}
|
|
230
219
|
ifndef::env-cloud[]
|
|
231
|
-
{{{
|
|
220
|
+
{{{content}}}
|
|
232
221
|
endif::[]
|
|
233
222
|
{{else}}
|
|
234
|
-
{{{
|
|
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
|
-
{{#
|
|
134
|
-
{{#
|
|
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
|
|
140
|
-
{{
|
|
141
|
-
* {{{value}}}
|
|
142
|
-
{{/with}}
|
|
139
|
+
|{{#if (gt seeAlso.items.length 1)}}{{#each seeAlso.items}}
|
|
140
|
+
* {{{content}}}
|
|
143
141
|
{{/each}}
|
|
144
142
|
{{else}}
|
|
145
|
-
{{#each
|
|
146
|
-
{{
|
|
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
|
|
156
|
-
{{
|
|
157
|
-
* {{{value}}}
|
|
158
|
-
{{/with}}
|
|
151
|
+
|{{#if (gt seeAlso.items.length 1)}}{{#each seeAlso.items}}
|
|
152
|
+
* {{{content}}}
|
|
159
153
|
{{/each}}
|
|
160
154
|
{{else}}
|
|
161
|
-
{{#each
|
|
162
|
-
{{
|
|
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
|
|
171
|
-
{{#
|
|
172
|
-
{{#if (eq type "cloud")}}
|
|
162
|
+
|{{#if (gt seeAlso.items.length 1)}}{{#each seeAlso.items}}
|
|
163
|
+
{{#if cloudOnly}}
|
|
173
164
|
ifdef::env-cloud[]
|
|
174
|
-
* {{{
|
|
165
|
+
* {{{content}}}
|
|
175
166
|
endif::[]
|
|
176
|
-
{{else if
|
|
167
|
+
{{else if selfHostedOnly}}
|
|
177
168
|
ifndef::env-cloud[]
|
|
178
|
-
* {{{
|
|
169
|
+
* {{{content}}}
|
|
179
170
|
endif::[]
|
|
180
171
|
{{else}}
|
|
181
|
-
* {{{
|
|
172
|
+
* {{{content}}}
|
|
182
173
|
{{/if}}
|
|
183
|
-
{{/with}}
|
|
184
174
|
{{/each}}
|
|
185
175
|
{{else}}
|
|
186
|
-
{{#each
|
|
187
|
-
{{#
|
|
188
|
-
{{#if (eq type "cloud")}}
|
|
176
|
+
{{#each seeAlso.items}}
|
|
177
|
+
{{#if cloudOnly}}
|
|
189
178
|
ifdef::env-cloud[]
|
|
190
|
-
{{{
|
|
179
|
+
{{{content}}}
|
|
191
180
|
endif::[]
|
|
192
|
-
{{else if
|
|
181
|
+
{{else if selfHostedOnly}}
|
|
193
182
|
ifndef::env-cloud[]
|
|
194
|
-
{{{
|
|
183
|
+
{{{content}}}
|
|
195
184
|
endif::[]
|
|
196
185
|
{{else}}
|
|
197
|
-
{{{
|
|
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
|
-
};
|