import type { Element } from "hast"; import "katex/dist/katex.min.css"; import type { Components } from "react-markdown"; import ReactMarkdown from "react-markdown"; import rehypeKatex from "rehype-katex"; import rehypeRaw from "rehype-raw"; import rehypeSanitize from "rehype-sanitize"; import remarkBreaks from "remark-breaks"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import { isMentionElement, isTagElement, isTaskListItemElement } from "@/types/markdown"; import { rehypeHeadingId } from "@/utils/rehype-plugins/rehype-heading-id"; import { remarkDisableSetext } from "@/utils/remark-plugins/remark-disable-setext"; import { remarkMention } from "@/utils/remark-plugins/remark-mention"; import { remarkPreserveType } from "@/utils/remark-plugins/remark-preserve-type"; import { remarkSplitMixedTaskLists } from "@/utils/remark-plugins/remark-split-mixed-task-lists"; import { remarkTag } from "@/utils/remark-plugins/remark-tag"; import { CodeBlock } from "./CodeBlock"; import { SANITIZE_SCHEMA } from "./constants"; import { MarkdownRenderContext, rootMarkdownRenderContext } from "./MarkdownRenderContext"; import { Mention } from "./Mention"; import { AnchorLink, Blockquote, Heading, HorizontalRule, Image, InlineCode, Link, List, ListItem, Paragraph } from "./markdown"; import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "./Table"; import { Tag } from "./Tag"; import { TaskListItem } from "./TaskListItem"; import { TrustedIframe } from "./TrustedIframe"; interface MemoMarkdownRendererProps { content: string; resolvedMentionUsernames: Set; /** Resource name of the memo (e.g. `memos/abc123`), used to target footnote links at the detail page. */ memoName?: string; /** Whether the memo is rendered as a collapsed feed card. */ compact?: boolean; } function getMentionUsername(node: Element, children?: React.ReactNode): string { const dataMention = node.properties?.["data-mention"]; if (typeof dataMention === "string" && dataMention !== "") { return dataMention; } const camelDataMention = (node.properties as Record | undefined)?.dataMention; if (typeof camelDataMention === "string" && camelDataMention !== "") { return camelDataMention; } const text = Array.isArray(children) ? children.join("") : children; if (typeof text === "string" && text.startsWith("@")) { return text.slice(1).toLowerCase(); } return ""; } export const MemoMarkdownRenderer = ({ content, resolvedMentionUsernames, memoName, compact }: MemoMarkdownRendererProps) => { const markdownComponents: Components = { input: ({ node, ...inputProps }) => { if (node && isTaskListItemElement(node)) { return ; } return ; }, span: ({ node, ...spanProps }) => { if (node && isMentionElement(node)) { const username = getMentionUsername(node, spanProps.children); return ; } if (node && isTagElement(node)) { return ; } return ; }, h1: ({ children, ...props }) => ( {children} ), h2: ({ children, ...props }) => ( {children} ), h3: ({ children, ...props }) => ( {children} ), h4: ({ children, ...props }) => ( {children} ), h5: ({ children, ...props }) => ( {children} ), h6: ({ children, ...props }) => ( {children} ), p: ({ children, ...props }) => {children}, blockquote: ({ children, ...props }) =>
{children}
, hr: (props) => , ul: ({ children, ...props }) => {children}, ol: ({ children, ...props }) => ( {children} ), li: ({ children, ...props }) => {children}, a: ({ children, href, ...props }) => { // In-page anchors (footnote refs/backrefs, heading links) navigate within the memo rather // than opening a new tab; everything else is treated as an external link. if (typeof href === "string" && href.startsWith("#")) { return ( {children} ); } return ( {children} ); }, code: ({ children, ...props }) => {children}, iframe: TrustedIframe, img: (props) => , pre: CodeBlock, table: ({ children, ...props }) => {children}
, thead: ({ children, ...props }) => {children}, tbody: ({ children, ...props }) => {children}, tr: ({ children, ...props }) => {children}, th: ({ children, ...props }) => {children}, td: ({ children, ...props }) => {children}, }; return ( {content} ); };