精读GitHub - swift-markdown-ui
一、项目介绍
swift-markdown-ui (也称为 MarkdownUI) 是一个用于在 SwiftUI 中显示和自定义 Markdown 文本的开源库。
其主要特性如下:
- 强大的 Markdown 支持
它兼容 GitHub 风格的 Markdown 规范(GitHub Flavored Markdown Spec),基本支持所有类型 Markdown 元素,如:普通文本、标题(H1、H2 等)、图片、链接、有序无序列表、任务(Task)、引用、代码块、表格、分割线、加粗斜体等文本样式
- 强大的自定义能力
提供了强大的主题(Theming)功能,让开发者可以精细的自定义 Markdown 样式,支持针对特定标签样式(如代码块、链接等)进行覆盖和修改
- 易用性
可以直接通过一个 Markdown 字符串来创建一个 Markdown 视图,也可以通过 MarkdownContentBuilder,使用类似 SwiftUI 的 DSL 来构建 Markdown 内容
该项目自 2021 年起,star 数一路飙升,到现在已斩获 3.6K 的 star:

二、使用介绍
使用方式很简单,可以直接传入通过 Markdown string 构造 UI:
struct TextView: View {
let content = """
Hello World
# Heading 1
## Heading 2
### Heading 3
"""
var body: some View {
DemoView {
Markdown(self.content)
}
}
}
可以通过markdownTextStyle覆盖默认主题样式,甚至通过markdownTheme完全传入一个新的主题:
struct TextView: View {
let content = """
Hello World
# Heading 1
## Heading 2
### Heading 3
"""
var body: some View {
DemoView {
Markdown(self.content)
.markdownTheme(CustomTheme())
Markdown(self.content)
.markdownTextStyle(\.code) {
FontFamilyVariant(.monospaced)
BackgroundColor(.yellow.opacity(0.5))
}
.markdownTextStyle(\.emphasis) {
FontStyle(.italic)
UnderlineStyle(.single)
}
.markdownTextStyle(\.strong) {
FontWeight(.heavy)
}
}
}
}
也可以通过 MarkdownContentBuilder,使用 DSL 的方式构造 UI:
var body: some View {
Markdown {
Heading(.level2) {
"Try MarkdownUI"
}
Paragraph {
Strong("MarkdownUI")
" is a native Markdown renderer for SwiftUI"
" compatible with the "
InlineLink(
"GitHub Flavored Markdown Spec",
destination: URL(string: "https://github.github.com/gfm/")!
)
"."
}
}
}
更多使用方式,可以参考官方 Demo:
三、架构分析
Sources/MarkdownUI/
├── Parser/ # Markdown 解析器
├── DSL/ # 领域特定语言(构建器)
├── Renderer/ # 渲染器
├── Theme/ # 主题系统
├── Views/ # SwiftUI 视图组件
├── Extensibility/ # 扩展性支持(图片提供者、语法高亮)
├── Utility/ # 工具函数
└── Documentation.docc/ # 文档
swift-markdown-ui 的目录结构如上,主要分为四大块:
- DSL:Markdown 构建器,提供 MarkdownContentBuilder,支持声明式语法构造 Markdown
- Parser:解析器,调用 cmark-gfm 将 Markdown 字符串解析成 BlockNode、InlineNode 节点
- Renderer & Views:渲染器,根据解析的节点类型渲染成对应的样式
- Theme:主题系统,提供强大的样式覆盖和自定义主题能力
整体流程如下:

架构分层如下:

四、源码分析
前面讲了大致的流程图,下面是详细的输入输出及处理过程:

下面我们将分别对解析、渲染、样式系统进行拆解。
4.1 Markdown 解析
使用三方库 cmark-gfm 进行 Markdown 解析,cmark-gfm 是从标准的 CommonMark 解析器 cmark fork 出来的一个扩展分支,由 GitHub 官方维护,除了 CommonMark 的标准语法外,还支持表格、删除线、任务(Task)、自动链接识别(AutoLink)等特性,通过插件的方式注入。
如下,是使用 cmark-gfm 解析的核心逻辑:

cmark-gfm 的解析原理是将 Markdown 字符串解析成语法树,外部可以通过遍历语法树来处理每一个节点,Markdown 的语法树可以通过网站 https://spec.commonmark.org/dingus/ 查看。
如下,一段简单的 Hello World 文本,对应的语法树(AST)如右图,通过 cmark-gfm 我们就能逐级访问 document -> paragarph -> text

再来看一个稍复杂一点的列表的例子:

