Merge branch 'dev' into feat/e2e-testing

This commit is contained in:
Rohan Verma 2026-05-09 16:10:45 -07:00 committed by GitHub
commit fa31da9937
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
100 changed files with 3751 additions and 1122 deletions

View file

@ -11,6 +11,7 @@ import { EditorSaveContext } from "@/components/editor/editor-save-context";
import { CitationKit, injectCitationNodes } from "@/components/editor/plugins/citation-kit";
import { type EditorPreset, presetMap } from "@/components/editor/presets";
import { escapeMdxExpressions } from "@/components/editor/utils/escape-mdx";
import { safeDeserializeMarkdown } from "@/components/editor/utils/safe-deserialize";
import { Editor, EditorContainer } from "@/components/ui/editor";
import { preprocessCitationMarkdown } from "@/lib/citations/citation-parser";
@ -169,15 +170,17 @@ export function PlateEditor({
: markdown
? (editor) => {
if (!enableCitations) {
return editor
.getApi(MarkdownPlugin)
.markdown.deserialize(escapeMdxExpressions(markdown));
return safeDeserializeMarkdown(
editor,
escapeMdxExpressions(markdown)
) as Value;
}
const { content: rewritten, urlMap } = preprocessCitationMarkdown(markdown);
const value = editor
.getApi(MarkdownPlugin)
.markdown.deserialize(escapeMdxExpressions(rewritten));
return injectCitationNodes(value as Descendant[], urlMap) as Value;
const value = safeDeserializeMarkdown(
editor,
escapeMdxExpressions(rewritten)
);
return injectCitationNodes(value, urlMap) as Value;
}
: undefined,
});
@ -200,14 +203,13 @@ export function PlateEditor({
let newValue: Descendant[];
if (enableCitations) {
const { content: rewritten, urlMap } = preprocessCitationMarkdown(markdown);
const deserialized = editor
.getApi(MarkdownPlugin)
.markdown.deserialize(escapeMdxExpressions(rewritten)) as Descendant[];
const deserialized = safeDeserializeMarkdown(
editor,
escapeMdxExpressions(rewritten)
);
newValue = injectCitationNodes(deserialized, urlMap);
} else {
newValue = editor
.getApi(MarkdownPlugin)
.markdown.deserialize(escapeMdxExpressions(markdown)) as Descendant[];
newValue = safeDeserializeMarkdown(editor, escapeMdxExpressions(markdown));
}
editor.tf.reset();
editor.tf.setValue(newValue as Value);

View file

@ -0,0 +1,64 @@
// ---------------------------------------------------------------------------
// Safe markdown deserialization for the Plate editor
// ---------------------------------------------------------------------------
// `remark-mdx` treats any HTML-like tag as JSX, so unbalanced inline HTML
// (very common in GitHub READMEs, web-scraped pages, PDF conversions) makes
// it throw "Expected a closing tag for `<a>`" and crash the editor.
//
// Per the MDX maintainers' guidance (mdx-js/mdx, ipikuka/next-mdx-remote-client
// #14), MDX is the wrong format for untrusted markdown and the recommended
// fix is to fall back to plain markdown parsing. `MarkdownPlugin.deserialize`
// accepts a per-call `remarkPlugins` override, so we can:
//
// 1. Try with `remarkMdx` (rich MDX features, e.g. JSX-style components).
// 2. On failure, retry without `remarkMdx` (lenient HTML, like GitHub).
// 3. As a last resort, render the raw source in a paragraph so the user
// never sees a crashed editor.
// ---------------------------------------------------------------------------
import { MarkdownPlugin, remarkMdx } from "@platejs/markdown";
import type { Descendant } from "platejs";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import type { PlateEditorInstance } from "@/components/editor/plate-editor";
const STRICT_PLUGINS = [remarkGfm, remarkMath, remarkMdx];
const LENIENT_PLUGINS = [remarkGfm, remarkMath];
function plainTextFallback(markdown: string): Descendant[] {
return [
{
type: "p",
children: [{ text: markdown }],
} as unknown as Descendant,
];
}
/**
* Deserialize markdown into a Plate value, gracefully degrading when the
* MDX-strict parser rejects raw HTML. Always returns a renderable value;
* never throws.
*/
export function safeDeserializeMarkdown(
editor: PlateEditorInstance,
markdown: string
): Descendant[] {
const api = editor.getApi(MarkdownPlugin).markdown;
try {
return api.deserialize(markdown, { remarkPlugins: STRICT_PLUGINS }) as Descendant[];
} catch (mdxError) {
if (process.env.NODE_ENV !== "production") {
console.warn(
"[plate-editor] MDX parse failed, retrying without remark-mdx:",
mdxError
);
}
try {
return api.deserialize(markdown, { remarkPlugins: LENIENT_PLUGINS }) as Descendant[];
} catch (fallbackError) {
console.error("[plate-editor] markdown deserialize failed:", fallbackError);
return plainTextFallback(markdown);
}
}
}

View file

@ -29,6 +29,13 @@ const nextConfig: NextConfig = {
hostname: "**",
},
],
// Allow remote SVGs (e.g. README badges from img.shields.io, trendshift.io,
// etc.) which are otherwise blocked by next/image. The CSP below sandboxes
// the SVG and forbids any embedded scripts, which is the mitigation
// recommended by Vercel's NEXTJS_SAFE_SVG_IMAGES conformance rule.
dangerouslyAllowSVG: true,
contentDispositionType: "attachment",
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
},
experimental: {
optimizePackageImports: [

View file

@ -1,6 +1,6 @@
{
"name": "surfsense_web",
"version": "0.0.22",
"version": "0.0.23",
"private": true,
"description": "SurfSense Frontend",
"scripts": {