Handle advisory html images in markdown editor

This commit is contained in:
Dotta committed 2026-04-15 07:23:00 -05:00
1 parent f8132bfd26
commit 8ed2a939df
2 files changed
+21 -2

No files matched your search

+18 -2
View File
@@ -19,6 +19,7 @@ const mdxEditorMockState = vi.hoisted(() => ({
emitMountParseError: false,
emitMountSilentEmptyState: false,
markdownValues: [] as string[],
suppressHtmlProcessingValues: [] as boolean[],
}));
vi.mock("@mdxeditor/editor", async () => {
@@ -41,16 +42,20 @@ vi.mock("@mdxeditor/editor", async () => {
onChange,
onError,
className,
suppressHtmlProcessing,
}: {
markdown: string;
placeholder?: string;
onChange?: (value: string) => void;
onError?: (error: unknown) => void;
className?: string;
suppressHtmlProcessing?: boolean;
className?: string;
},
forwardedRef: React.ForwardedRef<{ setMarkdown: (value: string) => void; focus: () => void } | null>,
) {
mdxEditorMockState.markdownValues.push(markdown);
mdxEditorMockState.suppressHtmlProcessingValues.push(Boolean(suppressHtmlProcessing));
const [content, setContent] = React.useState(markdown);
const editableRef = React.useRef<HTMLDivElement>(null);
const handle = React.useMemo(() => ({
@@ -59,8 +64,16 @@ vi.mock("@mdxeditor/editor", async () => {
}), []);
React.useEffect(() => {
if (!suppressHtmlProcessing && markdown.includes("<img ")) {
setContent("");
onError?.({
message: "Error parsing markdown: HTML-like formatting requires suppressHtmlProcessing",
source: markdown,
});
return;
}
setContent(markdown);
}, [markdown]);
}, [markdown, onError, suppressHtmlProcessing]);
React.useEffect(() => {
setForwardedRef(forwardedRef, null);
@@ -114,6 +127,8 @@ vi.mock("@mdxeditor/editor", async () => {
markdownShortcutPlugin: () => ({}),
quotePlugin: () => ({}),
realmPlugin: (plugin: unknown) => plugin,
jsxPlugin: () => ({}),
GenericJsxEditor: () => null,
tablePlugin: () => ({}),
thematicBreakPlugin: () => ({}),
};
@@ -165,6 +180,7 @@ describe("MarkdownEditor", () => {
mdxEditorMockState.emitMountParseError = false;
mdxEditorMockState.emitMountSilentEmptyState = false;
mdxEditorMockState.markdownValues = [];
mdxEditorMockState.suppressHtmlProcessingValues = [];
});
it("applies async external value updates once the editor ref becomes ready", async () => {
@@ -238,6 +254,7 @@ describe("MarkdownEditor", () => {
await flush();
expect(mdxEditorMockState.markdownValues.at(-1)).toContain("![image](https://example.com/test.png)");
expect(mdxEditorMockState.markdownValues.at(-1)).not.toContain("<img");
expect(mdxEditorMockState.suppressHtmlProcessingValues).toContain(true);
expect(container.textContent).toContain("Before");
expect(container.textContent).toContain("After");
@@ -309,7 +326,6 @@ describe("MarkdownEditor", () => {
root.unmount();
});
});
it("anchors the mention menu inside the visual viewport when mobile offsets are present", () => {
expect(
computeMentionMenuPosition(
+3
View File
@@ -505,6 +505,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
const [isDragOver, setIsDragOver] = useState(false);
const [richEditorError, setRichEditorError] = useState<string | null>(null);
const dragDepthRef = useRef(0);
const suppressHtmlProcessing = /<img\s/i.test(value);
// Stable ref for imageUploadHandler so plugins don't recreate on every render
const imageUploadHandlerRef = useRef(imageUploadHandler);
@@ -1055,9 +1056,11 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
onPasteCapture={handlePasteCapture}
>
<MDXEditor
key={suppressHtmlProcessing ? "mdxeditor-no-html" : "mdxeditor-default"}
ref={setEditorRef}
markdown={editorValue}
placeholder={placeholder}
suppressHtmlProcessing={suppressHtmlProcessing}
onChange={(next) => {
const echo = echoIgnoreMarkdownRef.current;
if (echo !== null && next === echo) {