diff --git a/src/components/markdown/Markdown.tsx b/src/components/markdown/Markdown.tsx index 9ff974fb..77c61dc9 100644 --- a/src/components/markdown/Markdown.tsx +++ b/src/components/markdown/Markdown.tsx @@ -3,11 +3,13 @@ import { Suspense, lazy } from "preact/compat"; const Renderer = lazy(() => import("./RemarkRenderer")); export interface MarkdownProps { - content?: string | null; + content: string; disallowBigEmoji?: boolean; } export default function Markdown(props: MarkdownProps) { + if (!props.content) return null; + return ( // @ts-expect-error Typings mis-match. diff --git a/src/components/markdown/RemarkRenderer.tsx b/src/components/markdown/RemarkRenderer.tsx index 06d1c3f1..9501a66f 100644 --- a/src/components/markdown/RemarkRenderer.tsx +++ b/src/components/markdown/RemarkRenderer.tsx @@ -15,14 +15,14 @@ import { memo } from "preact/compat"; import { useEffect, useMemo, useState } from "preact/hooks"; import { MarkdownProps } from "./Markdown"; +import { handlers } from "./hast"; import { RenderCodeblock } from "./plugins/Codeblock"; import { RenderAnchor } from "./plugins/anchors"; import { remarkChannels, RenderChannel } from "./plugins/channels"; import { isOnlyEmoji, remarkEmoji, RenderEmoji } from "./plugins/emoji"; import { remarkMention, RenderMention } from "./plugins/mentions"; -import { passThroughComponents } from "./plugins/remarkRegexComponent"; import { remarkSpoiler, RenderSpoiler } from "./plugins/spoiler"; -import { remarkTimestamps, timestampHandler } from "./plugins/timestamps"; +import { remarkTimestamps } from "./plugins/timestamps"; import "./prism"; /** @@ -139,10 +139,7 @@ const render = unified() .use(remarkEmoji) .use(remarkMention) .use(remarkRehype, { - handlers: { - ...passThroughComponents("emoji", "spoiler", "mention", "channel"), - timestamp: timestampHandler, - }, + handlers, }) .use(rehypeKatex, { maxSize: 10, @@ -173,15 +170,34 @@ const Container = styled.div<{ largeEmoji: boolean }>` --emoji-size: ${(props) => (props.largeEmoji ? "3em" : "1.25em")}; `; +/** + * Regex for matching execessive blockquotes + */ +const RE_QUOTE = /(^[>\s][>\s])[>\s]+([^]+)$/gm; + +/** + * Sanitise Markdown input before rendering + * @param content Input string + * @returns Sanitised string + */ +function sanitise(content: string) { + // Strip excessive blockquote indentation + return content.replace(RE_QUOTE, (_, m0, m1) => m0 + m1); +} + /** * Remark renderer component */ export default memo(({ content, disallowBigEmoji }: MarkdownProps) => { + const sanitisedContent = useMemo(() => sanitise(content), [content]); + const [Content, setContent] = useState(null!); useEffect(() => { - render.process(content!).then((file) => setContent(file.result)); - }, [content]); + render + .process(sanitisedContent) + .then((file) => setContent(file.result)); + }, [sanitisedContent]); const largeEmoji = useMemo( () => !disallowBigEmoji && isOnlyEmoji(content!), diff --git a/src/components/markdown/hast.ts b/src/components/markdown/hast.ts new file mode 100644 index 00000000..d2054aff --- /dev/null +++ b/src/components/markdown/hast.ts @@ -0,0 +1,7 @@ +import { passThroughComponents } from "./plugins/remarkRegexComponent"; +import { timestampHandler } from "./plugins/timestamps"; + +export const handlers = { + ...passThroughComponents("emoji", "spoiler", "mention", "channel"), + timestamp: timestampHandler, +};