@redpanda-data/docs-extensions-and-macros 4.5.0 → 4.6.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.
Files changed (32) hide show
  1. package/README.adoc +0 -163
  2. package/bin/doc-tools.js +492 -283
  3. package/cli-utils/antora-utils.js +127 -0
  4. package/cli-utils/generate-cluster-docs.sh +41 -29
  5. package/cli-utils/self-managed-docs-branch.js +2 -1
  6. package/cli-utils/start-cluster.sh +70 -30
  7. package/extensions/generate-rp-connect-info.js +14 -9
  8. package/package.json +6 -5
  9. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +233 -0
  10. package/tools/redpanda-connect/helpers/advancedConfig.js +17 -0
  11. package/tools/redpanda-connect/helpers/buildConfigYaml.js +53 -0
  12. package/tools/redpanda-connect/helpers/commonConfig.js +31 -0
  13. package/tools/redpanda-connect/helpers/eq.js +10 -0
  14. package/tools/redpanda-connect/helpers/index.js +19 -0
  15. package/tools/redpanda-connect/helpers/isObject.js +1 -0
  16. package/tools/redpanda-connect/helpers/join.js +6 -0
  17. package/tools/redpanda-connect/helpers/ne.js +10 -0
  18. package/tools/redpanda-connect/helpers/or.js +4 -0
  19. package/tools/redpanda-connect/helpers/renderConnectExamples.js +37 -0
  20. package/tools/redpanda-connect/helpers/renderConnectFields.js +148 -0
  21. package/tools/redpanda-connect/helpers/renderLeafField.js +64 -0
  22. package/tools/redpanda-connect/helpers/renderObjectField.js +41 -0
  23. package/tools/redpanda-connect/helpers/renderYamlList.js +24 -0
  24. package/tools/redpanda-connect/helpers/toYaml.js +11 -0
  25. package/tools/redpanda-connect/helpers/uppercase.js +9 -0
  26. package/tools/redpanda-connect/parse-csv-connectors.js +63 -0
  27. package/tools/redpanda-connect/report-delta.js +152 -0
  28. package/tools/redpanda-connect/templates/connector.hbs +20 -0
  29. package/tools/redpanda-connect/templates/examples-partials.hbs +7 -0
  30. package/tools/redpanda-connect/templates/fields-partials.hbs +13 -0
  31. package/tools/redpanda-connect/templates/intro.hbs +33 -0
  32. package/macros/data-template.js +0 -591
