@redpanda-data/docs-extensions-and-macros 4.5.0 → 4.6.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.
Files changed (29) hide show
  1. package/README.adoc +0 -163
  2. package/bin/doc-tools.js +491 -286
  3. package/cli-utils/antora-utils.js +127 -0
  4. package/cli-utils/self-managed-docs-branch.js +2 -1
  5. package/package.json +6 -5
  6. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +205 -0
  7. package/tools/redpanda-connect/helpers/advancedConfig.js +17 -0
  8. package/tools/redpanda-connect/helpers/buildConfigYaml.js +53 -0
  9. package/tools/redpanda-connect/helpers/commonConfig.js +31 -0
  10. package/tools/redpanda-connect/helpers/eq.js +10 -0
  11. package/tools/redpanda-connect/helpers/index.js +19 -0
  12. package/tools/redpanda-connect/helpers/isObject.js +1 -0
  13. package/tools/redpanda-connect/helpers/join.js +6 -0
  14. package/tools/redpanda-connect/helpers/ne.js +10 -0
  15. package/tools/redpanda-connect/helpers/or.js +4 -0
  16. package/tools/redpanda-connect/helpers/renderConnectExamples.js +37 -0
  17. package/tools/redpanda-connect/helpers/renderConnectFields.js +146 -0
  18. package/tools/redpanda-connect/helpers/renderLeafField.js +64 -0
  19. package/tools/redpanda-connect/helpers/renderObjectField.js +41 -0
  20. package/tools/redpanda-connect/helpers/renderYamlList.js +24 -0
  21. package/tools/redpanda-connect/helpers/toYaml.js +11 -0
  22. package/tools/redpanda-connect/helpers/uppercase.js +9 -0
  23. package/tools/redpanda-connect/parse-csv-connectors.js +63 -0
  24. package/tools/redpanda-connect/report-delta.js +152 -0
  25. package/tools/redpanda-connect/templates/connector.hbs +20 -0
  26. package/tools/redpanda-connect/templates/examples-partials.hbs +7 -0
  27. package/tools/redpanda-connect/templates/fields-partials.hbs +13 -0
  28. package/tools/redpanda-connect/templates/intro.hbs +35 -0
  29. package/macros/data-template.js +0 -591
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const yaml = require('js-yaml');
6
+
7
+ /**
8
+ * Attempts to locate and parse `antora.yml` in the current working directory.
9
+ *
10
+ * @returns {Object|undefined} The parsed YAML as a JavaScript object, or undefined if not found or on error.
11
+ */
12
+ function loadAntoraConfig() {
13
+ const antoraPath = path.join(process.cwd(), 'antora.yml');
14
+ if (!fs.existsSync(antoraPath)) {
15
+ // No antora.yml in project root
16
+ return undefined;
17
+ }
18
+
19
+ try {
20
+ const fileContents = fs.readFileSync(antoraPath, 'utf8');
21
+ const config = yaml.load(fileContents);
22
+ if (typeof config !== 'object' || config === null) {
23
+ console.error('Warning: antora.yml parsed to a non‐object value.');
24
+ return undefined;
25
+ }
26
+ return config;
27
+ } catch (err) {
28
+ console.error(`Error reading/parsing antora.yml: ${err.message}`);
29
+ return undefined;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Safely retrieves a nested value from the Antora configuration, given a "dot path".
35
+ *
36
+ * Example usage:
37
+ * const latestVersion = getAntoraValue('asciidoc.attributes.latest-connect-version');
38
+ *
39
+ * @param {string} keyPath
40
+ * A dot-separated path into the Antora object (e.g. "asciidoc.attributes.foo").
41
+ * @returns {*}
42
+ * The value at that path, or undefined if the file is missing or the key does not exist.
43
+ */
44
+ function getAntoraValue(keyPath) {
45
+ const config = loadAntoraConfig();
46
+ if (!config) {
47
+ return undefined;
48
+ }
49
+
50
+ // Split on dots, but ignore empty segments
51
+ const segments = keyPath.split('.').filter(Boolean);
52
+ let cursor = config;
53
+
54
+ for (const seg of segments) {
55
+ if (cursor && typeof cursor === 'object' && seg in cursor) {
56
+ cursor = cursor[seg];
57
+ } else {
58
+ return undefined;
59
+ }
60
+ }
61
+ return cursor;
62
+ }
63
+
64
+ /**
65
+ * Safely sets a nested value in the Antora configuration, given a "dot path".
66
+ * If the file or path does not exist, it will create intermediate objects as needed.
67
+ * After setting the value, writes the updated YAML back to `antora.yml`.
68
+ *
69
+ * @param {string} keyPath
70
+ * A dot-separated path to set (e.g. "asciidoc.attributes.latest-connect-version").
71
+ * @param {*} newValue
72
+ * The new value to assign at that path.
73
+ * @returns {boolean}
74
+ * True if it succeeded, false otherwise.
75
+ */
76
+ function setAntoraValue(keyPath, newValue) {
77
+ const antoraPath = path.join(process.cwd(), 'antora.yml');
78
+ if (!fs.existsSync(antoraPath)) {
79
+ console.error('Cannot update antora.yml: file not found in project root.');
80
+ return false;
81
+ }
82
+
83
+ let config;
84
+ try {
85
+ const fileContents = fs.readFileSync(antoraPath, 'utf8');
86
+ config = yaml.load(fileContents) || {};
87
+ if (typeof config !== 'object' || config === null) {
88
+ config = {};
89
+ }
90
+ } catch (err) {
91
+ console.error(`Error reading/parsing antora.yml: ${err.message}`);
92
+ return false;
93
+ }
94
+
95
+ // Traverse/construct nested objects
96
+ const segments = keyPath.split('.').filter(Boolean);
97
+ let cursor = config;
98
+ for (let i = 0; i < segments.length; i++) {
99
+ const seg = segments[i];
100
+ if (i === segments.length - 1) {
101
+ // Last segment: assign
102
+ cursor[seg] = newValue;
103
+ } else {
104
+ // Intermediate: ensure object
105
+ if (!(seg in cursor) || typeof cursor[seg] !== 'object' || cursor[seg] === null) {
106
+ cursor[seg] = {};
107
+ }
108
+ cursor = cursor[seg];
109
+ }
110
+ }
111
+
112
+ // Serialize back to YAML and write
113
+ try {
114
+ const newYaml = yaml.dump(config, { lineWidth: 120 });
115
+ fs.writeFileSync(antoraPath, newYaml, 'utf8');
116
+ return true;
117
+ } catch (err) {
118
+ console.error(`Error writing antora.yml: ${err.message}`);
119
+ return false;
120
+ }
121
+ }
122
+
123
+ module.exports = {
124
+ loadAntoraConfig,
125
+ getAntoraValue,
126
+ setAntoraValue,
127
+ };
@@ -56,7 +56,7 @@ async function determineDocsBranch(operatorTag) {
56
56
  if (!filtered) {
57
57
  throw new Error(`Could not parse major.minor from ${TAG}`)
58
58
  }
59
-
59
+ // We started versioning the operator in line with Redpanda core versions. But when v2.4.x was the latest version, the docs were still on 25.1 and v25.1.x of the operator was still in beta. So we need to handle this special case.
60
60
  let branch
61
61
  if (filtered === 'v2.4') {
62
62
  if (ANTORA === '25.1') {
@@ -64,6 +64,7 @@ async function determineDocsBranch(operatorTag) {
64
64
  } else {
65
65
  branch = 'v/24.3'
66
66
  }
67
+ // For all other versions use the v<major>.<minor> branch unless it is the current version, in which case we use 'main'.
67
68
  } else if (filtered === `v${ANTORA}`) {
68
69
  branch = 'main'
69
70
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "4.5.0",
3
+ "version": "4.6.0",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -20,7 +20,8 @@
20
20
  "get-redpanda-version": "doc-tools get-redpanda-version",
21
21
  "get-console-version": "doc-tools get-console-version",
22
22
  "build": "antora --to-dir docs --fetch local-antora-playbook.yml",
23
- "serve": "wds --node-resolve --open preview/test/ --watch --root-dir docs"
23
+ "serve": "wds --node-resolve --open preview/test/ --watch --root-dir docs",
24
+ "test": "jest"
24
25
  },
25
26
  "contributors": [
26
27
  {
@@ -56,8 +57,7 @@
56
57
  "./macros/glossary": "./macros/glossary.js",
57
58
  "./macros/rp-connect-components": "./macros/rp-connect-components.js",
58
59
  "./macros/config-ref": "./macros/config-ref.js",
59
- "./macros/helm-ref": "./macros/helm-ref.js",
60
- "./macros/data-template": "./macros/data-template.js"
60
+ "./macros/helm-ref": "./macros/helm-ref.js"
61
61
  },
62
62
  "files": [
63
63
  "extensions",
@@ -101,6 +101,7 @@
101
101
  "devDependencies": {
102
102
  "@antora/cli": "3.1.4",
103
103
  "@antora/site-generator": "3.1.4",
104
- "@web/dev-server": "^0.2.5"
104
+ "@web/dev-server": "^0.2.5",
105
+ "jest": "^29.7.0"
105
106
  }
106
107
  }
@@ -0,0 +1,205 @@
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
+ // Recurse for nested children
58
+ item = mergeOverrides(item, overrideItem);
59
+ }
60
+ return item;
61
+ });
62
+ } else if (
63
+ typeof target[key] === 'object' &&
64
+ typeof overrides[key] === 'object' &&
65
+ !Array.isArray(target[key]) &&
66
+ !Array.isArray(overrides[key])
67
+ ) {
68
+ // Deep-merge plain objects
69
+ target[key] = mergeOverrides(target[key], overrides[key]);
70
+ } else if (['description', 'type'].includes(key) && Object.hasOwn(overrides, key)) {
71
+ // Overwrite the primitive
72
+ target[key] = overrides[key];
73
+ }
74
+ }
75
+ return target;
76
+ }
77
+
78
+ async function generateRpcnConnectorDocs(options) {
79
+ const {
80
+ data,
81
+ overrides,
82
+ template, // main Handlebars template (for full-draft mode)
83
+ templateIntro,
84
+ templateFields,
85
+ templateExamples,
86
+ writeFullDrafts
87
+ } = options;
88
+
89
+ // Read connector index (JSON or YAML)
90
+ const raw = fs.readFileSync(data, 'utf8');
91
+ const ext = path.extname(data).toLowerCase();
92
+ const dataObj = ext === '.json' ? JSON.parse(raw) : yaml.parse(raw);
93
+
94
+ // Apply overrides if provided
95
+ if (overrides) {
96
+ const ovRaw = fs.readFileSync(overrides, 'utf8');
97
+ const ovObj = JSON.parse(ovRaw);
98
+ mergeOverrides(dataObj, ovObj);
99
+ }
100
+
101
+ // Compile the “main” template (used when writeFullDrafts = true)
102
+ const compiledTemplate = handlebars.compile(fs.readFileSync(template, 'utf8'));
103
+
104
+ // Determine which templates to use for “fields” and “examples”
105
+ // If templateFields is not provided, fall back to the single `template`.
106
+ // If templateExamples is not provided, skip examples entirely.
107
+ const fieldsTemplatePath = templateFields || template;
108
+ const examplesTemplatePath = templateExamples || null;
109
+
110
+ // Register partials
111
+ if (!writeFullDrafts) {
112
+ if (fieldsTemplatePath) {
113
+ registerPartial('fields', fieldsTemplatePath);
114
+ }
115
+ if (examplesTemplatePath) {
116
+ registerPartial('examples', examplesTemplatePath);
117
+ }
118
+ } else {
119
+ registerPartial('intro', templateIntro);
120
+ }
121
+
122
+ const outputRoot = path.resolve(process.cwd(), 'modules/components/partials');
123
+ const fieldsOutRoot = path.join(outputRoot, 'fields');
124
+ const examplesOutRoot = path.join(outputRoot, 'examples');
125
+ const draftsRoot = path.join(outputRoot, 'drafts');
126
+
127
+ if (!writeFullDrafts) {
128
+ fs.mkdirSync(fieldsOutRoot, { recursive: true });
129
+ fs.mkdirSync(examplesOutRoot, { recursive: true });
130
+ }
131
+
132
+ let partialsWritten = 0;
133
+ let draftsWritten = 0;
134
+ const partialFiles = [];
135
+ const draftFiles = [];
136
+
137
+ for (const [type, items] of Object.entries(dataObj)) {
138
+ if (!Array.isArray(items)) continue;
139
+
140
+ for (const item of items) {
141
+ if (!item.name) continue;
142
+ const name = item.name;
143
+
144
+ if (!writeFullDrafts) {
145
+ // Render fields using the registered “fields” partial
146
+ const fieldsOut = handlebars
147
+ .compile('{{> fields children=config.children}}')(item);
148
+
149
+ // Render examples only if an examples template was provided
150
+ let examplesOut = '';
151
+ if (examplesTemplatePath) {
152
+ examplesOut = handlebars
153
+ .compile('{{> examples examples=examples}}')(item);
154
+ }
155
+
156
+ if (fieldsOut.trim()) {
157
+ const fPath = path.join(fieldsOutRoot, type, `${name}.adoc`);
158
+ fs.mkdirSync(path.dirname(fPath), { recursive: true });
159
+ fs.writeFileSync(fPath, fieldsOut);
160
+ partialsWritten++;
161
+ partialFiles.push(path.relative(process.cwd(), fPath));
162
+ }
163
+
164
+ if (examplesOut.trim()) {
165
+ const ePath = path.join(examplesOutRoot, type, `${name}.adoc`);
166
+ fs.mkdirSync(path.dirname(ePath), { recursive: true });
167
+ fs.writeFileSync(ePath, examplesOut);
168
+ partialsWritten++;
169
+ partialFiles.push(path.relative(process.cwd(), ePath));
170
+ }
171
+ }
172
+
173
+ if (writeFullDrafts) {
174
+ let content;
175
+ try {
176
+ content = compiledTemplate(item);
177
+ } catch (err) {
178
+ throw new Error(`Template render failed for component "${name}": ${err.message}`);
179
+ }
180
+
181
+ const draftSubdir = name === 'gateway'
182
+ ? path.join(draftsRoot, 'cloud-only')
183
+ : draftsRoot;
184
+
185
+ const destFile = path.join(draftSubdir, `${name}.adoc`);
186
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
187
+ fs.writeFileSync(destFile, content, 'utf8');
188
+ draftsWritten++;
189
+ draftFiles.push(path.relative(process.cwd(), destFile));
190
+ }
191
+ }
192
+ }
193
+
194
+ return {
195
+ partialsWritten,
196
+ draftsWritten,
197
+ partialFiles,
198
+ draftFiles
199
+ };
200
+ }
201
+
202
+ module.exports = {
203
+ generateRpcnConnectorDocs,
204
+ mergeOverrides
205
+ };
@@ -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
+ }