Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@redpanda-data/docs-extensions-and-macros",
"version": "4.10.7",
"version": "4.10.8",
"description": "Antora extensions and macros developed for Redpanda documentation.",
"keywords": [
"antora",
Expand Down
142 changes: 103 additions & 39 deletions tools/property-extractor/generate-handlebars-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const helpers = require('./helpers');
* CLI Usage: node generate-handlebars-docs.js <input-file> <output-dir>
*/

// Register all helpers
// Register helpers
Object.entries(helpers).forEach(([name, fn]) => {
if (typeof fn !== 'function') {
console.error(`❌ Helper "${name}" is not a function`);
Expand Down Expand Up @@ -62,11 +62,6 @@ function getTemplatePath(defaultPath, envVar) {

/**
* Register Handlebars partials used to render property documentation.
*
* Registers:
* - "property"
* - "topic-property"
* - "deprecated-property"
*/
function registerPartials() {
const templatesDir = path.join(__dirname, 'templates');
Expand Down Expand Up @@ -129,11 +124,24 @@ function generatePropertyPartials(properties, partialsDir) {

Object.values(properties).forEach(prop => {
if (!prop.name || !prop.config_scope) return;
if (prop.config_scope === 'topic') propertyGroups.topic.push(prop);
else if (prop.config_scope === 'broker') propertyGroups.broker.push(prop);
else if (prop.config_scope === 'cluster') {
if (isObjectStorageProperty(prop)) propertyGroups['object-storage'].push(prop);
else propertyGroups.cluster.push(prop);

switch (prop.config_scope) {
case 'topic':
propertyGroups.topic.push(prop);
break;
case 'broker':
propertyGroups.broker.push(prop);
break;
case 'cluster':
if (isObjectStorageProperty(prop)) propertyGroups['object-storage'].push(prop);
else propertyGroups.cluster.push(prop);
break;
case 'object-storage':
propertyGroups['object-storage'].push(prop);
break;
default:
console.warn(`⚠️ Unknown config_scope: ${prop.config_scope} for ${prop.name}`);
break;
}
});

Expand All @@ -145,7 +153,7 @@ function generatePropertyPartials(properties, partialsDir) {
const selectedTemplate = type === 'topic' ? topicTemplate : propertyTemplate;
const content = props.map(p => selectedTemplate(p)).join('\n');
const filename = `${type}-properties.adoc`;
fs.writeFileSync(path.join(propertiesPartialsDir, filename), AUTOGEN_NOTICE + content, 'utf8');
fs.writeFileSync(path.join(propertiesPartialsDir, filename), AUTOGEN_NOTICE + content, 'utf8');
console.log(`✅ Generated ${filename} (${props.length} properties)`);
totalCount += props.length;
});
Expand Down Expand Up @@ -178,19 +186,18 @@ function generateDeprecatedDocs(properties, outputDir) {
clusterProperties: clusterProperties.length ? clusterProperties : null
};

const output = template(data);
const outputPath = process.env.OUTPUT_PARTIALS_DIR
? path.join(process.env.OUTPUT_PARTIALS_DIR, 'deprecated', 'deprecated-properties.adoc')
: path.join(outputDir, 'partials', 'deprecated', 'deprecated-properties.adoc');

fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, AUTOGEN_NOTICE + output, 'utf8');
fs.writeFileSync(outputPath, AUTOGEN_NOTICE + template(data), 'utf8');
console.log(`✅ Generated ${outputPath}`);
return deprecatedProperties.length;
}

/**
* Generate topic-property-mappings.adoc using the mappings template and topic properties.
* Generate topic-property-mappings.adoc
*/
function generateTopicPropertyMappings(properties, partialsDir) {
const templatesDir = path.join(__dirname, 'templates');
Expand Down Expand Up @@ -218,31 +225,42 @@ function generateTopicPropertyMappings(properties, partialsDir) {
}

/**
* Generate error reports for missing descriptions and deprecated properties.
* Generate error reports for missing descriptions, deprecated, and undocumented properties.
*/
function generateErrorReports(properties) {
function generateErrorReports(properties, documentedProperties = []) {
const emptyDescriptions = [];
const deprecatedProperties = [];
const allKeys = Object.keys(properties);

// Use documentedProperties array (property names that were rendered into partials)
const documentedSet = new Set(documentedProperties);
const undocumented = [];

Object.values(properties).forEach(p => {
if (!p.description || !p.description.trim()) emptyDescriptions.push(p.name);
if (p.is_deprecated) deprecatedProperties.push(p.name);
Object.entries(properties).forEach(([key, p]) => {
const name = p.name || key;
if (!p.description || !p.description.trim()) emptyDescriptions.push(name);
if (p.is_deprecated) deprecatedProperties.push(name);
if (!documentedSet.has(name)) undocumented.push(name);
});

const total = Object.keys(properties).length;
const total = allKeys.length;
const pctEmpty = total ? ((emptyDescriptions.length / total) * 100).toFixed(2) : '0.00';
const pctDeprecated = total ? ((deprecatedProperties.length / total) * 100).toFixed(2) : '0.00';
console.log(`Empty descriptions: ${emptyDescriptions.length} (${pctEmpty}%)`);
console.log(`Deprecated: ${deprecatedProperties.length} (${pctDeprecated}%)`);
const pctUndocumented = total ? ((undocumented.length / total) * 100).toFixed(2) : '0.00';

console.log(`📉 Empty descriptions: ${emptyDescriptions.length} (${pctEmpty}%)`);
console.log(`🕸️ Deprecated: ${deprecatedProperties.length} (${pctDeprecated}%)`);
console.log(`🚫 Not documented: ${undocumented.length} (${pctUndocumented}%)`);

return {
empty_descriptions: emptyDescriptions.sort(),
deprecated_properties: deprecatedProperties.sort()
deprecated_properties: deprecatedProperties.sort(),
undocumented_properties: undocumented.sort(),
};
}

/**
* Main generator — only supports partials and deprecated docs.
* Main generator
*/
function generateAllDocs(inputFile, outputDir) {
const data = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
Expand All @@ -252,13 +270,49 @@ function generateAllDocs(inputFile, outputDir) {

let partialsCount = 0;
let deprecatedCount = 0;
const documentedProps = []; // Track which property names were rendered

if (process.env.GENERATE_PARTIALS === '1' && process.env.OUTPUT_PARTIALS_DIR) {
console.log('📄 Generating property partials and deprecated docs...');
deprecatedCount = generateDeprecatedDocs(properties, outputDir);
partialsCount = generatePropertyPartials(properties, process.env.OUTPUT_PARTIALS_DIR);

// Generate topic-property-mappings.adoc
// Wrap generatePropertyPartials to also collect property names
const originalWrite = fs.writeFileSync;
const propertyTemplate = handlebars.compile(
fs.readFileSync(getTemplatePath(path.join(__dirname, 'templates', 'property.hbs'), 'TEMPLATE_PROPERTY'), 'utf8')
);
const topicTemplate = handlebars.compile(
fs.readFileSync(getTemplatePath(path.join(__dirname, 'templates', 'topic-property.hbs'), 'TEMPLATE_TOPIC_PROPERTY'), 'utf8')
);

const propertiesPartialsDir = path.join(process.env.OUTPUT_PARTIALS_DIR, 'properties');
fs.mkdirSync(propertiesPartialsDir, { recursive: true });

const propertyGroups = { cluster: [], topic: [], broker: [], 'object-storage': [] };
Object.values(properties).forEach(p => {
if (!p.name || !p.config_scope) return;
if (p.config_scope === 'topic') propertyGroups.topic.push(p);
else if (p.config_scope === 'broker') propertyGroups.broker.push(p);
else if (p.config_scope === 'cluster') {
if (isObjectStorageProperty(p)) propertyGroups['object-storage'].push(p);
else propertyGroups.cluster.push(p);
}
});

Object.entries(propertyGroups).forEach(([type, props]) => {
if (props.length === 0) return;
props.sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')));
const selectedTemplate = type === 'topic' ? topicTemplate : propertyTemplate;
const content = props.map(p => {
documentedProps.push(p.name);
return selectedTemplate(p);
}).join('\n');
const filename = `${type}-properties.adoc`;
originalWrite(path.join(propertiesPartialsDir, filename), AUTOGEN_NOTICE + content, 'utf8');
console.log(`✅ Generated ${filename} (${props.length} properties)`);
partialsCount += props.length;
});

try {
generateTopicPropertyMappings(properties, process.env.OUTPUT_PARTIALS_DIR);
} catch (err) {
Expand All @@ -268,24 +322,34 @@ function generateAllDocs(inputFile, outputDir) {
console.log('📄 Skipping partial generation (set GENERATE_PARTIALS=1 and OUTPUT_PARTIALS_DIR to enable)');
}

const errors = generateErrorReports(properties);
const inputData = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
inputData.empty_descriptions = errors.empty_descriptions;
inputData.deprecated_properties = errors.deprecated_properties;
fs.writeFileSync(inputFile, JSON.stringify(inputData, null, 2), 'utf8');
const errors = generateErrorReports(properties, documentedProps);

const totalProperties = Object.keys(properties).length;
const notRendered = errors.undocumented_properties.length;
const pctRendered = totalProperties
? ((partialsCount / totalProperties) * 100).toFixed(2)
: '0.00';

console.log('📊 Summary:');
console.log(` Total properties: ${Object.keys(properties).length}`);
console.log(` Total partials generated: ${partialsCount}`);
console.log(` Deprecated properties: ${deprecatedCount}`);
console.log('\n📊 Summary:');
console.log(` 🧱 Total properties found: ${totalProperties}`);
console.log(` 🧩 Property partials generated: ${partialsCount} (${pctRendered}% of total)`);
console.log(` 🚫 Not documented: ${notRendered}`);
console.log(` 🕸️ Deprecated properties: ${deprecatedCount}`);

if (notRendered > 0) {
console.log('⚠️ Undocumented properties:\n ' + errors.undocumented_properties.join('\n '));
}

return {
totalProperties: Object.keys(properties).length,
propertyPartials: partialsCount,
deprecatedProperties: deprecatedCount
totalProperties,
generatedPartials: partialsCount,
undocumentedProperties: errors.undocumented_properties,
deprecatedProperties: deprecatedCount,
percentageRendered: pctRendered
};
}


module.exports = {
generateAllDocs,
generateDeprecatedDocs,
Expand Down
19 changes: 19 additions & 0 deletions tools/property-extractor/helpers/allTopicsConditional.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Returns 'cloud' if all topics are cloud-only, 'self-managed' if all are self-managed-only, else 'normal'
module.exports = function allTopicsConditional(related_topics) {
if (!Array.isArray(related_topics) || related_topics.length === 0) return null;
let allCloud = true;
let allSelfManaged = true;
for (const t of related_topics) {
if (typeof t !== 'string') {
allCloud = false;
allSelfManaged = false;
break;
}
const trimmed = t.trim();
if (!trimmed.startsWith('cloud-only:')) allCloud = false;
if (!trimmed.startsWith('self-managed-only:')) allSelfManaged = false;
}
if (allCloud) return 'cloud';
if (allSelfManaged) return 'self-managed';
return 'normal';
};
2 changes: 2 additions & 0 deletions tools/property-extractor/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ module.exports = {
renderPropertyExample: require('./renderPropertyExample.js'),
formatUnits: require('./formatUnits.js'),
anchorName: require('./anchorName.js'),
parseRelatedTopic: require('./parseRelatedTopic.js'),
allTopicsConditional: require('./allTopicsConditional.js'),
};
13 changes: 13 additions & 0 deletions tools/property-extractor/helpers/parseRelatedTopic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Returns an object with type and value for a related topic
// type: 'cloud', 'self-managed', or 'normal'
module.exports = function parseRelatedTopic(topic) {
if (typeof topic !== 'string') return { type: 'normal', value: topic };
const trimmed = topic.trim();
if (trimmed.startsWith('cloud-only:')) {
return { type: 'cloud', value: trimmed.replace(/^cloud-only:/, '').trim() };
}
if (trimmed.startsWith('self-managed-only:')) {
return { type: 'self-managed', value: trimmed.replace(/^self-managed-only:/, '').trim() };
}
return { type: 'normal', value: trimmed };
};
43 changes: 41 additions & 2 deletions tools/property-extractor/templates/property.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ endif::[]
{{else}}

No description available.

{{/if}}
{{#if is_enterprise}}

Expand All @@ -31,9 +32,11 @@ ifndef::env-cloud[]
endif::[]
{{/if}}
{{#if cloud_byoc_only}}

ifdef::env-cloud[]
NOTE: This property is available only in Redpanda Cloud BYOC deployments.
endif::[]

{{/if}}
{{#if units}}

Expand Down Expand Up @@ -89,12 +92,48 @@ endif::[]
{{{renderPropertyExample this}}}
{{/if}}
{{#if related_topics}}
{{#with (allTopicsConditional related_topics) as |sectionType|}}

{{#if (eq sectionType "cloud")}}
ifdef::env-cloud[]
*Related topics:*

{{#each related_topics}}
* {{{this}}}
{{#each ../related_topics}}
{{#with (parseRelatedTopic this)}}
* {{{value}}}
{{/with}}
{{/each}}
endif::[]
{{else if (eq sectionType "self-managed")}}
ifndef::env-cloud[]
*Related topics:*

{{#each ../related_topics}}
{{#with (parseRelatedTopic this)}}
* {{{value}}}
{{/with}}
{{/each}}
endif::[]
{{else}}
*Related topics:*

{{#each ../related_topics}}
{{#with (parseRelatedTopic this)}}
{{#if (eq type "cloud")}}
ifdef::env-cloud[]
* {{{value}}}
endif::[]
{{else if (eq type "self-managed")}}
ifndef::env-cloud[]
* {{{value}}}
endif::[]
{{else}}
* {{{value}}}
{{/if}}
{{/with}}
{{/each}}
{{/if}}
{{/with}}
{{/if}}
{{#if aliases}}

Expand Down
Loading