mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(site): tabs (#144)
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Rehype plugin that generates stable IDs for TabsRoot and TabsPanel components.
|
||||
*
|
||||
* This plugin:
|
||||
* 1. Generates stable IDs for TabsRoot components (if not already present)
|
||||
* 2. Propagates TabsRoot IDs to child TabsPanel components via tabsId attribute
|
||||
*/
|
||||
|
||||
let tabsRootCounter = 0;
|
||||
|
||||
export default function rehypeGenerateTabsIds() {
|
||||
return (tree) => {
|
||||
// Process the tree with a stateful visitor
|
||||
function visitWithContext(node, context = { tabsRootId: null }) {
|
||||
// Handle TabsRoot JSX component
|
||||
if (node.type === 'mdxJsxFlowElement' && node.name === 'TabsRoot') {
|
||||
// Generate stable ID
|
||||
const generatedId = `tabs-${tabsRootCounter++}`;
|
||||
|
||||
// Check if ID already exists in attributes
|
||||
const hasExistingId = node.attributes?.some(
|
||||
attr => attr.type === 'mdxJsxAttribute' && attr.name === 'id',
|
||||
);
|
||||
|
||||
// Only add ID if not already present (allow manual override)
|
||||
if (!hasExistingId) {
|
||||
if (!node.attributes) node.attributes = [];
|
||||
node.attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'id',
|
||||
value: generatedId,
|
||||
});
|
||||
}
|
||||
|
||||
// Get the actual ID (generated or existing)
|
||||
const idAttr = node.attributes.find(attr => attr.type === 'mdxJsxAttribute' && attr.name === 'id');
|
||||
const tabsRootId = idAttr?.value || generatedId;
|
||||
|
||||
// Create new context with TabsRoot ID
|
||||
const newContext = { tabsRootId };
|
||||
|
||||
// Visit children with new context
|
||||
if (node.children) {
|
||||
node.children.forEach(child => visitWithContext(child, newContext));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle TabsPanel JSX component
|
||||
if (node.type === 'mdxJsxFlowElement' && node.name === 'TabsPanel') {
|
||||
// Check if tabsId already exists
|
||||
const hasExistingTabsId = node.attributes?.some(
|
||||
attr => attr.type === 'mdxJsxAttribute' && attr.name === 'tabsId',
|
||||
);
|
||||
|
||||
// Add tabsId from context if not present
|
||||
if (!hasExistingTabsId && context.tabsRootId) {
|
||||
if (!node.attributes) node.attributes = [];
|
||||
node.attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'tabsId',
|
||||
value: context.tabsRootId,
|
||||
});
|
||||
}
|
||||
|
||||
// Visit children (preserve context for nested panels)
|
||||
if (node.children) {
|
||||
node.children.forEach(child => visitWithContext(child, context));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,47 +1,56 @@
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
/**
|
||||
* Adapted from https://mdxjs.com/guides/syntax-highlighting/
|
||||
*
|
||||
* This plugin:
|
||||
* 1. Generates stable IDs for <pre> blocks (for tabs)
|
||||
* 2. Tags <code> children of <pre> blocks so they know they're in a pre block
|
||||
* 3. Marks <pre> blocks with hasFrame based on whether they're inside a <TabsPanel> JSX component
|
||||
*/
|
||||
export default function rehypePrepareCodeBlocks() {
|
||||
// A regex that looks for a simplified attribute name, optionally followed
|
||||
// by a double, single, or unquoted attribute value
|
||||
const re = /\b([-\w]+)(?:=(?:"([^"]*)"|'([^']*)'|([^"'\s]+)))?/g;
|
||||
return (tree) => {
|
||||
visit(tree, 'element', (node) => {
|
||||
let match;
|
||||
|
||||
/**
|
||||
* We're looking for <pre> blocks containing <code> blocks to do some work on them because MDX 2 is weird.
|
||||
* Here's what we're up to here:
|
||||
* 1. taking the classname from code and moving it up to pre
|
||||
* 2. notifying code that it's in a pre block so it wouldn't try to format itself as inline code
|
||||
* 3. parsing the code block's metadata and putting the results into the pre block
|
||||
* metadata, you ask? Stuff like title and lineNumbers in this example below.
|
||||
* ```js title="src/pages/index.js" lineNumbers=true
|
||||
* ```
|
||||
*/
|
||||
if (node.tagName === 'pre') {
|
||||
let preBlockCounter = 0;
|
||||
|
||||
export default function rehypePrepareCodeBlocks() {
|
||||
return (tree) => {
|
||||
// Process the tree with a stateful visitor
|
||||
function visitWithContext(node, context = { hasFrame: false }) {
|
||||
// Handle TabsPanel JSX component
|
||||
if (node.type === 'mdxJsxFlowElement' && node.name === 'TabsPanel') {
|
||||
// Create new context for children (inside tabs)
|
||||
const newContext = { hasFrame: true };
|
||||
|
||||
// Visit children with new context
|
||||
if (node.children) {
|
||||
node.children.forEach(child => visitWithContext(child, newContext));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle <pre> elements
|
||||
if (node.type === 'element' && node.tagName === 'pre') {
|
||||
// Mark whether this pre block is inside tabs
|
||||
node.properties.hasFrame = context.hasFrame;
|
||||
|
||||
// Generate stable ID for this pre block
|
||||
node.properties['data-tabs-id'] = `pre-${preBlockCounter++}`;
|
||||
|
||||
// Tag <code> children
|
||||
node.children.forEach((child) => {
|
||||
if (child.tagName === 'code') {
|
||||
const { className } = child.properties;
|
||||
if (className) {
|
||||
node.properties.className = className;
|
||||
}
|
||||
|
||||
child.properties.codeBlock = 'true';
|
||||
|
||||
if (child.data && child.data.meta) {
|
||||
re.lastIndex = 0; // Reset regex.
|
||||
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while ((match = re.exec(child.data.meta))) {
|
||||
node.properties[match[1]] = match[2] || match[3] || match[4] || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
const shikiTransformMetadata = {
|
||||
pre(hast) {
|
||||
// get stuff out of this.options.meta?.__raw;
|
||||
// for now, let's start with just title="abc" or title='abc' or title=abc
|
||||
const raw = this.options.meta?.__raw || '';
|
||||
const titleMatch = raw.match(/title=(?:"([^"]+)"|'([^']+)'|([^\s"']+))/);
|
||||
if (titleMatch) {
|
||||
hast.properties.title = titleMatch[1] || titleMatch[2] || titleMatch[3];
|
||||
}
|
||||
},
|
||||
};
|
||||
export default shikiTransformMetadata;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function useIsHydrated() {
|
||||
const [isHydrated, setIsHydrated] = useState(false);
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
|
||||
setIsHydrated(true);
|
||||
}, []);
|
||||
return isHydrated;
|
||||
}
|
||||
Reference in New Issue
Block a user