@redpanda-data/docs-extensions-and-macros 5.2.5 → 5.3.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 +159 -0
- package/package.json +1 -1
- package/tools/property-extractor/property_extractor.py +39 -2
- package/tools/property-extractor/transformers.py +29 -13
- package/tools/rpk-docs/detect-platform-commands.js +566 -0
- package/tools/rpk-docs/generate-plugin-stubs.js +291 -0
- package/tools/rpk-docs/generate-rpk-docs.js +466 -30
- package/tools/rpk-docs/helpers/index.js +13 -3
- package/tools/rpk-docs/report-delta.js +314 -32
- package/tools/rpk-docs/rpk-docs-handler.js +1329 -208
- package/tools/rpk-docs/templates/command.hbs +17 -0
- package/tools/rpk-docs/templates/content-item.hbs +2 -2
- package/tools/rpk-docs/validate-overrides.js +16 -0
|
@@ -291,6 +291,24 @@ function shouldUsePartialDir(overrides, commandPath) {
|
|
|
291
291
|
return false
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Check if a command belongs to the rpk cloud or rpk security secret families.
|
|
296
|
+
* Their pages are routed to the cloud partials directory (cloudSecretDir) and
|
|
297
|
+
* published by cloud-docs, so links to them from regular pages must cross to
|
|
298
|
+
* the cloud component.
|
|
299
|
+
* @param {string} commandPath - Full command path (e.g., "rpk cloud login")
|
|
300
|
+
* @returns {boolean}
|
|
301
|
+
*/
|
|
302
|
+
function isCloudSecretCommand(commandPath) {
|
|
303
|
+
return commandPath === 'rpk cloud' || commandPath.startsWith('rpk cloud ') ||
|
|
304
|
+
commandPath === 'rpk security secret' || commandPath.startsWith('rpk security secret ')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Antora component that publishes the rpk cloud and rpk security secret pages.
|
|
309
|
+
*/
|
|
310
|
+
const CLOUD_DOCS_COMPONENT = 'cloud-data-platform'
|
|
311
|
+
|
|
294
312
|
/**
|
|
295
313
|
* Get command metadata from overrides
|
|
296
314
|
* @param {Object} overrides - Overrides object (resolved)
|
|
@@ -778,6 +796,76 @@ function convertIndentedCodeBlocksToAsciiDoc(text, codeBlockStore = []) {
|
|
|
778
796
|
while (i < lines.length) {
|
|
779
797
|
const line = lines[i]
|
|
780
798
|
|
|
799
|
+
// Unindented shell example: "$ rpk ..." at column 0 followed by its
|
|
800
|
+
// sample output on the contiguous lines below. Rendered as a command
|
|
801
|
+
// block plus a no-copy output block so the output's =-underlined titles
|
|
802
|
+
// and aligned columns are never parsed as prose or headings.
|
|
803
|
+
if (/^\$\s/.test(line)) {
|
|
804
|
+
const command = line.replace(/^\$\s+/, '')
|
|
805
|
+
let j = i + 1
|
|
806
|
+
const outputLines = []
|
|
807
|
+
while (j < lines.length && lines[j].trim() !== '' && !/^\$\s/.test(lines[j])) {
|
|
808
|
+
outputLines.push(lines[j])
|
|
809
|
+
j++
|
|
810
|
+
}
|
|
811
|
+
let block = `\n[,bash]\n----\n${command}\n----\n`
|
|
812
|
+
if (outputLines.length > 0) {
|
|
813
|
+
block += `\n[.no-copy]\n----\n${outputLines.join('\n')}\n----\n`
|
|
814
|
+
}
|
|
815
|
+
const placeholder = `__EARLY_CODE_BLOCK_${codeBlockStore.length}__`
|
|
816
|
+
codeBlockStore.push(block)
|
|
817
|
+
result.push('')
|
|
818
|
+
result.push(placeholder)
|
|
819
|
+
result.push('')
|
|
820
|
+
i = j
|
|
821
|
+
continue
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Colon-introduced code sample: prose ending with ":" followed by an
|
|
825
|
+
// indented block that is not a list (Cedar policies, config snippets).
|
|
826
|
+
// Deeply indented (4+ space) blocks are literals by help-text convention
|
|
827
|
+
// even without a colon introducer (path templates like
|
|
828
|
+
// " kafka/{topic}/{partition}_{revision}/" would otherwise render as
|
|
829
|
+
// prose whose braces Asciidoctor eats as attribute references).
|
|
830
|
+
// Captured verbatim (dedented) so inline-code transforms never touch it.
|
|
831
|
+
const prevNonBlank = [...result].reverse().find(l => l.trim() !== '')
|
|
832
|
+
// Only a chunk that starts after a blank line is a standalone literal;
|
|
833
|
+
// a deeply indented line mid-chunk is a wrapped continuation of a table
|
|
834
|
+
// row or list item and belongs to the converters below.
|
|
835
|
+
const atChunkStart = result.length === 0 || result[result.length - 1].trim() === ''
|
|
836
|
+
if (
|
|
837
|
+
(
|
|
838
|
+
(prevNonBlank && /:\s*$/.test(prevNonBlank) && /^[ ]{2,}\S/.test(line)) ||
|
|
839
|
+
(atChunkStart && /^[ ]{4,}\S/.test(line))
|
|
840
|
+
) &&
|
|
841
|
+
!/^[ ]{2,}(-|\*|\d+[.)])\s/.test(line) &&
|
|
842
|
+
!/^[ ]{2,}(--|rpk\s|\$\s)/.test(line)
|
|
843
|
+
) {
|
|
844
|
+
const blockLines = []
|
|
845
|
+
let j = i
|
|
846
|
+
while (j < lines.length && (/^[ ]{2,}\S/.test(lines[j]) || lines[j].trim() === '')) {
|
|
847
|
+
if (lines[j].trim() === '' && (j + 1 >= lines.length || !/^[ ]{2,}\S/.test(lines[j + 1]))) break
|
|
848
|
+
blockLines.push(lines[j])
|
|
849
|
+
j++
|
|
850
|
+
}
|
|
851
|
+
// Column-aligned layouts (two or more lines with a run of spaces
|
|
852
|
+
// separating columns) are definition tables, not code: leave them for
|
|
853
|
+
// the indented-table/YAML converters below.
|
|
854
|
+
const alignedLines = blockLines.filter(l => /\S\s{2,}\S/.test(l.trim())).length
|
|
855
|
+
if (alignedLines < 2) {
|
|
856
|
+
const indent = Math.min(...blockLines.filter(l => l.trim() !== '').map(l => l.search(/\S/)))
|
|
857
|
+
const dedented = blockLines.map(l => l.slice(indent)).join('\n')
|
|
858
|
+
const codeBlock = `\n[,text]\n----\n${dedented}\n----\n`
|
|
859
|
+
const placeholder = `__EARLY_CODE_BLOCK_${codeBlockStore.length}__`
|
|
860
|
+
codeBlockStore.push(codeBlock)
|
|
861
|
+
result.push('')
|
|
862
|
+
result.push(placeholder)
|
|
863
|
+
result.push('')
|
|
864
|
+
i = j
|
|
865
|
+
continue
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
781
869
|
// Check for indented command example: 2+ spaces then --, rpk, or $ (shell prompt)
|
|
782
870
|
// e.g. " --job-name test --labels ..."
|
|
783
871
|
// e.g. " rpk cluster info"
|
|
@@ -1240,10 +1328,13 @@ function convertIndentedTablesToAsciiDoc(text, options = {}) {
|
|
|
1240
1328
|
* @param {Object|null} customTransformations
|
|
1241
1329
|
* @returns {string}
|
|
1242
1330
|
*/
|
|
1243
|
-
function applyTextTransformations(text, customTransformations) {
|
|
1331
|
+
function applyTextTransformations(text, customTransformations, options = {}) {
|
|
1244
1332
|
if (!text || !customTransformations?.replacements) return text
|
|
1245
1333
|
let result = text
|
|
1246
1334
|
for (const rule of customTransformations.replacements) {
|
|
1335
|
+
// Code blocks are verbatim: only rules explicitly marked applyToCode
|
|
1336
|
+
// (like the rpai -> rpk ai binary-name rewrite) may touch them.
|
|
1337
|
+
if (options.code && !rule.applyToCode) continue
|
|
1247
1338
|
try {
|
|
1248
1339
|
const flags = rule.flags || 'g'
|
|
1249
1340
|
result = result.replace(new RegExp(rule.pattern, flags), rule.replacement)
|
|
@@ -1254,6 +1345,24 @@ function applyTextTransformations(text, customTransformations) {
|
|
|
1254
1345
|
return result
|
|
1255
1346
|
}
|
|
1256
1347
|
|
|
1348
|
+
/**
|
|
1349
|
+
* Apply text transformations to examples content line by line. Indented
|
|
1350
|
+
* lines are verbatim commands: only rules flagged applyToCode may touch
|
|
1351
|
+
* them (a caption rule once rewrote '{"quotas":...}' inside a command to
|
|
1352
|
+
* '{`quotas`:...}'). Caption lines get the full rule set.
|
|
1353
|
+
* @param {string} text
|
|
1354
|
+
* @param {Object|null} customTransformations
|
|
1355
|
+
* @returns {string}
|
|
1356
|
+
*/
|
|
1357
|
+
function applyTextTransformationsToExamples(text, customTransformations) {
|
|
1358
|
+
if (!text || !customTransformations?.replacements) return text
|
|
1359
|
+
return text.split('\n').map(line =>
|
|
1360
|
+
/^[ ]{2,}\S/.test(line)
|
|
1361
|
+
? applyTextTransformations(line, customTransformations, { code: true })
|
|
1362
|
+
: applyTextTransformations(line, customTransformations)
|
|
1363
|
+
).join('\n')
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1257
1366
|
/**
|
|
1258
1367
|
* Format description by adding backticks around flags and code
|
|
1259
1368
|
* @param {string} desc - Description text
|
|
@@ -1470,9 +1579,19 @@ function formatDescription(desc, customTransformations = null, options = {}) {
|
|
|
1470
1579
|
// Also handles '--flag/-f', '--flag help', and similar patterns
|
|
1471
1580
|
.replace(/'(--[a-z][-a-z0-9]*(?:\/-[a-z])?(?:\s+\w+)?)'/gi, '$1')
|
|
1472
1581
|
.replace(/'(-[a-z])'/gi, '$1')
|
|
1582
|
+
// Drop a dangling example lead-in with nothing after it (upstream help
|
|
1583
|
+
// strings sometimes end mid-example, which would render as "for example,.")
|
|
1584
|
+
.replace(/[,;]?\s*\be\.g\.[\s,]*$/i, '')
|
|
1585
|
+
// Stray space before a closing parenthesis (upstream help typo)
|
|
1586
|
+
.replace(/ +\)/g, ')')
|
|
1473
1587
|
// === STYLE GUIDE COMPLIANCE ===
|
|
1474
|
-
.
|
|
1475
|
-
|
|
1588
|
+
// "e.g.:" introduces a block; keep the colon instead of rendering ", :".
|
|
1589
|
+
// Horizontal whitespace only: crossing a newline would glue the next
|
|
1590
|
+
// line (often a code-block placeholder) onto this sentence.
|
|
1591
|
+
.replace(/,?[^\S\n]*\be\.g\.:[^\S\n]*/gi, ', for example: ')
|
|
1592
|
+
.replace(/, for example: $/gm, ', for example:')
|
|
1593
|
+
.replace(/\be\.g\.[^\S\n]*/gi, 'for example, ')
|
|
1594
|
+
.replace(/\bi\.e\.[^\S\n]*/gi, 'that is, ')
|
|
1476
1595
|
// === TYPO CORRECTIONS ===
|
|
1477
1596
|
// Fix "an" before consonant sounds (common source typos)
|
|
1478
1597
|
.replace(/\ban\s+(prod|dev|test|local|remote|new|cluster|config|file)\b/gi, 'a $1')
|
|
@@ -1592,12 +1711,23 @@ function formatDescription(desc, customTransformations = null, options = {}) {
|
|
|
1592
1711
|
// e.g., "(see #2904)" → "(see https://github.com/redpanda-data/redpanda/issues/2904[#2904])"
|
|
1593
1712
|
result = result.replace(/(?<![`\w])#(\d{4,})(?![`\w])/g, 'https://github.com/redpanda-data/redpanda/issues/$1[#$1]')
|
|
1594
1713
|
|
|
1714
|
+
// Escape template placeholders like {name} in prose: Asciidoctor would
|
|
1715
|
+
// consume them as attribute references and drop them from the output.
|
|
1716
|
+
// Backtick spans can hold raw code at this stage, so only prose segments
|
|
1717
|
+
// between spans are touched. {vbar} stays: it is a real attribute used to
|
|
1718
|
+
// escape pipes in table cells.
|
|
1719
|
+
result = result.split(/(`[^`]*`)/).map((segment, idx) =>
|
|
1720
|
+
idx % 2 === 1 ? segment : segment.replace(/(?<![\\`\w{]){([a-z][\w-]{0,30})}/g, (match, token) =>
|
|
1721
|
+
token === 'vbar' ? match : `\\{${token}}`)
|
|
1722
|
+
).join('')
|
|
1723
|
+
|
|
1595
1724
|
// Restore early code blocks FIRST (from indented command/YAML detection)
|
|
1596
1725
|
// This must happen before inline code restoration so that placeholders
|
|
1597
1726
|
// inside the early code blocks (like __INLINE_CODE_X__) get resolved
|
|
1598
1727
|
// Use function replacements to prevent $ special-pattern interpretation (e.g. `$` → before-match)
|
|
1599
1728
|
earlyCodeBlocks.forEach((block, i) => {
|
|
1600
|
-
|
|
1729
|
+
const transformed = applyTextTransformations(block, customTransformations, { code: true })
|
|
1730
|
+
result = result.replace(`__EARLY_CODE_BLOCK_${i}__`, () => transformed)
|
|
1601
1731
|
})
|
|
1602
1732
|
|
|
1603
1733
|
// Restore inline code
|
|
@@ -1807,11 +1937,37 @@ function parseDescriptionSections(desc) {
|
|
|
1807
1937
|
let currentContent = []
|
|
1808
1938
|
const mainLines = []
|
|
1809
1939
|
|
|
1810
|
-
|
|
1940
|
+
let skipNext = false
|
|
1941
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1942
|
+
const line = lines[i]
|
|
1943
|
+
if (skipNext) {
|
|
1944
|
+
skipNext = false
|
|
1945
|
+
continue
|
|
1946
|
+
}
|
|
1811
1947
|
// Check for ALL CAPS header (at least 2 chars, possibly with spaces, hyphens, slashes, or ampersands)
|
|
1812
1948
|
// Examples: "FIELDS", "BALANCER STATUS", "PRODUCER ID & EPOCH"
|
|
1813
|
-
|
|
1949
|
+
// A line with runs of multiple spaces is a column-header row of aligned
|
|
1950
|
+
// sample output (for example "PARTITION REASON"), not a section
|
|
1951
|
+
// header, so leave it in the content.
|
|
1952
|
+
let headerMatch = / /.test(line) ? null : line.match(/^([A-Z][A-Z\s\-\/&]{0,}[A-Z])$/)
|
|
1953
|
+
// An all-caps line directly after a "$ command" invocation is the title
|
|
1954
|
+
// of that command's sample output (for example "DECOMMISSION PROGRESS"),
|
|
1955
|
+
// not a section header. Splitting there would strand the invocation in
|
|
1956
|
+
// one section and its output table in another.
|
|
1957
|
+
if (headerMatch) {
|
|
1958
|
+
for (let p = i - 1; p >= 0; p--) {
|
|
1959
|
+
if (lines[p].trim() === '') break
|
|
1960
|
+
if (/^\$\s/.test(lines[p])) { headerMatch = null; break }
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1814
1963
|
if (headerMatch) {
|
|
1964
|
+
// Help text often underlines section titles with a run of = or -
|
|
1965
|
+
// characters. Consume the underline: left in the content, a line of
|
|
1966
|
+
// 4+ = or - is an AsciiDoc block delimiter and breaks the page
|
|
1967
|
+
// (unterminated example block).
|
|
1968
|
+
if (i + 1 < lines.length && /^\s*(={3,}|-{3,})\s*$/.test(lines[i + 1])) {
|
|
1969
|
+
skipNext = true
|
|
1970
|
+
}
|
|
1815
1971
|
// Save previous section
|
|
1816
1972
|
if (currentSection) {
|
|
1817
1973
|
// Preserve indentation: only remove leading/trailing blank lines, not spaces
|
|
@@ -1832,6 +1988,13 @@ function parseDescriptionSections(desc) {
|
|
|
1832
1988
|
sections[currentSection] = trimBlankLines(currentContent.join('\n'))
|
|
1833
1989
|
}
|
|
1834
1990
|
|
|
1991
|
+
const delimiterRun = /^\s*(={4,}|-{4,})\s*$/
|
|
1992
|
+
for (const [name, body] of Object.entries(sections)) {
|
|
1993
|
+
if (body.split('\n').some(l => delimiterRun.test(l))) {
|
|
1994
|
+
console.warn(`Warning: section "${name}" contains a bare =/- delimiter run; it may break AsciiDoc rendering`)
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1835
1998
|
return {
|
|
1836
1999
|
mainDescription: mainLines.join('\n').trim(),
|
|
1837
2000
|
sections
|
|
@@ -1941,6 +2104,45 @@ function dashify(commandPath) {
|
|
|
1941
2104
|
* @param {string} desc - Full description
|
|
1942
2105
|
* @returns {string} Short description
|
|
1943
2106
|
*/
|
|
2107
|
+
/**
|
|
2108
|
+
* Collapse runs of three or more newlines to a single blank line, except
|
|
2109
|
+
* inside AsciiDoc delimited blocks (----, ====, ...., |===, and -- open
|
|
2110
|
+
* blocks), where blank lines are content.
|
|
2111
|
+
* @param {string} text - Rendered page content
|
|
2112
|
+
* @returns {string}
|
|
2113
|
+
*/
|
|
2114
|
+
function collapseBlankLines(text) {
|
|
2115
|
+
const lines = text.split('\n')
|
|
2116
|
+
const out = []
|
|
2117
|
+
let inBlock = false
|
|
2118
|
+
let blockDelim = null
|
|
2119
|
+
let blankRun = 0
|
|
2120
|
+
|
|
2121
|
+
for (const line of lines) {
|
|
2122
|
+
const trimmed = line.trimEnd()
|
|
2123
|
+
const isDelim = /^(-{4,}|={4,}|\.{4,}|\|===|--)$/.test(trimmed)
|
|
2124
|
+
if (isDelim) {
|
|
2125
|
+
if (!inBlock) {
|
|
2126
|
+
inBlock = true
|
|
2127
|
+
blockDelim = trimmed
|
|
2128
|
+
} else if (trimmed === blockDelim || (blockDelim === '|===' && trimmed === '|===')) {
|
|
2129
|
+
inBlock = false
|
|
2130
|
+
blockDelim = null
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
if (!inBlock && trimmed === '') {
|
|
2135
|
+
blankRun++
|
|
2136
|
+
if (blankRun > 1) continue
|
|
2137
|
+
} else {
|
|
2138
|
+
blankRun = 0
|
|
2139
|
+
}
|
|
2140
|
+
out.push(line)
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
return out.join('\n')
|
|
2144
|
+
}
|
|
2145
|
+
|
|
1944
2146
|
function capToTwoSentences(desc) {
|
|
1945
2147
|
if (!desc) return ''
|
|
1946
2148
|
if (typeof desc !== 'string') return String(desc)
|
|
@@ -1949,6 +2151,19 @@ function capToTwoSentences(desc) {
|
|
|
1949
2151
|
// Pattern: matches == Section Header and everything after it
|
|
1950
2152
|
let cleaned = desc.replace(/\s*==\s+.+$/s, '')
|
|
1951
2153
|
|
|
2154
|
+
// Delimited code/output blocks never belong in a summary. Indented help
|
|
2155
|
+
// examples are converted to [,text]/[,bash]/[.no-copy] blocks before this
|
|
2156
|
+
// runs. A block introduced by a colon ends the summary there (the prose
|
|
2157
|
+
// after it would read as a non sequitur without the sample); otherwise
|
|
2158
|
+
// drop the block and keep the surrounding prose.
|
|
2159
|
+
const blockRe = /\n?\[[^\]\n]*\]\n----\n[\s\S]*?\n----|\n----\n[\s\S]*?\n----/
|
|
2160
|
+
const firstBlock = cleaned.match(blockRe)
|
|
2161
|
+
if (firstBlock && /:\s*$/.test(cleaned.slice(0, firstBlock.index))) {
|
|
2162
|
+
cleaned = cleaned.slice(0, firstBlock.index)
|
|
2163
|
+
} else {
|
|
2164
|
+
cleaned = cleaned.replace(new RegExp(blockRe.source, 'g'), '')
|
|
2165
|
+
}
|
|
2166
|
+
|
|
1952
2167
|
// Remove indented content (tables, lists, etc.) after colons before sentence detection
|
|
1953
2168
|
// Pattern: text ending with colon, optionally followed by blank line, then indented content
|
|
1954
2169
|
// Examples:
|
|
@@ -1982,11 +2197,26 @@ function capToTwoSentences(desc) {
|
|
|
1982
2197
|
})
|
|
1983
2198
|
}
|
|
1984
2199
|
|
|
2200
|
+
// Protect decimal points in version-like numbers (OAuth 2.0, HTTP 1.1) so
|
|
2201
|
+
// they neither split a sentence nor cause the leading fragment to be
|
|
2202
|
+
// dropped by the sentence matcher
|
|
2203
|
+
normalized = normalized.replace(/(\d)\.(\d)/g, '$1__DECIMAL__$2')
|
|
2204
|
+
|
|
1985
2205
|
// Match sentences
|
|
1986
|
-
|
|
2206
|
+
let sentences = normalized.match(/[^.!?]+[.!?]+(?:\s|$)/g)
|
|
2207
|
+
|
|
2208
|
+
// Never drop a leading fragment: if the first matched sentence does not
|
|
2209
|
+
// start at the beginning of the text (an unterminated prefix was skipped),
|
|
2210
|
+
// glue the prefix back onto it
|
|
2211
|
+
if (sentences && sentences.length > 0) {
|
|
2212
|
+
const firstIdx = normalized.indexOf(sentences[0])
|
|
2213
|
+
if (firstIdx > 0) {
|
|
2214
|
+
sentences = [normalized.slice(0, firstIdx) + sentences[0], ...sentences.slice(1)]
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
1987
2217
|
if (!sentences || sentences.length === 0) {
|
|
1988
2218
|
// Restore and return
|
|
1989
|
-
let result = normalized
|
|
2219
|
+
let result = normalized.replace(/__DECIMAL__/g, '.')
|
|
1990
2220
|
placeholders.forEach(({ ph, original }) => {
|
|
1991
2221
|
result = result.replace(ph, original)
|
|
1992
2222
|
})
|
|
@@ -1998,6 +2228,9 @@ function capToTwoSentences(desc) {
|
|
|
1998
2228
|
|
|
1999
2229
|
let result = sentences.slice(0, 2).join('')
|
|
2000
2230
|
|
|
2231
|
+
// Restore decimal points
|
|
2232
|
+
result = result.replace(/__DECIMAL__/g, '.')
|
|
2233
|
+
|
|
2001
2234
|
// Restore abbreviations
|
|
2002
2235
|
placeholders.forEach(({ ph, original }) => {
|
|
2003
2236
|
result = result.replace(ph, original)
|
|
@@ -2087,6 +2320,61 @@ function findTopLevelWithSubcommands(tree) {
|
|
|
2087
2320
|
return result
|
|
2088
2321
|
}
|
|
2089
2322
|
|
|
2323
|
+
/**
|
|
2324
|
+
* Known rpk plugins that are managed separately from rpk core (they have
|
|
2325
|
+
* install/uninstall/upgrade shim commands, and their real command tree only
|
|
2326
|
+
* appears when the plugin binary is installed in the generation environment).
|
|
2327
|
+
* Shared with rpk-docs-handler.js, which iterates this list to install each
|
|
2328
|
+
* plugin before running --print-tree.
|
|
2329
|
+
*/
|
|
2330
|
+
const KNOWN_PLUGINS = ['ai', 'check', 'connect', 'k8s', 'oxla']
|
|
2331
|
+
|
|
2332
|
+
/**
|
|
2333
|
+
* Subcommands that rpk core ships as a built-in shim for managed plugins,
|
|
2334
|
+
* present even when the plugin binary itself is not installed.
|
|
2335
|
+
*/
|
|
2336
|
+
const PLUGIN_SHIM_COMMANDS = new Set(['install', 'uninstall', 'upgrade'])
|
|
2337
|
+
|
|
2338
|
+
/**
|
|
2339
|
+
* Check whether a command path belongs to a protected plugin's subtree
|
|
2340
|
+
* (including the plugin's own top-level command).
|
|
2341
|
+
* @param {string} commandPath - Full command path, e.g. 'rpk k8s multicluster'
|
|
2342
|
+
* @param {Array<string>} protectedPlugins - Plugin names, e.g. ['k8s']
|
|
2343
|
+
* @returns {boolean}
|
|
2344
|
+
*/
|
|
2345
|
+
function isProtectedCommandPath(commandPath, protectedPlugins) {
|
|
2346
|
+
return protectedPlugins.some(pl => commandPath === `rpk ${pl}` || commandPath.startsWith(`rpk ${pl} `))
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
/**
|
|
2350
|
+
* Determine which managed plugins must be protected for this run.
|
|
2351
|
+
*
|
|
2352
|
+
* Auto-detects plugins whose commands are missing from this run's tree:
|
|
2353
|
+
* rpk core ships only a shim (install/uninstall/upgrade) for managed
|
|
2354
|
+
* plugins; the real commands appear only when the plugin binary is
|
|
2355
|
+
* installed in the generation environment. If a known plugin's subtree is
|
|
2356
|
+
* absent or shim-only, its existing pages reflect the plugin, not stale
|
|
2357
|
+
* commands, so they must be preserved. Pre-GA windows hit this every time:
|
|
2358
|
+
* the plugin publisher only promotes stable X.Y.Z releases, so `rpk <plugin>
|
|
2359
|
+
* install` finds nothing until GA (see redpanda-data/docs#1831).
|
|
2360
|
+
*
|
|
2361
|
+
* @param {Array} commands - Flat command list from flattenCommands()
|
|
2362
|
+
* @param {Array<string>} explicitProtected - Plugins the caller already knows
|
|
2363
|
+
* failed to install this run (merged with auto-detection)
|
|
2364
|
+
* @returns {Array<string>} Deduplicated protected plugin names
|
|
2365
|
+
*/
|
|
2366
|
+
function detectProtectedPlugins(commands, explicitProtected = []) {
|
|
2367
|
+
const autoProtected = KNOWN_PLUGINS.filter(pl => {
|
|
2368
|
+
const subNames = commands
|
|
2369
|
+
.filter(c => c.path.startsWith(`rpk ${pl} `))
|
|
2370
|
+
.map(c => c.path.split(' ')[2])
|
|
2371
|
+
const hasTopLevel = commands.some(c => c.path === `rpk ${pl}`)
|
|
2372
|
+
if (!hasTopLevel) return true
|
|
2373
|
+
return subNames.length === 0 || subNames.every(n => PLUGIN_SHIM_COMMANDS.has(n))
|
|
2374
|
+
})
|
|
2375
|
+
return [...new Set([...explicitProtected, ...autoProtected])]
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2090
2378
|
/**
|
|
2091
2379
|
* Static entries that always appear at the top of the rpk nav section,
|
|
2092
2380
|
* before the auto-generated command entries. These are hand-written pages
|
|
@@ -2095,6 +2383,10 @@ function findTopLevelWithSubcommands(tree) {
|
|
|
2095
2383
|
* landing page (the section header), so it is skipped during generation.
|
|
2096
2384
|
*/
|
|
2097
2385
|
const STATIC_RPK_NAV_ENTRIES = [
|
|
2386
|
+
// The root rpk command page is generated but skipped by the entry loop
|
|
2387
|
+
// (its path has no group segment), so it is listed here to keep it in
|
|
2388
|
+
// the nav ahead of the hand-written pages.
|
|
2389
|
+
'*** xref:reference:rpk/rpk.adoc[]',
|
|
2098
2390
|
'*** xref:reference:rpk/rpk-commands.adoc[]',
|
|
2099
2391
|
'*** xref:reference:rpk/rpk-x-options.adoc[rpk -X]',
|
|
2100
2392
|
]
|
|
@@ -2109,7 +2401,7 @@ const STATIC_RPK_NAV_ENTRIES = [
|
|
|
2109
2401
|
* @param {Set} topLevelWithSubcommands - Set of top-level command names with subcommands
|
|
2110
2402
|
* @returns {Object} { navUpdated, navEntriesGenerated }
|
|
2111
2403
|
*/
|
|
2112
|
-
function updateNavFile(navFile, commands, resolvedOverrides, topLevelWithSubcommands) {
|
|
2404
|
+
function updateNavFile(navFile, commands, resolvedOverrides, topLevelWithSubcommands, protectedPlugins = []) {
|
|
2113
2405
|
if (!fs.existsSync(navFile)) {
|
|
2114
2406
|
console.warn(`Warning: nav file not found, skipping nav update: ${navFile}`)
|
|
2115
2407
|
return { navUpdated: false }
|
|
@@ -2140,10 +2432,13 @@ function updateNavFile(navFile, commands, resolvedOverrides, topLevelWithSubcomm
|
|
|
2140
2432
|
const newEntries = []
|
|
2141
2433
|
for (const { path: commandPath } of commands) {
|
|
2142
2434
|
if (commandPath === 'rpk') continue
|
|
2435
|
+
// Protected plugins get no generated entries: this run's tree has at
|
|
2436
|
+
// most the shim (install/uninstall/upgrade) for them, so their previous
|
|
2437
|
+
// nav block is preserved in place instead (below).
|
|
2438
|
+
if (isProtectedCommandPath(commandPath, protectedPlugins)) continue
|
|
2143
2439
|
if (shouldExcludeCommand(resolvedOverrides, commandPath)) continue
|
|
2144
2440
|
if (shouldUsePartialDir(resolvedOverrides, commandPath)) continue
|
|
2145
|
-
if (commandPath
|
|
2146
|
-
if (commandPath.startsWith('rpk security secret')) continue
|
|
2441
|
+
if (isCloudSecretCommand(commandPath)) continue
|
|
2147
2442
|
|
|
2148
2443
|
const parts = commandPath.split(' ')
|
|
2149
2444
|
const stars = '*'.repeat(parts.length + 1)
|
|
@@ -2155,12 +2450,65 @@ function updateNavFile(navFile, commands, resolvedOverrides, topLevelWithSubcomm
|
|
|
2155
2450
|
newEntries.push(`${stars} xref:${xrefPath}[]`)
|
|
2156
2451
|
}
|
|
2157
2452
|
|
|
2158
|
-
//
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2453
|
+
// Preserve existing entries for protected plugins. Their commands are
|
|
2454
|
+
// absent (or shim-only) in this run's tree, so nothing was generated for
|
|
2455
|
+
// them above. Each plugin's previous nav block (parent entry plus nested
|
|
2456
|
+
// children) is kept together and spliced back in at its original position,
|
|
2457
|
+
// anchored to the nearest preceding entry that survives regeneration —
|
|
2458
|
+
// never appended at the end, where the children would render under the
|
|
2459
|
+
// wrong parent.
|
|
2460
|
+
const stripStars = (line) => line.replace(/^\*+ /, '')
|
|
2461
|
+
const emittedTargets = new Set([...STATIC_RPK_NAV_ENTRIES, ...newEntries].map(stripStars))
|
|
2462
|
+
// Map of anchor target (or null for "before all surviving entries") to the
|
|
2463
|
+
// preserved lines that follow that anchor, in original order.
|
|
2464
|
+
const preservedBlocks = new Map()
|
|
2465
|
+
let preservedCount = 0
|
|
2466
|
+
const preservedPlugins = new Set()
|
|
2467
|
+
if (protectedPlugins.length > 0) {
|
|
2468
|
+
const patternsFor = (pl) => [`reference:rpk/rpk-${pl}/`, `reference:rpk/rpk-${pl}.adoc`]
|
|
2469
|
+
const pluginFor = (line) => protectedPlugins.find(pl => patternsFor(pl).some(pat => line.includes(pat)))
|
|
2470
|
+
let lastAnchor = null
|
|
2471
|
+
for (const line of lines.slice(sectionStart + 1, sectionEnd)) {
|
|
2472
|
+
const plugin = pluginFor(line)
|
|
2473
|
+
if (plugin) {
|
|
2474
|
+
// Never duplicate an entry that regeneration already emits.
|
|
2475
|
+
if (emittedTargets.has(stripStars(line))) continue
|
|
2476
|
+
if (!preservedBlocks.has(lastAnchor)) preservedBlocks.set(lastAnchor, [])
|
|
2477
|
+
preservedBlocks.get(lastAnchor).push(line)
|
|
2478
|
+
preservedCount++
|
|
2479
|
+
preservedPlugins.add(plugin)
|
|
2480
|
+
} else if (emittedTargets.has(stripStars(line))) {
|
|
2481
|
+
lastAnchor = stripStars(line)
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
if (preservedCount > 0) {
|
|
2485
|
+
console.log(` Preserving ${preservedCount} nav entries for plugins: ${[...preservedPlugins].join(', ')}`)
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// Rebuild: header + static entries + generated entries, splicing each
|
|
2490
|
+
// preserved block back in directly after its anchor entry.
|
|
2491
|
+
const newSection = [lines[sectionStart]]
|
|
2492
|
+
const pushEntry = (line) => {
|
|
2493
|
+
newSection.push(line)
|
|
2494
|
+
const target = stripStars(line)
|
|
2495
|
+
if (preservedBlocks.has(target)) {
|
|
2496
|
+
newSection.push(...preservedBlocks.get(target))
|
|
2497
|
+
preservedBlocks.delete(target)
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
// Blocks that preceded every surviving entry in the old nav go right after
|
|
2501
|
+
// the section header, before the static entries.
|
|
2502
|
+
if (preservedBlocks.has(null)) {
|
|
2503
|
+
newSection.push(...preservedBlocks.get(null))
|
|
2504
|
+
preservedBlocks.delete(null)
|
|
2505
|
+
}
|
|
2506
|
+
for (const line of STATIC_RPK_NAV_ENTRIES) pushEntry(line)
|
|
2507
|
+
for (const line of newEntries) pushEntry(line)
|
|
2508
|
+
// Safety net: if an anchor vanished between collection and rebuild (it
|
|
2509
|
+
// cannot with the logic above, but never silently drop preserved entries),
|
|
2510
|
+
// append any remaining blocks at the end.
|
|
2511
|
+
for (const block of preservedBlocks.values()) newSection.push(...block)
|
|
2164
2512
|
|
|
2165
2513
|
const newLines = [
|
|
2166
2514
|
...lines.slice(0, sectionStart),
|
|
@@ -2187,7 +2535,12 @@ async function generateRpkDocs(options = {}) {
|
|
|
2187
2535
|
pluginVersions = {},
|
|
2188
2536
|
draftMissing = false,
|
|
2189
2537
|
flatOutput = false, // If true, output all files flat (legacy behavior)
|
|
2190
|
-
navFile // Optional: path to nav.adoc for automatic nav updates
|
|
2538
|
+
navFile, // Optional: path to nav.adoc for automatic nav updates
|
|
2539
|
+
// Plugins that exist but could not be installed for this run (for example,
|
|
2540
|
+
// rpk k8s before its GA plugin publishes). Their pages and nav entries are
|
|
2541
|
+
// preserved instead of treated as stale, because their absence from the
|
|
2542
|
+
// command tree reflects the generation environment, not the product.
|
|
2543
|
+
protectedPlugins = []
|
|
2191
2544
|
} = options
|
|
2192
2545
|
|
|
2193
2546
|
// Register partials
|
|
@@ -2225,6 +2578,16 @@ async function generateRpkDocs(options = {}) {
|
|
|
2225
2578
|
// Flatten command tree
|
|
2226
2579
|
const commands = flattenCommands(tree)
|
|
2227
2580
|
|
|
2581
|
+
// Determine which managed plugins are protected this run: explicitly
|
|
2582
|
+
// passed by the caller (failed installs) merged with auto-detection of
|
|
2583
|
+
// absent or shim-only plugin subtrees. Protection covers the whole
|
|
2584
|
+
// pipeline — page writes, the stale-file sweep, and nav updates — so a
|
|
2585
|
+
// run without the plugin binary leaves the plugin's docs fully untouched.
|
|
2586
|
+
const effectiveProtectedPlugins = detectProtectedPlugins(commands, protectedPlugins)
|
|
2587
|
+
if (effectiveProtectedPlugins.length > 0) {
|
|
2588
|
+
console.log(` Protected plugins this run (pages and nav preserved, not regenerated): ${effectiveProtectedPlugins.join(', ')}`)
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2228
2591
|
// Find top-level commands with subcommands (for directory structure)
|
|
2229
2592
|
const topLevelWithSubcommands = findTopLevelWithSubcommands(tree)
|
|
2230
2593
|
|
|
@@ -2246,6 +2609,18 @@ async function generateRpkDocs(options = {}) {
|
|
|
2246
2609
|
continue
|
|
2247
2610
|
}
|
|
2248
2611
|
|
|
2612
|
+
// Protected plugins: skip generating the whole subtree, parent page
|
|
2613
|
+
// included. This run's tree has at most the shim
|
|
2614
|
+
// (install/uninstall/upgrade) for these plugins, so regenerating would
|
|
2615
|
+
// overwrite full-plugin pages from an earlier successful run — for
|
|
2616
|
+
// example, rewriting rpk-k8s.adoc with a Subcommands table reduced to
|
|
2617
|
+
// the shim and orphaning the preserved child pages. Existing pages stay
|
|
2618
|
+
// untouched, exactly as in the fully-absent-plugin case.
|
|
2619
|
+
if (isProtectedCommandPath(commandPath, effectiveProtectedPlugins)) {
|
|
2620
|
+
filesSkipped++
|
|
2621
|
+
continue
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2249
2624
|
// Check if command should be excluded
|
|
2250
2625
|
if (shouldExcludeCommand(resolvedOverrides, commandPath)) {
|
|
2251
2626
|
filesSkipped++
|
|
@@ -2273,8 +2648,9 @@ async function generateRpkDocs(options = {}) {
|
|
|
2273
2648
|
if (mergedCommand.excludeExamples && mergedCommand.excludeExamples.length > 0) {
|
|
2274
2649
|
examplesContent = filterExamples(sectionContent, mergedCommand.excludeExamples)
|
|
2275
2650
|
}
|
|
2276
|
-
// Apply text transformations (e.g. rpai → rpk ai) then format
|
|
2277
|
-
|
|
2651
|
+
// Apply text transformations (e.g. rpai → rpk ai) then format.
|
|
2652
|
+
// Command lines only get code-safe rules.
|
|
2653
|
+
examplesContent = applyTextTransformationsToExamples(examplesContent, textTransformations)
|
|
2278
2654
|
sections[sectionName] = formatExamples(examplesContent)
|
|
2279
2655
|
} else {
|
|
2280
2656
|
sections[sectionName] = formatDescription(sectionContent, textTransformations)
|
|
@@ -2291,8 +2667,9 @@ async function generateRpkDocs(options = {}) {
|
|
|
2291
2667
|
if (mergedCommand.excludeExamples && mergedCommand.excludeExamples.length > 0) {
|
|
2292
2668
|
examplesContent = filterExamples(examplesContent, mergedCommand.excludeExamples)
|
|
2293
2669
|
}
|
|
2294
|
-
// Apply text transformations (e.g. rpai → rpk ai) then format
|
|
2295
|
-
|
|
2670
|
+
// Apply text transformations (e.g. rpai → rpk ai) then format.
|
|
2671
|
+
// Command lines only get code-safe rules.
|
|
2672
|
+
examplesContent = applyTextTransformationsToExamples(examplesContent, textTransformations)
|
|
2296
2673
|
sections.EXAMPLES = formatExamples(examplesContent)
|
|
2297
2674
|
}
|
|
2298
2675
|
|
|
@@ -2365,6 +2742,10 @@ async function generateRpkDocs(options = {}) {
|
|
|
2365
2742
|
// Build subcommands with correct xref paths
|
|
2366
2743
|
// Filter out excluded and asPartial subcommands — excluded have no file,
|
|
2367
2744
|
// asPartial ones live in the partials directory with no linkable xref.
|
|
2745
|
+
// rpk cloud and rpk security secret rows stay in the table: their pages
|
|
2746
|
+
// are routed to the cloud partials directory and published by
|
|
2747
|
+
// cloud-docs, so the xref below links across to the cloud component
|
|
2748
|
+
// instead of dropping the row (which hid the subcommand entirely).
|
|
2368
2749
|
const subcommands = (command.commands || [])
|
|
2369
2750
|
.filter(sub => {
|
|
2370
2751
|
const subPath = `${commandPath} ${sub.name}`
|
|
@@ -2389,6 +2770,13 @@ async function generateRpkDocs(options = {}) {
|
|
|
2389
2770
|
}
|
|
2390
2771
|
}
|
|
2391
2772
|
|
|
2773
|
+
// rpk cloud and rpk security secret pages are routed to the cloud
|
|
2774
|
+
// partials directory and published by cloud-docs, so rows in regular
|
|
2775
|
+
// pages must link across to the cloud component.
|
|
2776
|
+
if (cloudSecretDir && isCloudSecretCommand(subPath) && !isCloudSecretCommand(commandPath)) {
|
|
2777
|
+
xrefPath = `${CLOUD_DOCS_COMPONENT}:${xrefPath}`
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2392
2780
|
// Use overridden description if available
|
|
2393
2781
|
const subOverride = resolvedOverrides?.commands?.[subPath]
|
|
2394
2782
|
const subDescription = subOverride?.description || sub.description || ''
|
|
@@ -2398,6 +2786,8 @@ async function generateRpkDocs(options = {}) {
|
|
|
2398
2786
|
fullPath: subPath,
|
|
2399
2787
|
dashifiedPath: dashifiedSubPath,
|
|
2400
2788
|
xrefPath,
|
|
2789
|
+
cloudOnly: subOverride?.cloudOnly === true,
|
|
2790
|
+
selfHostedOnly: subOverride?.selfHostedOnly === true,
|
|
2401
2791
|
shortDesc: ensurePeriod(capToTwoSentences(formatDescription(subDescription, textTransformations, { skipTableConversion: true, skipListConversion: true })))
|
|
2402
2792
|
}
|
|
2403
2793
|
})
|
|
@@ -2431,7 +2821,24 @@ async function generateRpkDocs(options = {}) {
|
|
|
2431
2821
|
return `${parentPath} ${alias}`
|
|
2432
2822
|
}),
|
|
2433
2823
|
aliasNotes: commandOverride.aliasNotes,
|
|
2434
|
-
|
|
2824
|
+
// A curated override section titled "Flags" wins over the extracted
|
|
2825
|
+
// flag table: rendering both produced duplicate == Flags headings and
|
|
2826
|
+
// conflicting content (rpk connect run). Curation is authoritative;
|
|
2827
|
+
// the warning tells maintainers the override can be dropped to adopt
|
|
2828
|
+
// the extracted table.
|
|
2829
|
+
flags: (() => {
|
|
2830
|
+
const hasCuratedFlagsSection = Object.values(processedContent.sections || {})
|
|
2831
|
+
.some(items => (items || []).some(item => /^flags$/i.test(item.title || '')))
|
|
2832
|
+
if (hasCuratedFlagsSection && (mergedCommand.flags || []).length > 0) {
|
|
2833
|
+
console.warn(
|
|
2834
|
+
`Warning: ${commandPath} has a curated "Flags" override section; ` +
|
|
2835
|
+
`skipping the extracted flag table (${mergedCommand.flags.length} flags). ` +
|
|
2836
|
+
'Remove the override section to adopt the extracted table.'
|
|
2837
|
+
)
|
|
2838
|
+
return []
|
|
2839
|
+
}
|
|
2840
|
+
return mergedCommand.flags || []
|
|
2841
|
+
})().map(flag => ({
|
|
2435
2842
|
...flag,
|
|
2436
2843
|
name: flag.shorthand ? `-${flag.shorthand}, --${flag.name}` : `--${flag.name}`,
|
|
2437
2844
|
type: flag.type || '-',
|
|
@@ -2542,15 +2949,31 @@ async function generateRpkDocs(options = {}) {
|
|
|
2542
2949
|
hasUnknownSections: unknownSections.length > 0
|
|
2543
2950
|
}
|
|
2544
2951
|
|
|
2545
|
-
// Render template
|
|
2546
|
-
|
|
2952
|
+
// Render template, then collapse runs of blank lines left behind by
|
|
2953
|
+
// absent optional sections (plugin commands have no aliases or flags, so
|
|
2954
|
+
// the separators between skipped blocks stack up). Blank lines inside
|
|
2955
|
+
// delimited blocks are preserved: they are content there.
|
|
2956
|
+
const content = collapseBlankLines(template(context))
|
|
2957
|
+
|
|
2958
|
+
// Duplicate top-level headings almost always mean an override adds a
|
|
2959
|
+
// section the page already renders (or embeds its own heading in raw
|
|
2960
|
+
// content). The page still builds, but anchors collide and readers see
|
|
2961
|
+
// the same section twice.
|
|
2962
|
+
const h2Counts = new Map()
|
|
2963
|
+
for (const line of content.split('\n')) {
|
|
2964
|
+
const h2 = line.match(/^== (\S.*)$/)
|
|
2965
|
+
if (h2) h2Counts.set(h2[1], (h2Counts.get(h2[1]) || 0) + 1)
|
|
2966
|
+
}
|
|
2967
|
+
for (const [heading, count] of h2Counts) {
|
|
2968
|
+
if (count > 1) {
|
|
2969
|
+
console.warn(`⚠️ ${commandPath}: heading "== ${heading}" appears ${count} times. Check the override content for this command.`)
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2547
2972
|
|
|
2548
2973
|
// Determine output path
|
|
2549
2974
|
// Check if this command should go to cloudSecretDir
|
|
2550
|
-
const isCloudCommand = commandPath.startsWith('rpk cloud')
|
|
2551
|
-
const isSecuritySecretCommand = commandPath.startsWith('rpk security secret')
|
|
2552
2975
|
const useCloudSecretDir = cloudSecretDir && (
|
|
2553
|
-
|
|
2976
|
+
isCloudSecretCommand(commandPath) || shouldUsePartialDir(resolvedOverrides, commandPath)
|
|
2554
2977
|
)
|
|
2555
2978
|
const effectiveOutputDir = useCloudSecretDir ? cloudSecretDir : outputDir
|
|
2556
2979
|
|
|
@@ -2614,9 +3037,19 @@ async function generateRpkDocs(options = {}) {
|
|
|
2614
3037
|
.map(entry => path.join(baseDir, entry.name))
|
|
2615
3038
|
}
|
|
2616
3039
|
|
|
3040
|
+
// Protected plugins (computed before the write loop, see
|
|
3041
|
+
// detectProtectedPlugins): their existing pages reflect the plugin, not
|
|
3042
|
+
// stale commands, so their directories are excluded from the sweep.
|
|
3043
|
+
const protectedDirNames = new Set(effectiveProtectedPlugins.map(pl => `rpk-${pl}`))
|
|
2617
3044
|
const scanDirs = new Set()
|
|
2618
3045
|
for (const dir of rpkSubdirs(outputDir)) scanDirs.add(dir)
|
|
2619
3046
|
for (const dir of rpkSubdirs(cloudSecretDir)) scanDirs.add(dir)
|
|
3047
|
+
for (const dir of [...scanDirs]) {
|
|
3048
|
+
if (protectedDirNames.has(path.basename(dir))) {
|
|
3049
|
+
console.log(` Preserving ${path.basename(dir)}/ (plugin commands absent from this run's tree)`)
|
|
3050
|
+
scanDirs.delete(dir)
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
2620
3053
|
|
|
2621
3054
|
for (const dir of scanDirs) {
|
|
2622
3055
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -2644,7 +3077,7 @@ async function generateRpkDocs(options = {}) {
|
|
|
2644
3077
|
// Update nav.adoc if a path was provided
|
|
2645
3078
|
let navResult = { navUpdated: false }
|
|
2646
3079
|
if (navFile) {
|
|
2647
|
-
navResult = updateNavFile(navFile, commands, resolvedOverrides, topLevelWithSubcommands)
|
|
3080
|
+
navResult = updateNavFile(navFile, commands, resolvedOverrides, topLevelWithSubcommands, effectiveProtectedPlugins)
|
|
2648
3081
|
}
|
|
2649
3082
|
|
|
2650
3083
|
return {
|
|
@@ -2681,9 +3114,12 @@ module.exports = {
|
|
|
2681
3114
|
shouldExcludeCommand,
|
|
2682
3115
|
shouldUsePartialDir,
|
|
2683
3116
|
updateNavFile,
|
|
3117
|
+
KNOWN_PLUGINS,
|
|
3118
|
+
detectProtectedPlugins,
|
|
2684
3119
|
getCommandMetadata,
|
|
2685
3120
|
processContentArray,
|
|
2686
3121
|
// Exported for testing
|
|
2687
3122
|
filterExamples,
|
|
2688
|
-
formatExamples
|
|
3123
|
+
formatExamples,
|
|
3124
|
+
applyTextTransformationsToExamples
|
|
2689
3125
|
}
|