Skip to content

Commit 80db87d

Browse files
authored
fix(editor): better scope unsupported content rendering (#4794)
1 parent 5d2ad19 commit 80db87d

File tree

3 files changed

+74
-87
lines changed

3 files changed

+74
-87
lines changed

packages/fern-dashboard/src/components/editor/editor-mdx-renderer/FernEditorMDXRenderer.tsx

Lines changed: 6 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
type MdastNodes,
66
type MdxJsxAttribute,
77
type MdxJsxExpressionAttribute,
8-
mdxToAST,
98
mdxToHtml
109
} from "@fern-docs/mdx";
1110
import { useMDXComponents } from "@mdx-js/react";
@@ -17,70 +16,13 @@ import { EditorComponentProvider } from "@/components/editor/editor-component/Ed
1716
import { CustomElementHoverWrapper } from "@/components/editor/NodeHoverWrapper";
1817
import TiptapEditor from "@/components/editor/TiptapEditor";
1918
import { ErrorBoundary } from "@/docs/components/error-boundary";
20-
import { MDX_COMPONENTS } from "@/docs/mdx/components";
2119
import { useDebounce } from "@/hooks/useDebounce";
2220
import type { EncodedDocsUrl } from "@/utils/types";
2321
import { UnsupportedContentDisplayOnly } from "../UnsupportedContent";
2422
import { cachedBundleMDX } from "./cache";
2523
import { boundaryElements, parseMDX } from "./parse";
2624
import type { AttributeValue, JSXElement, ParsedMarkdownElement } from "./types";
2725

