Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1,830 changes: 1,758 additions & 72 deletions __tests__/docs-data/property-overrides.json

Large diffs are not rendered by default.

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
118 changes: 77 additions & 41 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 @@ -112,7 +107,7 @@ function registerPartials() {
/**
* Generate consolidated AsciiDoc partials for properties grouped by type.
*/
function generatePropertyPartials(properties, partialsDir) {
function generatePropertyPartials(properties, partialsDir, onRender) {
console.log(`📝 Generating consolidated property partials in ${partialsDir}…`);

const propertyTemplate = handlebars.compile(
Expand All @@ -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 @@ -143,9 +151,16 @@ function generatePropertyPartials(properties, partialsDir) {
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 => selectedTemplate(p)).join('\n');
const pieces = [];
props.forEach(p => {
if (typeof onRender === 'function') {
try { onRender(p.name); } catch (err) { /* swallow callback errors */ }
}
pieces.push(selectedTemplate(p));
});
const content = pieces.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 +193,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 +232,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);

Object.values(properties).forEach(p => {
if (!p.description || !p.description.trim()) emptyDescriptions.push(p.name);
if (p.is_deprecated) deprecatedProperties.push(p.name);
// Use documentedProperties array (property names that were rendered into partials)
const documentedSet = new Set(documentedProperties);
const undocumented = [];

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 +277,14 @@ 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
// Generate property partials using the shared helper and collect names via callback
partialsCount = generatePropertyPartials(properties, process.env.OUTPUT_PARTIALS_DIR, name => documentedProps.push(name));
try {
generateTopicPropertyMappings(properties, process.env.OUTPUT_PARTIALS_DIR);
} catch (err) {
Expand All @@ -268,24 +294,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);

console.log('📊 Summary:');
console.log(` Total properties: ${Object.keys(properties).length}`);
console.log(` Total partials generated: ${partialsCount}`);
console.log(` Deprecated properties: ${deprecatedCount}`);
const totalProperties = Object.keys(properties).length;
const notRendered = errors.undocumented_properties.length;
const pctRendered = totalProperties
? ((partialsCount / totalProperties) * 100).toFixed(2)
: '0.00';

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';
};
4 changes: 4 additions & 0 deletions tools/property-extractor/helpers/formatPropertyValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ function processDefaults(inputString, suffix) {
return inputString;
}

// Remove C++ digit separators (apostrophes) from numbers/durations
// e.g. 30'000ms -> 30000ms, 1'500 -> 1500
inputString = inputString.replace(/(?<=\d)'(?=\d)/g, '');

// Test for ip:port in vector: std::vector<net::unresolved_address>({{...}})
const vectorMatch = inputString.match(/std::vector<net::unresolved_address>\(\{\{("([\d.]+)",\s*(\d+))\}\}\)/);
if (vectorMatch) {
Expand Down
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 };
};
25 changes: 23 additions & 2 deletions tools/property-extractor/property_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,9 @@ def process_cpp_patterns(arg_str):
processed (str): A string representing the JSON-ready value (for example: '"value"', 'null', '0', or the original input when no mapping applied).
"""
arg_str = arg_str.strip()
# Remove C++ digit separators (apostrophes) that may appear in numeric literals
# Example: "30'000ms" -> "30000ms". Use conservative replace only between digits.
arg_str = re.sub(r"(?<=\d)'(?=\d)", '', arg_str)

# Handle std::nullopt -> null
if arg_str == "std::nullopt":
Expand All @@ -1023,9 +1026,27 @@ def process_cpp_patterns(arg_str):
resolved_value = resolve_cpp_function_call(function_name)
if resolved_value is not None:
return f'"{resolved_value}"'

# Handle std::chrono literals like std::chrono::minutes{5} -> "5min"
chrono_match = re.match(r'std::chrono::([a-zA-Z]+)\s*\{\s*(\d+)\s*\}', arg_str)
if chrono_match:
unit = chrono_match.group(1)
value = chrono_match.group(2)
unit_map = {
'hours': 'h',
'minutes': 'min',
'seconds': 's',
'milliseconds': 'ms',
'microseconds': 'us',
'nanoseconds': 'ns'
}
short = unit_map.get(unit.lower(), unit)
return f'"{value} {short}"'

# Handle enum-like patterns (such as fips_mode_flag::disabled -> "disabled")
enum_match = re.match(r'[a-zA-Z0-9_:]+::([a-zA-Z0-9_]+)', arg_str)
# Handle enum-like patterns (such as fips_mode_flag::disabled -> "disabled").
# Only treat bare 'X::Y' tokens as enums — do not match when the token
# is followed by constructor braces/parentheses (e.g. std::chrono::minutes{5}).
enum_match = re.match(r'[a-zA-Z0-9_:]+::([a-zA-Z0-9_]+)\s*$', arg_str)
if enum_match:
enum_value = enum_match.group(1)
return f'"{enum_value}"'
Expand Down
Loading