mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core: Support remarkSteps plugin
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
Support `remarkSteps` plugin
|
||||
@@ -13,13 +13,13 @@ Create a new Next.js application with `create-next-app`, and install required pa
|
||||
fumadocs-ui fumadocs-core
|
||||
```
|
||||
|
||||
### MDX Components
|
||||
### 1. MDX Components
|
||||
|
||||
<include cwd meta='title="mdx-components.tsx"'>
|
||||
../../examples/next-mdx/mdx-components.tsx
|
||||
</include>
|
||||
|
||||
### Content Source
|
||||
### 2. Content Source
|
||||
|
||||
Fumadocs supports different content sources, you can choose one you prefer.
|
||||
|
||||
@@ -30,7 +30,7 @@ There is a list of officially supported sources:
|
||||
|
||||
Make sure to configure the library correctly following their setup guide before continuing, we will import the source adapter using `@/lib/source.ts` in this guide.
|
||||
|
||||
### Root Layout
|
||||
### 3. Root Layout
|
||||
|
||||
Wrap the entire application inside [Root Provider](/docs/ui/layouts/root-provider), and add required styles to `body`.
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
}
|
||||
```
|
||||
|
||||
### Styles
|
||||
### 4. Styles
|
||||
|
||||
Setup Tailwind CSS v4 on your Next.js app, add the following to `global.css`.
|
||||
|
||||
@@ -71,7 +71,7 @@ Setup Tailwind CSS v4 on your Next.js app, add the following to `global.css`.
|
||||
|
||||
> It doesn't come with a default font, you may choose one from `next/font`.
|
||||
|
||||
### Layout
|
||||
### 5. Layout
|
||||
|
||||
Create a `app/layout.config.tsx` file to put the shared options for our layouts.
|
||||
|
||||
@@ -97,7 +97,7 @@ Create a folder `/app/docs` for our docs, and give it a proper layout.
|
||||
|
||||
> `pageTree` refers to Page Tree, it should be provided by your content source.
|
||||
|
||||
### Page
|
||||
### 6. Page
|
||||
|
||||
Create a catch-all route `/app/docs/[[...slug]]` for docs pages.
|
||||
|
||||
@@ -112,7 +112,7 @@ It may vary depending on your content source. You should configure static render
|
||||
|
||||
</Tabs>
|
||||
|
||||
### Search
|
||||
### 7. Search
|
||||
|
||||
Use the default document search based on Orama.
|
||||
|
||||
@@ -122,7 +122,7 @@ Use the default document search based on Orama.
|
||||
|
||||
Learn more about [Document Search](/docs/headless/search).
|
||||
|
||||
### Done
|
||||
### 8. Done
|
||||
|
||||
You can start the dev server and create MDX files.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
defineCollections,
|
||||
defineConfig,
|
||||
defineDocs,
|
||||
defineCollections,
|
||||
frontmatterSchema,
|
||||
metaSchema,
|
||||
} from 'fumadocs-mdx/config';
|
||||
@@ -12,7 +12,10 @@ import { fileGenerator, remarkDocGen, remarkInstall } from 'fumadocs-docgen';
|
||||
import { remarkTypeScriptToJavaScript } from 'fumadocs-docgen/remark-ts2js';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import { z } from 'zod';
|
||||
import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins';
|
||||
import {
|
||||
rehypeCodeDefaultOptions,
|
||||
remarkSteps,
|
||||
} from 'fumadocs-core/mdx-plugins';
|
||||
import { remarkAutoTypeTable } from 'fumadocs-typescript';
|
||||
|
||||
export const docs = defineDocs({
|
||||
@@ -80,6 +83,7 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
remarkPlugins: [
|
||||
remarkSteps,
|
||||
remarkMath,
|
||||
remarkAutoTypeTable,
|
||||
[remarkInstall, { persist: { id: 'package-manager' } }],
|
||||
|
||||
@@ -9,3 +9,4 @@ export * from './remark-heading';
|
||||
export * from './remark-admonition';
|
||||
export * from './rehype-toc';
|
||||
export * from './remark-code-tab';
|
||||
export * from './remark-steps';
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
} from '@shikijs/transformers';
|
||||
import type { Processor, Transformer } from 'unified';
|
||||
import {
|
||||
type ShikiTransformer,
|
||||
type BuiltinTheme,
|
||||
bundledLanguages,
|
||||
type ShikiTransformer,
|
||||
} from 'shiki';
|
||||
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
|
||||
import type { IconOptions, CodeBlockIcon } from './transformer-icon';
|
||||
import type { CodeBlockIcon, IconOptions } from './transformer-icon';
|
||||
import { transformerIcon } from './transformer-icon';
|
||||
import {
|
||||
createStyleTransformer,
|
||||
@@ -42,10 +42,6 @@ const metaValues: MetaValue[] = [
|
||||
name: 'tab',
|
||||
regex: /tab="(?<value>[^"]+)"/,
|
||||
},
|
||||
{
|
||||
name: 'tab',
|
||||
regex: /tab/,
|
||||
},
|
||||
];
|
||||
|
||||
export const rehypeCodeDefaultOptions: RehypeCodeOptions = {
|
||||
@@ -192,10 +188,7 @@ function transformerTab(): ShikiTransformer {
|
||||
data: {
|
||||
_codeblock: true,
|
||||
},
|
||||
attributes:
|
||||
value !== ''
|
||||
? [{ type: 'mdxJsxAttribute', name: 'value', value }]
|
||||
: [],
|
||||
attributes: [{ type: 'mdxJsxAttribute', name: 'value', value }],
|
||||
children: root.children,
|
||||
} as MdxJsxFlowElement,
|
||||
],
|
||||
|
||||
@@ -67,39 +67,38 @@ function toTab(nodes: Code[]) {
|
||||
export function remarkCodeTab(): Transformer<Root, Root> {
|
||||
return (tree) => {
|
||||
visit(tree, (node) => {
|
||||
if (!('children' in node)) return;
|
||||
if (node.type === 'mdxJsxFlowElement' && node.name === 'Tabs') return;
|
||||
if ('children' in node) {
|
||||
let start = -1;
|
||||
let i = 0;
|
||||
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);
|
||||
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++;
|
||||
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++;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ export function remarkHeading({
|
||||
}
|
||||
}
|
||||
|
||||
let flattened: string | undefined;
|
||||
let flattened: string | null = null;
|
||||
if (!id) {
|
||||
flattened ??= flattenNode(heading);
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { Transformer } from 'unified';
|
||||
import type { BlockContent, Heading, Root, RootContent } from 'mdast';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
|
||||
|
||||
export interface RemarkStepsOptions {
|
||||
/**
|
||||
* Class name for steps container
|
||||
*
|
||||
* @defaultValue fd-steps
|
||||
*/
|
||||
steps?: string;
|
||||
|
||||
/**
|
||||
* Class name for step container
|
||||
*
|
||||
* @defaultValue fd-step
|
||||
*/
|
||||
step?: string;
|
||||
}
|
||||
|
||||
const StepRegex = /^(\d+)\.\s(.+)$/;
|
||||
|
||||
/**
|
||||
* Convert headings in the format of `1. Hello World` into steps.
|
||||
*/
|
||||
export function remarkSteps({
|
||||
steps = 'fd-steps',
|
||||
step = 'fd-step',
|
||||
}: RemarkStepsOptions = {}): Transformer<Root, Root> {
|
||||
function convertToSteps(nodes: RootContent[]): MdxJsxFlowElement {
|
||||
const depth = (nodes[0] as Heading).depth;
|
||||
const children: MdxJsxFlowElement[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.type === 'heading' && node.depth === depth) {
|
||||
children.push({
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'div',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'className',
|
||||
value: step,
|
||||
},
|
||||
],
|
||||
children: [node],
|
||||
});
|
||||
} else {
|
||||
children[children.length - 1].children.push(node as BlockContent);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'div',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'className',
|
||||
value: steps,
|
||||
},
|
||||
],
|
||||
data: {
|
||||
_fd_step: true,
|
||||
} as object,
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
return (tree) => {
|
||||
visit(tree, (parent) => {
|
||||
if (!('children' in parent) || parent.type === 'heading') return;
|
||||
if (parent.data && '_fd_step' in parent.data) return 'skip';
|
||||
|
||||
let startIdx = -1;
|
||||
let lastNumber = 0;
|
||||
let i = 0;
|
||||
|
||||
const onEnd = () => {
|
||||
if (startIdx === -1) return;
|
||||
// range: start index to i - 1
|
||||
const item = {};
|
||||
const nodes = parent.children.splice(
|
||||
startIdx,
|
||||
i - startIdx,
|
||||
item as RootContent,
|
||||
);
|
||||
Object.assign(item, convertToSteps(nodes));
|
||||
i = startIdx + 1;
|
||||
startIdx = -1;
|
||||
};
|
||||
|
||||
for (; i < parent.children.length; i++) {
|
||||
const node = parent.children[i];
|
||||
|
||||
if (node.type !== 'heading') continue;
|
||||
if (startIdx !== -1) {
|
||||
const startDepth = (parent.children[startIdx] as Heading).depth;
|
||||
|
||||
if (node.depth > startDepth) continue;
|
||||
else if (node.depth < startDepth) onEnd();
|
||||
}
|
||||
|
||||
const head = node.children.filter((c) => c.type === 'text').at(0);
|
||||
if (!head) {
|
||||
onEnd();
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = StepRegex.exec(head.value);
|
||||
if (!match) {
|
||||
onEnd();
|
||||
continue;
|
||||
}
|
||||
|
||||
const num = Number(match[1]);
|
||||
head.value = match[2];
|
||||
|
||||
if (startIdx !== -1 && num !== lastNumber + 1) onEnd();
|
||||
if (startIdx === -1) startIdx = i;
|
||||
lastNumber = num;
|
||||
}
|
||||
|
||||
onEnd();
|
||||
});
|
||||
};
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Hello
|
||||
|
||||
## 1. First
|
||||
|
||||
content
|
||||
|
||||
### Little Tip
|
||||
|
||||
content
|
||||
|
||||
## 2. Second
|
||||
|
||||
content
|
||||
|
||||
## 1. New: First
|
||||
|
||||
content
|
||||
|
||||
## 2. New: Second
|
||||
|
||||
content
|
||||
|
||||
## Ended
|
||||
|
||||
content
|
||||
|
||||
# 1. Big: First
|
||||
|
||||
content
|
||||
|
||||
# 2. Big: Second
|
||||
|
||||
content
|
||||
@@ -0,0 +1,51 @@
|
||||
# Hello
|
||||
|
||||
<div className="fd-steps">
|
||||
<div className="fd-step">
|
||||
## First
|
||||
|
||||
content
|
||||
|
||||
### Little Tip
|
||||
|
||||
content
|
||||
</div>
|
||||
|
||||
<div className="fd-step">
|
||||
## Second
|
||||
|
||||
content
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="fd-steps">
|
||||
<div className="fd-step">
|
||||
## New: First
|
||||
|
||||
content
|
||||
</div>
|
||||
|
||||
<div className="fd-step">
|
||||
## New: Second
|
||||
|
||||
content
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Ended
|
||||
|
||||
content
|
||||
|
||||
<div className="fd-steps">
|
||||
<div className="fd-step">
|
||||
# Big: First
|
||||
|
||||
content
|
||||
</div>
|
||||
|
||||
<div className="fd-step">
|
||||
# Big: Second
|
||||
|
||||
content
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,6 +13,8 @@ import { fileURLToPath } from 'node:url';
|
||||
import remarkMdx from 'remark-mdx';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { createProcessor } from '@mdx-js/mdx';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { remarkSteps } from '@/mdx-plugins/remark-steps';
|
||||
|
||||
const cwd = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -68,6 +70,18 @@ test('Remark Image', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Steps', async () => {
|
||||
const content = await fs.readFile(
|
||||
path.resolve(cwd, './fixtures/remark-steps.md'),
|
||||
);
|
||||
const processor = remark().use(remarkSteps).use(remarkMdx);
|
||||
const result = await processor.process(content);
|
||||
|
||||
await expect(result.value).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-steps.output.md'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Image: With Path', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/remark-image.md');
|
||||
const content = readFileSync(file);
|
||||
|
||||
Reference in New Issue
Block a user