精读GitHub - swift-markdown-ui
一、项目介绍
项目地址:https://github.com/gonzalezreal/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 softBreakcase lineBreakcase 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】专集,欢迎订阅 & 投稿!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/967523.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!