在 swift-markdown-ui 项目中,会将 Markdown 的语法树节点映射成 BlockNode 和 InlineNode,有前端经验的小伙伴应该比较容易理解,BlockNode 对应块级元素,如段落(paragraph),列表(list、item)等,InlineNode 对应行内元素,如文本、图片、链接等
enum BlockNode: Hashable {
case blockquote(children: [BlockNode])
case bulletedList(isTight: Bool, items: [RawListItem])
case numberedList(isTight: Bool, start: Int, items: [RawListItem])
case taskList(isTight: Bool, items: [RawTaskListItem])
case codeBlock(fenceInfo: String?, content: String)
case htmlBlock(content: String)
case paragraph(content: [InlineNode])
case heading(level: Int, content: [InlineNode])
case table(columnAlignments: [RawTableColumnAlignment], rows: [RawTableRow])
case thematicBreak
}
enum InlineNode: Hashable, Sendable {
case text(String)
case softBreak
case lineBreak
case code(String)
case html(String)
case emphasis(children: [InlineNode])
case strong(children: [InlineNode])
case strikethrough(children: [InlineNode])
case link(destination: String, children: [InlineNode])
case image(source: String, children: [InlineNode])
}
如下为详细的映射过程:最终解析完成的结果就是一个 [BlockNode]数组


4.2 Markdown 渲染
渲染过程分为 Block 节点处理和 Inline 节点处理。
BlockNode 处理流程如下:

InlineNode 处理流程如下:

关键代码:


每一个 Block 节点都是一个单独的自定义 View,文本节点使用 AttributedString 拼接各种加粗斜体等样式,最终由 Label 进行渲染。
下面我们挑几个难点进行讲解。
4.2.1 文本的加粗斜体下划线删除线样式是怎么实现的

这些都是使用 iOS 系统能力,配置 AttributeContainer 实现的,支持配置的样式如下:

4.2.2 引用的样式是怎么实现的

如上,引用有背景,左边有边框,背景色支持内容撑开,这是怎么做到的?
上面我们有提到每个 Block 节点都是一个单独的自定义 View,引用也是一个自定义 View,如下使用 HStack 将左边框和内容并排,高度靠内容撑开,关键配置是.fixedSize(horizontal: false, vertical: true),其中horizontal: false表示水平方向允许扩展,受父视图宽度约束影响,vertical: true表示垂直方向固定,完全靠内容撑开。

以此类推,代码块、任务等的样式也可以靠自定义 View 实现。
4.2.3 无序列表序号和任务标识是怎么实现的

无序列表前面的小圆点/方块,以及任务前面的已完成、待完成标识是怎么实现的呢。
主要代码如下,可以看出是通过 SF Symbols,即系统自带的符号 icon 实现的


4.2.4 表格的样式是怎么实现的
在 Parser 阶段,table 会被解析成多行结构:
enum BlockNode: Hashable {
...
case table(columnAlignments: [RawTableColumnAlignment], rows: [RawTableRow])
}
enum RawTableColumnAlignment: Character {
case none = "\0"
case left = "l"
case center = "c"
case right = "r"
}
struct RawTableRow: Hashable {
let cells: [RawTableCell]
}
struct RawTableCell: Hashable {
let content: [InlineNode]
}
渲染时使用 SwiftUI 中的 Grid 布局实现:Grid 布局天然支持了同行等高、同列等宽、跨行跨列(合并单元格)等特性,不需要复杂配置就能实现表格的效果。

但是 Grid 布局也有一些局限:
- Grid 布局不支持滚动,如下当列很多时内容会很窄;更好的做法是嵌套在 ScrollView 中,进行横向滚动
- 大数据量时可能有性能问题:Grid 布局是非懒加载的,也不存在 Cell 复用,在大数据量时 FPS、内存可能都是挑战
![在这里插入图片描述]()
4.3 自定义样式 & Theme 系统
如下是样式系统的架构图:

swift-markdown-ui 提供了 basic、github、docC 三种内置主题,在这三个主题的基础上,支持开发者覆盖默认配置,也可以完全自定义一个新的主题传入。
样式通过 SwiftUI 的 Environment,可以很方便的实现自动注入和父子视图数据传递:

五、广告位
每周精读一个开源项目,文章首发公众号「非专业程序员 Ping」【精读 GitHub Weekly】专集,欢迎订阅 & 投稿!
posted on 2025-11-17 01:10 非专业程序员Ping 阅读(0) 评论(0) 收藏 举报

浙公网安备 33010602011771号