Core: Support remarkCodeTab plugin

This commit is contained in:
Fuma Nama
2025-01-26 21:07:33 +08:00
parent d6781cc5e3
commit 581f4a55a2
11 changed files with 171 additions and 19 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
'fumadocs-ui': major
---
### Migrate to Tailwind CSS v4
**Migrate to Tailwind CSS v4**
**migrate:**
+39
View File
@@ -0,0 +1,39 @@
---
'fumadocs-core': minor
---
**Support code block tabs without hardcoding `<Tabs />` items**
**migrate:** Use the `remarkCodeTab` plugin.
**before:**
````mdx
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
<Tabs items={["Tab 1", "Tab 2"]}>
```ts tab
console.log('A');
```
```ts tab
console.log('B');
```
</Tabs>
````
**after:**
````mdx
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
```ts tab="Tab 1"
console.log('A');
```
```ts tab="Tab 2"
console.log('B');
```
````
+8 -14
View File
@@ -170,7 +170,7 @@ You can specify the type of callout.
Hello World
</Callout>
### Customisation
### Customise Components
See [all MDX components and available options](/docs/ui/mdx).
@@ -261,38 +261,32 @@ console.log('hewwo'); // [!code --]
console.log('hello'); // [!code ++]
```
### As Tab
### Tab Groups
You can use code block as a `<Tab />` component.
You can use code blocks with the `<Tab />` component.
````mdx
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
<Tabs items={["Tab 1", "Tab 2"]}>
```ts tab
```ts tab="Tab 1"
console.log('A');
```
```ts tab
```ts tab="Tab 2"
console.log('B');
```
</Tabs>
````
<Tabs items={["Tab 1", "Tab 2"]}>
> Note that you can add MDX components instead of importing them in MDX files.
```ts tab
```ts tab="Tab 1"
console.log('A');
```
```ts tab
```ts tab="Tab 2"
console.log('B');
```
</Tabs>
### Using Typescript Twoslash
Write Typescript codeblocks with hover type information and detected types errors.
+5
View File
@@ -8,6 +8,11 @@ const withAnalyzer = createBundleAnalyzer({
const config: NextConfig = {
reactStrictMode: true,
logging: {
fetches: {
fullUrl: true,
},
},
eslint: {
// Replaced by root workspace command
ignoreDuringBuilds: true,
+1 -1
View File
@@ -56,7 +56,7 @@ export default defineConfig({
return {
rehypeCodeOptions: {
lazy: true,
langs: ['ts', 'js', 'html'],
langs: ['ts', 'js', 'html', 'tsx', 'mdx'],
inline: 'tailing-curly-colon',
themes: {
light: 'catppuccin-latte',
+1
View File
@@ -8,3 +8,4 @@ export * from './remark-structure';
export * from './remark-heading';
export * from './remark-admonition';
export * from './rehype-toc';
export * from './remark-code-tab';
@@ -0,0 +1,106 @@
import type { Transformer } from 'unified';
import type { Code, Root, RootContent } from 'mdast';
import { visit } from 'unist-util-visit';
const TabRegex = /tab="(.+?)"/;
function toTab(nodes: Code[]) {
const names = nodes.map((node, i) => {
let title = `Tab ${i + 1}`;
node.meta = node.meta?.replace(TabRegex, (_, value) => {
title = value;
return '';
});
return title;
});
const itemsArr = {
type: 'ExpressionStatement',
expression: {
type: 'ArrayExpression',
elements: names.map((name) => ({
type: 'Literal',
value: name,
})),
},
};
return {
type: 'mdxJsxFlowElement',
name: 'Tabs',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'items',
value: {
type: 'mdxJsxAttributeValueExpression',
data: {
estree: {
type: 'Program',
sourceType: 'module',
comments: [],
body: [itemsArr],
},
},
},
},
],
children: nodes.map((node, i) => {
return {
type: 'mdxJsxFlowElement',
name: 'Tab',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'value',
value: names[i],
},
],
children: [node],
};
}),
};
}
export function remarkCodeTab(): Transformer<Root, Root> {
return (tree) => {
visit(tree, (node) => {
if (node.type === 'mdxJsxFlowElement' && node.name === 'Tabs') return;
if ('children' in node) {
let start = -1;
let i = 0;
while (i < node.children.length) {
const child = node.children[i];
const isSwitcher =
child.type === 'code' && child.meta && child.meta.match(TabRegex);
if (isSwitcher && start === -1) {
start = i;
}
// if switcher code blocks terminated, convert them to tabs
const isLast = i === node.children.length - 1;
if (start !== -1 && (isLast || !isSwitcher)) {
const end = isSwitcher ? i + 1 : i;
const targets = node.children.slice(start, end);
node.children.splice(
start,
end - start,
toTab(targets as Code[]) as RootContent,
);
if (isLast) break;
i = start;
start = -1;
}
i++;
}
}
});
};
}
+4
View File
@@ -2,6 +2,7 @@ import {
rehypeCode,
type RehypeCodeOptions,
rehypeToc,
remarkCodeTab,
remarkGfm,
remarkHeading,
type RemarkHeadingOptions,
@@ -31,6 +32,7 @@ export type DefaultMDXOptions = Omit<
remarkStructureOptions?: StructureOptions | false;
remarkHeadingOptions?: RemarkHeadingOptions;
remarkImageOptions?: RemarkImageOptions | false;
remarkCodeTabOptions?: false;
rehypeCodeOptions?: Partial<RehypeCodeOptions> | false;
};
@@ -55,6 +57,7 @@ export function getDefaultMDXOptions({
remarkImageOptions,
remarkHeadingOptions,
remarkStructureOptions,
remarkCodeTabOptions,
...mdxOptions
}: DefaultMDXOptions): ProcessorOptions {
const mdxExports = [
@@ -75,6 +78,7 @@ export function getDefaultMDXOptions({
},
],
remarkImageOptions !== false && [remarkImage, remarkImageOptions],
remarkCodeTabOptions !== false && remarkCodeTab,
...v,
remarkStructureOptions !== false && [
remarkStructure,
+4 -1
View File
@@ -313,7 +313,10 @@ export function TagsList({
return (
<div
{...props}
className={cn('flex flex-row items-center gap-1 flex-wrap', props.className)}
className={cn(
'flex flex-row items-center gap-1 flex-wrap',
props.className,
)}
>
{items.map((item) => (
<button
+1 -1
View File
@@ -77,7 +77,7 @@ export function DocsLayout({
const tabs = getSidebarTabsFromOptions(tabOptions, props.tree) ?? [];
const variables = cn(
'[--fd-tocnav-height:36px] md:[--fd-sidebar-width:268px] xl:[--fd-toc-width:268px] xl:[--fd-tocnav-height:0px]',
'[--fd-tocnav-height:36px] md:[--fd-sidebar-width:268px] xl:md:[--fd-sidebar-width:286px] xl:[--fd-toc-width:286px] xl:[--fd-tocnav-height:0px]',
!navReplace && navEnabled
? '[--fd-nav-height:3.5rem] md:[--fd-nav-height:0px]'
: undefined,
+1 -1
View File
@@ -63,7 +63,7 @@ const itemVariants = cva(
{
variants: {
active: {
true: 'bg-fd-primary/10 font-medium text-fd-primary',
true: 'bg-fd-primary/10 text-fd-primary',
false:
'transition-colors hover:bg-fd-accent/50 hover:text-fd-accent-foreground/80 hover:transition-none',
},