From fbcd80948efeca5337a778b62255082eeeb7bc9c Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 7 Mar 2026 09:56:46 -0600 Subject: [PATCH] Fix markdown editor escaped list markers --- ui/src/components/MarkdownBody.tsx | 4 ++- ui/src/components/MarkdownEditor.tsx | 20 +++++++----- ui/src/lib/markdown.test.ts | 20 ++++++++++++ ui/src/lib/markdown.ts | 47 ++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 ui/src/lib/markdown.test.ts create mode 100644 ui/src/lib/markdown.ts diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx index b996629a..d9a2afb6 100644 --- a/ui/src/components/MarkdownBody.tsx +++ b/ui/src/components/MarkdownBody.tsx @@ -4,6 +4,7 @@ import remarkGfm from "remark-gfm"; import { parseProjectMentionHref } from "@paperclipai/shared"; import { cn } from "../lib/utils"; import { useTheme } from "../context/ThemeContext"; +import { normalizeMarkdownArtifacts } from "../lib/markdown"; interface MarkdownBodyProps { children: string; @@ -114,6 +115,7 @@ function MermaidDiagramBlock({ source, darkMode }: { source: string; darkMode: b export function MarkdownBody({ children, className }: MarkdownBodyProps) { const { theme } = useTheme(); + const normalizedMarkdown = normalizeMarkdownArtifacts(children); return (
- {children} + {normalizedMarkdown}
); diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index 85b67c32..507308a1 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -29,6 +29,7 @@ import { } from "@mdxeditor/editor"; import { buildProjectMentionHref, parseProjectMentionHref } from "@paperclipai/shared"; import { cn } from "../lib/utils"; +import { normalizeMarkdownArtifacts } from "../lib/markdown"; /* ---- Mention types ---- */ @@ -203,7 +204,7 @@ export const MarkdownEditor = forwardRef }: MarkdownEditorProps, forwardedRef) { const containerRef = useRef(null); const ref = useRef(null); - const latestValueRef = useRef(value); + const latestValueRef = useRef(normalizeMarkdownArtifacts(value)); const [uploadError, setUploadError] = useState(null); const [isDragOver, setIsDragOver] = useState(false); const dragDepthRef = useRef(0); @@ -281,9 +282,10 @@ export const MarkdownEditor = forwardRef }, [hasImageUpload]); useEffect(() => { - if (value !== latestValueRef.current) { - ref.current?.setMarkdown(value); - latestValueRef.current = value; + const normalizedValue = normalizeMarkdownArtifacts(value); + if (normalizedValue !== latestValueRef.current) { + ref.current?.setMarkdown(normalizedValue); + latestValueRef.current = normalizedValue; } }, [value]); @@ -554,11 +556,15 @@ export const MarkdownEditor = forwardRef > { - latestValueRef.current = next; - onChange(next); + const normalizedNext = normalizeMarkdownArtifacts(next); + latestValueRef.current = normalizedNext; + if (normalizedNext !== next) { + ref.current?.setMarkdown(normalizedNext); + } + onChange(normalizedNext); }} onBlur={() => onBlur?.()} className={cn("paperclip-mdxeditor", !bordered && "paperclip-mdxeditor--borderless")} diff --git a/ui/src/lib/markdown.test.ts b/ui/src/lib/markdown.test.ts new file mode 100644 index 00000000..a455ea1f --- /dev/null +++ b/ui/src/lib/markdown.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { normalizeMarkdownArtifacts } from "./markdown"; + +describe("normalizeMarkdownArtifacts", () => { + it("normalizes escaped unordered list markers and space entities", () => { + const input = "Here is a list:\n\n\\* foo \n\\- bar "; + const output = normalizeMarkdownArtifacts(input); + expect(output).toBe("Here is a list:\n\n* foo \n- bar "); + }); + + it("does not rewrite escaped markers inside fenced code blocks", () => { + const input = "```md\n\\* keep literal \n\\- keep literal \n```"; + expect(normalizeMarkdownArtifacts(input)).toBe(input); + }); + + it("keeps escaped non-list syntax intact", () => { + const input = "\\*not-a-list"; + expect(normalizeMarkdownArtifacts(input)).toBe(input); + }); +}); diff --git a/ui/src/lib/markdown.ts b/ui/src/lib/markdown.ts new file mode 100644 index 00000000..ab485319 --- /dev/null +++ b/ui/src/lib/markdown.ts @@ -0,0 +1,47 @@ +const FENCE_RE = /^\s*(`{3,}|~{3,})/; +const SPACE_ENTITY_RE = / /gi; +const ESCAPED_UNORDERED_LIST_RE = /^(\s{0,3})\\([*+-])([ \t]+)/; + +/** + * Normalize markdown artifacts emitted by rich-text serialization so + * plain markdown list syntax remains usable in Paperclip editors. + */ +export function normalizeMarkdownArtifacts(markdown: string): string { + if (!markdown) return markdown; + + const lines = markdown.split(/\r?\n/); + let inFence = false; + let fenceMarker: "`" | "~" | null = null; + let fenceLength = 0; + let changed = false; + + const normalized = lines.map((line) => { + const fenceMatch = FENCE_RE.exec(line); + if (fenceMatch) { + const marker = fenceMatch[1]; + if (!inFence) { + inFence = true; + fenceMarker = marker[0] as "`" | "~"; + fenceLength = marker.length; + } else if (marker[0] === fenceMarker && marker.length >= fenceLength) { + inFence = false; + fenceMarker = null; + fenceLength = 0; + } + return line; + } + + if (inFence) return line; + + let next = line; + if (next.includes(" ")) { + next = next.replace(SPACE_ENTITY_RE, " "); + } + const unescaped = next.replace(ESCAPED_UNORDERED_LIST_RE, "$1$2$3"); + if (unescaped !== line) changed = true; + return unescaped; + }); + + if (!changed) return markdown; + return normalized.join("\n"); +}