Core: hotfix filter MDX elements

This commit is contained in:
Fuma Nama
2026-02-10 22:28:26 +08:00
parent 909974c57f
commit 3e8efb05b1
5 changed files with 73 additions and 42 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': patch
---
[remark-structure] hotfix filter MDX elements
@@ -31,11 +31,25 @@ export interface StructuredData {
contents: Content[];
}
export interface StructureOptions {
interface StringifyOptions {
/**
* MDAST **block** types to be scanned as content.
* Determine whether the element itself should be stringified in content block, only stringify its attributes & children if `false`.
*
* If a node's type is represented in this array, it will be stringified and its children will not be scanned separatedly.
* Always return `false` by default.
*/
filterMdxElements?: (node: MdxJsxFlowElement | MdxJsxTextElement) => boolean;
filterMdxAttributes?: (
node: MdxJsxFlowElement | MdxJsxTextElement,
attribute: MdxJsxAttribute | MdxJsxExpressionAttribute,
) => boolean;
}
export interface StructureOptions extends Omit<StringifyOptions, 'filterMdxAttributes'> {
/**
* MDAST node types to be scanned as a content block.
*
* If a node's type represents in this array, it will be converted into a single content block.
*
* @defaultValue ['heading', 'paragraph', 'blockquote', 'tableCell', 'mdxJsxFlowElement']
*/
@@ -85,15 +99,6 @@ declare module 'vfile' {
export const remarkStructureDefaultOptions = {
types: ['heading', 'paragraph', 'blockquote', 'tableCell', 'mdxJsxFlowElement'],
allowedMdxAttributes(node) {
switch (node.name) {
case 'TypeTable':
case 'Callout':
return true;
default:
return false;
}
},
exportAs: false,
} satisfies StructureOptions;
@@ -107,8 +112,9 @@ export function remarkStructure(
{
types = remarkStructureDefaultOptions.types,
stringify,
allowedMdxAttributes = remarkStructureDefaultOptions.allowedMdxAttributes,
allowedMdxAttributes,
exportAs = remarkStructureDefaultOptions.exportAs,
...stringifyOptions
}: StructureOptions = {},
): Transformer<Root, Root> {
if (Array.isArray(allowedMdxAttributes)) {
@@ -122,7 +128,10 @@ export function remarkStructure(
types = (node) => arr.includes(node.type);
}
stringify ??= defaultStringify({ filterMdxAttributes: allowedMdxAttributes });
stringify ??= defaultStringify({
filterMdxAttributes: allowedMdxAttributes,
...stringifyOptions,
});
return (tree, file) => {
const data: StructuredData = { contents: [], headings: [] };
let lastHeading: string | undefined;
@@ -164,17 +173,6 @@ export function remarkStructure(
return 'skip';
}
if (element.data?._string) {
for (const content of element.data._string) {
data.contents.push({
heading: lastHeading,
content,
});
}
return 'skip';
}
const content = stringify.call(this, element).trim();
if (content.length > 0) {
data.contents.push({
@@ -219,14 +217,20 @@ interface CustomRootNode {
}
export function defaultStringify(
config: Options & {
filterMdxAttributes?: (
node: MdxJsxFlowElement | MdxJsxTextElement,
attribute: MdxJsxAttribute | MdxJsxExpressionAttribute,
) => boolean;
} = {},
config: Options & StringifyOptions = {},
): (this: Processor, node: Nodes) => string {
const { filterMdxAttributes } = config;
const {
filterMdxAttributes = (node) => {
switch (node.name) {
case 'TypeTable':
case 'Callout':
return true;
default:
return false;
}
},
filterMdxElements = () => false,
} = config;
function modHandler(handler: Handle): Handle {
return function (node: Nodes, ...rest) {
@@ -234,14 +238,30 @@ export function defaultStringify(
switch (node.type) {
case 'mdxJsxFlowElement':
case 'mdxJsxTextElement':
if (filterMdxAttributes) {
const temp = node.attributes;
node.attributes = node.attributes.filter((attr) => filterMdxAttributes(node, attr));
const s = handler(node, ...rest);
node.attributes = temp;
return s;
case 'mdxJsxTextElement': {
const filteredAttributes = node.attributes.filter((attr) =>
filterMdxAttributes(node, attr),
);
if (!filterMdxElements(node)) {
let attrStr = '';
for (const attr of filteredAttributes) {
const str = typeof attr.value === 'string' ? attr.value : attr.value?.value;
if (!str) continue;
attrStr += attr.type === 'mdxJsxAttribute' ? `(${attr.name}=${str}) ` : `(${str}) `;
}
if (node.children.length === 0) return attrStr.trimEnd();
return attrStr + rest[1].handle({ type: 'root', children: node.children }, ...rest);
}
const temp = node.attributes;
node.attributes = filteredAttributes;
const s = handler(node, ...rest);
node.attributes = temp;
return s;
}
default:
return handler(node, ...rest);
}
+2
View File
@@ -25,3 +25,5 @@ function main() {
wow
</Card>
<TypeTable type={{ haha: test }} />
+6 -2
View File
@@ -26,11 +26,15 @@
},
{
"heading": "heading-3",
"content": "<Card title=\"Test\">wow</Card>"
"content": "wow"
},
{
"heading": "heading-3",
"content": "<Card title=\"Test\">\n wow\n</Card>"
"content": "wow"
},
{
"heading": "heading-3",
"content": "(type={ haha: test })"
}
],
"headings": [
+1 -1
View File
@@ -51,7 +51,7 @@ test('Remark Structure', async () => {
.use(remarkGfm)
.use(remarkMdx)
.use(remarkHeading)
.use(remarkStructure, { allowedMdxAttributes: ['title'] })
.use(remarkStructure)
.process(content);
await expect(JSON.stringify(result.data.structuredData, null, 2)).toMatchFileSnapshot(