@redpanda-data/docs-extensions-and-macros 4.10.7 → 4.11.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/extensions/convert-to-markdown.js +373 -0
- package/package.json +7 -3
- package/tools/property-extractor/generate-handlebars-docs.js +77 -41
- package/tools/property-extractor/helpers/allTopicsConditional.js +19 -0
- package/tools/property-extractor/helpers/formatPropertyValue.js +4 -0
- package/tools/property-extractor/helpers/index.js +2 -0
- package/tools/property-extractor/helpers/parseRelatedTopic.js +13 -0
- package/tools/property-extractor/property_extractor.py +23 -2
- package/tools/property-extractor/templates/property.hbs +45 -2
- package/tools/property-extractor/templates/topic-property.hbs +42 -2
- package/tools/property-extractor/topic_property_extractor.py +24 -5
- package/tools/property-extractor/transformers.py +12 -8
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const TurndownService = require('turndown')
|
|
4
|
+
const turndownPluginGfm = require('turndown-plugin-gfm')
|
|
5
|
+
const { gfm } = turndownPluginGfm
|
|
6
|
+
|
|
7
|
+
module.exports.register = function () {
|
|
8
|
+
const logger = this.getLogger('convert-to-markdown-extension')
|
|
9
|
+
let playbook
|
|
10
|
+
|
|
11
|
+
// Shared Turndown configuration
|
|
12
|
+
const baseConfig = {
|
|
13
|
+
headingStyle: 'atx',
|
|
14
|
+
codeBlockStyle: 'fenced',
|
|
15
|
+
bulletListMarker: '-',
|
|
16
|
+
linkReferenceStyle: 'full',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Factory: create a configured Turndown instance
|
|
20
|
+
function createTurndownBase() {
|
|
21
|
+
const td = new TurndownService(baseConfig)
|
|
22
|
+
td.use(gfm)
|
|
23
|
+
|
|
24
|
+
// Remove unwanted global elements (footers, modals, feedback, etc.)
|
|
25
|
+
td.addRule('remove-unwanted', {
|
|
26
|
+
filter: (node) => {
|
|
27
|
+
if (!node || !node.getAttribute) return false
|
|
28
|
+
|
|
29
|
+
const classAttr = (node.getAttribute('class') || '').toLowerCase()
|
|
30
|
+
const idAttr = (node.getAttribute('id') || '').toLowerCase()
|
|
31
|
+
const tag = node.nodeName.toLowerCase()
|
|
32
|
+
|
|
33
|
+
// Remove by tag
|
|
34
|
+
if (['script', 'style', 'footer', 'nav'].includes(tag)) return true
|
|
35
|
+
|
|
36
|
+
// Remove tracking or hidden images
|
|
37
|
+
if (
|
|
38
|
+
tag === 'img' &&
|
|
39
|
+
(classAttr.includes('tracking') ||
|
|
40
|
+
idAttr.includes('scarf') ||
|
|
41
|
+
node.getAttribute('role') === 'presentation' ||
|
|
42
|
+
node.style?.display === 'none')
|
|
43
|
+
) {
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Remove by class or id
|
|
48
|
+
const toRemove = [
|
|
49
|
+
'thumbs',
|
|
50
|
+
'back-to-top',
|
|
51
|
+
'contributors-modal',
|
|
52
|
+
'feedback-section',
|
|
53
|
+
'feedback-toast',
|
|
54
|
+
'pagination',
|
|
55
|
+
'footer',
|
|
56
|
+
'nav-expand',
|
|
57
|
+
'banner-container'
|
|
58
|
+
]
|
|
59
|
+
return toRemove.some(
|
|
60
|
+
(x) => classAttr.includes(x) || idAttr.includes(x)
|
|
61
|
+
)
|
|
62
|
+
},
|
|
63
|
+
replacement: () => '',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Keep critical content blocks only
|
|
67
|
+
td.keep(['div.openblock.tabs', 'article.doc'])
|
|
68
|
+
return td
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Factory: create page-specific Turndown converter
|
|
72
|
+
function createTurndownForPage(page) {
|
|
73
|
+
const outerTurndown = createTurndownBase()
|
|
74
|
+
const nestedTurndown = createTurndownBase()
|
|
75
|
+
|
|
76
|
+
// Helper to add custom rules
|
|
77
|
+
function addCustomRules(turndownInstance, isInner = false) {
|
|
78
|
+
// Determine heading depth for tab conversion
|
|
79
|
+
function findNearestHeadingLevel(el) {
|
|
80
|
+
let current = el.previousElementSibling
|
|
81
|
+
while (current) {
|
|
82
|
+
if (/^H[1-6]$/i.test(current.nodeName))
|
|
83
|
+
return parseInt(current.nodeName.substring(1))
|
|
84
|
+
current = current.previousElementSibling
|
|
85
|
+
}
|
|
86
|
+
let parent = el.parentElement
|
|
87
|
+
while (parent) {
|
|
88
|
+
const headings = Array.from(
|
|
89
|
+
parent.querySelectorAll('h1,h2,h3,h4,h5,h6')
|
|
90
|
+
)
|
|
91
|
+
if (headings.length > 0) {
|
|
92
|
+
const last = headings[headings.length - 1]
|
|
93
|
+
return parseInt(last.nodeName.substring(1))
|
|
94
|
+
}
|
|
95
|
+
parent = parent.parentElement
|
|
96
|
+
}
|
|
97
|
+
return 2
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Asciidoctor tab conversion
|
|
101
|
+
turndownInstance.addRule('asciidoctor-tabs', {
|
|
102
|
+
filter: (node) => {
|
|
103
|
+
if (node.nodeName !== 'DIV') return false
|
|
104
|
+
const classAttr = node.getAttribute?.('class') || node.className || ''
|
|
105
|
+
return classAttr.includes('openblock') && classAttr.includes('tabs')
|
|
106
|
+
},
|
|
107
|
+
replacement: function (_, node) {
|
|
108
|
+
function processTabGroup(group, parentHeadingLevel = null) {
|
|
109
|
+
const contentDiv = group.querySelector('.content') || group
|
|
110
|
+
const tabList = contentDiv.querySelectorAll('li.tab')
|
|
111
|
+
if (!tabList.length) return ''
|
|
112
|
+
|
|
113
|
+
const nearestLevel =
|
|
114
|
+
parentHeadingLevel != null
|
|
115
|
+
? parentHeadingLevel + 1
|
|
116
|
+
: findNearestHeadingLevel(group) + 1
|
|
117
|
+
const tabHeadingLevel = Math.min(nearestLevel, 6)
|
|
118
|
+
const headingPrefix = '#'.repeat(tabHeadingLevel)
|
|
119
|
+
|
|
120
|
+
let markdown = ''
|
|
121
|
+
tabList.forEach((tab) => {
|
|
122
|
+
const title =
|
|
123
|
+
tab.querySelector('p')?.textContent.trim() ||
|
|
124
|
+
tab.textContent.trim()
|
|
125
|
+
|
|
126
|
+
let panelId = tab.getAttribute('aria-controls')
|
|
127
|
+
if (!panelId && tab.id) panelId = tab.id + '--panel'
|
|
128
|
+
const panel = group.querySelector(`#${panelId}`)
|
|
129
|
+
if (!panel) return
|
|
130
|
+
|
|
131
|
+
const nestedTabs = panel.querySelectorAll('.openblock.tabs')
|
|
132
|
+
let nestedMdCombined = ''
|
|
133
|
+
nestedTabs.forEach((nested) => {
|
|
134
|
+
nestedMdCombined +=
|
|
135
|
+
'\n' + processTabGroup(nested, tabHeadingLevel) + '\n'
|
|
136
|
+
nested.remove()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const innerHtml = panel.innerHTML || ''
|
|
140
|
+
let md = ''
|
|
141
|
+
try {
|
|
142
|
+
const converter = isInner ? nestedTurndown : turndownInstance
|
|
143
|
+
md = converter.turndown(innerHtml)
|
|
144
|
+
} catch (e) {
|
|
145
|
+
logger.warn(`Turndown failed in nested tab: ${e.message}`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
markdown += `${headingPrefix} ${title}\n\n${md.trim()}\n${nestedMdCombined.trim()}\n\n`
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
return markdown.trim()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return '\n' + processTabGroup(node, null) + '\n'
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Admonition block conversion
|
|
159
|
+
turndownInstance.addRule('admonition', {
|
|
160
|
+
filter: (node) =>
|
|
161
|
+
node.nodeName === 'TABLE' &&
|
|
162
|
+
node.querySelector('td.icon') &&
|
|
163
|
+
node.querySelector('td.content'),
|
|
164
|
+
replacement: function (_, node) {
|
|
165
|
+
const iconCell = node.querySelector('td.icon')
|
|
166
|
+
const contentCell = node.querySelector('td.content')
|
|
167
|
+
if (!iconCell || !contentCell) return ''
|
|
168
|
+
|
|
169
|
+
const iconEl = iconCell.querySelector('i')
|
|
170
|
+
const classAttr = iconEl?.className || ''
|
|
171
|
+
const match = classAttr.match(/icon-([a-z]+)/i)
|
|
172
|
+
const type = match ? match[1].toUpperCase() : 'NOTE'
|
|
173
|
+
|
|
174
|
+
const titleEl =
|
|
175
|
+
node.querySelector('.title') ||
|
|
176
|
+
contentCell.querySelector('.title') ||
|
|
177
|
+
iconEl?.getAttribute('title')
|
|
178
|
+
const customTitle =
|
|
179
|
+
typeof titleEl === 'string'
|
|
180
|
+
? titleEl.trim()
|
|
181
|
+
: titleEl?.textContent?.trim() || ''
|
|
182
|
+
|
|
183
|
+
const emojiMap = {
|
|
184
|
+
CAUTION: '⚠️',
|
|
185
|
+
WARNING: '⚠️',
|
|
186
|
+
TIP: '💡',
|
|
187
|
+
NOTE: '📝',
|
|
188
|
+
IMPORTANT: '❗',
|
|
189
|
+
}
|
|
190
|
+
const emoji = emojiMap[type] || '📘'
|
|
191
|
+
|
|
192
|
+
const innerHtml = contentCell.innerHTML || ''
|
|
193
|
+
let innerMd = ''
|
|
194
|
+
try {
|
|
195
|
+
const converter = isInner ? nestedTurndown : turndownInstance
|
|
196
|
+
innerMd = converter.turndown(innerHtml).trim()
|
|
197
|
+
} catch (e) {
|
|
198
|
+
logger.warn(`Turndown failed in admonition: ${e.message}`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const titleLower = customTitle.toLowerCase()
|
|
202
|
+
const typeLower = type.toLowerCase()
|
|
203
|
+
const header =
|
|
204
|
+
customTitle && titleLower !== typeLower
|
|
205
|
+
? `${emoji} **${type}: ${customTitle}**`
|
|
206
|
+
: `${emoji} **${type}**`
|
|
207
|
+
|
|
208
|
+
const quoted = innerMd
|
|
209
|
+
.split('\n')
|
|
210
|
+
.map((line) => (line.startsWith('>') ? line : `> ${line}`))
|
|
211
|
+
.join('\n')
|
|
212
|
+
|
|
213
|
+
return `\n> ${header}\n>\n${quoted}\n`
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Markdown table conversion
|
|
218
|
+
turndownInstance.addRule('tables', {
|
|
219
|
+
filter: (node) => {
|
|
220
|
+
if (node.nodeName !== 'TABLE') return false
|
|
221
|
+
if (node.querySelector('td.icon') && node.querySelector('td.content'))
|
|
222
|
+
return false
|
|
223
|
+
return true
|
|
224
|
+
},
|
|
225
|
+
replacement: function (content, node) {
|
|
226
|
+
const rows = Array.from(node.querySelectorAll('tr'))
|
|
227
|
+
if (!rows.length) return content
|
|
228
|
+
const tableRows = []
|
|
229
|
+
rows.forEach((row, index) => {
|
|
230
|
+
const cells = Array.from(row.querySelectorAll('th, td'))
|
|
231
|
+
const cellContents = cells.map((cell) =>
|
|
232
|
+
(cell.textContent || '').trim().replace(/\s+/g, ' ')
|
|
233
|
+
)
|
|
234
|
+
if (!cellContents.length) return
|
|
235
|
+
const rowLine = '| ' + cellContents.join(' | ') + ' |'
|
|
236
|
+
tableRows.push(rowLine)
|
|
237
|
+
if (index === 0) {
|
|
238
|
+
const separator =
|
|
239
|
+
'| ' + cellContents.map(() => '---').join(' | ') + ' |'
|
|
240
|
+
tableRows.push(separator)
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
return '\n' + tableRows.join('\n') + '\n'
|
|
244
|
+
},
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
addCustomRules(outerTurndown, false)
|
|
249
|
+
addCustomRules(nestedTurndown, true)
|
|
250
|
+
return outerTurndown
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Conversion pipeline
|
|
254
|
+
this.on('pagesComposed', async ({ playbook: pb, contentCatalog }) => {
|
|
255
|
+
playbook = pb
|
|
256
|
+
const siteUrl = playbook.site?.url || ''
|
|
257
|
+
const pages = contentCatalog.getPages()
|
|
258
|
+
logger.info(
|
|
259
|
+
`Converting ${pages.length} pages to Markdown${
|
|
260
|
+
siteUrl ? ` (site.url=${siteUrl})` : ''
|
|
261
|
+
}...`
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
const concurrency = Math.max(2, Math.floor(os.cpus().length / 2))
|
|
265
|
+
const queue = [...pages]
|
|
266
|
+
let convertedCount = 0
|
|
267
|
+
|
|
268
|
+
async function processQueue() {
|
|
269
|
+
while (queue.length) {
|
|
270
|
+
const page = queue.shift()
|
|
271
|
+
if (!page?.contents) continue
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const html = page.contents.toString().trim()
|
|
275
|
+
if (!html) continue
|
|
276
|
+
|
|
277
|
+
// Extract only the <article class="doc"> portion
|
|
278
|
+
const match = html.match(
|
|
279
|
+
/<article[^>]*class=["'][^"']*\bdoc\b[^"']*["'][^>]*>([\s\S]*?)<\/article>/i
|
|
280
|
+
)
|
|
281
|
+
if (!match || !match[1]) {
|
|
282
|
+
logger.info(`No <article class="doc"> found for ${page.src?.path}`)
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
const articleHtml = match[1]
|
|
286
|
+
|
|
287
|
+
// Convert with Turndown
|
|
288
|
+
const td = createTurndownForPage(page)
|
|
289
|
+
let markdown = td.turndown(articleHtml).trim()
|
|
290
|
+
|
|
291
|
+
// Canonical source link
|
|
292
|
+
let canonicalUrl = ''
|
|
293
|
+
try {
|
|
294
|
+
if (siteUrl && page.pub?.url) {
|
|
295
|
+
const htmlStyle = playbook?.urls?.htmlExtensionStyle
|
|
296
|
+
const isIndexify = htmlStyle === 'indexify'
|
|
297
|
+
const baseUrl = new URL(page.pub.url, siteUrl)
|
|
298
|
+
let pathname = baseUrl.pathname
|
|
299
|
+
|
|
300
|
+
if (isIndexify) {
|
|
301
|
+
const looksLikeDir =
|
|
302
|
+
pathname.endsWith('/') ||
|
|
303
|
+
!path.basename(pathname).includes('.')
|
|
304
|
+
baseUrl.pathname = looksLikeDir
|
|
305
|
+
? pathname.replace(/\/?$/, '/index.md')
|
|
306
|
+
: pathname.replace(/\.html$/, '.md')
|
|
307
|
+
} else {
|
|
308
|
+
baseUrl.pathname = pathname.replace(/\.html$/, '.md')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
canonicalUrl = baseUrl.toString()
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {
|
|
314
|
+
logger.debug(
|
|
315
|
+
`Failed to build canonical URL for ${page.src?.path}: ${e.message}`
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Prepend Markdown source reference and URL construction hint
|
|
320
|
+
if (canonicalUrl) {
|
|
321
|
+
const urlHint = `<!-- Note for AI: Links in this doc are relative to the current page and use indexify format. Add /index.md to directory-style links for the Markdown version. -->`
|
|
322
|
+
|
|
323
|
+
markdown = `<!-- Source: ${canonicalUrl} -->\n${urlHint}\n\n${markdown}`
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Clean up unnecessary whitespace
|
|
327
|
+
if (markdown) {
|
|
328
|
+
// Remove excessive blank lines (more than 2 consecutive newlines)
|
|
329
|
+
markdown = markdown.replace(/\n{3,}/g, '\n\n')
|
|
330
|
+
// Remove trailing whitespace from lines
|
|
331
|
+
markdown = markdown.replace(/[ \t]+$/gm, '')
|
|
332
|
+
// Remove leading/trailing whitespace from the entire document
|
|
333
|
+
markdown = markdown.trim()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (markdown) {
|
|
337
|
+
page.markdownContents = Buffer.from(markdown, 'utf8')
|
|
338
|
+
convertedCount++
|
|
339
|
+
}
|
|
340
|
+
} catch (err) {
|
|
341
|
+
logger.error(
|
|
342
|
+
`Error converting ${page.src?.path || 'unknown'}: ${err.message}`
|
|
343
|
+
)
|
|
344
|
+
logger.debug(err.stack)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const workers = Array.from({ length: concurrency }, processQueue)
|
|
350
|
+
await Promise.all(workers)
|
|
351
|
+
logger.info(`Converted ${convertedCount} Markdown files.`)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
// Add Markdown files to site catalog
|
|
355
|
+
this.on('beforePublish', ({ siteCatalog, contentCatalog }) => {
|
|
356
|
+
const pages = contentCatalog.getPages((p) => p.markdownContents)
|
|
357
|
+
if (!pages.length) {
|
|
358
|
+
logger.info('No Markdown files to publish.')
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
logger.info(`Adding ${pages.length} Markdown files to site catalog...`)
|
|
362
|
+
for (const page of pages) {
|
|
363
|
+
const htmlOut = page.out?.path
|
|
364
|
+
if (!htmlOut) continue
|
|
365
|
+
const mdOutPath = htmlOut.replace(/\.html$/, '.md')
|
|
366
|
+
siteCatalog.addFile({
|
|
367
|
+
contents: page.markdownContents,
|
|
368
|
+
out: { path: mdOutPath },
|
|
369
|
+
})
|
|
370
|
+
logger.debug(`Added Markdown: ${mdOutPath}`)
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redpanda-data/docs-extensions-and-macros",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.11.0",
|
|
4
4
|
"description": "Antora extensions and macros developed for Redpanda documentation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antora",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"./extensions/process-context-switcher": "./extensions/process-context-switcher.js",
|
|
44
44
|
"./extensions/archive-attachments": "./extensions/archive-attachments.js",
|
|
45
45
|
"./extensions/add-pages-to-root": "./extensions/add-pages-to-root.js",
|
|
46
|
+
"./extensions/convert-to-markdown": "./extensions/convert-to-markdown.js",
|
|
46
47
|
"./extensions/collect-bloblang-samples": "./extensions/collect-bloblang-samples.js",
|
|
47
48
|
"./extensions/compute-end-of-life": "./extensions/compute-end-of-life.js",
|
|
48
49
|
"./extensions/generate-rp-connect-categories": "./extensions/generate-rp-connect-categories.js",
|
|
@@ -85,8 +86,10 @@
|
|
|
85
86
|
"@octokit/core": "^6.1.2",
|
|
86
87
|
"@octokit/plugin-retry": "^7.1.1",
|
|
87
88
|
"@octokit/rest": "^21.0.1",
|
|
89
|
+
"@redocly/cli": "^2.2.0",
|
|
88
90
|
"algoliasearch": "^4.17.0",
|
|
89
91
|
"chalk": "4.1.2",
|
|
92
|
+
"cheerio": "^1.1.2",
|
|
90
93
|
"commander": "^14.0.0",
|
|
91
94
|
"gulp": "^4.0.2",
|
|
92
95
|
"gulp-connect": "^5.7.0",
|
|
@@ -103,9 +106,10 @@
|
|
|
103
106
|
"sync-request": "^6.1.0",
|
|
104
107
|
"tar": "^7.4.3",
|
|
105
108
|
"tree-sitter": "^0.22.4",
|
|
109
|
+
"turndown": "^7.2.2",
|
|
110
|
+
"turndown-plugin-gfm": "^1.0.2",
|
|
106
111
|
"yaml": "^2.7.1",
|
|
107
|
-
"yargs": "^17.7.2"
|
|
108
|
-
"@redocly/cli": "^2.2.0"
|
|
112
|
+
"yargs": "^17.7.2"
|
|
109
113
|
},
|
|
110
114
|
"devDependencies": {
|
|
111
115
|
"@antora/cli": "3.1.4",
|
|
@@ -22,7 +22,7 @@ const helpers = require('./helpers');
|
|
|
22
22
|
* CLI Usage: node generate-handlebars-docs.js <input-file> <output-dir>
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
// Register
|
|
25
|
+
// Register helpers
|
|
26
26
|
Object.entries(helpers).forEach(([name, fn]) => {
|
|
27
27
|
if (typeof fn !== 'function') {
|
|
28
28
|
console.error(`❌ Helper "${name}" is not a function`);
|
|
@@ -62,11 +62,6 @@ function getTemplatePath(defaultPath, envVar) {
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* Register Handlebars partials used to render property documentation.
|
|
65
|
-
*
|
|
66
|
-
* Registers:
|
|
67
|
-
* - "property"
|
|
68
|
-
* - "topic-property"
|
|
69
|
-
* - "deprecated-property"
|
|
70
65
|
*/
|
|
71
66
|
function registerPartials() {
|
|
72
67
|
const templatesDir = path.join(__dirname, 'templates');
|
|
@@ -112,7 +107,7 @@ function registerPartials() {
|
|
|
112
107
|
/**
|
|
113
108
|
* Generate consolidated AsciiDoc partials for properties grouped by type.
|
|
114
109
|
*/
|
|
115
|
-
function generatePropertyPartials(properties, partialsDir) {
|
|
110
|
+
function generatePropertyPartials(properties, partialsDir, onRender) {
|
|
116
111
|
console.log(`📝 Generating consolidated property partials in ${partialsDir}…`);
|
|
117
112
|
|
|
118
113
|
const propertyTemplate = handlebars.compile(
|
|
@@ -129,11 +124,24 @@ function generatePropertyPartials(properties, partialsDir) {
|
|
|
129
124
|
|
|
130
125
|
Object.values(properties).forEach(prop => {
|
|
131
126
|
if (!prop.name || !prop.config_scope) return;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
127
|
+
|
|
128
|
+
switch (prop.config_scope) {
|
|
129
|
+
case 'topic':
|
|
130
|
+
propertyGroups.topic.push(prop);
|
|
131
|
+
break;
|
|
132
|
+
case 'broker':
|
|
133
|
+
propertyGroups.broker.push(prop);
|
|
134
|
+
break;
|
|
135
|
+
case 'cluster':
|
|
136
|
+
if (isObjectStorageProperty(prop)) propertyGroups['object-storage'].push(prop);
|
|
137
|
+
else propertyGroups.cluster.push(prop);
|
|
138
|
+
break;
|
|
139
|
+
case 'object-storage':
|
|
140
|
+
propertyGroups['object-storage'].push(prop);
|
|
141
|
+
break;
|
|
142
|
+
default:
|
|
143
|
+
console.warn(`⚠️ Unknown config_scope: ${prop.config_scope} for ${prop.name}`);
|
|
144
|
+
break;
|
|
137
145
|
}
|
|
138
146
|
});
|
|
139
147
|
|
|
@@ -143,9 +151,16 @@ function generatePropertyPartials(properties, partialsDir) {
|
|
|
143
151
|
if (props.length === 0) return;
|
|
144
152
|
props.sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')));
|
|
145
153
|
const selectedTemplate = type === 'topic' ? topicTemplate : propertyTemplate;
|
|
146
|
-
const
|
|
154
|
+
const pieces = [];
|
|
155
|
+
props.forEach(p => {
|
|
156
|
+
if (typeof onRender === 'function') {
|
|
157
|
+
try { onRender(p.name); } catch (err) { /* swallow callback errors */ }
|
|
158
|
+
}
|
|
159
|
+
pieces.push(selectedTemplate(p));
|
|
160
|
+
});
|
|
161
|
+
const content = pieces.join('\n');
|
|
147
162
|
const filename = `${type}-properties.adoc`;
|
|
148
|
-
|
|
163
|
+
fs.writeFileSync(path.join(propertiesPartialsDir, filename), AUTOGEN_NOTICE + content, 'utf8');
|
|
149
164
|
console.log(`✅ Generated ${filename} (${props.length} properties)`);
|
|
150
165
|
totalCount += props.length;
|
|
151
166
|
});
|
|
@@ -178,19 +193,18 @@ function generateDeprecatedDocs(properties, outputDir) {
|
|
|
178
193
|
clusterProperties: clusterProperties.length ? clusterProperties : null
|
|
179
194
|
};
|
|
180
195
|
|
|
181
|
-
const output = template(data);
|
|
182
196
|
const outputPath = process.env.OUTPUT_PARTIALS_DIR
|
|
183
197
|
? path.join(process.env.OUTPUT_PARTIALS_DIR, 'deprecated', 'deprecated-properties.adoc')
|
|
184
198
|
: path.join(outputDir, 'partials', 'deprecated', 'deprecated-properties.adoc');
|
|
185
199
|
|
|
186
200
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
187
|
-
fs.writeFileSync(outputPath, AUTOGEN_NOTICE +
|
|
201
|
+
fs.writeFileSync(outputPath, AUTOGEN_NOTICE + template(data), 'utf8');
|
|
188
202
|
console.log(`✅ Generated ${outputPath}`);
|
|
189
203
|
return deprecatedProperties.length;
|
|
190
204
|
}
|
|
191
205
|
|
|
192
206
|
/**
|
|
193
|
-
* Generate topic-property-mappings.adoc
|
|
207
|
+
* Generate topic-property-mappings.adoc
|
|
194
208
|
*/
|
|
195
209
|
function generateTopicPropertyMappings(properties, partialsDir) {
|
|
196
210
|
const templatesDir = path.join(__dirname, 'templates');
|
|
@@ -218,31 +232,42 @@ function generateTopicPropertyMappings(properties, partialsDir) {
|
|
|
218
232
|
}
|
|
219
233
|
|
|
220
234
|
/**
|
|
221
|
-
* Generate error reports for missing descriptions and
|
|
235
|
+
* Generate error reports for missing descriptions, deprecated, and undocumented properties.
|
|
222
236
|
*/
|
|
223
|
-
function generateErrorReports(properties) {
|
|
237
|
+
function generateErrorReports(properties, documentedProperties = []) {
|
|
224
238
|
const emptyDescriptions = [];
|
|
225
239
|
const deprecatedProperties = [];
|
|
240
|
+
const allKeys = Object.keys(properties);
|
|
226
241
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
242
|
+
// Use documentedProperties array (property names that were rendered into partials)
|
|
243
|
+
const documentedSet = new Set(documentedProperties);
|
|
244
|
+
const undocumented = [];
|
|
245
|
+
|
|
246
|
+
Object.entries(properties).forEach(([key, p]) => {
|
|
247
|
+
const name = p.name || key;
|
|
248
|
+
if (!p.description || !p.description.trim()) emptyDescriptions.push(name);
|
|
249
|
+
if (p.is_deprecated) deprecatedProperties.push(name);
|
|
250
|
+
if (!documentedSet.has(name)) undocumented.push(name);
|
|
230
251
|
});
|
|
231
252
|
|
|
232
|
-
const total =
|
|
253
|
+
const total = allKeys.length;
|
|
233
254
|
const pctEmpty = total ? ((emptyDescriptions.length / total) * 100).toFixed(2) : '0.00';
|
|
234
255
|
const pctDeprecated = total ? ((deprecatedProperties.length / total) * 100).toFixed(2) : '0.00';
|
|
235
|
-
|
|
236
|
-
|
|
256
|
+
const pctUndocumented = total ? ((undocumented.length / total) * 100).toFixed(2) : '0.00';
|
|
257
|
+
|
|
258
|
+
console.log(`📉 Empty descriptions: ${emptyDescriptions.length} (${pctEmpty}%)`);
|
|
259
|
+
console.log(`🕸️ Deprecated: ${deprecatedProperties.length} (${pctDeprecated}%)`);
|
|
260
|
+
console.log(`🚫 Not documented: ${undocumented.length} (${pctUndocumented}%)`);
|
|
237
261
|
|
|
238
262
|
return {
|
|
239
263
|
empty_descriptions: emptyDescriptions.sort(),
|
|
240
|
-
deprecated_properties: deprecatedProperties.sort()
|
|
264
|
+
deprecated_properties: deprecatedProperties.sort(),
|
|
265
|
+
undocumented_properties: undocumented.sort(),
|
|
241
266
|
};
|
|
242
267
|
}
|
|
243
268
|
|
|
244
269
|
/**
|
|
245
|
-
* Main generator
|
|
270
|
+
* Main generator
|
|
246
271
|
*/
|
|
247
272
|
function generateAllDocs(inputFile, outputDir) {
|
|
248
273
|
const data = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
|
|
@@ -252,13 +277,14 @@ function generateAllDocs(inputFile, outputDir) {
|
|
|
252
277
|
|
|
253
278
|
let partialsCount = 0;
|
|
254
279
|
let deprecatedCount = 0;
|
|
280
|
+
const documentedProps = []; // Track which property names were rendered
|
|
255
281
|
|
|
256
282
|
if (process.env.GENERATE_PARTIALS === '1' && process.env.OUTPUT_PARTIALS_DIR) {
|
|
257
283
|
console.log('📄 Generating property partials and deprecated docs...');
|
|
258
284
|
deprecatedCount = generateDeprecatedDocs(properties, outputDir);
|
|
259
|
-
partialsCount = generatePropertyPartials(properties, process.env.OUTPUT_PARTIALS_DIR);
|
|
260
285
|
|
|
261
|
-
// Generate
|
|
286
|
+
// Generate property partials using the shared helper and collect names via callback
|
|
287
|
+
partialsCount = generatePropertyPartials(properties, process.env.OUTPUT_PARTIALS_DIR, name => documentedProps.push(name));
|
|
262
288
|
try {
|
|
263
289
|
generateTopicPropertyMappings(properties, process.env.OUTPUT_PARTIALS_DIR);
|
|
264
290
|
} catch (err) {
|
|
@@ -268,24 +294,34 @@ function generateAllDocs(inputFile, outputDir) {
|
|
|
268
294
|
console.log('📄 Skipping partial generation (set GENERATE_PARTIALS=1 and OUTPUT_PARTIALS_DIR to enable)');
|
|
269
295
|
}
|
|
270
296
|
|
|
271
|
-
const errors = generateErrorReports(properties);
|
|
272
|
-
const inputData = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
|
|
273
|
-
inputData.empty_descriptions = errors.empty_descriptions;
|
|
274
|
-
inputData.deprecated_properties = errors.deprecated_properties;
|
|
275
|
-
fs.writeFileSync(inputFile, JSON.stringify(inputData, null, 2), 'utf8');
|
|
297
|
+
const errors = generateErrorReports(properties, documentedProps);
|
|
276
298
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
299
|
+
const totalProperties = Object.keys(properties).length;
|
|
300
|
+
const notRendered = errors.undocumented_properties.length;
|
|
301
|
+
const pctRendered = totalProperties
|
|
302
|
+
? ((partialsCount / totalProperties) * 100).toFixed(2)
|
|
303
|
+
: '0.00';
|
|
304
|
+
|
|
305
|
+
console.log('\n📊 Summary:');
|
|
306
|
+
console.log(` Total properties found: ${totalProperties}`);
|
|
307
|
+
console.log(` Property partials generated: ${partialsCount} (${pctRendered}% of total)`);
|
|
308
|
+
console.log(` Not documented: ${notRendered}`);
|
|
309
|
+
console.log(` Deprecated properties: ${deprecatedCount}`);
|
|
310
|
+
|
|
311
|
+
if (notRendered > 0) {
|
|
312
|
+
console.log('⚠️ Undocumented properties:\n ' + errors.undocumented_properties.join('\n '));
|
|
313
|
+
}
|
|
281
314
|
|
|
282
315
|
return {
|
|
283
|
-
totalProperties
|
|
284
|
-
|
|
285
|
-
|
|
316
|
+
totalProperties,
|
|
317
|
+
generatedPartials: partialsCount,
|
|
318
|
+
undocumentedProperties: errors.undocumented_properties,
|
|
319
|
+
deprecatedProperties: deprecatedCount,
|
|
320
|
+
percentageRendered: pctRendered
|
|
286
321
|
};
|
|
287
322
|
}
|
|
288
323
|
|
|
324
|
+
|
|
289
325
|
module.exports = {
|
|
290
326
|
generateAllDocs,
|
|
291
327
|
generateDeprecatedDocs,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Returns 'cloud' if all topics are cloud-only, 'self-managed' if all are self-managed-only, else 'normal'
|
|
2
|
+
module.exports = function allTopicsConditional(related_topics) {
|
|
3
|
+
if (!Array.isArray(related_topics) || related_topics.length === 0) return null;
|
|
4
|
+
let allCloud = true;
|
|
5
|
+
let allSelfManaged = true;
|
|
6
|
+
for (const t of related_topics) {
|
|
7
|
+
if (typeof t !== 'string') {
|
|
8
|
+
allCloud = false;
|
|
9
|
+
allSelfManaged = false;
|
|
10
|
+
break;
|
|
11
|
+
}
|
|
12
|
+
const trimmed = t.trim();
|
|
13
|
+
if (!trimmed.startsWith('cloud-only:')) allCloud = false;
|
|
14
|
+
if (!trimmed.startsWith('self-managed-only:')) allSelfManaged = false;
|
|
15
|
+
}
|
|
16
|
+
if (allCloud) return 'cloud';
|
|
17
|
+
if (allSelfManaged) return 'self-managed';
|
|
18
|
+
return 'normal';
|
|
19
|
+
};
|
|
@@ -28,6 +28,10 @@ function processDefaults(inputString, suffix) {
|
|
|
28
28
|
return inputString;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Remove C++ digit separators (apostrophes) from numbers/durations
|
|
32
|
+
// e.g. 30'000ms -> 30000ms, 1'500 -> 1500
|
|
33
|
+
inputString = inputString.replace(/(?<=\d)'(?=\d)/g, '');
|
|
34
|
+
|
|
31
35
|
// Test for ip:port in vector: std::vector<net::unresolved_address>({{...}})
|
|
32
36
|
const vectorMatch = inputString.match(/std::vector<net::unresolved_address>\(\{\{("([\d.]+)",\s*(\d+))\}\}\)/);
|
|
33
37
|
if (vectorMatch) {
|
|
@@ -11,4 +11,6 @@ module.exports = {
|
|
|
11
11
|
renderPropertyExample: require('./renderPropertyExample.js'),
|
|
12
12
|
formatUnits: require('./formatUnits.js'),
|
|
13
13
|
anchorName: require('./anchorName.js'),
|
|
14
|
+
parseRelatedTopic: require('./parseRelatedTopic.js'),
|
|
15
|
+
allTopicsConditional: require('./allTopicsConditional.js'),
|
|
14
16
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Returns an object with type and value for a related topic
|
|
2
|
+
// type: 'cloud', 'self-managed', or 'normal'
|
|
3
|
+
module.exports = function parseRelatedTopic(topic) {
|
|
4
|
+
if (typeof topic !== 'string') return { type: 'normal', value: topic };
|
|
5
|
+
const trimmed = topic.trim();
|
|
6
|
+
if (trimmed.startsWith('cloud-only:')) {
|
|
7
|
+
return { type: 'cloud', value: trimmed.replace(/^cloud-only:/, '').trim() };
|
|
8
|
+
}
|
|
9
|
+
if (trimmed.startsWith('self-managed-only:')) {
|
|
10
|
+
return { type: 'self-managed', value: trimmed.replace(/^self-managed-only:/, '').trim() };
|
|
11
|
+
}
|
|
12
|
+
return { type: 'normal', value: trimmed };
|
|
13
|
+
};
|
|
@@ -1010,6 +1010,9 @@ def resolve_type_and_default(properties, definitions):
|
|
|
1010
1010
|
processed (str): A string representing the JSON-ready value (for example: '"value"', 'null', '0', or the original input when no mapping applied).
|
|
1011
1011
|
"""
|
|
1012
1012
|
arg_str = arg_str.strip()
|
|
1013
|
+
# Remove C++ digit separators (apostrophes) that may appear in numeric literals
|
|
1014
|
+
# Example: "30'000ms" -> "30000ms". Use conservative replace only between digits.
|
|
1015
|
+
arg_str = re.sub(r"(?<=\d)'(?=\d)", '', arg_str)
|
|
1013
1016
|
|
|
1014
1017
|
# Handle std::nullopt -> null
|
|
1015
1018
|
if arg_str == "std::nullopt":
|
|
@@ -1023,9 +1026,27 @@ def resolve_type_and_default(properties, definitions):
|
|
|
1023
1026
|
resolved_value = resolve_cpp_function_call(function_name)
|
|
1024
1027
|
if resolved_value is not None:
|
|
1025
1028
|
return f'"{resolved_value}"'
|
|
1029
|
+
|
|
1030
|
+
# Handle std::chrono literals like std::chrono::minutes{5} -> "5min"
|
|
1031
|
+
chrono_match = re.match(r'std::chrono::([a-zA-Z]+)\s*\{\s*(\d+)\s*\}', arg_str)
|
|
1032
|
+
if chrono_match:
|
|
1033
|
+
unit = chrono_match.group(1)
|
|
1034
|
+
value = chrono_match.group(2)
|
|
1035
|
+
unit_map = {
|
|
1036
|
+
'hours': 'h',
|
|
1037
|
+
'minutes': 'min',
|
|
1038
|
+
'seconds': 's',
|
|
1039
|
+
'milliseconds': 'ms',
|
|
1040
|
+
'microseconds': 'us',
|
|
1041
|
+
'nanoseconds': 'ns'
|
|
1042
|
+
}
|
|
1043
|
+
short = unit_map.get(unit.lower(), unit)
|
|
1044
|
+
return f'"{value} {short}"'
|
|
1026
1045
|
|
|
1027
|
-
# Handle enum-like patterns (such as fips_mode_flag::disabled -> "disabled")
|
|
1028
|
-
|
|
1046
|
+
# Handle enum-like patterns (such as fips_mode_flag::disabled -> "disabled").
|
|
1047
|
+
# Only treat bare 'X::Y' tokens as enums — do not match when the token
|
|
1048
|
+
# is followed by constructor braces/parentheses (e.g. std::chrono::minutes{5}).
|
|
1049
|
+
enum_match = re.match(r'[a-zA-Z0-9_:]+::([a-zA-Z0-9_]+)\s*$', arg_str)
|
|
1029
1050
|
if enum_match:
|
|
1030
1051
|
enum_value = enum_match.group(1)
|
|
1031
1052
|
return f'"{enum_value}"'
|
|
@@ -23,6 +23,7 @@ endif::[]
|
|
|
23
23
|
{{else}}
|
|
24
24
|
|
|
25
25
|
No description available.
|
|
26
|
+
|
|
26
27
|
{{/if}}
|
|
27
28
|
{{#if is_enterprise}}
|
|
28
29
|
|
|
@@ -31,9 +32,11 @@ ifndef::env-cloud[]
|
|
|
31
32
|
endif::[]
|
|
32
33
|
{{/if}}
|
|
33
34
|
{{#if cloud_byoc_only}}
|
|
35
|
+
|
|
34
36
|
ifdef::env-cloud[]
|
|
35
37
|
NOTE: This property is available only in Redpanda Cloud BYOC deployments.
|
|
36
38
|
endif::[]
|
|
39
|
+
|
|
37
40
|
{{/if}}
|
|
38
41
|
{{#if units}}
|
|
39
42
|
|
|
@@ -50,6 +53,10 @@ endif::[]
|
|
|
50
53
|
*Requires restart:* {{#if needs_restart}}Yes{{else}}No{{/if}}
|
|
51
54
|
{{/if}}
|
|
52
55
|
{{/if}}
|
|
56
|
+
|
|
57
|
+
ifndef::env-cloud[]
|
|
58
|
+
*Restored during xref:manage:whole-cluster-restore.adoc[Whole Cluster Restore]:* {{#if (ne gets_restored false)}}Yes{{else}}No{{/if}}
|
|
59
|
+
endif::[]
|
|
53
60
|
{{#if visibility}}
|
|
54
61
|
|
|
55
62
|
// tag::self-managed-only[]
|
|
@@ -89,13 +96,49 @@ endif::[]
|
|
|
89
96
|
{{{renderPropertyExample this}}}
|
|
90
97
|
{{/if}}
|
|
91
98
|
{{#if related_topics}}
|
|
99
|
+
{{#with (allTopicsConditional related_topics) as |sectionType|}}
|
|
100
|
+
|
|
101
|
+
{{#if (eq sectionType "cloud")}}
|
|
102
|
+
ifdef::env-cloud[]
|
|
103
|
+
*Related topics:*
|
|
104
|
+
|
|
105
|
+
{{#each ../related_topics}}
|
|
106
|
+
{{#with (parseRelatedTopic this)}}
|
|
107
|
+
* {{{value}}}
|
|
108
|
+
{{/with}}
|
|
109
|
+
{{/each}}
|
|
110
|
+
endif::[]
|
|
111
|
+
{{else if (eq sectionType "self-managed")}}
|
|
112
|
+
ifndef::env-cloud[]
|
|
113
|
+
*Related topics:*
|
|
92
114
|
|
|
115
|
+
{{#each ../related_topics}}
|
|
116
|
+
{{#with (parseRelatedTopic this)}}
|
|
117
|
+
* {{{value}}}
|
|
118
|
+
{{/with}}
|
|
119
|
+
{{/each}}
|
|
120
|
+
endif::[]
|
|
121
|
+
{{else}}
|
|
93
122
|
*Related topics:*
|
|
94
123
|
|
|
95
|
-
{{#each related_topics}}
|
|
96
|
-
|
|
124
|
+
{{#each ../related_topics}}
|
|
125
|
+
{{#with (parseRelatedTopic this)}}
|
|
126
|
+
{{#if (eq type "cloud")}}
|
|
127
|
+
ifdef::env-cloud[]
|
|
128
|
+
* {{{value}}}
|
|
129
|
+
endif::[]
|
|
130
|
+
{{else if (eq type "self-managed")}}
|
|
131
|
+
ifndef::env-cloud[]
|
|
132
|
+
* {{{value}}}
|
|
133
|
+
endif::[]
|
|
134
|
+
{{else}}
|
|
135
|
+
* {{{value}}}
|
|
136
|
+
{{/if}}
|
|
137
|
+
{{/with}}
|
|
97
138
|
{{/each}}
|
|
98
139
|
{{/if}}
|
|
140
|
+
{{/with}}
|
|
141
|
+
{{/if}}
|
|
99
142
|
{{#if aliases}}
|
|
100
143
|
|
|
101
144
|
// tag::self-managed-only[]
|
|
@@ -74,17 +74,57 @@ endif::[]
|
|
|
74
74
|
{{/if}}
|
|
75
75
|
|
|
76
76
|
*Nullable:* {{#if nullable}}Yes{{else}}No{{/if}}
|
|
77
|
+
|
|
78
|
+
ifndef::env-cloud[]
|
|
79
|
+
*Restored during xref:manage:whole-cluster-restore.adoc[Whole Cluster Restore]:* {{#if (ne gets_restored false)}}Yes{{else}}No{{/if}}
|
|
80
|
+
endif::[]
|
|
77
81
|
{{#if example}}
|
|
78
82
|
|
|
79
83
|
{{{renderPropertyExample this}}}
|
|
80
84
|
{{/if}}
|
|
81
85
|
{{#if related_topics}}
|
|
86
|
+
{{#with (allTopicsConditional related_topics) as |sectionType|}}
|
|
82
87
|
|
|
88
|
+
{{#if (eq sectionType "cloud")}}
|
|
89
|
+
ifdef::env-cloud[]
|
|
83
90
|
*Related topics:*
|
|
84
91
|
|
|
85
|
-
{{#each related_topics}}
|
|
86
|
-
|
|
92
|
+
{{#each ../related_topics}}
|
|
93
|
+
{{#with (parseRelatedTopic this)}}
|
|
94
|
+
* {{{value}}}
|
|
95
|
+
{{/with}}
|
|
87
96
|
{{/each}}
|
|
97
|
+
endif::[]
|
|
98
|
+
{{else if (eq sectionType "self-managed")}}
|
|
99
|
+
ifndef::env-cloud[]
|
|
100
|
+
*Related topics:*
|
|
101
|
+
|
|
102
|
+
{{#each ../related_topics}}
|
|
103
|
+
{{#with (parseRelatedTopic this)}}
|
|
104
|
+
* {{{value}}}
|
|
105
|
+
{{/with}}
|
|
106
|
+
{{/each}}
|
|
107
|
+
endif::[]
|
|
108
|
+
{{else}}
|
|
109
|
+
*Related topics:*
|
|
110
|
+
|
|
111
|
+
{{#each ../related_topics}}
|
|
112
|
+
{{#with (parseRelatedTopic this)}}
|
|
113
|
+
{{#if (eq type "cloud")}}
|
|
114
|
+
ifdef::env-cloud[]
|
|
115
|
+
* {{{value}}}
|
|
116
|
+
endif::[]
|
|
117
|
+
{{else if (eq type "self-managed")}}
|
|
118
|
+
ifndef::env-cloud[]
|
|
119
|
+
* {{{value}}}
|
|
120
|
+
endif::[]
|
|
121
|
+
{{else}}
|
|
122
|
+
* {{{value}}}
|
|
123
|
+
{{/if}}
|
|
124
|
+
{{/with}}
|
|
125
|
+
{{/each}}
|
|
126
|
+
{{/if}}
|
|
127
|
+
{{/with}}
|
|
88
128
|
{{/if}}
|
|
89
129
|
{{#if aliases}}
|
|
90
130
|
|
|
@@ -304,11 +304,29 @@ class TopicPropertyExtractor:
|
|
|
304
304
|
|
|
305
305
|
def _determine_property_type(self, property_name: str) -> str:
|
|
306
306
|
"""Determine the type of a property based on its name and usage patterns"""
|
|
307
|
-
|
|
308
|
-
#
|
|
307
|
+
# Explicit exceptions / overrides for properties whose name contains
|
|
308
|
+
# keywords that would otherwise map to boolean but which are actually
|
|
309
|
+
# string-valued (for example, a bucket name).
|
|
310
|
+
if property_name == "redpanda.remote.readreplica":
|
|
311
|
+
# This topic property contains the read-replica bucket identifier
|
|
312
|
+
# and should be treated as a string (not a boolean).
|
|
313
|
+
return "string"
|
|
314
|
+
# Explicit override: iceberg.delete is a boolean (whether to delete
|
|
315
|
+
# the corresponding Iceberg table when the topic is deleted).
|
|
316
|
+
if property_name == "redpanda.iceberg.delete":
|
|
317
|
+
return "boolean"
|
|
318
|
+
|
|
319
|
+
# Type mapping based on property name patterns (heuristic)
|
|
309
320
|
if any(keyword in property_name for keyword in ["caching", "recovery", "read", "write", "delete"]):
|
|
310
|
-
|
|
311
|
-
|
|
321
|
+
# Known boolean topic properties (keep list conservative)
|
|
322
|
+
boolean_props = [
|
|
323
|
+
"write.caching",
|
|
324
|
+
"redpanda.remote.recovery",
|
|
325
|
+
"redpanda.remote.write",
|
|
326
|
+
"redpanda.remote.read",
|
|
327
|
+
"redpanda.remote.delete",
|
|
328
|
+
]
|
|
329
|
+
if property_name in boolean_props:
|
|
312
330
|
return "boolean"
|
|
313
331
|
|
|
314
332
|
elif any(suffix in property_name for suffix in [".bytes", ".ms", ".factor", ".lag.ms"]):
|
|
@@ -583,7 +601,8 @@ NOTE: All topic properties take effect immediately after being set.
|
|
|
583
601
|
*Type:* {prop_type}
|
|
584
602
|
|
|
585
603
|
"""
|
|
586
|
-
|
|
604
|
+
# If the property type is boolean, never include an Accepted values section
|
|
605
|
+
if acceptable_values and str(prop_type).lower() not in ("boolean", "bool"):
|
|
587
606
|
adoc_content += f"*Accepted values:* {acceptable_values}\n\n"
|
|
588
607
|
|
|
589
608
|
adoc_content += "*Default:* null\n\n"
|
|
@@ -145,15 +145,19 @@ class IsArrayTransformer:
|
|
|
145
145
|
|
|
146
146
|
class NeedsRestartTransformer:
|
|
147
147
|
def accepts(self, info, file_pair):
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
# Only accept when the params blob exists and contains a needs_restart entry
|
|
149
|
+
return (
|
|
150
|
+
len(info.get("params", [])) > 2
|
|
151
|
+
and isinstance(info["params"][2].get("value"), dict)
|
|
152
|
+
and "needs_restart" in info["params"][2]["value"]
|
|
153
|
+
)
|
|
154
|
+
|
|
150
155
|
def parse(self, property, info, file_pair):
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
property["needs_restart"] = needs_restart != "no" # True by default, unless we find "no"
|
|
156
|
+
# We only get here if accepts(...) returned True, so the metadata blob is present
|
|
157
|
+
raw = info["params"][2]["value"]["needs_restart"]
|
|
158
|
+
flag = re.sub(r"^.*::", "", raw)
|
|
159
|
+
# Store as boolean; do not set any default when metadata is absent
|
|
160
|
+
property["needs_restart"] = (flag != "no")
|
|
157
161
|
|
|
158
162
|
class GetsRestoredTransformer:
|
|
159
163
|
def accepts(self, info, file_pair):
|