mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
Core & MDX: Support customising heading id in headings
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"fumadocs-core": patch
|
||||
"fumadocs-mdx": patch
|
||||
---
|
||||
|
||||
Support customising heading id in headings
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"fumadocs-core": patch
|
||||
"fumadocs-mdx": patch
|
||||
---
|
||||
|
||||
Support custom heading slugger
|
||||
@@ -21,6 +21,7 @@ dist
|
||||
/build
|
||||
|
||||
# misc
|
||||
.idea
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ export default function Layout({
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<Body>
|
||||
<DocsLayout {...layoutOptions}>{children}</DocsLayout>
|
||||
<DocsLayout {...layoutOptions}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
</Body>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export type AutoTypeTableProps = ComponentPropsWithoutRef<typeof AutoTypeTable>;
|
||||
|
||||
export interface AutoTypeTableExample {
|
||||
/**
|
||||
* markdown syntax like links, `code` are supported.
|
||||
* Markdown syntax like links, `code` are supported.
|
||||
*
|
||||
* See https://fumadocs.vercel.app/docs/ui/components/type-table
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Slugger from 'github-slugger';
|
||||
import type { Root } from 'mdast';
|
||||
import type { Heading, Root } from 'mdast';
|
||||
import type { Transformer } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { TOCItemType } from '@/server/get-toc';
|
||||
@@ -15,12 +15,18 @@ declare module 'mdast' {
|
||||
}
|
||||
}
|
||||
|
||||
export interface RemarkHeadingOptions {
|
||||
slug?: (root: Root, heading: Heading, text: string) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add heading ids and extract TOC
|
||||
*
|
||||
* Attach an array of `TOCItemType` to `vfile.data.toc`
|
||||
* Attach an array of `TOCItemType` to `file.data.toc`
|
||||
*/
|
||||
export function remarkHeading(): Transformer<Root, Root> {
|
||||
export function remarkHeading(
|
||||
options: RemarkHeadingOptions = {},
|
||||
): Transformer<Root, Root> {
|
||||
return (node, file) => {
|
||||
const toc: TOCItemType[] = [];
|
||||
slugger.reset();
|
||||
@@ -30,7 +36,9 @@ export function remarkHeading(): Transformer<Root, Root> {
|
||||
heading.data.hProperties ||= {};
|
||||
|
||||
const text = flattenNode(heading);
|
||||
const id = slugger.slug(heading.data.hProperties.id ?? text);
|
||||
const id =
|
||||
heading.data.hProperties.id ??
|
||||
(options.slug ? options.slug(node, heading, text) : defaultSlug(text));
|
||||
|
||||
heading.data.hProperties.id = id;
|
||||
|
||||
@@ -46,3 +54,15 @@ export function remarkHeading(): Transformer<Root, Root> {
|
||||
file.data.toc = toc;
|
||||
};
|
||||
}
|
||||
|
||||
function defaultSlug(text: string): string {
|
||||
// match {slug}, while escape `\{slug}`
|
||||
if (text.endsWith('}')) {
|
||||
const start = text.lastIndexOf('{');
|
||||
|
||||
if (start !== -1 && text[start - 1] !== '\\')
|
||||
return text.substring(start + 1, text.length - 1);
|
||||
}
|
||||
|
||||
return slugger.slug(text);
|
||||
}
|
||||
|
||||
+12
@@ -9,3 +9,15 @@ Some text here
|
||||
### Heading 3
|
||||
|
||||
Some text here
|
||||
|
||||
### Custom heading id {hello-world}
|
||||
|
||||
Some text here
|
||||
|
||||
### Custom heading id \\{hello-world}
|
||||
|
||||
Some text here
|
||||
|
||||
### Custom heading id hello-world}
|
||||
|
||||
Some text here
|
||||
|
||||
+32
-1
@@ -1 +1,32 @@
|
||||
[{"title":"Heading 1","url":"#heading-1","depth":1},{"title":"Heading 2","url":"#heading-2","depth":2},{"title":"Heading 3","url":"#heading-3","depth":3}]
|
||||
[
|
||||
{
|
||||
"depth": 1,
|
||||
"title": "Heading 1",
|
||||
"url": "#heading-1",
|
||||
},
|
||||
{
|
||||
"depth": 2,
|
||||
"title": "Heading 2",
|
||||
"url": "#heading-2",
|
||||
},
|
||||
{
|
||||
"depth": 3,
|
||||
"title": "Heading 3",
|
||||
"url": "#heading-3",
|
||||
},
|
||||
{
|
||||
"depth": 3,
|
||||
"title": "Custom heading id {hello-world}",
|
||||
"url": "#hello-world",
|
||||
},
|
||||
{
|
||||
"depth": 3,
|
||||
"title": "Custom heading id \{hello-world}",
|
||||
"url": "#custom-heading-id-hello-world",
|
||||
},
|
||||
{
|
||||
"depth": 3,
|
||||
"title": "Custom heading id hello-world}",
|
||||
"url": "#custom-heading-id-hello-world-1",
|
||||
},
|
||||
]
|
||||
+26
-1
@@ -1 +1,26 @@
|
||||
{"contents":[{"heading":"heading-1","content":"Some text here"},{"heading":"heading-2","content":"Some text here"}],"headings":[{"id":"heading-1","content":"Heading 1"},{"id":"heading-2","content":"Heading 2"},{"id":"heading-3","content":"Heading 3"}]}
|
||||
{
|
||||
"contents": [
|
||||
{
|
||||
"content": "Some text here",
|
||||
"heading": "heading-1",
|
||||
},
|
||||
{
|
||||
"content": "Some text here",
|
||||
"heading": "heading-2",
|
||||
},
|
||||
],
|
||||
"headings": [
|
||||
{
|
||||
"content": "Heading 1",
|
||||
"id": "heading-1",
|
||||
},
|
||||
{
|
||||
"content": "Heading 2",
|
||||
"id": "heading-2",
|
||||
},
|
||||
{
|
||||
"content": "Heading 3",
|
||||
"id": "heading-3",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -13,7 +13,7 @@ test('Remark Heading', async () => {
|
||||
|
||||
const result = await remark().use(remarkHeading).process(content);
|
||||
|
||||
expect(JSON.stringify(result.data.toc)).toMatchFileSnapshot(
|
||||
expect(result.data.toc).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-heading.output.json'),
|
||||
);
|
||||
});
|
||||
@@ -24,7 +24,7 @@ test('Remark Structure', async () => {
|
||||
);
|
||||
const result = await remark().use(remarkStructure).process(content);
|
||||
|
||||
expect(JSON.stringify(result.data.structuredData)).toMatchFileSnapshot(
|
||||
expect(result.data.structuredData).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-structure.output.json'),
|
||||
);
|
||||
});
|
||||
|
||||
+28
-27
@@ -8,8 +8,9 @@ import {
|
||||
type RehypeCodeOptions,
|
||||
remarkImage,
|
||||
type RemarkImageOptions,
|
||||
type RemarkHeadingOptions,
|
||||
} from 'fumadocs-core/mdx-plugins';
|
||||
import type { Pluggable, PluggableList } from 'unified';
|
||||
import type { Pluggable } from 'unified';
|
||||
import type { Configuration } from 'webpack';
|
||||
import { MapWebpackPlugin } from './webpack-plugins/map-plugin';
|
||||
import remarkMdxExport from './mdx-plugins/remark-exports';
|
||||
@@ -32,11 +33,12 @@ type MDXOptions = Omit<
|
||||
*/
|
||||
valueToExport?: string[];
|
||||
|
||||
remarkHeadingOptions?: RemarkHeadingOptions;
|
||||
remarkImageOptions?: RemarkImageOptions | false;
|
||||
rehypeCodeOptions?: RehypeCodeOptions | false;
|
||||
};
|
||||
|
||||
type ResolvePlugins = PluggableList | ((v: PluggableList) => PluggableList);
|
||||
type ResolvePlugins = Pluggable[] | ((v: Pluggable[]) => Pluggable[]);
|
||||
|
||||
export interface CreateMDXOptions {
|
||||
cwd?: string;
|
||||
@@ -68,10 +70,12 @@ export interface CreateMDXOptions {
|
||||
}
|
||||
|
||||
function pluginOption(
|
||||
def: (v: PluggableList) => PluggableList,
|
||||
def: (v: Pluggable[]) => (Pluggable | false)[],
|
||||
options: ResolvePlugins = [],
|
||||
): PluggableList {
|
||||
const list = def(Array.isArray(options) ? options : []);
|
||||
): Pluggable[] {
|
||||
const list = def(Array.isArray(options) ? options : []).filter(
|
||||
Boolean,
|
||||
) as Pluggable[];
|
||||
|
||||
if (typeof options === 'function') {
|
||||
return options(list);
|
||||
@@ -84,6 +88,7 @@ function getMDXLoaderOptions({
|
||||
valueToExport = [],
|
||||
rehypeCodeOptions,
|
||||
remarkImageOptions,
|
||||
remarkHeadingOptions,
|
||||
...mdxOptions
|
||||
}: MDXOptions): MDXLoaderOptions {
|
||||
const mdxExports = [
|
||||
@@ -94,29 +99,25 @@ function getMDXLoaderOptions({
|
||||
...valueToExport,
|
||||
];
|
||||
|
||||
const remarkPlugins = pluginOption((v) => {
|
||||
const plugins: Pluggable[] = [remarkGfm, remarkHeading];
|
||||
const remarkPlugins = pluginOption(
|
||||
(v) => [
|
||||
remarkGfm,
|
||||
[remarkHeading, remarkHeadingOptions],
|
||||
remarkImageOptions !== false && [remarkImage, remarkImageOptions],
|
||||
...v,
|
||||
remarkStructure,
|
||||
[remarkMdxExport, { values: mdxExports }],
|
||||
],
|
||||
mdxOptions.remarkPlugins,
|
||||
);
|
||||
|
||||
if (remarkImageOptions !== false)
|
||||
plugins.push([remarkImage, remarkImageOptions]);
|
||||
|
||||
plugins.push(...v, remarkStructure, [
|
||||
remarkMdxExport,
|
||||
{ values: mdxExports },
|
||||
]);
|
||||
|
||||
return plugins;
|
||||
}, mdxOptions.remarkPlugins);
|
||||
|
||||
const rehypePlugins: PluggableList = pluginOption((v) => {
|
||||
const plugins: Pluggable[] = [...v];
|
||||
|
||||
if (rehypeCodeOptions !== false) {
|
||||
plugins.unshift([rehypeCode, rehypeCodeOptions]);
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}, mdxOptions.rehypePlugins);
|
||||
const rehypePlugins = pluginOption(
|
||||
(v) => [
|
||||
rehypeCodeOptions !== false && [rehypeCode, rehypeCodeOptions],
|
||||
...v,
|
||||
],
|
||||
mdxOptions.rehypePlugins,
|
||||
);
|
||||
|
||||
return {
|
||||
providerImportSource: 'next-mdx-import-source-file',
|
||||
|
||||
Reference in New Issue
Block a user