@redpanda-data/docs-extensions-and-macros 5.0.4 → 5.1.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.
@@ -0,0 +1,458 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Handlebars helpers for rpk documentation generation
5
+ */
6
+
7
+ /**
8
+ * Convert string to dash-separated format
9
+ * @param {string} str - Input string
10
+ * @returns {string} Dashified string
11
+ */
12
+ function dashify(str) {
13
+ if (!str) return ''
14
+ return str.replace(/\s+/g, '-')
15
+ }
16
+
17
+ /**
18
+ * Ensure string ends with a period
19
+ * @param {string} str - Input string
20
+ * @returns {string} String ending with period
21
+ */
22
+ function ensurePeriod(str) {
23
+ if (!str) return ''
24
+ const trimmed = str.trim()
25
+ if (!trimmed) return ''
26
+ if (/[.!?]$/.test(trimmed)) return trimmed
27
+ return trimmed + '.'
28
+ }
29
+
30
+ /**
31
+ * Cap description to two sentences
32
+ * @param {string} str - Full description
33
+ * @returns {string} Shortened description
34
+ */
35
+ function shortDescription(str) {
36
+ if (!str) return ''
37
+ if (typeof str !== 'string') return String(str)
38
+
39
+ // Normalize newlines to spaces for inline use
40
+ const singleLine = str.replace(/\s*\n+\s*/g, ' ').trim()
41
+
42
+ const abbrevs = ['e.g.', 'i.e.', 'etc.', 'vs.']
43
+ let normalized = singleLine
44
+ const placeholders = []
45
+
46
+ for (const abbrev of abbrevs) {
47
+ const regex = new RegExp(abbrev.replace(/\./g, '\\.'), 'gi')
48
+ normalized = normalized.replace(regex, match => {
49
+ const ph = `__ABBREV${placeholders.length}__`
50
+ placeholders.push({ ph, original: match })
51
+ return ph
52
+ })
53
+ }
54
+
55
+ const sentences = normalized.match(/[^.!?]+[.!?]+(?:\s|$)/g)
56
+ if (!sentences || sentences.length === 0) {
57
+ let result = normalized
58
+ placeholders.forEach(({ ph, original }) => {
59
+ result = result.replace(ph, original)
60
+ })
61
+ return result.trim()
62
+ }
63
+
64
+ let result = sentences.slice(0, 2).join('')
65
+ placeholders.forEach(({ ph, original }) => {
66
+ result = result.replace(ph, original)
67
+ })
68
+
69
+ return result.trim()
70
+ }
71
+
72
+ /**
73
+ * Get flag type display value
74
+ * @param {Object} flag - Flag object
75
+ * @returns {string} Type string
76
+ */
77
+ function flagType(flag) {
78
+ if (!flag) return '-'
79
+ return flag.type || '-'
80
+ }
81
+
82
+ /**
83
+ * Check equality
84
+ * @param {*} a - First value
85
+ * @param {*} b - Second value
86
+ * @returns {boolean}
87
+ */
88
+ function eq(a, b) {
89
+ return a === b
90
+ }
91
+
92
+ /**
93
+ * Check inequality
94
+ * @param {*} a - First value
95
+ * @param {*} b - Second value
96
+ * @returns {boolean}
97
+ */
98
+ function ne(a, b) {
99
+ return a !== b
100
+ }
101
+
102
+ /**
103
+ * Logical OR (for any number of arguments)
104
+ * @param {...*} args - Values to OR (last arg is Handlebars options)
105
+ * @returns {boolean}
106
+ */
107
+ function or(...args) {
108
+ // Remove Handlebars options object (last argument)
109
+ const values = args.slice(0, -1)
110
+ return values.some(Boolean)
111
+ }
112
+
113
+ /**
114
+ * Logical AND
115
+ * @param {...*} args - Values to AND (last arg is Handlebars options)
116
+ * @returns {boolean}
117
+ */
118
+ function and(...args) {
119
+ const values = args.slice(0, -1)
120
+ return values.every(Boolean)
121
+ }
122
+
123
+ /**
124
+ * Logical NOT
125
+ * @param {*} value - Value to negate
126
+ * @returns {boolean}
127
+ */
128
+ function not(value) {
129
+ return !value
130
+ }
131
+
132
+ /**
133
+ * Join array elements
134
+ * @param {Array} arr - Array to join
135
+ * @param {string} sep - Separator
136
+ * @returns {string}
137
+ */
138
+ function join(arr, sep) {
139
+ if (!Array.isArray(arr)) return arr
140
+ return arr.join(sep)
141
+ }
142
+
143
+ /**
144
+ * Get array length
145
+ * @param {Array} arr - Array
146
+ * @returns {number}
147
+ */
148
+ function length(arr) {
149
+ if (!arr) return 0
150
+ if (Array.isArray(arr)) return arr.length
151
+ if (typeof arr === 'string') return arr.length
152
+ return 0
153
+ }
154
+
155
+ /**
156
+ * Check if value is greater than
157
+ * @param {number} a - First value
158
+ * @param {number} b - Second value
159
+ * @returns {boolean}
160
+ */
161
+ function gt(a, b) {
162
+ return a > b
163
+ }
164
+
165
+ /**
166
+ * Check if value is less than
167
+ * @param {number} a - First value
168
+ * @param {number} b - Second value
169
+ * @returns {boolean}
170
+ */
171
+ function lt(a, b) {
172
+ return a < b
173
+ }
174
+
175
+ /**
176
+ * Check if value is in array
177
+ * @param {*} value - Value to find
178
+ * @param {Array} arr - Array to search
179
+ * @returns {boolean}
180
+ */
181
+ function includes(value, arr) {
182
+ if (!Array.isArray(arr)) return false
183
+ return arr.includes(value)
184
+ }
185
+
186
+ /**
187
+ * Convert to uppercase
188
+ * @param {string} str - Input string
189
+ * @returns {string}
190
+ */
191
+ function uppercase(str) {
192
+ if (!str) return ''
193
+ return str.toUpperCase()
194
+ }
195
+
196
+ /**
197
+ * Convert to lowercase
198
+ * @param {string} str - Input string
199
+ * @returns {string}
200
+ */
201
+ function lowercase(str) {
202
+ if (!str) return ''
203
+ return str.toLowerCase()
204
+ }
205
+
206
+ /**
207
+ * Capitalize first letter
208
+ * @param {string} str - Input string
209
+ * @returns {string}
210
+ */
211
+ function capitalize(str) {
212
+ if (!str) return ''
213
+ return str.charAt(0).toUpperCase() + str.slice(1)
214
+ }
215
+
216
+ /**
217
+ * Convert to sentence case (capitalize first letter of each sentence)
218
+ * @param {string} str - Input string
219
+ * @returns {string}
220
+ */
221
+ function toSentenceCase(str) {
222
+ if (!str) return ''
223
+ return str.replace(/(^\s*\w|[.!?]\s*\w)/g, c => c.toUpperCase())
224
+ }
225
+
226
+ /**
227
+ * Format flag name with dashes
228
+ * @param {Object} flag - Flag object
229
+ * @returns {string} Formatted flag name
230
+ */
231
+ function formatFlagName(flag) {
232
+ if (!flag) return ''
233
+ if (flag.shorthand) {
234
+ return `-${flag.shorthand}, --${flag.name}`
235
+ }
236
+ return `--${flag.name}`
237
+ }
238
+
239
+ /**
240
+ * Format default value for display
241
+ * @param {*} value - Default value
242
+ * @param {string} type - Value type
243
+ * @returns {string}
244
+ */
245
+ function formatDefault(value, type) {
246
+ if (value === undefined || value === null) return ''
247
+ if (value === '') return '""'
248
+ if (type === 'bool' || type === 'boolean') {
249
+ return value ? 'true' : 'false'
250
+ }
251
+ if (Array.isArray(value)) {
252
+ if (value.length === 0) return '[]'
253
+ return `[${value.join(', ')}]`
254
+ }
255
+ return String(value)
256
+ }
257
+
258
+ /**
259
+ * Check if flag has a default value worth showing
260
+ * @param {Object} flag - Flag object
261
+ * @returns {boolean}
262
+ */
263
+ function hasDefaultValue(flag) {
264
+ if (!flag) return false
265
+ const value = flag.default
266
+ if (value === undefined || value === null) return false
267
+ if (value === '' || value === false) return false
268
+ if (Array.isArray(value) && value.length === 0) return false
269
+ return true
270
+ }
271
+
272
+ /**
273
+ * Get parent command path
274
+ * @param {string} commandPath - Full command path
275
+ * @returns {string} Parent path
276
+ */
277
+ function parentPath(commandPath) {
278
+ if (!commandPath) return ''
279
+ const parts = commandPath.split(' ')
280
+ if (parts.length <= 1) return ''
281
+ return parts.slice(0, -1).join(' ')
282
+ }
283
+
284
+ /**
285
+ * Check if command is a plugin command
286
+ * @param {string} commandPath - Full command path
287
+ * @param {Object} pluginVersions - Plugin versions map
288
+ * @returns {boolean}
289
+ */
290
+ function isPluginCommand(commandPath, pluginVersions) {
291
+ if (!commandPath || !pluginVersions) return false
292
+ const parts = commandPath.split(' ')
293
+ if (parts.length < 2) return false
294
+ return !!pluginVersions[parts[1]]
295
+ }
296
+
297
+ /**
298
+ * Convert ALL CAPS section name to title case for display
299
+ * @param {string} name - ALL CAPS section name
300
+ * @returns {string} Title case name
301
+ */
302
+ function sectionTitle(name) {
303
+ if (!name) return ''
304
+
305
+ // Acronyms that should remain ALL CAPS
306
+ const acronyms = new Set([
307
+ 'id', 'ip', 'api', 'url', 'uri', 'cpu', 'gpu', 'ram', 'io',
308
+ 'tls', 'ssl', 'mtls', 'sasl', 'oauth', 'oidc', 'jwt',
309
+ 'json', 'yaml', 'xml', 'csv', 'http', 'https', 'grpc', 'rpc',
310
+ 'aws', 'gcp', 'azure', 's3', 'eos'
311
+ ])
312
+
313
+ // Product names that should be title cased
314
+ const properNouns = new Set([
315
+ 'redpanda', 'kafka', 'kubernetes', 'schema', 'registry'
316
+ ])
317
+
318
+ // Split on spaces/hyphens but keep & as its own token
319
+ // "PRODUCER ID & EPOCH" -> ["producer", "id", "&", "epoch"]
320
+ const words = name.toLowerCase().split(/[\s-]+/)
321
+
322
+ return words.map((word, index) => {
323
+ // Preserve ampersand as-is
324
+ if (word === '&') {
325
+ return '&'
326
+ }
327
+
328
+ // Handle words with slashes (e.g., "enabled/disabled")
329
+ if (word.includes('/')) {
330
+ const parts = word.split('/')
331
+ return parts.map((part, partIndex) => {
332
+ if (acronyms.has(part)) {
333
+ return part.toUpperCase()
334
+ }
335
+ if (index === 0 && partIndex === 0) {
336
+ return part.charAt(0).toUpperCase() + part.slice(1)
337
+ }
338
+ if (properNouns.has(part)) {
339
+ return part.charAt(0).toUpperCase() + part.slice(1)
340
+ }
341
+ return part
342
+ }).join('/')
343
+ }
344
+
345
+ // Acronyms: ALL CAPS
346
+ if (acronyms.has(word)) {
347
+ return word.toUpperCase()
348
+ }
349
+
350
+ // First word: always capitalize
351
+ if (index === 0) {
352
+ return word.charAt(0).toUpperCase() + word.slice(1)
353
+ }
354
+
355
+ // Proper nouns: capitalize
356
+ if (properNouns.has(word)) {
357
+ return word.charAt(0).toUpperCase() + word.slice(1)
358
+ }
359
+
360
+ // All other words in title: capitalize (standard title case)
361
+ return word.charAt(0).toUpperCase() + word.slice(1)
362
+ }).join(' ')
363
+ }
364
+
365
+ /**
366
+ * Add two numbers
367
+ * @param {number} a - First number
368
+ * @param {number} b - Second number
369
+ * @returns {number} Sum
370
+ */
371
+ function add(a, b) {
372
+ return (a || 0) + (b || 0)
373
+ }
374
+
375
+ /**
376
+ * Return first truthy value or default
377
+ * @param {*} value - Value to check
378
+ * @param {*} defaultValue - Default if value is falsy
379
+ * @returns {*} Value or default
380
+ */
381
+ function defaultHelper(value, defaultValue) {
382
+ return value !== undefined && value !== null ? value : defaultValue
383
+ }
384
+
385
+ /**
386
+ * Repeat a string n times (used for generating heading levels)
387
+ * @param {number} n - Number of repetitions
388
+ * @param {Object} options - Handlebars options
389
+ * @returns {string} Repeated content
390
+ */
391
+ function repeat(n, options) {
392
+ if (!n || n <= 0) return ''
393
+ let result = ''
394
+ for (let i = 0; i < n; i++) {
395
+ result += options.fn(this)
396
+ }
397
+ return result
398
+ }
399
+
400
+ /**
401
+ * Escape pipe characters for use in AsciiDoc table cells
402
+ * @param {string} str - Input string
403
+ * @returns {string} String with escaped pipes
404
+ */
405
+ function escapePipes(str) {
406
+ if (!str) return ''
407
+ // Escape literal pipe characters that would break AsciiDoc tables
408
+ // Use {vbar} which is an AsciiDoc attribute for the pipe character
409
+ return str.replace(/\|/g, '{vbar}')
410
+ }
411
+
412
+ /**
413
+ * Wrap description in pass:q[] if it contains inline AsciiDoc formatting
414
+ * This is needed for :description: attributes to properly render backticks
415
+ * @param {string} str - Input string
416
+ * @returns {string} String wrapped in pass:q[] if contains formatting
417
+ */
418
+ function wrapDescriptionPassthrough(str) {
419
+ if (!str) return ''
420
+ // Check if contains backticks (inline code) or other formatting markers
421
+ if (/`/.test(str)) {
422
+ return `pass:q[${str}]`
423
+ }
424
+ return str
425
+ }
426
+
427
+ module.exports = {
428
+ dashify,
429
+ ensurePeriod,
430
+ shortDescription,
431
+ flagType,
432
+ eq,
433
+ ne,
434
+ or,
435
+ and,
436
+ not,
437
+ join,
438
+ length,
439
+ gt,
440
+ lt,
441
+ includes,
442
+ uppercase,
443
+ lowercase,
444
+ capitalize,
445
+ toSentenceCase,
446
+ formatFlagName,
447
+ formatDefault,
448
+ hasDefaultValue,
449
+ parentPath,
450
+ isPluginCommand,
451
+ sectionTitle,
452
+ add,
453
+ repeat,
454
+ default: defaultHelper,
455
+ escapePipes,
456
+ wrapDescriptionPassthrough,
457
+ isObject: (value) => typeof value === 'object' && value !== null && !Array.isArray(value)
458
+ }