@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.
Files changed (46) hide show
  1. package/bin/doc-tools-mcp.js +88 -0
  2. package/bin/doc-tools.js +349 -102
  3. package/bin/mcp-tools/index.js +20 -0
  4. package/bin/mcp-tools/lint-doc-strings.js +75 -0
  5. package/bin/mcp-tools/overrides-audit.js +100 -0
  6. package/bin/mcp-tools/preview-doc-string.js +71 -0
  7. package/cli-utils/diff-utils.js +77 -0
  8. package/cli-utils/helm-commented-values.js +172 -23
  9. package/cli-utils/sync-schemas.js +144 -0
  10. package/docs-data/rpk-overrides.schema.json +4 -0
  11. package/package.json +2 -1
  12. package/tools/generate-cli-docs.js +28 -67
  13. package/tools/lint-strings/diff.js +137 -0
  14. package/tools/lint-strings/engine.js +163 -0
  15. package/tools/lint-strings/fixtures/connect/lint_connect.go +67 -0
  16. package/tools/lint-strings/fixtures/crd/crd-ref-docs-config.yaml +15 -0
  17. package/tools/lint-strings/fixtures/crd/lint_types.go +57 -0
  18. package/tools/lint-strings/fixtures/helm/lint_values.yaml +47 -0
  19. package/tools/lint-strings/fixtures/metrics/lint_probe.cc +41 -0
  20. package/tools/lint-strings/fixtures/properties/lint_fixture.cc +51 -0
  21. package/tools/lint-strings/fixtures/properties/lint_fixture.h +33 -0
  22. package/tools/lint-strings/fixtures/rpk/lint_cmd.go +69 -0
  23. package/tools/lint-strings/go-source.js +327 -0
  24. package/tools/lint-strings/index.js +229 -0
  25. package/tools/lint-strings/rules/common.js +148 -0
  26. package/tools/lint-strings/rules/verbatim-asciidoc.js +108 -0
  27. package/tools/lint-strings/source-text.js +41 -0
  28. package/tools/lint-strings/surfaces/connect.js +233 -0
  29. package/tools/lint-strings/surfaces/crd.js +239 -0
  30. package/tools/lint-strings/surfaces/helm.js +159 -0
  31. package/tools/lint-strings/surfaces/metrics.js +368 -0
  32. package/tools/lint-strings/surfaces/properties.js +216 -0
  33. package/tools/lint-strings/surfaces/rpk.js +377 -0
  34. package/tools/overrides-audit/README.adoc +152 -0
  35. package/tools/overrides-audit/adapters/connect.js +144 -0
  36. package/tools/overrides-audit/adapters/properties.js +117 -0
  37. package/tools/overrides-audit/adapters/rpk.js +192 -0
  38. package/tools/overrides-audit/classify.js +546 -0
  39. package/tools/overrides-audit/index.js +185 -0
  40. package/tools/preview-string/index.js +245 -0
  41. package/tools/property-extractor/Makefile +9 -4
  42. package/tools/property-extractor/parser.py +26 -0
  43. package/tools/property-extractor/property_extractor.py +41 -0
  44. package/tools/property-extractor/transformers.py +15 -2
  45. package/tools/rpk-docs/detect-platform-commands.js +8 -1
  46. 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
+ };
@@ -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
- * Parse values.yaml text and return entries for documented commented-out keys.
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
- * @returns {Array<{path: string, description: string, default: string}>}
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 extractCommentedValueDocs (yamlText) {
87
+ function parseValuesFile (yamlText, { attachRealKeys = false } = {}) {
60
88
  const lines = yamlText.split(/\r?\n/)
61
- const entries = []
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
- entries.push({
120
+ record({
76
121
  path: atDoc.path,
77
- description: atDoc.descLines.join('\n').trim(),
78
- default: atDoc.default || '`nil`',
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 && block.candidate) {
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
- entries.push({
156
+ record({
92
157
  path: [...parents, name].join('.'),
93
- description: block.descLines.join('\n').trim(),
94
- default: block.default || '`nil`',
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 (const line of lines) {
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
- else if (block && !block.candidate) block.descLines.push(comment[1])
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
- stack.push({ name: realKey[2].replace(/^"|"$/g, ''), indent })
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 entries
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
  }