feat(site): generated multipart component api reference (#468)

This commit is contained in:
Darius Cepulis
2026-02-09 12:20:45 +11:00
committed by GitHub
parent 3c4152fb58
commit 4b1e863883
31 changed files with 1413 additions and 327 deletions
+77 -1
View File
@@ -1,10 +1,28 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { kebabCase } from 'es-toolkit/string';
import GithubSlugger from 'github-slugger';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const API_REF_DIR = path.resolve(__dirname, '../content/generated-api-reference');
function readApiRefJson(componentName) {
const kebab = kebabCase(componentName);
const filePath = path.join(API_REF_DIR, `${kebab}.json`);
try {
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
} catch {
return null;
}
}
/**
* Remark plugin that tracks headings wrapped in FrameworkCase or StyleCase components
* and adds conditional metadata to them.
*
* This allows filtering headings in the TOC based on the current framework/style combination.
* Also detects `<ApiReference>` components and injects heading metadata from
* generated JSON, so component-rendered headings appear in the table of contents.
*/
export default function remarkConditionalHeadings() {
return (tree, file) => {
@@ -40,6 +58,9 @@ export default function remarkConditionalHeadings() {
node.children.forEach((child) => visitWithContext(child, newContext));
}
return;
} else if (node.name === 'ApiReference') {
injectApiReferenceHeadings(node, slugger, headingsWithMetadata);
return;
}
}
@@ -86,6 +107,61 @@ export default function remarkConditionalHeadings() {
};
}
/**
* Inject heading metadata from generated API reference JSON.
*
* For multi-part components, injects framework-conditional part headings.
* For single-part components, injects "API reference".
* For each, injects Props/State/Data attributes headings
*/
function injectApiReferenceHeadings(node, slugger, headingsWithMetadata) {
const componentAttr = node.attributes?.find((a) => a.name === 'component');
const componentName = typeof componentAttr?.value === 'string' ? componentAttr.value : null;
if (!componentName) return;
const json = readApiRefJson(componentName);
if (!json) return;
if (json.parts && Object.keys(json.parts).length > 0) {
for (const [partKebab, part] of Object.entries(json.parts)) {
const tagName = part.platforms?.html?.tagName;
const partKebabSlug = slugger.slug(partKebab);
headingsWithMetadata.push({
depth: 2,
text: `<${componentName}.${part.name} /> reference`,
slug: partKebabSlug,
frameworks: ['react'],
});
headingsWithMetadata.push({
depth: 2,
text: `${tagName ? `<${tagName}>` : part.name} reference`,
slug: partKebabSlug,
frameworks: ['html'],
});
if (part.props && Object.keys(part.props).length > 0) {
headingsWithMetadata.push({ depth: 3, text: 'Props', slug: slugger.slug('Props') });
}
if (part.state && Object.keys(part.state).length > 0) {
headingsWithMetadata.push({ depth: 3, text: 'State', slug: slugger.slug('State') });
}
if (part.dataAttributes && Object.keys(part.dataAttributes).length > 0) {
headingsWithMetadata.push({ depth: 3, text: 'Data attributes', slug: slugger.slug('Data attributes') });
}
}
} else {
headingsWithMetadata.push({ depth: 2, text: 'API reference', slug: slugger.slug('API reference') });
if (json.props && Object.keys(json.props).length > 0) {
headingsWithMetadata.push({ depth: 3, text: 'Props', slug: slugger.slug('Props') });
}
if (json.state && Object.keys(json.state).length > 0) {
headingsWithMetadata.push({ depth: 3, text: 'State', slug: slugger.slug('State') });
}
if (json.dataAttributes && Object.keys(json.dataAttributes).length > 0) {
headingsWithMetadata.push({ depth: 3, text: 'Data attributes', slug: slugger.slug('Data attributes') });
}
}
}
/**
* Extract array value from JSX attribute like frameworks={["react", "html"]}
*/