精读 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:
可以通过markdownTextStyle覆盖默认主题样式,甚至通过markdownTheme完全传入一个新的主题:
也可以通过 MarkdownContentBuilder,使用 DSL 的方式构造 UI:
更多使用方式,可以参考官方 Demo:
三、架构分析
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 对应行内元素,如文本、图片、链接等
如下为详细的映射过程:最终解析完成的结果就是一个 [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 会被解析成多行结构:
渲染时使用 SwiftUI 中的 Grid 布局实现:Grid 布局天然支持了同行等高、同列等宽、跨行跨列(合并单元格)等特性,不需要复杂配置就能实现表格的效果。
但是 Grid 布局也有一些局限:
Grid 布局不支持滚动,如下当列很多时内容会很窄;更好的做法是嵌套在 ScrollView 中,进行横向滚动
大数据量时可能有性能问题:Grid 布局是非懒加载的,也不存在 Cell 复用,在大数据量时 FPS、内存可能都是挑战
4.3 自定义样式 & Theme 系统
如下是样式系统的架构图:
swift-markdown-ui 提供了 basic、github、docC 三种内置主题,在这三个主题的基础上,支持开发者覆盖默认配置,也可以完全自定义一个新的主题传入。
样式通过 SwiftUI 的 Environment,可以很方便的实现自动注入和父子视图数据传递:
五、广告位
每周精读一个开源项目,文章首发公众号「非专业程序员 Ping」【精读 GitHub Weekly】专集,欢迎订阅 & 投稿!
版权声明: 本文为 InfoQ 作者【非专业程序员Ping】的原创文章。
原文链接:【http://xie.infoq.cn/article/6b8041d892f1b5a87e0d9eb09】。文章转载请联系作者。







评论