@redpanda-data/docs-extensions-and-macros 5.0.4 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doc-tools-mcp.js +30 -2
- package/bin/doc-tools.js +260 -51
- package/bin/mcp-tools/rpk-docs.js +109 -39
- package/extensions/version-fetcher/set-latest-version.js +0 -131
- package/package.json +2 -2
- package/tools/rpk-docs/DEPRECATION-SCANNING.md +192 -0
- package/tools/rpk-docs/extract-conditional-content.js +169 -0
- package/tools/rpk-docs/extract-overrides.js +566 -0
- package/tools/rpk-docs/generate-rpk-docs.js +2433 -0
- package/tools/rpk-docs/helpers/index.js +458 -0
- package/tools/rpk-docs/report-delta.js +479 -0
- package/tools/rpk-docs/rpk-docs-handler.js +1650 -0
- package/tools/rpk-docs/scan-deprecated-commands.js +295 -0
- package/tools/rpk-docs/templates/command.hbs +576 -0
- package/tools/rpk-docs/templates/content-item.hbs +106 -0
- package/tools/rpk-docs/templates/subsection.hbs +37 -0
- package/tools/rpk-docs/validate-output.js +414 -0
- package/tools/rpk-docs/validate-overrides.js +624 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Ajv2020 = require('ajv/dist/2020')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Valid admonition locations for notes, warnings, tips, cautions, importants
|
|
9
|
+
*/
|
|
10
|
+
const VALID_ADMONITION_LOCATIONS = [
|
|
11
|
+
'after_header',
|
|
12
|
+
'after_description',
|
|
13
|
+
'after_usage',
|
|
14
|
+
'after_aliases',
|
|
15
|
+
'after_flags',
|
|
16
|
+
'after_examples',
|
|
17
|
+
'before_see_also',
|
|
18
|
+
'end'
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Valid platforms
|
|
23
|
+
*/
|
|
24
|
+
const VALID_PLATFORMS = ['linux', 'darwin', 'windows']
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Valid include/preservation locations
|
|
28
|
+
*/
|
|
29
|
+
const VALID_INCLUDE_LOCATIONS = [
|
|
30
|
+
'after_header',
|
|
31
|
+
'after_description',
|
|
32
|
+
'after_usage',
|
|
33
|
+
'after_aliases',
|
|
34
|
+
'after_flags',
|
|
35
|
+
'after_modifiers',
|
|
36
|
+
'after_examples',
|
|
37
|
+
'before_see_also',
|
|
38
|
+
'end'
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Valid custom section positions
|
|
43
|
+
*/
|
|
44
|
+
const VALID_SECTION_POSITIONS = [
|
|
45
|
+
'after_description',
|
|
46
|
+
'after_usage',
|
|
47
|
+
'after_flags',
|
|
48
|
+
'after_examples',
|
|
49
|
+
'before_see_also',
|
|
50
|
+
'end'
|
|
51
|
+
]
|
|
52
|
+
|
|
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
|
+
/**
|
|
99
|
+
* Normalize a command path (trim whitespace, collapse multiple spaces)
|
|
100
|
+
* @param {string} cmdPath - Command path to normalize
|
|
101
|
+
* @returns {string} Normalized command path
|
|
102
|
+
*/
|
|
103
|
+
function normalizeCommandPath(cmdPath) {
|
|
104
|
+
if (!cmdPath || typeof cmdPath !== 'string') return ''
|
|
105
|
+
return cmdPath.trim().replace(/\s+/g, ' ')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Load and compile the JSON Schema for validation
|
|
110
|
+
* @returns {Function} Compiled schema validator
|
|
111
|
+
*/
|
|
112
|
+
function loadSchemaValidator() {
|
|
113
|
+
const schemaPath = path.resolve(__dirname, '../../docs-data/rpk-overrides.schema.json')
|
|
114
|
+
|
|
115
|
+
if (!fs.existsSync(schemaPath)) {
|
|
116
|
+
throw new Error(`Schema file not found: ${schemaPath}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
|
|
120
|
+
const ajv = new Ajv2020({ allErrors: true, verbose: true })
|
|
121
|
+
return ajv.compile(schema)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validate overrides against JSON Schema
|
|
126
|
+
* @param {Object} overrides - Overrides object to validate
|
|
127
|
+
* @returns {ValidationResult}
|
|
128
|
+
*/
|
|
129
|
+
function validateSchema(overrides) {
|
|
130
|
+
const result = new ValidationResult()
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const validate = loadSchemaValidator()
|
|
134
|
+
const valid = validate(overrides)
|
|
135
|
+
|
|
136
|
+
if (!valid) {
|
|
137
|
+
for (const error of validate.errors || []) {
|
|
138
|
+
const path = error.instancePath || '/'
|
|
139
|
+
const message = `${error.message}`
|
|
140
|
+
result.addError(message, path)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
result.addError(`Schema validation failed: ${err.message}`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Validate command paths in overrides against actual command tree
|
|
152
|
+
* @param {Object} overrides - Overrides object (resolved)
|
|
153
|
+
* @param {Object} commandTree - rpk command tree
|
|
154
|
+
* @returns {ValidationResult}
|
|
155
|
+
*/
|
|
156
|
+
function validateCommandPaths(overrides, commandTree) {
|
|
157
|
+
const result = new ValidationResult()
|
|
158
|
+
|
|
159
|
+
if (!overrides || !overrides.commands) return result
|
|
160
|
+
if (!commandTree) {
|
|
161
|
+
result.addWarning('No command tree provided, skipping command path validation')
|
|
162
|
+
return result
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Flatten command tree to get all valid paths
|
|
166
|
+
const validPaths = new Set()
|
|
167
|
+
const flattenTree = (node, parentPath = '') => {
|
|
168
|
+
const currentPath = parentPath ? `${parentPath} ${node.name}` : node.name
|
|
169
|
+
validPaths.add(currentPath)
|
|
170
|
+
if (node.commands && Array.isArray(node.commands)) {
|
|
171
|
+
for (const child of node.commands) {
|
|
172
|
+
flattenTree(child, currentPath)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
flattenTree(commandTree)
|
|
177
|
+
|
|
178
|
+
// Check each override command path
|
|
179
|
+
for (const cmdPath of Object.keys(overrides.commands)) {
|
|
180
|
+
const normalized = normalizeCommandPath(cmdPath)
|
|
181
|
+
|
|
182
|
+
// Check for normalization issues
|
|
183
|
+
if (normalized !== cmdPath) {
|
|
184
|
+
result.addWarning(
|
|
185
|
+
`Command path has irregular whitespace: "${cmdPath}"`,
|
|
186
|
+
`commands["${cmdPath}"]`
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check if path exists in tree
|
|
191
|
+
if (!validPaths.has(normalized)) {
|
|
192
|
+
// Find similar paths for suggestion
|
|
193
|
+
const similar = [...validPaths]
|
|
194
|
+
.filter(p => {
|
|
195
|
+
const parts = normalized.split(' ')
|
|
196
|
+
const pParts = p.split(' ')
|
|
197
|
+
return parts.some(part => pParts.includes(part))
|
|
198
|
+
})
|
|
199
|
+
.slice(0, 3)
|
|
200
|
+
|
|
201
|
+
let message = `Unknown command path: "${cmdPath}"`
|
|
202
|
+
if (similar.length > 0) {
|
|
203
|
+
message += `\n Did you mean one of: ${similar.join(', ')}?`
|
|
204
|
+
}
|
|
205
|
+
result.addError(message, `commands["${cmdPath}"]`)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Unescape RFC 6901 JSON Pointer encoding
|
|
214
|
+
* @param {string} segment - Path segment to unescape
|
|
215
|
+
* @returns {string} Unescaped segment
|
|
216
|
+
*/
|
|
217
|
+
function unescapeRfc6901(segment) {
|
|
218
|
+
// RFC 6901: ~1 → /, ~0 → ~ (order matters: ~1 first)
|
|
219
|
+
return segment.replace(/~1/g, '/').replace(/~0/g, '~')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Validate $ref and $refs references in overrides
|
|
224
|
+
* @param {Object} overrides - Raw overrides object (before resolution)
|
|
225
|
+
* @returns {ValidationResult}
|
|
226
|
+
*/
|
|
227
|
+
function validateReferences(overrides) {
|
|
228
|
+
const result = new ValidationResult()
|
|
229
|
+
|
|
230
|
+
if (!overrides) return result
|
|
231
|
+
|
|
232
|
+
// Track visited refs for cycle detection
|
|
233
|
+
const checkRef = (ref, context, visited = new Set()) => {
|
|
234
|
+
if (!ref || typeof ref !== 'string') {
|
|
235
|
+
result.addError(`Invalid $ref value: ${ref}`, context)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Validate ref format
|
|
240
|
+
if (!ref.startsWith('#/')) {
|
|
241
|
+
result.addError(
|
|
242
|
+
`Invalid $ref format: "${ref}" (must start with #/)`,
|
|
243
|
+
context
|
|
244
|
+
)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check for cycles
|
|
249
|
+
if (visited.has(ref)) {
|
|
250
|
+
result.addError(
|
|
251
|
+
`Circular reference detected: "${ref}"`,
|
|
252
|
+
context
|
|
253
|
+
)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Resolve the reference with RFC 6901 unescaping
|
|
258
|
+
const refPath = ref.replace(/^#\//, '').split('/').map(unescapeRfc6901)
|
|
259
|
+
let resolved = overrides
|
|
260
|
+
for (const part of refPath) {
|
|
261
|
+
if (resolved && typeof resolved === 'object') {
|
|
262
|
+
resolved = resolved[part]
|
|
263
|
+
} else {
|
|
264
|
+
resolved = undefined
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (resolved === undefined) {
|
|
270
|
+
result.addError(
|
|
271
|
+
`Cannot resolve $ref: "${ref}" - path does not exist`,
|
|
272
|
+
context
|
|
273
|
+
)
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Check for nested references (cycle detection)
|
|
278
|
+
const newVisited = new Set(visited)
|
|
279
|
+
newVisited.add(ref)
|
|
280
|
+
checkObjectForRefs(resolved, `${context} -> ${ref}`, newVisited)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const checkRefs = (refs, context, visited = new Set()) => {
|
|
284
|
+
if (!Array.isArray(refs)) {
|
|
285
|
+
result.addError(`$refs must be an array`, context)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
for (let i = 0; i < refs.length; i++) {
|
|
289
|
+
// Each array item gets a fresh copy of the visited set
|
|
290
|
+
// This allows the same definition to be referenced multiple times
|
|
291
|
+
// without being incorrectly flagged as a cycle
|
|
292
|
+
checkRef(refs[i], `${context}.$refs[${i}]`, new Set(visited))
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check object for $ref/$refs with cycle tracking
|
|
297
|
+
const checkObjectForRefs = (obj, context = '', visited = new Set()) => {
|
|
298
|
+
if (!obj || typeof obj !== 'object') return
|
|
299
|
+
if (Array.isArray(obj)) {
|
|
300
|
+
obj.forEach((item, i) => checkObjectForRefs(item, `${context}[${i}]`, visited))
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (obj.$ref) {
|
|
305
|
+
checkRef(obj.$ref, context, visited)
|
|
306
|
+
}
|
|
307
|
+
if (obj.$refs) {
|
|
308
|
+
checkRefs(obj.$refs, context, visited)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
312
|
+
if (key !== '$ref' && key !== '$refs') {
|
|
313
|
+
checkObjectForRefs(value, context ? `${context}.${key}` : key, visited)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
checkObjectForRefs(overrides)
|
|
319
|
+
return result
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Validate admonition locations
|
|
324
|
+
* @param {Object} commandOverride - Single command override object
|
|
325
|
+
* @param {string} context - Context for error messages
|
|
326
|
+
* @returns {ValidationResult}
|
|
327
|
+
*/
|
|
328
|
+
function validateAdmonitionLocations(commandOverride, context) {
|
|
329
|
+
const result = new ValidationResult()
|
|
330
|
+
|
|
331
|
+
const admonitionTypes = ['notes', 'warnings', 'tips', 'cautions', 'importants']
|
|
332
|
+
|
|
333
|
+
for (const type of admonitionTypes) {
|
|
334
|
+
if (commandOverride[type]) {
|
|
335
|
+
for (const location of Object.keys(commandOverride[type])) {
|
|
336
|
+
if (!VALID_ADMONITION_LOCATIONS.includes(location)) {
|
|
337
|
+
result.addError(
|
|
338
|
+
`Invalid ${type} location: "${location}"`,
|
|
339
|
+
`${context}.${type}.${location}`
|
|
340
|
+
)
|
|
341
|
+
result.addWarning(
|
|
342
|
+
`Valid locations are: ${VALID_ADMONITION_LOCATIONS.join(', ')}`
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return result
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Validate include locations
|
|
354
|
+
* @param {Object} commandOverride - Single command override object
|
|
355
|
+
* @param {string} context - Context for error messages
|
|
356
|
+
* @returns {ValidationResult}
|
|
357
|
+
*/
|
|
358
|
+
function validateIncludeLocations(commandOverride, context) {
|
|
359
|
+
const result = new ValidationResult()
|
|
360
|
+
|
|
361
|
+
if (commandOverride.includes) {
|
|
362
|
+
for (const location of Object.keys(commandOverride.includes)) {
|
|
363
|
+
if (!VALID_INCLUDE_LOCATIONS.includes(location)) {
|
|
364
|
+
result.addError(
|
|
365
|
+
`Invalid include location: "${location}"`,
|
|
366
|
+
`${context}.includes.${location}`
|
|
367
|
+
)
|
|
368
|
+
result.addWarning(
|
|
369
|
+
`Valid locations are: ${VALID_INCLUDE_LOCATIONS.join(', ')}`
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check cloudContent and selfHostedContent
|
|
376
|
+
for (const contentType of ['cloudContent', 'selfHostedContent']) {
|
|
377
|
+
if (commandOverride[contentType]) {
|
|
378
|
+
for (const location of Object.keys(commandOverride[contentType])) {
|
|
379
|
+
if (!VALID_INCLUDE_LOCATIONS.includes(location)) {
|
|
380
|
+
result.addError(
|
|
381
|
+
`Invalid ${contentType} location: "${location}"`,
|
|
382
|
+
`${context}.${contentType}.${location}`
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return result
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Validate custom section positions
|
|
394
|
+
* @param {Object} commandOverride - Single command override object
|
|
395
|
+
* @param {string} context - Context for error messages
|
|
396
|
+
* @returns {ValidationResult}
|
|
397
|
+
*/
|
|
398
|
+
function validateCustomSectionPositions(commandOverride, context) {
|
|
399
|
+
const result = new ValidationResult()
|
|
400
|
+
|
|
401
|
+
if (commandOverride.customSections) {
|
|
402
|
+
for (const [name, section] of Object.entries(commandOverride.customSections)) {
|
|
403
|
+
if (section.position && !VALID_SECTION_POSITIONS.includes(section.position)) {
|
|
404
|
+
result.addError(
|
|
405
|
+
`Invalid custom section position: "${section.position}"`,
|
|
406
|
+
`${context}.customSections.${name}.position`
|
|
407
|
+
)
|
|
408
|
+
result.addWarning(
|
|
409
|
+
`Valid positions are: ${VALID_SECTION_POSITIONS.join(', ')}`
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return result
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Validate platform values
|
|
420
|
+
* @param {Object} commandOverride - Single command override object
|
|
421
|
+
* @param {string} context - Context for error messages
|
|
422
|
+
* @returns {ValidationResult}
|
|
423
|
+
*/
|
|
424
|
+
function validatePlatforms(commandOverride, context) {
|
|
425
|
+
const result = new ValidationResult()
|
|
426
|
+
|
|
427
|
+
if (commandOverride.platforms) {
|
|
428
|
+
for (const platform of commandOverride.platforms) {
|
|
429
|
+
if (!VALID_PLATFORMS.includes(platform)) {
|
|
430
|
+
result.addError(
|
|
431
|
+
`Invalid platform: "${platform}"`,
|
|
432
|
+
`${context}.platforms`
|
|
433
|
+
)
|
|
434
|
+
result.addWarning(
|
|
435
|
+
`Valid platforms are: ${VALID_PLATFORMS.join(', ')}`
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return result
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Validate flag overrides
|
|
446
|
+
* @param {Object} commandOverride - Single command override object
|
|
447
|
+
* @param {string} context - Context for error messages
|
|
448
|
+
* @returns {ValidationResult}
|
|
449
|
+
*/
|
|
450
|
+
function validateFlags(commandOverride, context) {
|
|
451
|
+
const result = new ValidationResult()
|
|
452
|
+
|
|
453
|
+
if (commandOverride.flags) {
|
|
454
|
+
for (const [flagName, flagOverride] of Object.entries(commandOverride.flags)) {
|
|
455
|
+
// Validate flag name format
|
|
456
|
+
if (!flagName.match(/^[a-zA-Z][-a-zA-Z0-9]*$/)) {
|
|
457
|
+
result.addWarning(
|
|
458
|
+
`Unusual flag name format: "${flagName}"`,
|
|
459
|
+
`${context}.flags.${flagName}`
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Validate type if specified
|
|
464
|
+
if (flagOverride.type) {
|
|
465
|
+
const validTypes = ['bool', 'string', 'int', 'int32', 'int64', 'uint', 'uint32',
|
|
466
|
+
'uint64', 'float', 'float32', 'float64', 'duration',
|
|
467
|
+
'stringSlice', 'stringArray', 'intSlice', '-']
|
|
468
|
+
if (!validTypes.includes(flagOverride.type)) {
|
|
469
|
+
result.addWarning(
|
|
470
|
+
`Unusual flag type: "${flagOverride.type}" for flag "${flagName}"`,
|
|
471
|
+
`${context}.flags.${flagName}.type`
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return result
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Validate description content (check for common issues)
|
|
483
|
+
* @param {Object} commandOverride - Single command override object
|
|
484
|
+
* @param {string} context - Context for error messages
|
|
485
|
+
* @returns {ValidationResult}
|
|
486
|
+
*/
|
|
487
|
+
function validateDescriptions(commandOverride, context) {
|
|
488
|
+
const result = new ValidationResult()
|
|
489
|
+
|
|
490
|
+
const checkDescription = (desc, descContext) => {
|
|
491
|
+
if (!desc || typeof desc !== 'string') return
|
|
492
|
+
|
|
493
|
+
// Check for HTML entities that should have been decoded
|
|
494
|
+
if (desc.match(/&#x[0-9a-fA-F]+;|&#\d+;|&[a-z]+;/)) {
|
|
495
|
+
result.addWarning(
|
|
496
|
+
`Description may contain unescaped HTML entities`,
|
|
497
|
+
descContext
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Check for common AsciiDoc syntax issues
|
|
502
|
+
if (desc.includes('[') && !desc.includes(']')) {
|
|
503
|
+
result.addWarning(
|
|
504
|
+
`Possible unclosed bracket in description`,
|
|
505
|
+
descContext
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Check for very long single-line descriptions (might need wrapping)
|
|
510
|
+
if (desc.length > 500 && !desc.includes('\n')) {
|
|
511
|
+
result.addWarning(
|
|
512
|
+
`Very long description (${desc.length} chars) without line breaks`,
|
|
513
|
+
descContext
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (commandOverride.description) {
|
|
519
|
+
checkDescription(commandOverride.description, `${context}.description`)
|
|
520
|
+
}
|
|
521
|
+
if (commandOverride.appendToDescription) {
|
|
522
|
+
checkDescription(commandOverride.appendToDescription, `${context}.appendToDescription`)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return result
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Run all validations on an overrides object
|
|
530
|
+
* @param {Object} overrides - Overrides object to validate
|
|
531
|
+
* @param {Object} [commandTree] - Optional command tree for path validation
|
|
532
|
+
* @returns {ValidationResult}
|
|
533
|
+
*/
|
|
534
|
+
function validateOverrides(overrides, commandTree = null) {
|
|
535
|
+
const result = new ValidationResult()
|
|
536
|
+
|
|
537
|
+
if (!overrides || typeof overrides !== 'object') {
|
|
538
|
+
result.addError('Overrides must be a non-null object')
|
|
539
|
+
return result
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 1. Schema validation
|
|
543
|
+
result.merge(validateSchema(overrides))
|
|
544
|
+
|
|
545
|
+
// 2. Reference validation
|
|
546
|
+
result.merge(validateReferences(overrides))
|
|
547
|
+
|
|
548
|
+
// 3. Command path validation (if tree provided)
|
|
549
|
+
if (commandTree) {
|
|
550
|
+
result.merge(validateCommandPaths(overrides, commandTree))
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// 4. Per-command validations
|
|
554
|
+
if (overrides.commands) {
|
|
555
|
+
for (const [cmdPath, cmdOverride] of Object.entries(overrides.commands)) {
|
|
556
|
+
const context = `commands["${cmdPath}"]`
|
|
557
|
+
|
|
558
|
+
result.merge(validateAdmonitionLocations(cmdOverride, context))
|
|
559
|
+
result.merge(validateIncludeLocations(cmdOverride, context))
|
|
560
|
+
result.merge(validateCustomSectionPositions(cmdOverride, context))
|
|
561
|
+
result.merge(validatePlatforms(cmdOverride, context))
|
|
562
|
+
result.merge(validateFlags(cmdOverride, context))
|
|
563
|
+
result.merge(validateDescriptions(cmdOverride, context))
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return result
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Load and validate overrides file
|
|
572
|
+
* @param {string} overridesPath - Path to overrides JSON file
|
|
573
|
+
* @param {Object} [commandTree] - Optional command tree for path validation
|
|
574
|
+
* @returns {{ overrides: Object|null, validation: ValidationResult }}
|
|
575
|
+
*/
|
|
576
|
+
function loadAndValidateOverrides(overridesPath, commandTree = null) {
|
|
577
|
+
const validation = new ValidationResult()
|
|
578
|
+
|
|
579
|
+
if (!overridesPath) {
|
|
580
|
+
return { overrides: null, validation }
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (!fs.existsSync(overridesPath)) {
|
|
584
|
+
validation.addWarning(`Overrides file not found: ${overridesPath}`)
|
|
585
|
+
return { overrides: null, validation }
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Parse JSON
|
|
589
|
+
let overrides
|
|
590
|
+
try {
|
|
591
|
+
const content = fs.readFileSync(overridesPath, 'utf8')
|
|
592
|
+
overrides = JSON.parse(content)
|
|
593
|
+
} catch (err) {
|
|
594
|
+
validation.addError(`Failed to parse overrides JSON: ${err.message}`, overridesPath)
|
|
595
|
+
return { overrides: null, validation }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Run validations
|
|
599
|
+
validation.merge(validateOverrides(overrides, commandTree))
|
|
600
|
+
|
|
601
|
+
return { overrides, validation }
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
module.exports = {
|
|
605
|
+
ValidationResult,
|
|
606
|
+
normalizeCommandPath,
|
|
607
|
+
loadSchemaValidator,
|
|
608
|
+
validateSchema,
|
|
609
|
+
validateCommandPaths,
|
|
610
|
+
validateReferences,
|
|
611
|
+
validateAdmonitionLocations,
|
|
612
|
+
validateIncludeLocations,
|
|
613
|
+
validateCustomSectionPositions,
|
|
614
|
+
validatePlatforms,
|
|
615
|
+
validateFlags,
|
|
616
|
+
validateDescriptions,
|
|
617
|
+
validateOverrides,
|
|
618
|
+
loadAndValidateOverrides,
|
|
619
|
+
unescapeRfc6901,
|
|
620
|
+
VALID_ADMONITION_LOCATIONS,
|
|
621
|
+
VALID_PLATFORMS,
|
|
622
|
+
VALID_INCLUDE_LOCATIONS,
|
|
623
|
+
VALID_SECTION_POSITIONS
|
|
624
|
+
}
|