Skip to content

Commit

Permalink
TypeScript: Support overriding renderMarkdown function
Browse files Browse the repository at this point in the history
  • Loading branch information
fuma-nama committed Feb 27, 2025
1 parent 9e57f2a commit 5d0dd11
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 77 deletions.
5 changes: 5 additions & 0 deletions .changeset/every-parks-stand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'fumadocs-typescript': patch
---

Support overriding `renderMarkdown` function
6 changes: 3 additions & 3 deletions packages/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@
"dependencies": {
"fast-glob": "^3.3.3",
"hast-util-to-jsx-runtime": "^2.3.5",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-gfm": "^3.1.0",
"mdast-util-to-hast": "^13.2.0",
"remark": "^15.0.1",
"remark-rehype": "^11.1.1",
"shiki": "^3.1.0",
"ts-morph": "^25.0.1"
},
Expand All @@ -51,6 +50,7 @@
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"eslint-config-custom": "workspace:*",
"fumadocs-core": "workspace:*",
"fumadocs-ui": "workspace:*",
"tsconfig": "workspace:*",
"typescript": "^5.7.3"
Expand Down
84 changes: 28 additions & 56 deletions packages/typescript/src/markdown.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,36 @@
import { fromMarkdown } from 'mdast-util-from-markdown';
import { gfmFromMarkdown } from 'mdast-util-gfm';
import { toHast } from 'mdast-util-to-hast';
import { getSingletonHighlighter, type LanguageRegistration } from 'shiki';
import type { Nodes } from 'hast';
import { type Code, type RootContent } from 'mdast';
import { remark } from 'remark';
import {
remarkGfm,
rehypeCode,
type RehypeCodeOptions,
} from 'fumadocs-core/mdx-plugins';
import remarkRehype from 'remark-rehype';

export async function renderMarkdownToHast(md: string): Promise<Nodes> {
const mdast = fromMarkdown(
md.replace(/{@link (?<link>[^}]*)}/g, '$1'), // replace jsdoc links
{ mdastExtensions: [gfmFromMarkdown()] },
);

const highlighter = await getSingletonHighlighter({
themes: ['vitesse-light', 'vitesse-dark'],
});
const processor = remark()
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeCode, {
lazy: true,

async function preload(contents: RootContent[]): Promise<void> {
await Promise.all(
contents.map(async (c) => {
if ('children' in c) await preload(c.children);
themes: {
light: 'github-light',
dark: 'github-dark',
},
} satisfies RehypeCodeOptions)
// @ts-expect-error -- safe
.use(() => {
return (tree, file: { data: Record<string, unknown> }) => {
file.data.tree = tree;

if (c.type === 'code' && c.lang) {
await highlighter.loadLanguage(
c.lang as unknown as LanguageRegistration,
);
}
}),
);
}
return '';
};
});

await preload(mdast.children);
export async function renderMarkdownToHast(md: string): Promise<Nodes> {
md = md.replace(/{@link (?<link>[^}]*)}/g, '$1'); // replace jsdoc links

return toHast(mdast, {
handlers: {
// @ts-expect-error hast with mdx
code(_, node: Code) {
const lang = node.lang ?? 'plaintext';
const out = await processor.process(md);

return highlighter.codeToHast(node.value, {
lang,
themes: {
light: 'vitesse-light',
dark: 'vitesse-dark',
},
defaultColor: false,
transformers: [
{
name: 'rehype-code:pre-process',
line(hast) {
if (hast.children.length > 0) return;
// Keep the empty lines when using grid layout
hast.children.push({
type: 'text',
value: ' ',
});
},
},
],
}).children;
},
},
});
return out.data.tree as Nodes;
}
11 changes: 9 additions & 2 deletions packages/typescript/src/ui/auto-type-table.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// <reference types="react/experimental" />
import fs from 'node:fs/promises';
import * as fs from 'node:fs/promises';
import { TypeTable } from 'fumadocs-ui/components/type-table';
import { type Jsx, toJsxRuntime } from 'hast-util-to-jsx-runtime';
import * as runtime from 'react/jsx-runtime';
Expand All @@ -11,6 +11,7 @@ import {
} from '@/generate/base';
import 'server-only';
import { getProject } from '@/get-project';
import type { ReactNode } from 'react';

export interface AutoTypeTableProps {
/**
Expand Down Expand Up @@ -46,6 +47,11 @@ export interface AutoTypeTableProps {
*/
type?: string;

/**
* Override the function to render markdown into JSX nodes
*/
renderMarkdown?: typeof renderMarkdownDefault;

options?: GenerateDocumentationOptions;
}

Expand All @@ -72,6 +78,7 @@ export async function AutoTypeTable({
path,
name,
type,
renderMarkdown = renderMarkdownDefault,
options = {},
}: AutoTypeTableProps): Promise<React.ReactElement> {
let typeName = name;
Expand Down Expand Up @@ -125,7 +132,7 @@ export async function AutoTypeTable({
);
}

async function renderMarkdown(md: string): Promise<React.ReactElement> {
async function renderMarkdownDefault(md: string): Promise<ReactNode> {
return toJsxRuntime(await renderMarkdownToHast(md), {
Fragment: runtime.Fragment,
jsx: runtime.jsx as Jsx,
Expand Down
2 changes: 1 addition & 1 deletion packages/typescript/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from 'tsup';

export default defineConfig({
external: ['server-only', 'fumadocs-ui', 'react'],
external: ['server-only', 'fumadocs-ui', 'fumadocs-core', 'react'],
dts: true,
target: 'es6',
format: 'esm',
Expand Down
25 changes: 10 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 5d0dd11

Please sign in to comment.