字节笔记本字节笔记本

react-markdown code 的样式渲染

2023-05-10

该代码段展示了如何使用`react-markdown`及其插件来渲染和自定义Markdown中的代码块样式,包括处理特殊字符、应用不同的CSS类以及使用自定义组件来增强代码块的功能,如复制和下载代码。

<MemoizedReactMarkdown
    className="flex-1 h-full p-5 bg-black"
    remarkPlugins={[remarkGfm, remarkMath]}
    rehypePlugins={[rehypeMathjax]}
    components={{
        code({node, inline, className, children, ...props}) {
            if (children.length) {
                if (children[0] == '▍') {
                    return <span className="animate-pulse cursor-default mt-1"></span>
                }

                children[0] = (children[0] as string).replace("`▍`", "▍")
            }

            const match = /language-(\w+)/.exec(className || '');

            return !inline ? (
                <CodeBlock
                    key={Math.random()}
                    language={(match && match[1]) || ''}
                    value={String(children).replace(/\n$/, '')}
                    {...props}
                />
            ) : (
                <code className={className} {...props}>
                    {children}
                </code>
            );
        },
        table({children}) {
            return (
                <table
                    className="border-collapse border border-black px-3 py-1 dark:border-white">
                    {children}
                </table>
            );
        },
        th({children}) {
            return (
                <th className="break-words border border-black bg-gray-500 px-3 py-1 text-white dark:border-white">
                    {children}
                </th>
            );
        },
        td({children}) {
            return (
                <td className="break-words border border-black px-3 py-1 dark:border-white">
                    {children}
                </td>
            );
        },
        ol({children}) {
            return (
                <ol className="list-decimal list-inside">
                    {children}
                </ol>
            )
        },
        ul({children}) {
            return (
                <ul className="list-disc list-inside">
                    {children}
                </ul>
            )
        }
    }}
>
    {`${markdown}`}
</MemoizedReactMarkdown>

提供的修改类型包括

export type SpecialComponents = {
  code: CodeComponent | ReactMarkdownNames
  h1: HeadingComponent | ReactMarkdownNames
  h2: HeadingComponent | ReactMarkdownNames
  h3: HeadingComponent | ReactMarkdownNames
  h4: HeadingComponent | ReactMarkdownNames
  h5: HeadingComponent | ReactMarkdownNames
  h6: HeadingComponent | ReactMarkdownNames
  li: LiComponent | ReactMarkdownNames
  ol: OrderedListComponent | ReactMarkdownNames
  td: TableDataCellComponent | ReactMarkdownNames
  th: TableHeaderCellComponent | ReactMarkdownNames
  tr: TableRowComponent | ReactMarkdownNames
  ul: UnorderedListComponent | ReactMarkdownNames
}

CodeBlock

interface Props {
  language: string;
  value: string;
}

export const CodeBlock: FC<Props> = memo(({ language, value }) => {
  const { t } = useTranslation('markdown');
  const [isCopied, setIsCopied] = useState<Boolean>(false);

  const copyToClipboard = () => {
    if (!navigator.clipboard || !navigator.clipboard.writeText) {
      return;
    }

    navigator.clipboard.writeText(value).then(() => {
      setIsCopied(true);

      setTimeout(() => {
        setIsCopied(false);
      }, 2000);
    });
  };
  const downloadAsFile = () => {
    const fileExtension = programmingLanguages[language] || '.file';
    const suggestedFileName = `file-${generateRandomString(
      3,
      true,
    )}${fileExtension}`;
    const fileName = window.prompt(
      t('Enter file name') || '',
      suggestedFileName,
    );

    if (!fileName) {
      // user pressed cancel on prompt
      return;
    }

    const blob = new Blob([value], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.download = fileName;
    link.href = url;
    link.style.display = 'none';
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
  };
  return (
    <div className="codeblock relative font-sans text-[16px]">
      <div className="flex items-center justify-between py-1.5 px-4">
        <span className="text-xs lowercase text-white">{language}</span>

        <div className="flex items-center">
          <button
            className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-white"
            onClick={copyToClipboard}
          >
            {isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
            {isCopied ? t('Copied!') : t('Copy code')}
          </button>
          <button
            className="flex items-center rounded bg-none p-1 text-xs text-white"
            onClick={downloadAsFile}
          >
            <IconDownload size={18} />
          </button>
        </div>
      </div>

      <SyntaxHighlighter
        language={language}
        style={oneDark}
        customStyle={{ margin: 0 }}
      >
        {value}
      </SyntaxHighlighter>
    </div>
  );
});
CodeBlock.displayName = 'CodeBlock';