Docgen: Support custom defaultValue for remark-ts2js

This commit is contained in:
Fuma Nama
2025-09-23 19:42:01 +08:00
parent a6109b5eff
commit 655bb46b67
8 changed files with 43 additions and 11 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': patch
---
[Internal] `parseCodeBlockAttributes` include null values, restrict `rehype-code` to only parse `title` and `tab` attributes.
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-docgen': patch
---
Support custom `defaultValue` for `remark-ts2js`
@@ -91,7 +91,7 @@ export function generateCodeBlockTabs({
}
export interface CodeBlockAttributes<Name extends string = string> {
attributes: Partial<Record<Name, string>>;
attributes: Partial<Record<Name, string | null>>;
rest: string;
}
@@ -103,13 +103,13 @@ export function parseCodeBlockAttributes<Name extends string = string>(
allowedNames?: Name[],
): CodeBlockAttributes<Name> {
let str = meta;
const StringRegex = /(?<=^|\s)(?<name>\w+)=(?:"([^"]*)"|'([^']*)')/g;
const attributes: Partial<Record<Name, string>> = {};
const StringRegex = /(?<=^|\s)(?<name>\w+)(?:=(?:"([^"]*)"|'([^']*)'))?/g;
const attributes: CodeBlockAttributes['attributes'] = {};
str = str.replaceAll(StringRegex, (match, name, value_1, value_2) => {
if (allowedNames && !allowedNames.includes(name)) return match;
attributes[name as Name] = value_1 ?? value_2;
attributes[name] = value_1 ?? value_2 ?? null;
return '';
});
+1 -1
View File
@@ -40,7 +40,7 @@ export const rehypeCodeDefaultOptions: RehypeCodeOptions = {
}),
],
parseMetaString(meta) {
const parsed = parseCodeBlockAttributes(meta);
const parsed = parseCodeBlockAttributes(meta, ['title', 'tab']);
const data: Record<string, unknown> = parsed.attributes;
parsed.rest = parseLineNumber(parsed.rest, data);
+19
View File
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
import path from 'node:path';
import { remark } from 'remark';
import {
parseCodeBlockAttributes,
rehypeToc,
remarkAdmonition,
remarkHeading,
@@ -160,3 +161,21 @@ test('Rehype Toc', async () => {
path.resolve(cwd, './fixtures/rehype-toc.output.js'),
);
});
test('parse meta strings', () => {
expect(
parseCodeBlockAttributes(
`title="hello 'world'" tab='hello "world"' twoslash funny:invalid="" 'invalid'name="test"`,
),
).toMatchInlineSnapshot(`
{
"attributes": {
"funny": null,
"tab": "hello "world"",
"title": "hello 'world'",
"twoslash": null,
},
"rest": " :invalid="" 'invalid'name="test"",
}
`);
});
+7 -4
View File
@@ -18,6 +18,8 @@ export interface TypeScriptToJavaScriptOptions {
}
| false;
defaultValue?: 'js' | 'ts';
/**
* Transform all TypeScript codeblocks by default, without a trigger
*/
@@ -40,6 +42,7 @@ export interface TypeScriptToJavaScriptOptions {
*/
export function remarkTypeScriptToJavaScript({
persist = false,
defaultValue = 'ts',
disableTrigger = false,
}: TypeScriptToJavaScriptOptions = {}): Transformer<Root> {
return async (tree, file) => {
@@ -48,7 +51,9 @@ export function remarkTypeScriptToJavaScript({
visit(tree, 'code', (node) => {
const lang = node.lang;
if (lang !== 'ts' && lang !== 'tsx') return;
if (!disableTrigger && !node.meta?.includes('ts2js')) return;
const meta = parseCodeBlockAttributes(node.meta ?? '', ['ts2js']);
if (!disableTrigger && !('ts2js' in meta.attributes)) return;
const result = oxc.transform(
`${file.path ?? 'test'}.${lang}`,
@@ -60,11 +65,9 @@ export function remarkTypeScriptToJavaScript({
);
const targetLang = lang === 'tsx' ? 'jsx' : 'js';
const meta = parseCodeBlockAttributes(node.meta ?? '', ['ts2js']);
const replacement = generateCodeBlockTabs({
persist,
defaultValue: 'js',
defaultValue,
triggers: [
{
value: 'ts',
+1 -1
View File
@@ -10,7 +10,7 @@ function _createMdxContent(props) {
if (!CodeBlockTabs) _missingMdxReference("CodeBlockTabs", true);
if (!CodeBlockTabsList) _missingMdxReference("CodeBlockTabsList", true);
if (!CodeBlockTabsTrigger) _missingMdxReference("CodeBlockTabsTrigger", true);
return <CodeBlockTabs defaultValue="js"><CodeBlockTabsList><CodeBlockTabsTrigger value="ts">{"TypeScript"}</CodeBlockTabsTrigger><CodeBlockTabsTrigger value="js">{"JavaScript"}</CodeBlockTabsTrigger></CodeBlockTabsList><CodeBlockTab value="ts"><_components.pre><_components.code className="language-tsx">{"import { ReactNode } from 'react';\n\nexport default function Layout({ children }: { children: ReactNode }) {\n const v: string = 'hello world' as any;\n\n return (\n <div>\n {children} {v}\n </div>\n );\n}\n"}</_components.code></_components.pre></CodeBlockTab><CodeBlockTab value="js"><_components.pre><_components.code className="language-jsx">{"export default function Layout({ children }) {\n\tconst v = \"hello world\";\n\treturn <div>\n {children} {v}\n </div>;\n}\n\n"}</_components.code></_components.pre></CodeBlockTab></CodeBlockTabs>;
return <CodeBlockTabs defaultValue="ts"><CodeBlockTabsList><CodeBlockTabsTrigger value="ts">{"TypeScript"}</CodeBlockTabsTrigger><CodeBlockTabsTrigger value="js">{"JavaScript"}</CodeBlockTabsTrigger></CodeBlockTabsList><CodeBlockTab value="ts"><_components.pre><_components.code className="language-tsx">{"import { ReactNode } from 'react';\n\nexport default function Layout({ children }: { children: ReactNode }) {\n const v: string = 'hello world' as any;\n\n return (\n <div>\n {children} {v}\n </div>\n );\n}\n"}</_components.code></_components.pre></CodeBlockTab><CodeBlockTab value="js"><_components.pre><_components.code className="language-jsx">{"export default function Layout({ children }) {\n\tconst v = \"hello world\";\n\treturn <div>\n {children} {v}\n </div>;\n}\n\n"}</_components.code></_components.pre></CodeBlockTab></CodeBlockTabs>;
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
+1 -1
View File
@@ -60,7 +60,7 @@ export function remarkPostprocess(
if (includeProcessedMarkdown) {
file.data._markdown = toMarkdown(tree, {
...this.data('settings'),
// @ts-expect-error - from https://github.com/remarkjs/remark/blob/main/packages/remark-stringify/lib/index.js
// from https://github.com/remarkjs/remark/blob/main/packages/remark-stringify/lib/index.js
extensions: this.data('toMarkdownExtensions') || [],
});
}