@redpanda-data/docs-extensions-and-macros
Version:
Antora extensions and macros developed for Redpanda documentation.
121 lines (111 loc) • 5.15 kB
JavaScript
;
/**
* Normalize the formatting of an extracted `== Metadata` block so the generated
* metadata partials are consistent regardless of how each connector authored
* its metadata section upstream.
*
* Two inconsistencies are normalized:
* 1. Some connectors wrap their field list in a fenced code block
* (```text ... ``` or ``` ... ```) of bare names; others use an AsciiDoc
* bullet list with the field name in inline code. We strip the fences
* around a field list and render the fields as a plain bullet list.
* 2. Within a field bullet, the field name (a lowercase snake_case
* identifier) is wrapped in inline code. Descriptive bullets such as
* "All headers (only first values are taken)" are left as prose, and
* bullets that already contain inline code are left untouched.
*
* Everything else (intro sentences, notes, `===` subheadings that legitimately
* group metadata by operation, non-field-list fenced blocks, and AsciiDoc
* `----` literal blocks) is preserved verbatim.
*/
const FENCE_OPEN = /^(\s*)(```|~~~)(.*)$/;
const BULLET = /^(\s*[-*]\s+)(.*)$/;
// AsciiDoc literal/listing block delimiter. Content inside `----` blocks is
// rendered verbatim, so backticks would show up literally — such regions must
// pass through untouched (no field-name inline-coding).
const BLOCK_DELIMITER = /^-{4,}$/;
// A metadata field bullet: the leading token is a lowercase snake_case
// identifier (gcs_key, http_server_verb, header), optionally followed by a
// parenthetical annotation (for example "(RFC3339)"), and optionally a
// description separated by ":" or " - ". This distinguishes real field bullets
// from descriptive ones like "All headers ..." (which start with a capital).
// The description may itself contain inline code, so it is matched loosely.
const FIELD_BULLET = /^([a-z][a-z0-9_]*)((?:\s*\([^)]*\))?(?:\s*(?::|-)\s.*)?)$/s;
function normalizeBullet (prefix, content) {
// Skip only when the field name itself is already inline-coded; a backtick
// later in the description must not prevent coding the field name.
if (content.startsWith('`')) return prefix + content;
const m = content.match(FIELD_BULLET);
if (!m) return prefix + content; // descriptive, leave as prose
return `${prefix}\`${m[1]}\`${m[2]}`; // inline-code the field name
}
/** True when a fenced block's info string and content are a bare field list. */
function isFieldListFence (infoString, contentLines) {
const info = infoString.trim();
if (info !== '' && info !== 'text') return false;
const nonBlank = contentLines.filter((l) => l.trim() !== '');
if (nonBlank.length === 0 || !nonBlank.every((l) => BULLET.test(l))) return false;
// Require at least one real field-name bullet. A fenced block of purely
// descriptive bullets (no snake_case field name) is not a field list, so its
// fences must be preserved rather than stripped.
return nonBlank.some((l) => {
const b = l.match(BULLET);
return b && FIELD_BULLET.test(b[2]);
});
}
/**
* @param {string} block the extracted `== Metadata` block
* @returns {string} the block with consistent field-list formatting
*/
function normalizeMetadataBlock (block) {
if (!block || typeof block !== 'string') return block;
const lines = block.split('\n');
const out = [];
for (let i = 0; i < lines.length; i++) {
// AsciiDoc literal/listing block: pass the whole `----` … `----` region
// through verbatim. Field-name lines inside are rendered literally, so they
// must not be inline-coded (backticks would appear as visible characters).
if (BLOCK_DELIMITER.test(lines[i].trim())) {
out.push(lines[i]);
let j = i + 1;
for (; j < lines.length; j++) {
out.push(lines[j]);
if (BLOCK_DELIMITER.test(lines[j].trim())) break;
}
i = j;
continue;
}
const fence = lines[i].match(FENCE_OPEN);
if (fence) {
// Collect the fenced content up to a closing fence of the SAME marker
// (a ```-opened block is not closed by ~~~, and vice versa).
const marker = fence[2];
const content = [];
let j = i + 1;
let closed = false;
for (; j < lines.length; j++) {
if (lines[j].trim() === marker) { closed = true; break; }
content.push(lines[j]);
}
if (closed && isFieldListFence(fence[3], content)) {
// Drop the fences and render the fields as a normal bullet list.
for (const c of content) {
const b = c.match(BULLET);
out.push(b ? normalizeBullet(b[1], b[2]) : c);
}
} else {
// Not a field list (e.g. a YAML example) — keep verbatim.
out.push(lines[i]);
for (const c of content) out.push(c);
if (closed) out.push(lines[j]);
}
i = closed ? j : lines.length;
} else {
// Bare bullet outside a fence: normalize the field name too.
const b = lines[i].match(BULLET);
out.push(b ? normalizeBullet(b[1], b[2]) : lines[i]);
}
}
return out.join('\n');
}
module.exports = { normalizeMetadataBlock };