@@ -1,591 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * data_template macro for Asciidoctor.js
5
- *
6
- * This module defines a [data_template] block macro that leverages Handlebars templates to allow us to reference data in JSON or YAML files directly inside Asciidoc pages.
7
- * It processes external or local data sources (in JSON, YAML, or raw text), compiles a Handlebars template
8
- * provided within the block, and then parses the resulting content as AsciiDoc using Asciidoctor.
9
- */
10
-
11
- // This global Opal object is available because Antora uses Asciidoctor.js (compiled using Opal) to convert AsciiDoc into HTML.
12
- const loggerLib = require('@antora/logger');
13
- loggerLib.configure({
14
- format: 'pretty',
15
- level: 'error'
16
- });
17
- const path = require('path').posix;
18
- const logger = loggerLib.getLogger('data-template');
19
- const handlebars = require('handlebars');
20
- const loadAsciiDoc = require('@antora/asciidoc-loader')
21
- const jsonpath = require('jsonpath-plus');
22
- const yaml = require('yaml');
23
- // For synchronous HTTP fetching.
24
- const request = require('sync-request');
25
- const computeOut = require('../extensions/util/compute-out.js');
26
- const createAsciiDocFile = require('../extensions/util/create-asciidoc-file.js');
27
-
28
- // In-memory cache for external resources (avoid repeated network calls)
29
- const externalCache = new Map();
30
-
31
- // ========= Handlebars helpers =============
32
-
33
- /**
34
- * Converts a string to uppercase.
35
- *
36
- * @param {string} str - The string to convert.
37
- * @returns {string} The uppercase version of the input string.
38
- */
39
- function uppercase(str) {
40
- return String(str).toUpperCase();
41
- }
42
-
43
- /**
44
- * Checks if two values are equal.
45
- *
46
- * @param {*} a - The first value.
47
- * @param {*} b - The second value.
48
- * @returns {string} True if the values are equal.
49
- */
50
- function eq(a, b) {
51
- if (a === b) {
52
- return true;
53
- }
54
- return false;
55
- }
56
-
57
- /**
58
- * Checks if two values are not equal.
59
- *
60
- * @param {*} a - The first value.
61
- * @param {*} b - The second value.
62
- * @returns {string} False if the values are not equal.
63
- */
64
- function ne(a, b) {
65
- if (a !== b) {
66
- return true;
67
- }
68
- return false;
69
- }
70
-
71
- /**
72
- * Renders the children of a configuration object.
73
- *
74
- * @param {Array<Object>} children - An array of child objects.
75
- * @returns {string} The rendered string containing the configuration details.
76
- */
77
- function renderConnectFields(children, prefix = '') {
78
- if (!children || !Array.isArray(children) || children.length === 0) {
79
- return '';
80
- }
81
-
82
- let output = '';
83
- prefix = typeof prefix === 'string' ? prefix : '';
84
-
85
- children.forEach(child => {
86
- const isArray = child.kind === 'array';
87
- if (!child.name) return;
88
- const currentPath = prefix ? `${prefix}.${child.name}${isArray ? '[]' : ''}` : `${child.name}${isArray ? '[]' : ''}`;
89
-
90
- // Section header for the field.
91
- output += `=== \`${currentPath}\`\n\n`;
92
-
93
- // Append description if available.
94
- if (child.description) {
95
- output += `${child.description}\n\n`;
96
- }
97
-
98
- // Inject admonition if the config is secret.
99
- if (child.is_secret === true) {
100
- output += `include::redpanda-connect:components:partial$secret_warning.adoc[]\n\n`;
101
- }
102
-
103
- // Insert version requirement if a version is provided.
104
- if (child.version) {
105
- output += `Requires version ${child.version} or later.\n\n`;
106
- }
107
-
108
- // Append type.
109
- output += `*Type*: \`${child.type}\`\n\n`;
110
-
111
- // For non-object types, output the default value if present.
112
- if (child.type !== 'object' && child.default !== undefined) {
113
- if (child.default === "") {
114
- output += `*Default*: \`""\`\n\n`;
115
- } else {
116
- output += `*Default*: \`${child.default}\`\n\n`;
117
- }
118
- }
119
-
120
- // If annotated_options is present, build the AsciiDoc table.
121
- if (child.annotated_options && Array.isArray(child.annotated_options) && child.annotated_options.length > 0) {
122
- output += "[cols=\"1m,2a\"]\n";
123
- output += "|===\n";
124
- output += "|Option |Summary\n\n";
125
- child.annotated_options.forEach(optionPair => {
126
- // Ensure each optionPair is an array with at least two items:
127
- if (Array.isArray(optionPair) && optionPair.length >= 2) {
128
- output += `|${optionPair[0]}\n|${optionPair[1]}\n\n`;
129
- }
130
- });
131
- output += "|===\n\n";
132
- }
133
-
134
- if (child.options && Array.isArray(child.options) && child.options.length > 0) {
135
- output += `*Options*: ${child.options.map(option => `\`${option}\``).join(', ')}\n\n`;
136
- }
137
-
138
-
139
- // If examples are provided, add a fenced YAML block.
140
- if (child.examples) {
141
- output += "```yaml\n";
142
- output += "# Examples:\n";
143
-
144
- // Branch for string fields.
145
- if (child.type === 'string') {
146
- // If the field is an array of strings.
147
- if (child.kind === 'array') {
148
- child.examples.forEach(exampleGroup => {
149
- output += `${child.name}:\n`;
150
- if (Array.isArray(exampleGroup)) {
151
- exampleGroup.forEach(exampleValue => {
152
- if (typeof exampleValue === 'string' && exampleValue.includes('\n')) {
153
- // Use literal block syntax for multi-line strings.
154
- output += ` - |-\n`;
155
- let indentedLines = exampleValue
156
- .split('\n')
157
- .map(line => ' ' + line)
158
- .join('\n');
159
- output += `${indentedLines}\n`;
160
- } else {
161
- output += ` - ${exampleValue}\n`;
162
- }
163
- });
164
- } else {
165
- // Fallback for single value example group.
166
- if (typeof exampleGroup === 'string' && exampleGroup.includes('\n')) {
167
- output += ` - |-\n`;
168
- let indentedLines = exampleGroup
169
- .split('\n')
170
- .map(line => ' ' + line)
171
- .join('\n');
172
- output += `${indentedLines}\n`;
173
- } else {
174
- output += ` - ${exampleGroup}\n`;
175
- }
176
- }
177
- output += "\n";
178
- });
179
- } else {
180
- // For non-array string examples, output them as key/value pairs.
181
- child.examples.forEach(example => {
182
- if (example.includes('\n')) {
183
- output += `${child.name}: |-\n`;
184
- let indentedLines = example.split('\n').map(line => ' ' + line).join('\n');
185
- output += `${indentedLines}\n`;
186
- } else {
187
- output += `${child.name}: ${example}\n`;
188
- }
189
- });
190
- }
191
- }
192
- // Branch for processor fields.
193
- else if (child.type === 'processor') {
194
- if (child.kind === 'array') {
195
- child.examples.forEach(exampleGroup => {
196
- output += `${child.name}:\n`;
197
- if (Array.isArray(exampleGroup)) {
198
- exampleGroup.forEach(exampleObj => {
199
- let yamlSnippet = yaml.stringify(exampleObj).trim();
200
- let lines = yamlSnippet.split('\n');
201
- let formattedLines = lines.map((line, idx) => {
202
- return idx === 0 ? " - " + line : " " + line;
203
- }).join('\n');
204
- output += formattedLines + "\n";
205
- });
206
- } else {
207
- let yamlSnippet = yaml.stringify(exampleGroup).trim();
208
- let lines = yamlSnippet.split('\n');
209
- let formattedLines = lines.map((line, idx) => {
210
- return idx === 0 ? " - " + line : " " + line;
211
- }).join('\n');
212
- output += formattedLines + "\n";
213
- }
214
- output += "\n";
215
- });
216
- } else {
217
- child.examples.forEach(example => {
218
- output += `${child.name}: ${example}\n`;
219
- });
220
- }
221
- }
222
- // Branch for object fields.
223
- else if (child.type === 'object') {
224
- // If this object is actually an array of objects.
225
- if (child.kind === 'array') {
226
- child.examples.forEach(exampleGroup => {
227
- output += `${child.name}:\n`;
228
- if (Array.isArray(exampleGroup)) {
229
- exampleGroup.forEach(exampleObj => {
230
- let yamlSnippet = yaml.stringify(exampleObj).trim();
231
- let lines = yamlSnippet.split('\n');
232
- let formattedLines = lines.map((line, idx) => {
233
- return idx === 0 ? " - " + line : " " + line;
234
- }).join('\n');
235
- output += formattedLines + "\n";
236
- });
237
- } else {
238
- let yamlSnippet = yaml.stringify(exampleGroup).trim();
239
- let lines = yamlSnippet.split('\n');
240
- let formattedLines = lines.map((line, idx) => {
241
- return idx === 0 ? " - " + line : " " + line;
242
- }).join('\n');
243
- output += formattedLines + "\n";
244
- }
245
- output += "\n";
246
- });
247
- } else {
248
- // Fallback for non-array object examples.
249
- child.examples.forEach(example => {
250
- if (typeof example === 'object') {
251
- let yamlSnippet = yaml.stringify(example).trim();
252
- let lines = yamlSnippet.split('\n');
253
- let formattedLines = lines.map((line, idx) => idx === 0 ? line : " " + line).join('\n');
254
- output += `${child.name}:\n${formattedLines}\n`;
255
- } else {
256
- output += `${child.name}: ${example}\n`;
257
- }
258
- });
259
- }
260
- }
261
- // Fallback for any other field types.
262
- else {
263
- child.examples.forEach(example => {
264
- output += `${child.name}: ${example}\n`;
265
- });
266
- }
267
-
268
- output += "```\n\n";
269
- }
270
-
271
- // Recursively render any nested children.
272
- if (child.children && Array.isArray(child.children) && child.children.length > 0) {
273
- output += renderConnectFields(child.children, currentPath);
274
- }
275
- });
276
-
277
- // Return a SafeString so that Handlebars doesn't escape special characters.
278
- return new handlebars.SafeString(output);
279
- }
280
-
281
- /**
282
- * Renders a list of examples.
283
- *
284
- * @param {Array<Object>} examples - An array of example objects.
285
- * @returns {string} The rendered string containing the examples.
286
- */
287
- function renderConnectExamples(examples) {
288
- // If there are no examples, return an empty string.
289
- if (!examples || !Array.isArray(examples) || examples.length === 0) {
290
- return '';
291
- }
292
- // Start with a level-2 heading for all examples.
293
- let output = '';
294
- // Iterate over each example.
295
- examples.forEach(example => {
296
- // Render the example title as a level-3 heading.
297
- if (example.title) {
298
- output += `=== ${example.title}\n\n`;
299
- }
300
-
301
- // Render the summary if provided.
302
- if (example.summary) {
303
- output += `${example.summary}\n\n`;
304
- }
305
-
306
- // Render the example config inside an AsciiDoc code block.
307
- // Using a [source,yaml] block and "----" as delimiters.
308
- if (example.config) {
309
- output += `[source,yaml]\n----\n`;
310
- output += example.config.trim() + "\n";
311
- output += "----\n\n";
312
- }
313
- });
314
- // Return as a SafeString so that Handlebars doesn't escape markup.
315
- return new handlebars.SafeString(output);
316
- }
317
-
318
- /**
319
- * Selects data from a JSON object using a JSONPath expression.
320
- *
321
- * @param {Object} context - The JSON object to query.
322
- * @param {string} pathExpression - The JSONPath expression to use for selection.
323
- * @param {Object} options - Handlebars options object.
324
- * @returns {string} The rendered string containing the selected data.
325
- */
326
-
327
- function selectByJsonPath(context, pathExpression, options) {
328
- // Query the context with the provided JSONPath expression.
329
- pathExpression = (typeof pathExpression === 'string' && pathExpression !== '') ? pathExpression : "$";
330
- const results = jsonpath.JSONPath({ path: pathExpression, json: context });
331
- // If no results are found, render the inverse block.
332
- if (!results || results.length === 0) {
333
- return options.inverse ? options.inverse(this) : '';
334
- }
335
-
336
- // If exactly one result is found, use that as the context.
337
- if (results.length === 1) {
338
- return options.fn(results[0]);
339
- }
340
-
341
- // Otherwise, if multiple results are found, iterate over them.
342
- let resultString = '';
343
- results.forEach(result => {
344
- resultString += options.fn(result);
345
- });
346
- return resultString;
347
- }
348
-
349
- // Register all helpers with Handlebars
350
- handlebars.registerHelper('uppercase', uppercase);
351
- handlebars.registerHelper('eq', eq);
352
- handlebars.registerHelper('ne', ne);
353
- handlebars.registerHelper('renderConnectFields', renderConnectFields);
354
- handlebars.registerHelper('renderConnectExamples', renderConnectExamples);
355
- handlebars.registerHelper('selectByJsonPath', selectByJsonPath);
356
-
357
-
358
-
359
- // ============= End of helpers ===========================
360
-
361
-
362
- /**
363
- * Recursively merges properties from the `overrides` object into the `target` object.
364
- *
365
- * - If both `target[key]` and `overrides[key]` are arrays, it matches each item in `target` with an item in `overrides`
366
- * that has the same `name` property, then merges selected fields and also recursively processes nested objects.
367
- * - If both `target[key]` and `overrides[key]` are plain objects, it merges them recursively.
368
- * - Otherwise, it simply replaces `target[key]` with `overrides[key]`.
369
- *
370
- * @param {Object} target The object into which overrides will be merged.
371
- * @param {Object} overrides The object containing override properties.
372
- * @returns {Object} The updated `target` object.
373
- */
374
- function mergeOverrides(target, overrides) {
375
- if (!overrides || typeof overrides !== 'object') return target;
376
-
377
- for (let key in overrides) {
378
- // Handle arrays by matching items on 'name'
379
- if (Array.isArray(target[key]) && Array.isArray(overrides[key])) {
380
- target[key] = target[key].map(item => {
381
- const overrideItem = overrides[key].find(o => o.name === item.name);
382
- if (overrideItem) {
383
- // Only override allowed fields if they are explicitly defined
384
- ['description', 'type'].forEach(field => {
385
- if (overrideItem.hasOwnProperty(field)) {
386
- item[field] = overrideItem[field];
387
- }
388
- });
389
-
390
- // Recursively handle nested children
391
- item = mergeOverrides(item, overrideItem);
392
- }
393
- return item;
394
- });
395
-
396
- // Recurse into nested objects
397
- } else if (
398
- typeof target[key] === 'object' &&
399
- typeof overrides[key] === 'object' &&
400
- !Array.isArray(target[key]) &&
401
- !Array.isArray(overrides[key])
402
- ) {
403
- target[key] = mergeOverrides(target[key], overrides[key]);
404
-
405
- // Only override top-level description/type if defined in overrides
406
- } else if (['description', 'type'].includes(key) && overrides.hasOwnProperty(key)) {
407
- target[key] = overrides[key];
408
- }
409
- }
410
- return target;
411
- }
412
-
413
-
414
- function processData_TemplateBlock(parent, reader, attrs, config, extensionRef) {
415
- const catalog = config.contentCatalog;
416
- if (!catalog) {
417
- logger.error('[data_template] Error: content catalog not found');
418
- return extensionRef.createBlock(parent, 'paragraph', 'Error: content catalog not found', attrs);
419
- }
420
-
421
- // The dataPath may be an Antora resource ID (for local files)
422
- // or an external URL (like https://example.com/data.json)
423
- const resourceId = attrs.dataPath;
424
- if (!resourceId) {
425
- const msg = '[data_template] Error: No data resource ID provided.';
426
- return extensionRef.createBlock(parent, 'paragraph', msg, attrs);
427
- }
428
-
429
- let contentStr;
430
- let ext = '';
431
-
432
- if (resourceId.startsWith('http://') || resourceId.startsWith('https://')) {
433
- // Handle external resource
434
- try {
435
- if (externalCache.has(resourceId)) {
436
- contentStr = externalCache.get(resourceId);
437
- } else {
438
- const res = request('GET', resourceId, { timeout: 5000 });
439
- contentStr = res.getBody('utf8');
440
- externalCache.set(resourceId, contentStr);
441
- }
442
- // Determine file extension from the URL’s pathname.
443
- try {
444
- const urlObj = new URL(resourceId);
445
- const pathname = urlObj.pathname;
446
- ext = pathname.substring(pathname.lastIndexOf('.')).toLowerCase();
447
- } catch (err) {
448
- ext = '';
449
- }
450
- } catch (err) {
451
- const msg = `[data_template] Error fetching external resource: ${err}`;
452
- return extensionRef.createBlock(parent, 'paragraph', msg, attrs);
453
- }
454
- } else {
455
- // Handle local resource using Antora's content catalog.
456
- const fileSrc = config.file && config.file.src;
457
- const resourceFile = catalog.resolveResource(resourceId, fileSrc);
458
- if (!resourceFile) {
459
- const msg = `[data_template] Could not resolve resource: ${resourceId}`;
460
- return extensionRef.createBlock(parent, 'paragraph', msg, attrs);
461
- }
462
- try {
463
- contentStr = resourceFile.contents.toString();
464
- ext = resourceFile.src.extname.toLowerCase();
465
- } catch (err) {
466
- const msg = `[data_template] Error reading local resource: ${err}`;
467
- return extensionRef.createBlock(parent, 'paragraph', msg, attrs);
468
- }
469
- }
470
-
471
- // Load and parse the data from the resource.
472
- let dataObj = null;
473
- try {
474
- if (ext === '.json') {
475
- dataObj = JSON.parse(contentStr);
476
- } else if (ext === '.yaml' || ext === '.yml') {
477
- dataObj = yaml.parse(contentStr);
478
- } else {
479
- // Fallback: try JSON first, then yaml, then default to raw text.
480
- try {
481
- dataObj = JSON.parse(contentStr);
482
- } catch (jsonErr) {
483
- try {
484
- dataObj = yaml.parse(contentStr);
485
- } catch (yamlErr) {
486
- dataObj = { text: contentStr };
487
- }
488
- }
489
- }
490
- } catch (err) {
491
- const msg = `[data_template] Error parsing data: ${err}`;
492
- return extensionRef.createBlock(parent, 'paragraph', msg, attrs);
493
- }
494
-
495
- if (attrs.overrides) {
496
- try {
497
- const overridesFile = catalog.resolveResource(attrs.overrides, config.file.src);
498
- if (overridesFile) {
499
- const overridesStr = overridesFile.contents.toString();
500
- const overridesObj = JSON.parse(overridesStr);
501
- dataObj = mergeOverrides(dataObj, overridesObj);
502
- }
503
- } catch (err) {
504
- logger.error(`[data_template] Error applying overrides: ${err}`);
505
- }
506
- }
507
- dataObj.__rawYAML = contentStr;
508
-
509
- // Compile the Handlebars template from the block’s content.
510
- let templateSource = reader.getLines().join('\n');
511
- templateSource = templateSource.replace(/@@tab-content@@/g, '--')
512
- let compiledText = '';
513
- try {
514
- const template = handlebars.compile(templateSource);
515
- compiledText = template(dataObj);
516
- } catch (err) {
517
- const msg = `[data_template] Handlebars error: ${err}`;
518
- return extensionRef.createBlock(parent, 'paragraph', msg, attrs);
519
- }
520
-
521
- // The following block takes the Handlebars-generated AsciiDoc content (`compiledText`)
522
- // and parses it in the context of the parent document (`doc`). This is important
523
- // because it allows Antora’s custom include logic and other extensions to be applied
524
- // to the content as if it were part of the original AsciiDoc source. After parsing
525
- // and converting the new document, we append the resulting blocks back into the
526
- // parent node, merging the dynamically generated content into the final
527
- // output. Any errors during parsing are caught and reported as a paragraph block.
528
- try {
529
- const sourceFile = config.file?.src;
530
- const baseDir = sourceFile?.relative ? path.dirname(sourceFile.relative) : 'fragments';
531
- const uniqueName = `data_template-${Date.now()}.adoc`;
532
- const relativePath = path.join(baseDir, uniqueName);
533
- const doc = parent.getDocument();
534
- const attributes = doc.getAttributes()
535
-
536
- const file = {
537
- contents: Buffer.from(`${compiledText}`),
538
- src: {
539
- component: attributes['page-component-name'],
540
- version: attributes['page-component-version'],
541
- module: attributes['page-module'],
542
- family: 'page',
543
- relative: relativePath,
544
- },
545
- };
546
- try {
547
- file.out = computeOut.call(config.contentCatalog, file.src)
548
- const outFile = createAsciiDocFile(config.contentCatalog, file);
549
- const newDoc = loadAsciiDoc(outFile, config.contentCatalog, {
550
- ...doc.getOptions(),
551
- relativizeResourceRefs: true,
552
- attributes: {
553
- ...(doc.getOptions().attributes || {}),
554
- ...(attributes || {}),
555
- },
556
- });
557
- newDoc.getBlocks().forEach((b) => {
558
- parent.append(b);
559
- });
560
- return null;
561
- } catch (err) {
562
- console.warn('❌ loadAsciiDoc threw:', err);
563
- return extensionRef.createBlock(parent, 'paragraph', `[data_template] loadAsciiDoc error: ${err.message}`, attrs);
564
- }
565
- } catch (err) {
566
- const msg = `[data_template] Error parsing compiled template as AsciiDoc: ${err}`;
567
- return extensionRef.createBlock(parent, 'paragraph', msg, attrs);
568
- }
569
- }
570
-
571
- module.exports.register = (registry, context) => {
572
- if (!registry && context) return;
573
-
574
- const toProc = (fn) => Object.defineProperty(fn, '$$arity', { value: fn.length });
575
-
576
- function createExtensionGroup({ contentCatalog, file }) {
577
- return function () {
578
- this.block('data_template', function () {
579
- this.positionalAttributes(['dataPath', 'overrides']);
580
- this.onContext('open');
581
- this.process((parent, reader, attrs) => {
582
- return processData_TemplateBlock(parent, reader, attrs, { contentCatalog, file }, this);
583
- });
584
- });
585
- };
586
- }
587
-
588
- registry.$groups().$store('data-template-ext', toProc(createExtensionGroup(context)));
589
- return registry
590
- };
591
-