@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
@@ -0,0 +1,233 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const handlebars = require('handlebars');
6
+ const yaml = require('yaml');
7
+ const helpers = require('./helpers');
8
+
9
+ // Register each helper under handlebars, verifying that it’s a function
10
+ Object.entries(helpers).forEach(([name, fn]) => {
11
+ if (typeof fn !== 'function') {
12
+ console.error(`❌ Helper "${name}" is not a function`);
13
+ process.exit(1);
14
+ }
15
+ handlebars.registerHelper(name, fn);
16
+ });
17
+
18
+ // Default “main” template (connector.hbs) which invokes partials {{> intro}}, {{> fields}}, {{> examples}}
19
+ const DEFAULT_TEMPLATE = path.resolve(__dirname, './templates/connector.hbs');
20
+
21
+ /**
22
+ * Reads a file at `filePath` and registers it as a Handlebars partial called `name`.
23
+ * Throws if the file cannot be read.
24
+ */
25
+ function registerPartial(name, filePath) {
26
+ const resolved = path.resolve(filePath);
27
+ let source;
28
+ try {
29
+ source = fs.readFileSync(resolved, 'utf8');
30
+ } catch (err) {
31
+ throw new Error(`Unable to read "${name}" template at ${resolved}: ${err.message}`);
32
+ }
33
+ handlebars.registerPartial(name, source);
34
+ }
35
+
36
+ /**
37
+ * Deep-merge `overrides` into `target`. Only 'description', 'type',
38
+ * plus nested array/object entries get overridden; other keys remain intact.
39
+ */
40
+ function mergeOverrides(target, overrides) {
41
+ if (!overrides || typeof overrides !== 'object') return target;
42
+ if (!target || typeof target !== 'object') {
43
+ throw new Error('Target must be a valid object');
44
+ }
45
+ for (const key in overrides) {
46
+ if (Array.isArray(target[key]) && Array.isArray(overrides[key])) {
47
+ // Merge two parallel arrays by matching items on `.name`
48
+ target[key] = target[key].map(item => {
49
+ const overrideItem = overrides[key].find(o => o.name === item.name);
50
+ if (overrideItem) {
51
+ // Overwrite description/type if present
52
+ ['description', 'type'].forEach(field => {
53
+ if (Object.hasOwn(overrideItem, field)) {
54
+ item[field] = overrideItem[field];
55
+ }
56
+ });
57
+ // Copy through selfManagedOnly flag
58
+ if (Object.hasOwn(overrideItem, 'selfManagedOnly')) {
59
+ item.selfManagedOnly = overrideItem.selfManagedOnly;
60
+ }
61
+ // Recurse for nested children
62
+ item = mergeOverrides(item, overrideItem);
63
+ }
64
+ return item;
65
+ });
66
+ } else if (
67
+ typeof target[key] === 'object' &&
68
+ typeof overrides[key] === 'object' &&
69
+ !Array.isArray(target[key]) &&
70
+ !Array.isArray(overrides[key])
71
+ ) {
72
+ // Deep-merge plain objects
73
+ target[key] = mergeOverrides(target[key], overrides[key]);
74
+ } else if (['description', 'type'].includes(key) && Object.hasOwn(overrides, key)) {
75
+ // Overwrite the primitive
76
+ target[key] = overrides[key];
77
+ }
78
+ }
79
+ return target;
80
+ }
81
+
82
+ /**
83
+ * Generates documentation files for RPCN connectors using Handlebars templates.
84
+ *
85
+ * Depending on the {@link writeFullDrafts} flag, generates either partial documentation files for connector fields and examples, or full draft documentation for each connector component. Supports merging override data and skips draft generation for components marked as deprecated.
86
+ *
87
+ * @param {Object} options - Configuration options for documentation generation.
88
+ * @param {string} options.data - Path to the connector data file (JSON or YAML).
89
+ * @param {string} [options.overrides] - Optional path to a JSON file with override data.
90
+ * @param {string} options.template - Path to the main Handlebars template.
91
+ * @param {string} [options.templateIntro] - Path to the intro partial template (used in full draft mode).
92
+ * @param {string} [options.templateFields] - Path to the fields partial template.
93
+ * @param {string} [options.templateExamples] - Path to the examples partial template.
94
+ * @param {boolean} options.writeFullDrafts - If true, generates full draft documentation; otherwise, generates partials.
95
+ * @returns {Promise<Object>} An object summarizing the number and paths of generated partials and drafts.
96
+ *
97
+ * @throws {Error} If reading or parsing input files fails, or if template rendering fails for a component.
98
+ *
99
+ * @remark
100
+ * When generating full drafts, components with a `status` of `'deprecated'` are skipped.
101
+ */
102
+ async function generateRpcnConnectorDocs(options) {
103
+ const {
104
+ data,
105
+ overrides,
106
+ template, // main Handlebars template (for full-draft mode)
107
+ templateIntro,
108
+ templateFields,
109
+ templateExamples,
110
+ writeFullDrafts
111
+ } = options;
112
+
113
+ // Read connector index (JSON or YAML)
114
+ const raw = fs.readFileSync(data, 'utf8');
115
+ const ext = path.extname(data).toLowerCase();
116
+ const dataObj = ext === '.json' ? JSON.parse(raw) : yaml.parse(raw);
117
+
118
+ // Apply overrides if provided
119
+ if (overrides) {
120
+ const ovRaw = fs.readFileSync(overrides, 'utf8');
121
+ const ovObj = JSON.parse(ovRaw);
122
+ mergeOverrides(dataObj, ovObj);
123
+ }
124
+
125
+ // Compile the “main” template (used when writeFullDrafts = true)
126
+ const compiledTemplate = handlebars.compile(fs.readFileSync(template, 'utf8'));
127
+
128
+ // Determine which templates to use for “fields” and “examples”
129
+ // If templateFields is not provided, fall back to the single `template`.
130
+ // If templateExamples is not provided, skip examples entirely.
131
+ const fieldsTemplatePath = templateFields || template;
132
+ const examplesTemplatePath = templateExamples || null;
133
+
134
+ // Register partials
135
+ if (!writeFullDrafts) {
136
+ if (fieldsTemplatePath) {
137
+ registerPartial('fields', fieldsTemplatePath);
138
+ }
139
+ if (examplesTemplatePath) {
140
+ registerPartial('examples', examplesTemplatePath);
141
+ }
142
+ } else {
143
+ registerPartial('intro', templateIntro);
144
+ }
145
+
146
+ const outputRoot = path.resolve(process.cwd(), 'modules/components/partials');
147
+ const fieldsOutRoot = path.join(outputRoot, 'fields');
148
+ const examplesOutRoot = path.join(outputRoot, 'examples');
149
+ const draftsRoot = path.join(outputRoot, 'drafts');
150
+
151
+ if (!writeFullDrafts) {
152
+ fs.mkdirSync(fieldsOutRoot, { recursive: true });
153
+ fs.mkdirSync(examplesOutRoot, { recursive: true });
154
+ }
155
+
156
+ let partialsWritten = 0;
157
+ let draftsWritten = 0;
158
+ const partialFiles = [];
159
+ const draftFiles = [];
160
+
161
+ for (const [type, items] of Object.entries(dataObj)) {
162
+ if (!Array.isArray(items)) continue;
163
+
164
+ for (const item of items) {
165
+ if (!item.name) continue;
166
+ const name = item.name;
167
+
168
+ if (!writeFullDrafts) {
169
+ // Render fields using the registered “fields” partial
170
+ const fieldsOut = handlebars
171
+ .compile('{{> fields children=config.children}}')(item);
172
+
173
+ // Render examples only if an examples template was provided
174
+ let examplesOut = '';
175
+ if (examplesTemplatePath) {
176
+ examplesOut = handlebars
177
+ .compile('{{> examples examples=examples}}')(item);
178
+ }
179
+
180
+ if (fieldsOut.trim()) {
181
+ const fPath = path.join(fieldsOutRoot, type, `${name}.adoc`);
182
+ fs.mkdirSync(path.dirname(fPath), { recursive: true });
183
+ fs.writeFileSync(fPath, fieldsOut);
184
+ partialsWritten++;
185
+ partialFiles.push(path.relative(process.cwd(), fPath));
186
+ }
187
+
188
+ if (examplesOut.trim()) {
189
+ const ePath = path.join(examplesOutRoot, type, `${name}.adoc`);
190
+ fs.mkdirSync(path.dirname(ePath), { recursive: true });
191
+ fs.writeFileSync(ePath, examplesOut);
192
+ partialsWritten++;
193
+ partialFiles.push(path.relative(process.cwd(), ePath));
194
+ }
195
+ }
196
+
197
+ if (writeFullDrafts) {
198
+ if (String(item.status || '').toLowerCase() === 'deprecated') {
199
+ console.log(`Skipping draft for deprecated component: ${type}/${name}`);
200
+ continue;
201
+ }
202
+ let content;
203
+ try {
204
+ content = compiledTemplate(item);
205
+ } catch (err) {
206
+ throw new Error(`Template render failed for component "${name}": ${err.message}`);
207
+ }
208
+
209
+ const draftSubdir = name === 'gateway'
210
+ ? path.join(draftsRoot, 'cloud-only')
211
+ : draftsRoot;
212
+
213
+ const destFile = path.join(draftSubdir, `${name}.adoc`);
214
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
215
+ fs.writeFileSync(destFile, content, 'utf8');
216
+ draftsWritten++;
217
+ draftFiles.push(path.relative(process.cwd(), destFile));
218
+ }
219
+ }
220
+ }
221
+
222
+ return {
223
+ partialsWritten,
224
+ draftsWritten,
225
+ partialFiles,
226
+ draftFiles
227
+ };
228
+ }
229
+
230
+ module.exports = {
231
+ generateRpcnConnectorDocs,
232
+ mergeOverrides
233
+ };
@@ -0,0 +1,17 @@
1
+ const buildConfigYaml = require('./buildConfigYaml.js');
2
+ const handlebars = require('handlebars');
3
+
4
+ /**
5
+ * Handlebars helper “advancedConfig”. Omits only deprecated.
6
+ *
7
+ * Usage in template:
8
+ * {{advancedConfig this.type this.name this.config.children}}
9
+ */
10
+ module.exports = function advancedConfig(type, connectorName, children) {
11
+ if (typeof type !== 'string' || typeof connectorName !== 'string' || !Array.isArray(children)) {
12
+ return '';
13
+ }
14
+
15
+ const yamlText = buildConfigYaml(type, connectorName, children, /*includeAdvanced=*/ true);
16
+ return new handlebars.SafeString(yamlText);
17
+ }
@@ -0,0 +1,53 @@
1
+ const renderLeafField = require('./renderLeafField');
2
+ const renderObjectField = require('./renderObjectField');
3
+
4
+ /**
5
+ * Builds either “Common” or “Advanced” YAML for one connector.
6
+ *
7
+ * - type = “input” or “output” (or whatever type)
8
+ * - connectorName = such as “amqp_1”
9
+ * - children = the array of field‐definitions (entry.config.children)
10
+ * - includeAdvanced = if false → only fields where is_advanced !== true
11
+ * if true → all fields (except deprecated)
12
+ *
13
+ * Structure produced:
14
+ *
15
+ * type:
16
+ * label: ""
17
+ * connectorName:
18
+ * ...child fields (with comments for “no default”)
19
+ */
20
+ module.exports = function buildConfigYaml(type, connectorName, children, includeAdvanced) {
21
+ const lines = [];
22
+
23
+ // “type:” top‐level
24
+ lines.push(`${type}:`);
25
+
26
+ // Two‐space indent for “label”
27
+ lines.push(` label: ""`);
28
+
29
+ // Two‐space indent for connectorName heading
30
+ lines.push(` ${connectorName}:`);
31
+
32
+ // Four‐space indent for children
33
+ const baseIndent = 4;
34
+ children.forEach(field => {
35
+ if (field.is_deprecated) {
36
+ return; // skip deprecated fields
37
+ }
38
+ if (!includeAdvanced && field.is_advanced) {
39
+ return; // skip advanced fields in “common” mode
40
+ }
41
+
42
+ if (field.type === 'object' && Array.isArray(field.children)) {
43
+ // Render nested object
44
+ const nestedLines = renderObjectField(field, baseIndent);
45
+ lines.push(...nestedLines);
46
+ } else {
47
+ // Render a scalar or array leaf
48
+ lines.push(renderLeafField(field, baseIndent));
49
+ }
50
+ });
51
+
52
+ return lines.join('\n');
53
+ }
@@ -0,0 +1,31 @@
1
+ const buildConfigYaml = require('./buildConfigYaml.js');
2
+ const handlebars = require('handlebars');
3
+
4
+ /**
5
+ * Handlebars helper “commonConfig”. Omits deprecated + advanced.
6
+ *
7
+ * Usage in template:
8
+ * {{commonConfig this.type this.name this.config.children}}
9
+ */
10
+ module.exports = function commonConfig(type, connectorName, children) {
11
+ if (typeof type !== 'string' || !type.trim()) {
12
+ console.warn('commonConfig: type must be a non-empty string');
13
+ return '';
14
+ }
15
+ if (typeof connectorName !== 'string' || !connectorName.trim()) {
16
+ console.warn('commonConfig: connectorName must be a non-empty string');
17
+ return '';
18
+ }
19
+ if (!Array.isArray(children)) {
20
+ console.warn('commonConfig: children must be an array');
21
+ return '';
22
+ }
23
+
24
+ try {
25
+ const yamlText = buildConfigYaml(type, connectorName, children, /*includeAdvanced=*/ false);
26
+ return new handlebars.SafeString(yamlText);
27
+ } catch (error) {
28
+ console.error('Error in commonConfig helper:', error);
29
+ return new handlebars.SafeString('<!-- Error generating configuration -->');
30
+ }
31
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Checks if two values are equal.
3
+ *
4
+ * @param {*} a - The first value.
5
+ * @param {*} b - The second value.
6
+ * @returns {boolean} True if the values are equal.
7
+ */
8
+ module.exports = function eq(a, b) {
9
+ return a === b;
10
+ }
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ uppercase: require('./uppercase.js'),
5
+ eq: require('./eq.js'),
6
+ ne: require('./ne.js'),
7
+ join: require('./join.js'),
8
+ or: require('./or.js'),
9
+ toYaml: require('./toYaml.js'),
10
+ isObject: require('./isObject.js'),
11
+ renderYamlList: require('./renderYamlList.js'),
12
+ renderConnectFields: require('./renderConnectFields.js'),
13
+ renderConnectExamples: require('./renderConnectExamples.js'),
14
+ renderLeafField: require('./renderLeafField.js'),
15
+ renderObjectField: require('./renderObjectField.js'),
16
+ buildConfigYaml: require('./buildConfigYaml.js'),
17
+ commonConfig: require('./commonConfig.js'),
18
+ advancedConfig: require('./advancedConfig.js'),
19
+ };
@@ -0,0 +1 @@
1
+ module.exports = v => v !== null && typeof v === 'object' && !Array.isArray(v)
@@ -0,0 +1,6 @@
1
+ module.exports = function join(array, separator) {
2
+ if (!Array.isArray(array)) {
3
+ return '';
4
+ }
5
+ return array.join(separator);
6
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Checks if two values are not equal.
3
+ *
4
+ * @param {*} a - The first value.
5
+ * @param {*} b - The second value.
6
+ * @returns {boolean} True if the values are not equal.
7
+ */
8
+ module.exports = function ne(a, b) {
9
+ return a !== b;
10
+ }
@@ -0,0 +1,4 @@
1
+ module.exports = function (...args) {
2
+ const options = args.pop(); // Last argument is always the options object
3
+ return args.some(Boolean);
4
+ };
@@ -0,0 +1,37 @@
1
+ const handlebars = require('handlebars');
2
+
3
+ /**
4
+ * Renders a list of examples.
5
+ *
6
+ * @param {Array<Object>} examples - An array of example objects.
7
+ * @returns {handlebars.SafeString} The rendered SafeString containing the examples.
8
+ */
9
+ module.exports = function renderConnectExamples(examples) {
10
+ if (!examples || !Array.isArray(examples) || examples.length === 0) {
11
+ return '';
12
+ }
13
+ let output = '';
14
+ examples.forEach(example => {
15
+ if (example.title) {
16
+ const sanitizedTitle = example.title.replace(/[=]/g, '\\=');
17
+ output += `=== ${sanitizedTitle}\n\n`;
18
+ }
19
+ if (example.summary) {
20
+ output += `${example.summary}\n\n`;
21
+ }
22
+ if (example.config) {
23
+ if (typeof example.config !== 'string') {
24
+ console.warn('Example config must be a string, skipping');
25
+ return;
26
+ }
27
+ const configContent = example.config.trim();
28
+ if (configContent.includes('----')) {
29
+ console.warn('Example config contains AsciiDoc delimiters, this may break rendering');
30
+ }
31
+ output += '[source,yaml]\n----\n';
32
+ output += configContent + '\n';
33
+ output += '----\n\n';
34
+ }
35
+ });
36
+ return new handlebars.SafeString(output);
37
+ }
@@ -0,0 +1,148 @@
1
+ const yaml = require('yaml');
2
+ const renderYamlList = require('./renderYamlList');
3
+ const handlebars = require('handlebars');
4
+
5
+ /**
6
+ * Renders the children of a configuration object into AsciiDoc.
7
+ *
8
+ * @param {Array<Object>} children - An array of child objects.
9
+ * @param {string} [prefix=''] - The prefix path for nested fields.
10
+ * @returns {handlebars.SafeString} The rendered SafeString containing the configuration details.
11
+ */
12
+ module.exports = function renderConnectFields(children, prefix = '') {
13
+ if (!children || !Array.isArray(children) || children.length === 0) {
14
+ return '';
15
+ }
16
+
17
+ const sorted = [...children].sort((a, b) => {
18
+ const an = a.name || '';
19
+ const bn = b.name || '';
20
+ return an.localeCompare(bn, undefined, { sensitivity: 'base' });
21
+ });
22
+
23
+ let output = '';
24
+ prefix = typeof prefix === 'string' ? prefix : '';
25
+
26
+ sorted.forEach(child => {
27
+ if (child.is_deprecated || !child.name) return;
28
+
29
+ // Normalize type
30
+ let displayType;
31
+ if (child.type === 'string' && child.kind === 'array') {
32
+ displayType = 'array';
33
+ } else if (child.type === 'unknown' && child.kind === 'map') {
34
+ displayType = 'object';
35
+ } else {
36
+ displayType = child.type;
37
+ }
38
+
39
+ let block = '';
40
+ const isArray = child.kind === 'array';
41
+ const currentPath = prefix
42
+ ? `${prefix}.${child.name}${isArray ? '[]' : ''}`
43
+ : `${child.name}${isArray ? '[]' : ''}`;
44
+
45
+ block += `=== \`${currentPath}\`\n\n`;
46
+
47
+ if (child.description) {
48
+ block += `${child.description}\n\n`;
49
+ }
50
+ if (child.is_secret) {
51
+ block += `include::redpanda-connect:components:partial$secret_warning.adoc[]\n\n`;
52
+ }
53
+ if (child.version) {
54
+ block += `ifndef::env-cloud[]\nRequires version ${child.version} or later.\nendif::[]\n\n`;
55
+ }
56
+
57
+ block += `*Type*: \`${displayType}\`\n\n`;
58
+
59
+ // Default
60
+ if (child.type !== 'object' && child.default !== undefined) {
61
+ if (typeof child.default !== 'object') {
62
+ const display = child.default === '' ? '""' : String(child.default);
63
+ block += `*Default*: \`${display}\`\n\n`;
64
+ } else {
65
+ const defYaml = yaml.stringify(child.default).trim();
66
+ block += `*Default*:\n[source,yaml]\n----\n${defYaml}\n----\n\n`;
67
+ }
68
+ }
69
+
70
+ // Annotated options
71
+ if (child.annotated_options && child.annotated_options.length) {
72
+ block += `[cols=\"1m,2a\"]\n|===\n|Option |Summary\n\n`;
73
+ child.annotated_options.forEach(([opt, summary]) => {
74
+ block += `|${opt}\n|${summary}\n\n`;
75
+ });
76
+ block += `|===\n\n`;
77
+ }
78
+
79
+ // Options list
80
+ if (child.options && child.options.length) {
81
+ block += `*Options*: ${child.options.map(opt => `\`${opt}\``).join(', ')}\n\n`;
82
+ }
83
+
84
+ // Examples
85
+ if (child.examples && child.examples.length) {
86
+ block += `[source,yaml]\n----\n# Examples:\n`;
87
+ if (child.type === 'string') {
88
+ if (child.kind === 'array') {
89
+ block += renderYamlList(child.name, child.examples);
90
+ } else {
91
+ child.examples.forEach(example => {
92
+ if (typeof example === 'string' && example.includes('\n')) {
93
+ block += `${child.name}: |-\n`;
94
+ block += example.split('\n').map(line => ' ' + line).join('\n') + '\n';
95
+ } else {
96
+ block += `${child.name}: \`${example}\`\n`;
97
+ }
98
+ });
99
+ block += '\n';
100
+ }
101
+ } else if (child.type === 'processor') {
102
+ if (child.kind === 'array') {
103
+ block += renderYamlList(child.name, child.examples);
104
+ } else {
105
+ child.examples.forEach(example => {
106
+ block += `${child.name}: \`${String(example)}\`\n`;
107
+ });
108
+ block += '\n';
109
+ }
110
+ } else if (child.type === 'object') {
111
+ if (child.kind === 'array') {
112
+ block += renderYamlList(child.name, child.examples);
113
+ } else {
114
+ child.examples.forEach(example => {
115
+ if (typeof example === 'object') {
116
+ const snippet = yaml.stringify(example).trim();
117
+ block += `${child.name}:\n`;
118
+ block += snippet.split('\n').map(line => ' ' + line).join('\n') + '\n';
119
+ } else {
120
+ block += `${child.name}: \`${String(example)}\`\n`;
121
+ }
122
+ });
123
+ block += '\n';
124
+ }
125
+ } else {
126
+ child.examples.forEach(example => {
127
+ block += `${child.name}: \`${String(example)}\`\n`;
128
+ });
129
+ block += '\n';
130
+ }
131
+ block += `----\n\n`;
132
+ }
133
+
134
+ // Nested
135
+ if (child.children && child.children.length) {
136
+ block += renderConnectFields(child.children, currentPath);
137
+ }
138
+
139
+ // Cloud guard
140
+ if (child.selfManagedOnly) {
141
+ output += `ifndef::env-cloud[]\n${block}endif::[]\n\n`;
142
+ } else {
143
+ output += block;
144
+ }
145
+ });
146
+
147
+ return new handlebars.SafeString(output);
148
+ };
@@ -0,0 +1,64 @@
1
+ const yaml = require('yaml');
2
+
3
+ /**
4
+ * Renders a single “leaf” field (scalar or array) at the given indentation.
5
+ * If `field.default` is present, prints that. Otherwise prints an empty-string
6
+ * or empty-array plus an inline comment (# No default (optional/required)).
7
+ *
8
+ * @param {Object} field – one field object from “children”
9
+ * @param {number} indentLevel – number of spaces to indent
10
+ * @returns {string} – one line, including comment if needed
11
+ */
12
+ module.exports = function renderLeafField(field, indentLevel) {
13
+ if (!field || typeof field !== 'object') {
14
+ throw new Error('renderLeafField: field must be an object');
15
+ }
16
+ if (typeof indentLevel !== 'number' || indentLevel < 0) {
17
+ throw new Error('renderLeafField: indentLevel must be a non-negative number');
18
+ }
19
+ if (!field.name || typeof field.name !== 'string') {
20
+ throw new Error('renderLeafField: field.name must be a non-empty string');
21
+ }
22
+
23
+ const indent = ' '.repeat(indentLevel);
24
+ const name = field.name;
25
+
26
+ // Decide whether optional or required
27
+ const optional = Boolean(field.is_optional);
28
+ const comment = optional
29
+ ? '# No default (optional)'
30
+ : '# No default (required)';
31
+
32
+ // If a default is provided, use it:
33
+ if (field.default !== undefined) {
34
+ // If default is itself an object or array → dump as YAML block
35
+ if (typeof field.default === 'object') {
36
+ try {
37
+ // Turn the object/array into a YAML string. We also need to indent that block
38
+ const rawYaml = yaml.stringify(field.default).trim();
39
+ // Indent each line of rawYaml by (indentLevel + 2) spaces:
40
+ const indentedYaml = rawYaml
41
+ .split('\n')
42
+ .map(line => ' '.repeat(indentLevel + 2) + line)
43
+ .join('\n');
44
+ return `${indent}${name}:\n${indentedYaml}`;
45
+ } catch (error) {
46
+ console.warn(`Failed to serialize default value for field ${field.name}:`, error);
47
+ return `${indent}${name}: {} # Error serializing default value`;
48
+ }
49
+ }
50
+
51
+ // Otherwise, default is a primitive (string/number/bool)
52
+ if (field.type === 'string') {
53
+ return `${indent}${name}: ${yaml.stringify(field.default)}`;
54
+ }
55
+ return `${indent}${name}: ${field.default}`;
56
+ }
57
+
58
+ // No default → choose representation based on kind
59
+ if (field.kind === 'array') {
60
+ return `${indent}${name}: [] ${comment}`;
61
+ } else {
62
+ return `${indent}${name}: "" ${comment}`;
63
+ }
64
+ }