@redpanda-data/docs-extensions-and-macros 5.13.5 → 5.17.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 +88 -0
- package/bin/doc-tools.js +349 -102
- package/bin/mcp-tools/index.js +20 -0
- package/bin/mcp-tools/lint-doc-strings.js +75 -0
- package/bin/mcp-tools/overrides-audit.js +100 -0
- package/bin/mcp-tools/preview-doc-string.js +71 -0
- package/cli-utils/diff-utils.js +77 -0
- package/cli-utils/helm-commented-values.js +172 -23
- package/cli-utils/sync-schemas.js +144 -0
- package/docs-data/rpk-overrides.schema.json +4 -0
- package/package.json +2 -1
- package/tools/generate-cli-docs.js +28 -67
- package/tools/lint-strings/diff.js +137 -0
- package/tools/lint-strings/engine.js +163 -0
- package/tools/lint-strings/fixtures/connect/lint_connect.go +67 -0
- package/tools/lint-strings/fixtures/crd/crd-ref-docs-config.yaml +15 -0
- package/tools/lint-strings/fixtures/crd/lint_types.go +57 -0
- package/tools/lint-strings/fixtures/helm/lint_values.yaml +47 -0
- package/tools/lint-strings/fixtures/metrics/lint_probe.cc +41 -0
- package/tools/lint-strings/fixtures/properties/lint_fixture.cc +51 -0
- package/tools/lint-strings/fixtures/properties/lint_fixture.h +33 -0
- package/tools/lint-strings/fixtures/rpk/lint_cmd.go +69 -0
- package/tools/lint-strings/go-source.js +327 -0
- package/tools/lint-strings/index.js +229 -0
- package/tools/lint-strings/rules/common.js +148 -0
- package/tools/lint-strings/rules/verbatim-asciidoc.js +108 -0
- package/tools/lint-strings/source-text.js +41 -0
- package/tools/lint-strings/surfaces/connect.js +233 -0
- package/tools/lint-strings/surfaces/crd.js +239 -0
- package/tools/lint-strings/surfaces/helm.js +159 -0
- package/tools/lint-strings/surfaces/metrics.js +368 -0
- package/tools/lint-strings/surfaces/properties.js +216 -0
- package/tools/lint-strings/surfaces/rpk.js +377 -0
- package/tools/overrides-audit/README.adoc +152 -0
- package/tools/overrides-audit/adapters/connect.js +144 -0
- package/tools/overrides-audit/adapters/properties.js +117 -0
- package/tools/overrides-audit/adapters/rpk.js +192 -0
- package/tools/overrides-audit/classify.js +546 -0
- package/tools/overrides-audit/index.js +185 -0
- package/tools/preview-string/index.js +245 -0
- package/tools/property-extractor/Makefile +9 -4
- package/tools/property-extractor/parser.py +26 -0
- package/tools/property-extractor/property_extractor.py +41 -0
- package/tools/property-extractor/transformers.py +15 -2
- package/tools/rpk-docs/detect-platform-commands.js +8 -1
- package/tools/rpk-docs/scan-deprecated-commands.js +15 -18
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tools - Lint embedded doc strings in engineering source
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const { MAX_EXEC_BUFFER_SIZE, DEFAULT_COMMAND_TIMEOUT } = require('./utils');
|
|
8
|
+
|
|
9
|
+
// The CLI ships in this same package, so invoke it directly: this tool is
|
|
10
|
+
// designed to run inside ENGINEERING repos (redpanda, redpanda-operator,
|
|
11
|
+
// connect), where the docs-repo detection behind getDocToolsCommand does
|
|
12
|
+
// not apply.
|
|
13
|
+
const DOC_TOOLS_BIN = path.resolve(__dirname, '..', 'doc-tools.js');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Lint user-facing doc strings embedded in engineering source code
|
|
17
|
+
* @param {Object} args - Arguments
|
|
18
|
+
* @param {string} args.repo - Path to the engineering checkout (required)
|
|
19
|
+
* @param {string} [args.surface] - Comma-separated surfaces (properties, metrics, rpk, helm, crd, connect)
|
|
20
|
+
* @param {string} [args.diff] - Base ref for declaration-anchored diff mode
|
|
21
|
+
* @param {string} [args.skip_rules] - Comma-separated rule ids to skip
|
|
22
|
+
* @param {string} [args.only_rules] - Comma-separated rule ids to run exclusively
|
|
23
|
+
* @returns {Object} Lint results (findings + summary, JSON)
|
|
24
|
+
*/
|
|
25
|
+
function lintDocStrings(args = {}) {
|
|
26
|
+
if (!args.repo) {
|
|
27
|
+
return {
|
|
28
|
+
success: false,
|
|
29
|
+
error: 'repo is required',
|
|
30
|
+
suggestion: 'Pass the absolute path of the engineering checkout to lint (for example, a local redpanda clone)'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cliArgs = [DOC_TOOLS_BIN, 'lint-strings', '--repo', args.repo, '--format', 'json'];
|
|
35
|
+
if (args.surface) cliArgs.push('--surface', args.surface);
|
|
36
|
+
if (args.diff) cliArgs.push('--diff', args.diff);
|
|
37
|
+
if (args.skip_rules) cliArgs.push('--skip-rules', args.skip_rules);
|
|
38
|
+
if (args.only_rules) cliArgs.push('--only-rules', args.only_rules);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const result = spawnSync(process.execPath, cliArgs, {
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
stdio: 'pipe',
|
|
44
|
+
maxBuffer: MAX_EXEC_BUFFER_SIZE,
|
|
45
|
+
timeout: DEFAULT_COMMAND_TIMEOUT
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (result.error) {
|
|
49
|
+
throw new Error(`Failed to execute command: ${result.error.message}`);
|
|
50
|
+
}
|
|
51
|
+
if (result.status !== 0) {
|
|
52
|
+
throw new Error(result.stderr || `Command failed with exit code ${result.status}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const parsed = JSON.parse(result.stdout);
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
summary: parsed.summary,
|
|
59
|
+
findings: parsed.findings,
|
|
60
|
+
unsupported_surfaces: parsed.unsupported_surfaces || []
|
|
61
|
+
};
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
error: err.message,
|
|
66
|
+
stdout: err.stdout || '',
|
|
67
|
+
stderr: err.stderr || '',
|
|
68
|
+
suggestion: 'Check that the repo path exists. The properties surface additionally needs python3 and network access on first run (venv + tree-sitter grammar); lint a specific --surface to skip it.'
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
lintDocStrings
|
|
75
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tools - Overrides Audit
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const { findRepoRoot, getDocToolsCommand, MAX_EXEC_BUFFER_SIZE, DEFAULT_COMMAND_TIMEOUT } = require('./utils');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Audit docs-side override entries against extracted source strings
|
|
10
|
+
* @param {Object} args - Arguments
|
|
11
|
+
* @param {string} args.overrides - Path to the overrides JSON file (for example "docs-data/property-overrides.json")
|
|
12
|
+
* @param {string} [args.extracted] - Path to the extracted source JSON (property extractor raw output; required for the properties surface)
|
|
13
|
+
* @param {string} [args.repo] - Redpanda checkout to extract raw source strings from (alternative to args.extracted, properties surface only)
|
|
14
|
+
* @param {string} [args.surface] - Override surface: 'properties', 'rpk', or 'connect'
|
|
15
|
+
* @param {string} [args.output] - Also write the JSON result to this file (relative to repo root)
|
|
16
|
+
* @returns {Object} Audit results with manifest and per-class summary
|
|
17
|
+
*/
|
|
18
|
+
function auditOverrides(args = {}) {
|
|
19
|
+
const repoRoot = findRepoRoot();
|
|
20
|
+
|
|
21
|
+
if (!args.overrides) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
error: 'Missing required argument: overrides',
|
|
25
|
+
suggestion: 'Pass the path to the overrides JSON file (for example "docs-data/property-overrides.json")'
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Get doc-tools command (handles both local and installed)
|
|
31
|
+
const docTools = getDocToolsCommand(repoRoot);
|
|
32
|
+
|
|
33
|
+
// Build command arguments array (JSON output so the result is parseable)
|
|
34
|
+
const baseArgs = ['overrides', 'audit', '--overrides', args.overrides, '--format', 'json'];
|
|
35
|
+
|
|
36
|
+
if (args.extracted) {
|
|
37
|
+
baseArgs.push('--extracted');
|
|
38
|
+
baseArgs.push(args.extracted);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (args.repo) {
|
|
42
|
+
baseArgs.push('--repo');
|
|
43
|
+
baseArgs.push(args.repo);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (args.surface) {
|
|
47
|
+
baseArgs.push('--surface');
|
|
48
|
+
baseArgs.push(args.surface);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (args.output) {
|
|
52
|
+
baseArgs.push('--output');
|
|
53
|
+
baseArgs.push(args.output);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result = spawnSync(docTools.program, docTools.getArgs(baseArgs), {
|
|
57
|
+
cwd: repoRoot.root,
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
stdio: 'pipe',
|
|
60
|
+
maxBuffer: MAX_EXEC_BUFFER_SIZE,
|
|
61
|
+
timeout: DEFAULT_COMMAND_TIMEOUT
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Check for spawn errors
|
|
65
|
+
if (result.error) {
|
|
66
|
+
throw new Error(`Failed to execute command: ${result.error.message}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check for non-zero exit codes
|
|
70
|
+
if (result.status !== 0) {
|
|
71
|
+
const errorMsg = result.stderr || `Command failed with exit code ${result.status}`;
|
|
72
|
+
throw new Error(errorMsg);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const audit = JSON.parse(result.stdout);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
surface: audit.surface,
|
|
80
|
+
summary: audit.summary,
|
|
81
|
+
cross_check: audit.cross_check || null,
|
|
82
|
+
manifest: audit.manifest,
|
|
83
|
+
files_generated: args.output ? [args.output] : [],
|
|
84
|
+
summary_text: `Classified ${audit.summary.total} override field(s) on the ${audit.surface} surface`
|
|
85
|
+
};
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
error: err.message,
|
|
90
|
+
stdout: err.stdout || '',
|
|
91
|
+
stderr: err.stderr || '',
|
|
92
|
+
exitCode: err.status,
|
|
93
|
+
suggestion: 'Check that the overrides file exists and that --extracted points to the property extractor\'s raw JSON output (without overrides applied)'
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
auditOverrides
|
|
100
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tools - Preview one embedded doc string as it will publish
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const { MAX_EXEC_BUFFER_SIZE, DEFAULT_COMMAND_TIMEOUT } = require('./utils');
|
|
8
|
+
|
|
9
|
+
// The CLI ships in this same package, so invoke it directly: this tool is
|
|
10
|
+
// designed to run inside ENGINEERING repos (redpanda, redpanda-operator,
|
|
11
|
+
// connect), where the docs-repo detection behind getDocToolsCommand does
|
|
12
|
+
// not apply.
|
|
13
|
+
const DOC_TOOLS_BIN = path.resolve(__dirname, '..', 'doc-tools.js');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Render one doc-string declaration to its published snippet
|
|
17
|
+
* @param {Object} args - Arguments
|
|
18
|
+
* @param {string} args.repo - Path to the engineering checkout (required)
|
|
19
|
+
* @param {string} args.surface - properties|rpk|metrics|helm|crd|connect (required)
|
|
20
|
+
* @param {string} args.name - Declaration name (required)
|
|
21
|
+
* @param {string} [args.overrides] - Overrides JSON path (properties only)
|
|
22
|
+
* @returns {Object} Preview text
|
|
23
|
+
*/
|
|
24
|
+
function previewDocString(args = {}) {
|
|
25
|
+
for (const required of ['repo', 'surface', 'name']) {
|
|
26
|
+
if (!args[required]) {
|
|
27
|
+
return {
|
|
28
|
+
success: false,
|
|
29
|
+
error: `${required} is required`,
|
|
30
|
+
suggestion: 'Pass repo (engineering checkout path), surface (properties|rpk|metrics|helm|crd|connect), and name (the declaration to preview)'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const cliArgs = [DOC_TOOLS_BIN, 'preview-string', '--repo', args.repo, '--surface', args.surface, '--name', args.name];
|
|
36
|
+
if (args.overrides) cliArgs.push('--overrides', args.overrides);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const result = spawnSync(process.execPath, cliArgs, {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
stdio: 'pipe',
|
|
42
|
+
maxBuffer: MAX_EXEC_BUFFER_SIZE,
|
|
43
|
+
timeout: DEFAULT_COMMAND_TIMEOUT
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (result.error) {
|
|
47
|
+
throw new Error(`Failed to execute command: ${result.error.message}`);
|
|
48
|
+
}
|
|
49
|
+
if (result.status !== 0) {
|
|
50
|
+
throw new Error(result.stderr || `Command failed with exit code ${result.status}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
preview: result.stdout,
|
|
56
|
+
masked_by_override: result.stdout.includes('MASKED-BY-OVERRIDE')
|
|
57
|
+
};
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
error: err.message,
|
|
62
|
+
stdout: err.stdout || '',
|
|
63
|
+
stderr: err.stderr || '',
|
|
64
|
+
suggestion: 'Check the repo path and declaration name. For rpk flags, prefix the name with -- (for example --format).'
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
previewDocString
|
|
71
|
+
};
|
package/cli-utils/diff-utils.js
CHANGED
|
@@ -287,6 +287,82 @@ function insertVersionIntoRawJson (raw, propName, newTag, indentUnit) {
|
|
|
287
287
|
return raw.slice(0, objStart) + insertion + raw.slice(objStart)
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Repair property-reference anchors in the published properties JSON.
|
|
292
|
+
*
|
|
293
|
+
* The generated .adoc partials get their anchors normalized in Phase 3, but the
|
|
294
|
+
* JSON attachment is written in Phase 2 and the docs UI reads THAT for its
|
|
295
|
+
* property tooltips. So the two disagreed: the partials linked
|
|
296
|
+
* `<<flush-bytes>>` while the attachment still said `<<flushbytes>>`, which
|
|
297
|
+
* resolves to nothing.
|
|
298
|
+
*
|
|
299
|
+
* Like updatePropertiesJsonWithVersion above, this is a TEXTUAL edit and not a
|
|
300
|
+
* parse/re-serialize round-trip. That matters more here than anywhere else in
|
|
301
|
+
* the pipeline: this file carries the uint64 and int64 limits as property
|
|
302
|
+
* maxima, and JSON.stringify rounds all three (18446744073709551615 becomes
|
|
303
|
+
* 18446744073709552000, which the server rejects). Two separate attempts to
|
|
304
|
+
* make a round-trip safe by writing a lossless JSON parser were both wrong -
|
|
305
|
+
* one corrupted escaped-JSON examples, the other could be forged by a string
|
|
306
|
+
* containing its own marker - and neither was necessary, because
|
|
307
|
+
* normalizePropertyAnchors only ever rewrites `<<anchor` tokens and those can
|
|
308
|
+
* only occur inside a JSON string value. Applying it to the raw text therefore
|
|
309
|
+
* cannot touch a number, a key or the structure.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} jsonPath - Path to the properties attachment.
|
|
312
|
+
* @returns {number} Anchors rewritten.
|
|
313
|
+
*/
|
|
314
|
+
function repairPropertyAnchorsInJson (jsonPath) {
|
|
315
|
+
try {
|
|
316
|
+
if (!fs.existsSync(jsonPath)) {
|
|
317
|
+
console.warn(`Warning: Cannot repair anchors: ${jsonPath} does not exist`)
|
|
318
|
+
return 0
|
|
319
|
+
}
|
|
320
|
+
const raw = fs.readFileSync(jsonPath, 'utf8')
|
|
321
|
+
// Parsed only to learn the property names. The parsed object is never
|
|
322
|
+
// written back; the edit is applied to `raw`.
|
|
323
|
+
let properties
|
|
324
|
+
try {
|
|
325
|
+
const data = JSON.parse(raw)
|
|
326
|
+
properties = data.properties || data.topic_properties
|
|
327
|
+
} catch (err) {
|
|
328
|
+
console.warn(`Warning: Cannot repair anchors: ${jsonPath} is not valid JSON (${err.message})`)
|
|
329
|
+
return 0
|
|
330
|
+
}
|
|
331
|
+
if (!properties || typeof properties !== 'object') return 0
|
|
332
|
+
|
|
333
|
+
const normalizePropertyAnchors = require('../tools/property-extractor/helpers/normalizePropertyAnchors')
|
|
334
|
+
const { text, rewrites } = normalizePropertyAnchors(raw, properties)
|
|
335
|
+
if (!rewrites.length) return 0
|
|
336
|
+
|
|
337
|
+
// Belt and braces: the edit is meant to touch nothing but anchors, so prove
|
|
338
|
+
// it before overwriting published data. Masking every <<...>> in both
|
|
339
|
+
// versions must leave the two byte-identical.
|
|
340
|
+
const mask = (v) => v.replace(/<<[^<>]*>>/g, '\u0000')
|
|
341
|
+
if (mask(raw) !== mask(text)) {
|
|
342
|
+
console.error('Error: anchor repair changed something other than an anchor; not writing')
|
|
343
|
+
return 0
|
|
344
|
+
}
|
|
345
|
+
// And the result must still parse.
|
|
346
|
+
try {
|
|
347
|
+
JSON.parse(text)
|
|
348
|
+
} catch (err) {
|
|
349
|
+
console.error(`Error: anchor repair produced invalid JSON (${err.message}); not writing`)
|
|
350
|
+
return 0
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fs.writeFileSync(jsonPath, text)
|
|
354
|
+
const shown = [...new Set(rewrites.map((r) => `${r.from} -> ${r.to}`))]
|
|
355
|
+
console.log(
|
|
356
|
+
`Repaired ${rewrites.length} property anchor(s) in ${path.basename(jsonPath)}: ` +
|
|
357
|
+
shown.slice(0, 8).join(', ') + (shown.length > 8 ? `, +${shown.length - 8} more` : '')
|
|
358
|
+
)
|
|
359
|
+
return rewrites.length
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.warn(`Warning: Failed to repair property anchors: ${err.message}`)
|
|
362
|
+
return 0
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
290
366
|
/**
|
|
291
367
|
* Create a unified diff patch between two directories and clean them up.
|
|
292
368
|
*
|
|
@@ -387,5 +463,6 @@ module.exports = {
|
|
|
387
463
|
generatePropertyComparisonReport,
|
|
388
464
|
updatePropertyOverridesWithVersion,
|
|
389
465
|
updatePropertiesJsonWithVersion,
|
|
466
|
+
repairPropertyAnchorsInJson,
|
|
390
467
|
diffDirs
|
|
391
468
|
}
|
|
@@ -44,6 +44,8 @@ const REAL_KEY_RE = /^(\s*)([A-Za-z0-9_."/-]+):(.*)$/
|
|
|
44
44
|
const DESC_MARKER_RE = /^\s*#\s*--\s*(?!-)(.*)$/
|
|
45
45
|
const AT_DOC_RE = /^\s*#\s*@doc\s+([A-Za-z0-9_./-]+)\s+--\s*(.*)$/
|
|
46
46
|
const AT_DEFAULT_RE = /^\s*#\s*@default\s+--\s*(.*)$/
|
|
47
|
+
// helm-docs' bare word annotations: `# @raw`, `# @ignored`.
|
|
48
|
+
const AT_WORD_RE = /^\s*#\s*@(\w+)\s*$/
|
|
47
49
|
const COMMENT_RE = /^\s*#\s?(.*)$/
|
|
48
50
|
// A block scalar header may carry an explicit indentation indicator digit on
|
|
49
51
|
// either side of the chomping indicator: `|2`, `|-2`, `|2-`, `>2`, ...
|
|
@@ -51,14 +53,40 @@ const BLOCK_SCALAR_RE = /[|>][0-9]?[+-]?[0-9]?\s*(#.*)?$/
|
|
|
51
53
|
const LIST_ITEM_RE = /^\s*-\s/
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
|
-
*
|
|
56
|
+
* The one values.yaml comment walk in this repo. Two consumers share it:
|
|
57
|
+
*
|
|
58
|
+
* - `extractCommentedValueDocs` below, which the helm-spec generator uses to
|
|
59
|
+
* inject rows for documented commented-out keys.
|
|
60
|
+
* - `tools/lint-strings/surfaces/helm.js`, which needs the same attachment
|
|
61
|
+
* decisions plus line spans, real-key (helm-docs) attachments, and the
|
|
62
|
+
* blocks that attach to NOTHING.
|
|
63
|
+
*
|
|
64
|
+
* The linter used to hold a byte-identical copy of every regex above and a
|
|
65
|
+
* near-copy of this walk, so that "the linter never calls a marker dead that
|
|
66
|
+
* the generator in this repo renders" - a promise nothing enforced. When the
|
|
67
|
+
* copies drifted, the linter emitted exactly the false dead-marker the
|
|
68
|
+
* comment promised to prevent and every test stayed green. There is now one
|
|
69
|
+
* walk, so they cannot disagree.
|
|
70
|
+
*
|
|
71
|
+
* Records are the union of what both consumers need. Line numbers are
|
|
72
|
+
* 0-indexed; descriptions are returned as the raw comment lines so each
|
|
73
|
+
* consumer can join them its own way (the generator preserves newlines, the
|
|
74
|
+
* linter flattens to one line).
|
|
55
75
|
*
|
|
56
76
|
* @param {string} yamlText - Raw contents of values.yaml.
|
|
57
|
-
* @
|
|
77
|
+
* @param {object} [options]
|
|
78
|
+
* @param {boolean} [options.attachRealKeys=false] - Model helm-docs' own
|
|
79
|
+
* attachment: a `# --` block sitting DIRECTLY above an uncommented key
|
|
80
|
+
* documents that key, and a top-level uncommented key with no block above
|
|
81
|
+
* it is recorded as undocumented. The generator leaves this off because
|
|
82
|
+
* helm-docs already renders those rows; only the linter judges them.
|
|
83
|
+
* @returns {Array<object>} records: { kind: 'key'|'dead-marker', path,
|
|
84
|
+
* descLines, default, annotations, lineStart, lineEnd, commentedOut,
|
|
85
|
+
* topLevel, undocumented }
|
|
58
86
|
*/
|
|
59
|
-
function
|
|
87
|
+
function parseValuesFile (yamlText, { attachRealKeys = false } = {}) {
|
|
60
88
|
const lines = yamlText.split(/\r?\n/)
|
|
61
|
-
const
|
|
89
|
+
const records = []
|
|
62
90
|
const stack = [] // enclosing real keys: { name, indent }
|
|
63
91
|
|
|
64
92
|
// Accumulating comment block: { descLines, default, candidate }. The
|
|
@@ -69,36 +97,81 @@ function extractCommentedValueDocs (yamlText) {
|
|
|
69
97
|
let atDoc = null // accumulating @doc entry: { path, descLines, default }
|
|
70
98
|
let skipScalarIndent = -1 // inside a block scalar when >= 0
|
|
71
99
|
let suppressInnerIndent = -1 // inside an emitted commented key's subtree when >= 0
|
|
100
|
+
let looseIgnored = false // bare `# @ignored` with no `# --` block above a key
|
|
101
|
+
|
|
102
|
+
const record = (fields) => {
|
|
103
|
+
records.push({
|
|
104
|
+
kind: 'key',
|
|
105
|
+
path: null,
|
|
106
|
+
descLines: [],
|
|
107
|
+
default: '',
|
|
108
|
+
annotations: {},
|
|
109
|
+
lineStart: 0,
|
|
110
|
+
lineEnd: 0,
|
|
111
|
+
commentedOut: false,
|
|
112
|
+
topLevel: false,
|
|
113
|
+
undocumented: false,
|
|
114
|
+
...fields,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
72
117
|
|
|
73
118
|
const flushAtDoc = () => {
|
|
74
119
|
if (atDoc) {
|
|
75
|
-
|
|
120
|
+
record({
|
|
76
121
|
path: atDoc.path,
|
|
77
|
-
|
|
78
|
-
default: atDoc.default
|
|
122
|
+
descLines: atDoc.descLines,
|
|
123
|
+
default: atDoc.default,
|
|
124
|
+
annotations: atDoc.annotations,
|
|
125
|
+
lineStart: atDoc.line,
|
|
126
|
+
lineEnd: atDoc.lastLine,
|
|
127
|
+
commentedOut: true,
|
|
128
|
+
topLevel: !atDoc.path.includes('.'),
|
|
79
129
|
})
|
|
80
130
|
atDoc = null
|
|
81
131
|
}
|
|
82
132
|
}
|
|
83
133
|
|
|
134
|
+
// A `# --` block that reached the end of its life with no key to attach to.
|
|
135
|
+
// The generator drops these; the linter reports them, because their
|
|
136
|
+
// description silently never ships.
|
|
137
|
+
const emitDead = (b) => {
|
|
138
|
+
record({
|
|
139
|
+
kind: 'dead-marker',
|
|
140
|
+
descLines: b.descLines,
|
|
141
|
+
default: b.default,
|
|
142
|
+
annotations: b.annotations,
|
|
143
|
+
lineStart: b.markerLine,
|
|
144
|
+
lineEnd: b.lastLine,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
84
148
|
// Emit the active block's candidate, if any, and close the block. The
|
|
85
149
|
// emitted key suppresses its own commented subtree until real content
|
|
86
150
|
// appears, so nested example structures do not emit bogus paths.
|
|
87
151
|
const flushBlock = () => {
|
|
88
|
-
if (block
|
|
152
|
+
if (!block) return
|
|
153
|
+
if (block.candidate) {
|
|
89
154
|
const { name, effIndent } = block.candidate
|
|
90
155
|
const parents = stack.filter((k) => k.indent < effIndent).map((k) => k.name)
|
|
91
|
-
|
|
156
|
+
record({
|
|
92
157
|
path: [...parents, name].join('.'),
|
|
93
|
-
|
|
94
|
-
default: block.default
|
|
158
|
+
descLines: block.descLines,
|
|
159
|
+
default: block.default,
|
|
160
|
+
annotations: block.annotations,
|
|
161
|
+
lineStart: block.markerLine,
|
|
162
|
+
lineEnd: block.lastLine,
|
|
163
|
+
commentedOut: true,
|
|
164
|
+
topLevel: parents.length === 0,
|
|
95
165
|
})
|
|
96
166
|
suppressInnerIndent = effIndent
|
|
167
|
+
} else {
|
|
168
|
+
emitDead(block)
|
|
97
169
|
}
|
|
98
170
|
block = null
|
|
99
171
|
}
|
|
100
172
|
|
|
101
|
-
for (
|
|
173
|
+
for (let i = 0; i < lines.length; i++) {
|
|
174
|
+
const line = lines[i]
|
|
102
175
|
// Skip the body of block scalars so their content is never mistaken for
|
|
103
176
|
// keys or comments.
|
|
104
177
|
if (skipScalarIndent >= 0) {
|
|
@@ -111,14 +184,21 @@ function extractCommentedValueDocs (yamlText) {
|
|
|
111
184
|
if (atDocMatch) {
|
|
112
185
|
flushAtDoc()
|
|
113
186
|
flushBlock()
|
|
114
|
-
atDoc = { path: atDocMatch[1], descLines: [atDocMatch[2]], default: '' }
|
|
187
|
+
atDoc = { line: i, lastLine: i, path: atDocMatch[1], descLines: [atDocMatch[2]], default: '', annotations: {} }
|
|
115
188
|
continue
|
|
116
189
|
}
|
|
117
190
|
|
|
118
191
|
const atDefault = line.match(AT_DEFAULT_RE)
|
|
119
192
|
if (atDefault) {
|
|
120
|
-
if (atDoc) atDoc.default = atDefault[1].trim()
|
|
121
|
-
else if (block) block.default = atDefault[1].trim()
|
|
193
|
+
if (atDoc) { atDoc.default = atDefault[1].trim(); atDoc.lastLine = i }
|
|
194
|
+
else if (block) { block.default = atDefault[1].trim(); block.lastLine = i }
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const atWord = line.match(AT_WORD_RE)
|
|
199
|
+
if (atWord) {
|
|
200
|
+
if (block) { block.annotations[atWord[1]] = true; block.lastLine = i }
|
|
201
|
+
else if (atWord[1] === 'ignored') looseIgnored = true
|
|
122
202
|
continue
|
|
123
203
|
}
|
|
124
204
|
|
|
@@ -137,8 +217,10 @@ function extractCommentedValueDocs (yamlText) {
|
|
|
137
217
|
// the result lines up with the real-key indents held on the stack.
|
|
138
218
|
const effIndent = commentedKey[1].length + Math.max(0, commentedKey[2].length - 1)
|
|
139
219
|
// A previously emitted commented key suppresses its own commented
|
|
140
|
-
// subtree, so nested example structures do not emit bogus paths.
|
|
220
|
+
// subtree, so nested example structures do not emit bogus paths. A
|
|
221
|
+
// marker in there is documentation neither pipeline ever renders.
|
|
141
222
|
if (suppressInnerIndent >= 0 && effIndent > suppressInnerIndent) {
|
|
223
|
+
if (block) emitDead(block)
|
|
142
224
|
block = null
|
|
143
225
|
continue
|
|
144
226
|
}
|
|
@@ -157,6 +239,7 @@ function extractCommentedValueDocs (yamlText) {
|
|
|
157
239
|
// and deeper lines are nested example structures.
|
|
158
240
|
block.candidate = { name: commentedKey[3], effIndent }
|
|
159
241
|
}
|
|
242
|
+
block.lastLine = i
|
|
160
243
|
continue
|
|
161
244
|
}
|
|
162
245
|
}
|
|
@@ -165,16 +248,18 @@ function extractCommentedValueDocs (yamlText) {
|
|
|
165
248
|
if (descMarker) {
|
|
166
249
|
flushAtDoc()
|
|
167
250
|
flushBlock()
|
|
168
|
-
block = { descLines: [descMarker[1]], default: '', candidate: null }
|
|
251
|
+
block = { markerLine: i, lastLine: i, descLines: [descMarker[1]], default: '', annotations: {}, candidate: null }
|
|
169
252
|
continue
|
|
170
253
|
}
|
|
171
254
|
|
|
172
255
|
const comment = line.match(COMMENT_RE)
|
|
173
256
|
if (comment && line.trim().startsWith('#')) {
|
|
174
|
-
if (atDoc) atDoc.descLines.push(comment[1])
|
|
257
|
+
if (atDoc) { atDoc.descLines.push(comment[1]); atDoc.lastLine = i }
|
|
175
258
|
// Description lines precede the documented key; once a candidate
|
|
176
|
-
// exists, trailing comment lines belong to its example structure
|
|
177
|
-
|
|
259
|
+
// exists, trailing comment lines belong to its example structure and
|
|
260
|
+
// only extend the span.
|
|
261
|
+
else if (block && !block.candidate) { block.descLines.push(comment[1]); block.lastLine = i }
|
|
262
|
+
else if (block) block.lastLine = i
|
|
178
263
|
continue
|
|
179
264
|
}
|
|
180
265
|
|
|
@@ -183,26 +268,76 @@ function extractCommentedValueDocs (yamlText) {
|
|
|
183
268
|
// structure must not re-enable emission against a stale parent key, since
|
|
184
269
|
// the real-key stack is not rebuilt by comment lines.
|
|
185
270
|
flushAtDoc()
|
|
271
|
+
|
|
272
|
+
const realKey = line.match(REAL_KEY_RE)
|
|
273
|
+
if (attachRealKeys && realKey && block && !block.candidate && block.lastLine === i - 1) {
|
|
274
|
+
// helm-docs' own attachment: the block sits DIRECTLY above a real key.
|
|
275
|
+
const indent = realKey[1].length
|
|
276
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop()
|
|
277
|
+
const name = realKey[2].replace(/^"|"$/g, '')
|
|
278
|
+
const keyPath = [...stack.map((k) => k.name), name].join('.')
|
|
279
|
+
stack.push({ name, indent })
|
|
280
|
+
if (!block.annotations.ignored) {
|
|
281
|
+
record({
|
|
282
|
+
path: keyPath,
|
|
283
|
+
descLines: block.descLines,
|
|
284
|
+
default: block.default,
|
|
285
|
+
annotations: block.annotations,
|
|
286
|
+
lineStart: block.markerLine,
|
|
287
|
+
lineEnd: i,
|
|
288
|
+
topLevel: indent === 0,
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
block = null
|
|
292
|
+
suppressInnerIndent = -1
|
|
293
|
+
if (BLOCK_SCALAR_RE.test(realKey[3])) skipScalarIndent = indent
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
|
|
186
297
|
flushBlock()
|
|
187
298
|
if (line.trim() === '') continue
|
|
188
299
|
suppressInnerIndent = -1
|
|
189
300
|
|
|
190
301
|
if (LIST_ITEM_RE.test(line)) continue
|
|
191
302
|
|
|
192
|
-
const realKey = line.match(REAL_KEY_RE)
|
|
193
303
|
if (realKey) {
|
|
194
304
|
const indent = realKey[1].length
|
|
195
305
|
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
|
|
196
306
|
stack.pop()
|
|
197
307
|
}
|
|
198
|
-
|
|
308
|
+
const name = realKey[2].replace(/^"|"$/g, '')
|
|
309
|
+
stack.push({ name, indent })
|
|
199
310
|
if (BLOCK_SCALAR_RE.test(realKey[3])) skipScalarIndent = indent
|
|
311
|
+
if (attachRealKeys && indent === 0 && !looseIgnored) {
|
|
312
|
+
// Top-level user-visible key with no attached `# --` marker.
|
|
313
|
+
record({ path: name, lineStart: i, lineEnd: i, topLevel: true, undocumented: true })
|
|
314
|
+
}
|
|
200
315
|
}
|
|
316
|
+
looseIgnored = false
|
|
201
317
|
}
|
|
202
318
|
|
|
203
319
|
flushAtDoc()
|
|
204
320
|
flushBlock()
|
|
205
|
-
return
|
|
321
|
+
return records
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Parse values.yaml text and return entries for documented commented-out keys.
|
|
326
|
+
*
|
|
327
|
+
* A filter over `parseValuesFile`: the commented-out keys it attaches, with
|
|
328
|
+
* their comment lines joined the way the AsciiDoc injector wants them.
|
|
329
|
+
*
|
|
330
|
+
* @param {string} yamlText - Raw contents of values.yaml.
|
|
331
|
+
* @returns {Array<{path: string, description: string, default: string}>}
|
|
332
|
+
*/
|
|
333
|
+
function extractCommentedValueDocs (yamlText) {
|
|
334
|
+
return parseValuesFile(yamlText)
|
|
335
|
+
.filter((r) => r.kind === 'key' && r.commentedOut)
|
|
336
|
+
.map((r) => ({
|
|
337
|
+
path: r.path,
|
|
338
|
+
description: r.descLines.join('\n').trim(),
|
|
339
|
+
default: r.default || '`nil`',
|
|
340
|
+
}))
|
|
206
341
|
}
|
|
207
342
|
|
|
208
343
|
/**
|
|
@@ -354,8 +489,22 @@ function injectIntoAsciiDoc (adoc, entries) {
|
|
|
354
489
|
}
|
|
355
490
|
|
|
356
491
|
module.exports = {
|
|
492
|
+
parseValuesFile,
|
|
357
493
|
extractCommentedValueDocs,
|
|
358
494
|
injectIntoAsciiDoc,
|
|
359
495
|
filterEntriesBySchema,
|
|
360
496
|
isPathAllowedBySchema,
|
|
497
|
+
// Exported so the lint surface cannot hold its own copy. Every one of these
|
|
498
|
+
// was duplicated character-for-character before.
|
|
499
|
+
PATTERNS: {
|
|
500
|
+
COMMENTED_KEY_RE,
|
|
501
|
+
REAL_KEY_RE,
|
|
502
|
+
DESC_MARKER_RE,
|
|
503
|
+
AT_DOC_RE,
|
|
504
|
+
AT_DEFAULT_RE,
|
|
505
|
+
AT_WORD_RE,
|
|
506
|
+
COMMENT_RE,
|
|
507
|
+
BLOCK_SCALAR_RE,
|
|
508
|
+
LIST_ITEM_RE,
|
|
509
|
+
},
|
|
361
510
|
}
|