mirror of
https://github.com/zoriya/v10.git
synced 2026-08-12 00:49:29 +00:00
283 lines
9.3 KiB
JavaScript
283 lines
9.3 KiB
JavaScript
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';
|
|
import { resolveReferenceSlug } from './api-reference-overrides';
|
|
import { buildComponentReferenceTocHeadings, createComponentReferenceModel } from './componentReferenceModel';
|
|
import { buildFeatureReferenceTocHeadings, createFeatureReferenceModel } from './featureReferenceModel';
|
|
import { buildMediaReferenceTocHeadings, createMediaReferenceModel } from './mediaReferenceModel';
|
|
import { buildUtilReferenceTocHeadings, createUtilReferenceModel } from './utilReferenceModel';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const COMPONENT_REF_DIR = path.resolve(__dirname, '../content/generated-component-reference');
|
|
const FEATURE_REF_DIR = path.resolve(__dirname, '../content/generated-feature-reference');
|
|
const UTIL_REF_DIR = path.resolve(__dirname, '../content/generated-util-reference');
|
|
const MEDIA_REF_DIR = path.resolve(__dirname, '../content/generated-media-reference');
|
|
|
|
function readComponentRefJson(slug) {
|
|
const filePath = path.join(COMPONENT_REF_DIR, `${slug}.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.
|
|
*
|
|
* Also detects `<ComponentReference>` 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) => {
|
|
const headingsWithMetadata = [];
|
|
const slugger = new GithubSlugger();
|
|
const reservedSlugs = new Set();
|
|
|
|
// Process the tree with a stateful visitor
|
|
function visitWithContext(node, context = { frameworks: null, styles: null }) {
|
|
// Handle FrameworkCase and StyleCase components
|
|
if (node.type === 'mdxJsxFlowElement') {
|
|
if (node.name === 'FrameworkCase') {
|
|
const frameworksAttr = node.attributes?.find((attr) => attr.name === 'frameworks');
|
|
const frameworks = extractArrayValue(frameworksAttr);
|
|
|
|
// Create new context for children
|
|
const newContext = { ...context, frameworks };
|
|
|
|
// Visit children with new context
|
|
if (node.children) {
|
|
node.children.forEach((child) => visitWithContext(child, newContext));
|
|
}
|
|
|
|
return;
|
|
} else if (node.name === 'StyleCase') {
|
|
const stylesAttr = node.attributes?.find((attr) => attr.name === 'styles');
|
|
const styles = extractArrayValue(stylesAttr);
|
|
|
|
// Create new context for children
|
|
const newContext = { ...context, styles };
|
|
|
|
// Visit children with new context
|
|
if (node.children) {
|
|
node.children.forEach((child) => visitWithContext(child, newContext));
|
|
}
|
|
|
|
return;
|
|
} else if (node.name === 'ComponentReference') {
|
|
injectComponentReferenceHeadings(node, headingsWithMetadata, reservedSlugs);
|
|
} else if (node.name === 'FeatureReference') {
|
|
injectFeatureReferenceHeadings(node, headingsWithMetadata, reservedSlugs);
|
|
} else if (node.name === 'UtilReference') {
|
|
injectUtilReferenceHeadings(node, headingsWithMetadata, reservedSlugs);
|
|
return;
|
|
} else if (node.name === 'MediaReference') {
|
|
injectMediaReferenceHeadings(node, headingsWithMetadata, reservedSlugs);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Handle headings
|
|
if (node.type === 'heading') {
|
|
const text = extractText(node);
|
|
let slug = slugger.slug(text);
|
|
|
|
// Avoid collisions with explicit API reference ids.
|
|
while (reservedSlugs.has(slug)) {
|
|
slug = slugger.slug(text);
|
|
}
|
|
reservedSlugs.add(slug);
|
|
|
|
const metadata = {
|
|
depth: node.depth,
|
|
text,
|
|
slug,
|
|
};
|
|
|
|
// Add conditional context if present
|
|
if (context.frameworks) {
|
|
metadata.frameworks = context.frameworks;
|
|
}
|
|
if (context.styles) {
|
|
metadata.styles = context.styles;
|
|
}
|
|
|
|
headingsWithMetadata.push(metadata);
|
|
}
|
|
|
|
// Recursively visit children for other node types
|
|
if (node.children) {
|
|
node.children.forEach((child) => visitWithContext(child, context));
|
|
}
|
|
}
|
|
|
|
// Start visiting from root
|
|
if (tree.children) {
|
|
tree.children.forEach((child) => visitWithContext(child));
|
|
}
|
|
|
|
// Attach to file data for retrieval via remarkPluginFrontmatter
|
|
if (!file.data.astro) {
|
|
file.data.astro = {};
|
|
}
|
|
if (!file.data.astro.frontmatter) {
|
|
file.data.astro.frontmatter = {};
|
|
}
|
|
file.data.astro.frontmatter.conditionalHeadings = headingsWithMetadata;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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 injectComponentReferenceHeadings(node, headingsWithMetadata, reservedSlugs) {
|
|
const componentAttr = node.attributes?.find((a) => a.name === 'component');
|
|
const componentName = typeof componentAttr?.value === 'string' ? componentAttr.value : null;
|
|
if (!componentName) return;
|
|
|
|
const json = readComponentRefJson(resolveReferenceSlug(componentName));
|
|
if (!json) return;
|
|
|
|
const partOrderAttr = node.attributes?.find((a) => a.name === 'partOrder');
|
|
const partOrder = extractArrayValue(partOrderAttr);
|
|
|
|
const componentModel = createComponentReferenceModel(componentName, json, partOrder);
|
|
const componentHeadings = buildComponentReferenceTocHeadings(componentModel);
|
|
|
|
headingsWithMetadata.push(...componentHeadings);
|
|
for (const heading of componentHeadings) {
|
|
reservedSlugs.add(heading.slug);
|
|
}
|
|
}
|
|
|
|
function readFeatureRefJson(featureName) {
|
|
const filePath = path.join(FEATURE_REF_DIR, `${featureName}.json`);
|
|
try {
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function injectFeatureReferenceHeadings(node, headingsWithMetadata, reservedSlugs) {
|
|
const featureAttr = node.attributes?.find((a) => a.name === 'feature');
|
|
const featureName = typeof featureAttr?.value === 'string' ? featureAttr.value : null;
|
|
if (!featureName) return;
|
|
|
|
const json = readFeatureRefJson(featureName);
|
|
if (!json) return;
|
|
|
|
const featureModel = createFeatureReferenceModel(featureName, json);
|
|
const featureHeadings = buildFeatureReferenceTocHeadings(featureModel);
|
|
|
|
headingsWithMetadata.push(...featureHeadings);
|
|
for (const heading of featureHeadings) {
|
|
reservedSlugs.add(heading.slug);
|
|
}
|
|
}
|
|
|
|
function readUtilRefJson(slug) {
|
|
const filePath = path.join(UTIL_REF_DIR, `${slug}.json`);
|
|
try {
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function injectUtilReferenceHeadings(node, headingsWithMetadata, reservedSlugs) {
|
|
const utilAttr = node.attributes?.find((a) => a.name === 'util');
|
|
const utilName = typeof utilAttr?.value === 'string' ? utilAttr.value : null;
|
|
if (!utilName) return;
|
|
|
|
const slugAttr = node.attributes?.find((a) => a.name === 'slug');
|
|
const slugValue = typeof slugAttr?.value === 'string' ? slugAttr.value : null;
|
|
|
|
const json = readUtilRefJson(slugValue ?? kebabCase(utilName));
|
|
if (!json) return;
|
|
|
|
const utilModel = createUtilReferenceModel(utilName, json);
|
|
const utilHeadings = buildUtilReferenceTocHeadings(utilModel);
|
|
|
|
headingsWithMetadata.push(...utilHeadings);
|
|
for (const heading of utilHeadings) {
|
|
reservedSlugs.add(heading.slug);
|
|
}
|
|
}
|
|
|
|
function readMediaRefJson(tagName) {
|
|
const filePath = path.join(MEDIA_REF_DIR, `${tagName}.json`);
|
|
try {
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function injectMediaReferenceHeadings(node, headingsWithMetadata, reservedSlugs) {
|
|
const mediaAttr = node.attributes?.find((a) => a.name === 'media');
|
|
const mediaName = typeof mediaAttr?.value === 'string' ? mediaAttr.value : null;
|
|
if (!mediaName) return;
|
|
|
|
const json = readMediaRefJson(resolveReferenceSlug(mediaName));
|
|
if (!json) return;
|
|
|
|
const mediaModel = createMediaReferenceModel(mediaName, json);
|
|
const mediaHeadings = buildMediaReferenceTocHeadings(mediaModel);
|
|
|
|
headingsWithMetadata.push(...mediaHeadings);
|
|
for (const heading of mediaHeadings) {
|
|
reservedSlugs.add(heading.slug);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract array value from JSX attribute like frameworks={["react", "html"]}
|
|
*/
|
|
function extractArrayValue(attr) {
|
|
if (!attr || !attr.value) {
|
|
return null;
|
|
}
|
|
|
|
// Handle JSX expression
|
|
if (attr.value.type === 'mdxJsxAttributeValueExpression') {
|
|
const expression = attr.value.value;
|
|
try {
|
|
// Parse the array from the expression
|
|
// This is a simple approach that works for basic arrays
|
|
return JSON.parse(expression.trim());
|
|
} catch (e) {
|
|
console.warn(`Failed to parse JSX expression: ${expression}`, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Extract text content from a heading node
|
|
*/
|
|
function extractText(node) {
|
|
if (node.type === 'text') {
|
|
return node.value;
|
|
}
|
|
|
|
if (node.type === 'inlineCode') {
|
|
return node.value;
|
|
}
|
|
|
|
if (node.children) {
|
|
return node.children.map((child) => extractText(child)).join('');
|
|
}
|
|
|
|
return '';
|
|
}
|