Core: Support tab-group in codeblock tabs

This commit is contained in:
Fuma Nama
2026-05-09 18:35:18 +08:00
parent 3148cc071b
commit 2ca3eab788
4 changed files with 96 additions and 34 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': patch
---
Support `tab-group` in codeblock tabs
@@ -330,6 +330,38 @@ console.log('A');
console.log('B');
```
To add a persist ID, define `tab-group` at the first codeblock:
````mdx
```ts tab="Tab 1" tab-group="my-custom-group"
console.log('A');
```
```ts tab="Tab 2"
console.log('B');
```
````
```ts tab="Tab 1" tab-group="my-custom-group"
console.log('A');
```
```ts tab="Tab 2"
console.log('B');
```
Another group:
```ts tab="Tab 1" tab-group="my-custom-group"
console.log('A');
```
```ts tab="Tab 2"
console.log('B');
```
#### MDX in Tab Groups
While disabled by default, you use MDX in tab values by configuring the remark plugin:
```ts tab="Fumadocs MDX" title="source.config.ts"
@@ -91,6 +91,8 @@ export interface CodeBlockAttributes<Name extends string = string> {
rest: string;
}
const AttributeRegex = /(?<=^|\s)(?<name>[a-zA-Z0-9_-]+)(?:=(?:"([^"]*)"|'([^']*)'))?/g;
/**
* Parse Fumadocs-style code block attributes from meta string, like `title="hello world"`
*/
@@ -98,11 +100,8 @@ export function parseCodeBlockAttributes<Name extends string = string>(
meta: string,
allowedNames?: Name[],
): CodeBlockAttributes<Name> {
let str = meta;
const StringRegex = /(?<=^|\s)(?<name>\w+)(?:=(?:"([^"]*)"|'([^']*)'))?/g;
const attributes: CodeBlockAttributes['attributes'] = {};
str = str.replaceAll(StringRegex, (match, name, value_1, value_2) => {
const rest = meta.replaceAll(AttributeRegex, (match, name, value_1, value_2) => {
if (allowedNames && !allowedNames.includes(name)) return match;
attributes[name] = value_1 ?? value_2 ?? null;
@@ -110,7 +109,7 @@ export function parseCodeBlockAttributes<Name extends string = string>(
});
return {
rest: str,
rest,
attributes,
};
}
@@ -2,7 +2,11 @@ import type { Processor, Transformer } from 'unified';
import type { BlockContent, Code, Root, RootContent } from 'mdast';
import { visit } from 'unist-util-visit';
import type { MdxJsxFlowElement } from 'mdast-util-mdx';
import { generateCodeBlockTabs, parseCodeBlockAttributes } from '@/mdx-plugins/codeblock-utils';
import {
type CodeBlockTabsOptions,
generateCodeBlockTabs,
parseCodeBlockAttributes,
} from '@/mdx-plugins/codeblock-utils';
type TabType = 'CodeBlockTabs' | 'Tabs';
export interface RemarkCodeTabOptions {
@@ -22,6 +26,10 @@ declare module 'mdast' {
* [Fumadocs: remark-code-tab] the associated tab value
*/
tab?: string;
/**
* [Fumadocs: remark-code-tab] the associated tab group ID
*/
tabGroup?: string;
}
export interface Data {
@@ -41,27 +49,40 @@ type TabHandler = (
const Types: Record<TabType, TabHandler> = {
CodeBlockTabs(processor, nodes, withMdx, withParent) {
const tabs = Array.from(processTabValue(nodes).entries());
const tabs = processTabValue(nodes);
let isFirstTab = true;
const options: CodeBlockTabsOptions = {
triggers: [],
tabs: [],
};
const node = generateCodeBlockTabs({
defaultValue: tabs[0][0],
triggers: tabs.map(([name]) => ({
value: name,
children: [
withMdx
? (mdxToAst(processor, name) as unknown as BlockContent)
: {
for (const [value, list] of tabs) {
if (isFirstTab) {
const tagGroup = list[0].data?.tabGroup;
options.defaultValue = value;
if (tagGroup) options.persist = { id: tagGroup };
isFirstTab = false;
}
options.triggers.push({
value,
children: withMdx
? (mdxToAst(processor, value).children as BlockContent[])
: [
{
type: 'text',
value: name,
value,
},
],
})),
tabs: tabs.map(([name, codes]) => ({
value: name,
children: codes,
})),
});
],
});
options.tabs.push({
value,
children: list,
});
}
const node = generateCodeBlockTabs(options);
if (!withParent) return node.children;
return [node];
},
@@ -231,7 +252,7 @@ export function remarkCodeTab(
continue;
}
const meta = parseCodeBlockAttributes(child.meta, ['tab']);
const meta = parseCodeBlockAttributes(child.meta, ['tab', 'tab-group']);
if (!meta.attributes.tab) {
close();
continue;
@@ -241,6 +262,9 @@ export function remarkCodeTab(
child.meta = meta.rest;
child.data ??= {};
child.data.tab = meta.attributes.tab;
if (meta.attributes['tab-group']) {
child.data.tabGroup = meta.attributes['tab-group'];
}
}
close();
@@ -254,26 +278,28 @@ function processTabValue(nodes: Code[]) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const name = node.data?.tab ?? `Tab ${i + 1}`;
const li = out.get(name) ?? [];
let li = out.get(name);
if (!li) {
li = [];
out.set(name, li);
}
li.push(node);
out.set(name, li);
}
return out;
}
/**
* MDX tab name to tab trigger node children
*/
function mdxToAst(processor: Processor, name: string) {
function mdxToAst(processor: Processor, name: string): Root {
const node = processor.parse(name) as Root;
node.children = node.children.flatMap((child) => {
if (child.type === 'paragraph') return child.children;
if (node.type === 'root') {
node.children = node.children.flatMap((child) => {
if (child.type === 'paragraph') return child.children;
return child;
});
}
return child;
});
return node;
}