Plus 组件

Markdown 渲染器

在网页中即时渲染 Markdown 和 MDX

使用场景

Markdown 是一种文档标记语言,可以通过简单的标记实现例如标题,图片,表格,链接,加粗等基本常用富文本功能。MDX 是在 Markdown 基础上,允许引入 JSX 实现更加复杂定制化的文档撰写与展示需求。

Semi 提供的 MarkdownRender 组件支持渲染 Markdown 和 MDX,无需特别配置,传入纯文本即可渲染出符合 Semi 样式规范的富文本内容。代码块由 CodeHighlight 高亮,用到的语言需先通过 CodeHighlight.register 注册,未注册的语言按纯文本显示。

通常用于下列场景:

  • 文档站编写与渲染
  • 服务端动态生成富文本内容时,前端渲染
  • 偏内容展示的轻交互网站

代码演示

如何引入

import { MarkdownRender } from '@fibrous/semi-ui-solid';

基本用法

导入 MarkdownRender 后,直接传入 Markdown 或 MDX 纯文本即可。

注意因为 < { 等符号是合法的 JSX 符号会被判定为代码,无法直接渲染,需要使用 \ 转义,如果你只需要渲染纯 Markdown,参考下方仅渲染 Markdown 一节。

代码
import { MarkdownRender } from '@fibrous/semi-ui-solid';

const raw = `
正文内容是普通的文本,也可以**加粗**~~删除线~~和<u>下划线</u> [超链接](https://semi.design) 等 Markdown 与 HTML 的基本语法所支持的富文本,也支持 emoji 🍰


部分符号需要转义 \\{\\} \\<\\> ...

<br/>
<br/>
---
#### Semi Design DSM
[Semi DSM](https://semi.design/dsm) 是 Semi Design 提供的设计系统管理工具(Design System Management),支持全局、组件级别的样式定制,并在 Figma 和前端代码之间保持同步\\
适用于各种规模的团队,无论你是需要简化工作流程,提高团队协作,还是增加生产力,我们都有适合你的功能

##### 中大型企业
- 多达 3000+ Design Token,深入每一处细节的定制可能,色彩,阴影,边距,圆角,动效,渲染结构均可自由定制,告别 ~~CSS 硬编码~~
- 功能强大,经过抖音内部数千项目验证过的 UI lib,轻松应对各类复杂场景
- A11y 无障碍友好,国际化功能完备
- 面向社区建设,完全开源,无使用限制
- 从 designOps 到 devOps,自动化工作流,Figma UI Kit 一键刷入主题,生成 Style Guideline,研发一行 npm 代码配置接入

##### 初创企业
- 无需从 0 到 1 投入大量研发资源,快速复用开源社区优秀方案, 低成本快速定制具备品牌特色的设计系统。
- 一键支持暗色模式生成,支持根据品牌色快速生成包含 320 个全色阶、兼容深/浅两种模式的色彩系统,并支持动态切换
- 不断进化,DSM + Semi Design 组件由<u>抖音前端架构团队</u>专业维护,已稳定迭代五年+,值得信赖

##### 自由设计师/个人开发者
- 低成本快速创建风格各异的设计系统,更少时间,更快交付
- 研发接入友好,无需反复沟通,交付npm包产物,一键完成代码接入


![Abstract](/assets/root-web-sites/abstract.jpg)

---

#### MarkdownRender 渲染列表语法
- 好好地吃饭
- 好好地睡觉
- 好好地游玩
- 好好地学习
- 好好地聊天
- 好好地吵架
- 过着平凡普通的每日

| 支持 | Markdown 表格 |  c |  d  |
| - | :- | -: | :-: |
| 1 | 2 | 3 | 4 |
| 21 | 22 | 23 | 24 |
| 31 | 32 | 33 | 34 |
| 41 | 42 | 43 | 44 |

    `;

export default function MarkdownRenderDemo() {
    return <MarkdownRender raw={raw} />;
}

修改元素样式

你可以任意替换 Markdown 或 MDX 文档中的文档元素的显示效果,只需向 components props 中传入你的 Solid 组件覆盖即可。元素的属性(如 class、colspan)以 props 传给组件,元素的内容通过 props.children 读取。

比如,现在需要将所有2号标题的颜色设置成主色

代码
import type { JSX } from 'solid-js';
import { MarkdownRender, Typography, type MarkdownRenderComponents } from '@fibrous/semi-ui-solid';

const components: MarkdownRenderComponents = {
    h2: (props: { children?: JSX.Element }) => (
        <Typography.Title heading={2} style={{ color: 'var(--semi-color-primary)' }}>
            {props.children}
        </Typography.Title>
    ),
};

export default function MarkdownRenderDemo() {
    return <MarkdownRender raw="## 从 Semi Design 到 Any Design  快速定义你的设计系统,并应用在设计稿和代码中" components={components} />;
}

任何由 Markdown 生成的元素标签都可以覆盖,常用的有 a blockquote br code del em h1 h2 h3 h4 h5 h6 hr img li ol p pre strong ul table

仅纯 Markdown

当你渲染的 Markdown 仅仅是纯 markdown,不包含任何 JSX 代码时,可传入 format="md" 来开启仅 Markdown 模式,在这种模式下无需转义特殊字符

无需转义的符号{}<> ...

代码
import { MarkdownRender } from '@fibrous/semi-ui-solid';

export default function MarkdownRenderDemo() {
    return <MarkdownRender raw="无需转义的符号{}<> ..." format="md" />;
}

format="md" 经 unified 管线渲染:remark-parse 解析,依次执行 remark-gfm(remarkGfm 为 true 时)与 remarkPlugins,remark-rehype 转为 HTML 语法树,执行 rehypePlugins,最后由 hast-util-to-jsx-runtime 生成 Solid 元素并套用 components。整个过程不生成、不执行代码,在禁用 unsafe-eval 的 CSP 下也能渲染,也不会加载 MDX 编译器。

format="mdx" 经 @mdx-js/mdx 把源文本编译成代码并在运行时执行,CSP 需要允许 unsafe-eval;@mdx-js/mdx 在首次渲染 MDX 时按需加载。

import { MarkdownRender } from '@fibrous/semi-ui-solid';
import rehypeRaw from 'rehype-raw';

function Demo() {
    return <MarkdownRender format="md" raw={`<span style="color:red">红色文字</span>`} rehypePlugins={[rehypeRaw]} />;
}

添加自定义组件

通过传入自定义组件到 components props,能够实现在 Markdown 中直接书写 JSX,组件会被渲染到最终页面上,支持 JS 事件。默认的 Markdown 组件可从 MarkdownRender.defaultComponents 中获取,可以用于二次封装。

MDX 中的 JSX 属性以 props 交给组件,className 并入 class;事件属性(如 onClick)以函数形式传入,组件用 onClick={props.onClick} 绑定,子内容通过 props.children 读取。

代码
import type { JSX } from 'solid-js';
import { Button, MarkdownRender, type MarkdownRenderComponents } from '@fibrous/semi-ui-solid';

interface MyButtonProps {
    children?: JSX.Element;
    onClick?: (event: MouseEvent) => void;
}

const components: MarkdownRenderComponents = {
    MyButton: (props: MyButtonProps) => (
        <Button type="primary" onClick={props.onClick} style={{ 'margin-bottom': '12px' }}>
            {' '}
            {props.children}{' '}
        </Button>
    ),
};

const raw = `
#### 下面是一个渲染在 Markdown 中的按钮
<MyButton onClick={()=>alert("点击了 MyButton")}>MyButton 点我</MyButton>

直接在 Markdown 中书写 JSX 即可
        `;

export default function MarkdownRenderDemo() {
    return <MarkdownRender raw={raw} components={components} />;
}

添加插件

通过 remarkPlugins rehypePlugins 支持 unified 生态的 Remark 与 Rehype 插件,详情请参考 MDXJS 与 unified

raw、format、remarkGfm 变化,或插件数组中的元素变化时会重新编译;components 变化只重新渲染,不重新编译。插件数组按元素比较;[插件, 配置对象] 写法中的配置对象按引用比较,每次重新求值都新建的字面量会导致重新编译,需要定义在组件外保持引用稳定:

import { MarkdownRender, type MarkdownRenderProps } from '@fibrous/semi-ui-solid';
import remarkToc from 'remark-toc';

const remarkPlugins: MarkdownRenderProps['remarkPlugins'] = [[remarkToc, { heading: '目录' }]];

function Demo(props: { raw: string }) {
    return <MarkdownRender format="md" raw={props.raw} remarkPlugins={remarkPlugins} />;
}

编译出错

MDX 语法错误或插件抛错时,保留上一次成功渲染的内容,开发环境下通过 console.error 输出错误,不会产生未处理的 Promise rejection。

无障碍

传入 aria-label 或 aria-labelledby 且没有 role 时,根元素带 role="group",使用方显式传入的 role 优先。链接内的图片不可预览,链接是唯一的交互元素;图片下方的说明文字对辅助技术隐藏,图片名称只来自 alt。

API

Props

属性说明类型默认值
components用于覆盖 Markdown 元素,也可添加 MDX 中引用的自定义组件;值为 Solid 组件或 HTML 标签名,元素键(如 h1)只接受组件或自身的标签名。与 MarkdownRender.defaultComponents 合并,变化时只重新渲染,不重新编译。值为 undefined 的键回退为内置组件。MarkdownRenderComponents-
formatraw 的格式,md 为纯 Markdown,mdx 允许内嵌 JSX。变化时重新编译。'md' | 'mdx'"mdx"
rawMarkdown 或 MDX 的纯文本。变化时重新编译。string-
rehypePlugins自定义 Rehype 插件,数组元素变化时重新编译。PluggableList-
remarkGfm是否开启 GitHub GFM 语法。变化时重新编译。booleantrue
remarkPlugins自定义 Remark 插件,数组元素变化时重新编译。PluggableList-

class、style、ref 与原生属性和事件(如 id、data-*、onClick)落在根元素 div 上。

设计变量

下表是组件内部的 Sass 变量,不能覆写。定制样式请覆写 --semi-* CSS 变量(见 Design Token 页),或写不分层的 CSS 规则。
Sass 变量值用法
$spacing-markdownRender_component_image-marginLeft16px
$spacing-markdownRender_component_image-marginRight16px
$spacing-markdownRender_component_image-marginTop16px
$spacing-markdownRender_component_image-marginBottom16px
$width-markdownRender_component_image-maxWidth50%
$width-markdownRender_component_image-maxHeight500px
$spacing-markdownRender_component_image_alt-marginTop8px
$spacing-markdownRender_component_header1-marginTop16px
$spacing-markdownRender_component_header1-marginBottom16px
$spacing-markdownRender_component_header2-marginTop16px
$spacing-markdownRender_component_header2-marginBottom16px
$spacing-markdownRender_component_header3-marginTop16px
$spacing-markdownRender_component_header3-marginBottom16px
$spacing-markdownRender_component_header4-marginTop16px
$spacing-markdownRender_component_header4-marginBottom16px
$spacing-markdownRender_component_header5-marginTop16px
$spacing-markdownRender_component_header5-marginBottom16px
$spacing-markdownRender_component_header6-marginTop16px
$spacing-markdownRender_component_header6-marginBottom16px
$spacing-markdownRender_component_list-paddingLeft24px
$spacing-markdownRender_component_list-marginTop16px
$spacing-markdownRender_component_list-marginBottom16px
$color-markdownRender_simpleCode-bgvar(--semi-color-fill-0)
$color-markdownRender_simpleCode-textvar(--semi-color-text-1)
$color-markdownRender_component_listvar(--semi-color-text-0)
$color-markdownRender_component_image_altvar(--semi-color-tertiary)