Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions __tests__/tools/rpk-docs/text-transformations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,59 @@ describe('applyToCode rules in early code blocks', () => {
})
})

describe('applyToCode rules in inline code spans', () => {
const { formatDescription } = require('../../../tools/rpk-docs/generate-rpk-docs.js')

const transforms = {
replacements: [
{ pattern: '\\brpai\\b', replacement: 'rpk ai', flags: 'g', applyToCode: true },
{ pattern: '(^|\\n\\s*)Note:\\s', replacement: '$1NOTE: ', flags: 'g' }
]
}

test('rewrites the binary name inside protected inline code spans', () => {
const out = formatDescription('Run `rpai auth token` to authenticate first.', transforms)
expect(out).toContain('`rpk ai auth token`')
expect(out).not.toContain('rpai')
})

test('rules without applyToCode never touch inline code spans', () => {
const out = formatDescription('The literal `Note: keep this` stays verbatim.', transforms)
expect(out).toContain('`Note: keep this`')
expect(out).not.toContain('NOTE: keep this')
})
})

describe('known command path formatting', () => {
const { formatDescription, registerKnownCommandPaths } = require('../../../tools/rpk-docs/generate-rpk-docs.js')

afterEach(() => registerKnownCommandPaths([]))

test('wraps a full multi-word command path as a unit', () => {
registerKnownCommandPaths(['rpk', 'rpk ai', 'rpk ai run', 'rpk ai run codex'])
const out = formatDescription('Use rpk ai run codex to start a session.', null)
expect(out).toContain('`rpk ai run codex` to start a session.')
expect(out).not.toContain('`rpk` ai')
})

test('prefers the longest registered path over a shorter prefix', () => {
registerKnownCommandPaths(['rpk', 'rpk ai', 'rpk ai run', 'rpk ai run claude'])
const out = formatDescription('Then rpk ai run claude resumes the session.', null)
expect(out).toContain('`rpk ai run claude` resumes')
})

test('leaves prose that resembles a command alone when not in the tree', () => {
registerKnownCommandPaths(['rpk', 'rpk cloud'])
const out = formatDescription('Manage rpk cloud authentications for details.', null)
expect(out).not.toContain('`rpk cloud authentications`')
})

test('is inert when no paths are registered', () => {
const out = formatDescription('Use rpk ai run codex to start.', null)
expect(out).not.toContain('`rpk ai run codex`')
})
})

describe('applyTextTransformationsToExamples', () => {
const { applyTextTransformationsToExamples } = require('../../../tools/rpk-docs/generate-rpk-docs.js')

Expand Down
46 changes: 43 additions & 3 deletions tools/rpk-docs/generate-rpk-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ Object.entries(helpers).forEach(([name, fn]) => {
// Template paths
const TEMPLATES_DIR = path.resolve(__dirname, './templates')

// Full command paths from the tree being generated (e.g. "rpk ai run codex").
// Registered by generateRpkDocs so formatDescription can wrap real multi-word
// command paths as a unit instead of heuristically wrapping `rpk` alone.
let knownCommandPaths = new Set()

/**
* Register the set of real command paths for the current generation run.
* @param {string[]} paths - Full command paths (e.g. "rpk ai run codex")
*/
function registerKnownCommandPaths(paths) {
knownCommandPaths = new Set(paths)
}

/**
* Register a Handlebars partial from file
* @param {string} name - Partial name
Expand Down Expand Up @@ -1607,6 +1620,25 @@ function formatDescription(desc, customTransformations = null, options = {}) {
// Fix product name: "Redpanda cloud" → "Redpanda Cloud" (product name)
.replace(/Redpanda\s+cloud\b/g, 'Redpanda Cloud')

// === RPK COMMAND FORMATTING (known paths first, ground truth) ===
// Wrap full multi-word command paths that exist in the generated tree
// (e.g. "rpk ai run codex" -> `rpk ai run codex`). The heuristic formatter
// below only ever matches "rpk <word>", so without this pass it wraps
// `rpk` alone and splits the command in half. Matching against the real
// tree means prose that merely resembles a command is never wrapped.
if (knownCommandPaths.size > 0) {
result = result.replace(/(?<![`\w])rpk((?: [a-z][-a-z0-9]*)+)/g, (match, rest) => {
const tokens = rest.trim().split(' ')
for (let n = tokens.length; n >= 2; n--) {
const path = `rpk ${tokens.slice(0, n).join(' ')}`
if (knownCommandPaths.has(path)) {
return `\`${path}\`` + match.slice(path.length)
}
}
return match
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
}

// === RPK COMMAND FORMATTING (context-aware) ===
// Process "rpk X" patterns based on what follows
result = result.replace(/(?<![`\w])rpk(\s+)([a-z][-a-z0-9]*)(?:\s+|(?=[.,;:!?)'"}\]]|$))/gi, (match, space, word, offset, str) => {
Expand Down Expand Up @@ -1730,9 +1762,13 @@ function formatDescription(desc, customTransformations = null, options = {}) {
result = result.replace(`__EARLY_CODE_BLOCK_${i}__`, () => transformed)
})

// Restore inline code
// Restore inline code. Spans are verbatim, but rules flagged applyToCode
// (like the rpai -> rpk ai binary-name rewrite) must reach them the same
// way they reach code blocks, or the internal binary name survives in
// published spans like `rpai auth token`.
inlineCode.forEach((code, i) => {
result = result.replace(`__INLINE_CODE_${i}__`, () => code)
const transformed = applyTextTransformations(code, customTransformations, { code: true })
result = result.replace(`__INLINE_CODE_${i}__`, () => transformed)
})

// Restore xrefs
Expand Down Expand Up @@ -2578,6 +2614,9 @@ async function generateRpkDocs(options = {}) {
// Flatten command tree
const commands = flattenCommands(tree)

// Let formatDescription wrap real multi-word command paths as a unit
registerKnownCommandPaths(commands.map(c => c.path))

// Determine which managed plugins are protected this run: explicitly
// passed by the caller (failed installs) merged with auto-detection of
// absent or shim-only plugin subtrees. Protection covers the whole
Expand Down Expand Up @@ -3121,5 +3160,6 @@ module.exports = {
// Exported for testing
filterExamples,
formatExamples,
applyTextTransformationsToExamples
applyTextTransformationsToExamples,
registerKnownCommandPaths
}
Loading