28-
/**
29-
* Check if MDX contains unsupported custom components
30-
* Returns the name of the first unsupported component found, or null if all are supported
31-
*/
32-
function findUnsupportedComponent(mdx: string): string | null {
33-
const IGNORED_COMPONENTS = new Set(["InterceptedChildren"]);
34-
35-
try {
36-
const { mdast } = mdxToAST(mdx);
37-
38-
function traverse(node: any): string | null {
39-
if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
40-
const componentName = node.name;
41-
42-
if (!componentName) {
43-
return null;
44-
}
45-
46-
if (IGNORED_COMPONENTS.has(componentName)) {
47-
return null;
48-
}
49-
50-
if (componentName === componentName.toLowerCase()) {
51-
return null;
52-
}
53-
54-
if (!Object.hasOwn(MDX_COMPONENTS, componentName)) {
55-
return componentName;
56-
}
57-
}
58-
59-
if (node.children && Array.isArray(node.children)) {
60-
for (const child of node.children) {
61-
const unsupported = traverse(child);
62-
if (unsupported) {
63-
return unsupported;
64-
}
65-
}
66-
}
67-
68-
return null;
69-
}
70-
71-
for (const child of mdast.children) {
72-
const unsupported = traverse(child);
73-
if (unsupported) {
74-
return unsupported;
75-
}
76-
}
77-
78-
return null;
79-
} catch (error) {
80-
return null;
81-
}
82-
}
83-
8426
function buildMdxElement(
8527
name: string,
8628
keyedAttributes: Record<string, AttributeValue>,
@@ -221,19 +163,8 @@ interface MDXRendererProps {
221163
branch?: string;
222164
}
223165

224-
const CustomErrorFallback = ({ mdx, unsupportedComponent }: { mdx: string; unsupportedComponent?: string | null }) => {
225-
const computedUnsupportedComponent = useMemo(() => {
226-
if (unsupportedComponent != null) {
227-
return unsupportedComponent;
228-
}
229-
return findUnsupportedComponent(mdx);
230-
}, [unsupportedComponent, mdx]);
231-
232-
const displayMessage = computedUnsupportedComponent
233-
? `Unsupported markdown tag: ${computedUnsupportedComponent}`
234-
: !mdx.includes("<InterceptedChildren />")
235-
? mdx
236-
: "Unsupported markdown";
166+
const CustomErrorFallback = ({ mdx }: { mdx: string }) => {
167+
const displayMessage = !mdx.includes("<InterceptedChildren />") ? mdx : "Unsupported markdown";
237168

238169
return <UnsupportedContentDisplayOnly>{displayMessage}</UnsupportedContentDisplayOnly>;
239170
};
@@ -273,14 +204,7 @@ const MDXRenderer = React.memo(({ mdx, docsUrl, branch }: MDXRendererProps) => {
273204
const components = useMDXComponents();
274205
const isBoundary = useMemo(() => isBoundaryElement(mdx), [mdx]);
275206

276-
const unsupportedComponent = useMemo(() => findUnsupportedComponent(mdx), [mdx]);
277-
278207
useEffect(() => {
279-
if (unsupportedComponent) {
280-
setState({ type: "ERROR", message: `Unsupported component: ${unsupportedComponent}` });
281-
return;
282-
}
283-
284208
let cancelled = false;
285209

286210
void (async () => {
@@ -300,7 +224,7 @@ const MDXRenderer = React.memo(({ mdx, docsUrl, branch }: MDXRendererProps) => {
300224
return () => {
301225
cancelled = true;
302226
};
303-
}, [mdx, docsUrl, branch, unsupportedComponent]);
227+
}, [mdx, docsUrl, branch]);
304228

305229
// Compute content based on state - all hooks must be called before this point
306230
const content = useMemo(() => {
@@ -309,21 +233,17 @@ const MDXRenderer = React.memo(({ mdx, docsUrl, branch }: MDXRendererProps) => {
309233
}
310234

311235
if (state.type === "ERROR") {
312-
const displayMessage = unsupportedComponent
313-
? `Unsupported markdown tag: ${unsupportedComponent}`
314-
: !mdx.includes("<InterceptedChildren />")
315-
? mdx
316-
: "Unsupported markdown";
236+
const displayMessage = !mdx.includes("<InterceptedChildren />") ? mdx : "Unsupported markdown";
317237

318238
return <UnsupportedContentDisplayOnly>{displayMessage}</UnsupportedContentDisplayOnly>;
319239
}
320240

321241
return (
322-
<ErrorBoundary fallback={<CustomErrorFallback mdx={mdx} unsupportedComponent={unsupportedComponent} />}>
242+
<ErrorBoundary fallback={<CustomErrorFallback mdx={mdx} />}>
323243
<TerminalMDXRenderer code={state.code} components={components} />
324244
</ErrorBoundary>
325245
);
326-
}, [state, mdx, components, unsupportedComponent]);
246+
}, [state, mdx, components]);
327247

328248
if (isBoundary) {
329249
// Only wrap with CustomElementHoverWrapper if it's a boundary element

packages/fern-dashboard/src/components/editor/editor-mdx-renderer/parse.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { astToMDX, type MdastNodes, type MdxJsxExpressionAttribute, mdxToAST } from "@fern-docs/mdx";
2+
import { MDX_COMPONENTS } from "@/docs/mdx/components";
23

34
import type { AttributeValue, JSXElement, JSXElementChildren, ParsedMarkdownElement } from "./types";
45

@@ -36,10 +37,70 @@ const contentDraggingDisabledComponents = ["Button"];
3637
// HTML elements that mark boundaries - these and all their children will be treated as terminal/non-editable
3738
const boundaryElements = ["div", "span", "section", "article", "body", "aside"];
3839

40+
const IGNORED_COMPONENTS = new Set(["InterceptedChildren"]);
41+
42+
/**
43+
* Check if a component is supported
44+
*/
45+
function isComponentSupported(componentName: string | null | undefined): boolean {
46+
if (!componentName) {
47+
return true;
48+
}
49+
50+
if (IGNORED_COMPONENTS.has(componentName)) {
51+
return true;
52+
}
53+
54+
// lowercase components are HTML elements, which are supported
55+
if (componentName === componentName.toLowerCase()) {
56+
return true;
57+
}
58+
59+
return Object.hasOwn(MDX_COMPONENTS, componentName);
60+
}
61+
62+
/**
63+
* Replace unsupported components in the AST with a placeholder
64+
*/
65+
function replaceUnsupportedComponents(node: MdastNodes): MdastNodes {
66+
// Check if this node is an unsupported component
67+
if ((node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && !isComponentSupported(node.name)) {
68+
// Replace with a placeholder that shows what component was unsupported
69+
return {
70+
type: "mdxJsxFlowElement",
71+
name: "UnsupportedContentPlaceholder",
72+
attributes: [
73+
{
74+
type: "mdxJsxAttribute",
75+
name: "componentName",
76+
value: node.name || "unknown"
77+
}
78+
],
79+
children: []
80+
};
81+
}
82+
83+
// Recursively process children
84+
if ("children" in node && node.children && Array.isArray(node.children)) {
85+
return {
86+
...node,
87+
children: node.children.map((child) => replaceUnsupportedComponents(child)) as any
88+
};
89+
}
90+
91+
return node;
92+
}
93+
3994
export function parseMDX(mdx: string): ParsedMarkdownElement[] {
4095
// Parse MDX to AST using mdxToAST
4196
const { mdast } = mdxToAST(mdx);
4297

98+
// Replace unsupported components before processing
99+
const processedMdast = {
100+
...mdast,
101+
children: mdast.children.map((child) => replaceUnsupportedComponents(child))
102+
};
103+
43104
const result: ParsedMarkdownElement[] = [];
44105

45106
// Function to traverse the AST and extract parent-child relationships
@@ -168,7 +229,7 @@ export function parseMDX(mdx: string): ParsedMarkdownElement[] {
168229
}
169230

170231
// Start traversing from the root's children
171-
const rootNode = mdast;
232+
const rootNode = processedMdast;
172233
for (const child of rootNode.children) {
173234
const element = traverse(child);
174235
// Handle case where traverse returns multiple elements (e.g., paragraph with only images)

packages/fern-dashboard/src/docs/mdx/components/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { MDXComponents } from "@fern-docs/mdx";
44
import dynamic from "next/dynamic";
55
import React, { type ComponentProps } from "react";
66

7+
import { UnsupportedContentDisplayOnly } from "@/components/editor/UnsupportedContent";
78
import { ErrorBoundary, ErrorBoundaryFallback } from "@/docs/components/error-boundary";
89
import { Embed } from "@/editor/components/Embed";
910

@@ -103,6 +104,11 @@ const FERN_COMPONENTS = {
103104
const INTERNAL_COMPONENTS = {
104105
ErrorBoundary,
105106
ElevenLabsWaveform,
107+
UnsupportedContentPlaceholder: ({ componentName }: { componentName?: string }) => (
108+
<UnsupportedContentDisplayOnly>
109+
{componentName ? `Unsupported markdown tag: ${componentName}` : "Unsupported markdown"}
110+
</UnsupportedContentDisplayOnly>
111+
),
106112

107113
/**
108114
* deprecated but kept for backwards compatibility

0 commit comments

Comments
 (0)