写点什么

精读 GitHub - swift-markdown-ui

  • 2025-11-17
    北京
  • 本文字数:3530 字

    阅读完需:约 12 分钟

一、项目介绍

项目地址:https://github.com/gonzalezreal/swift-markdown-ui


swift-markdown-ui (也称为 MarkdownUI) 是一个用于在 SwiftUI 中显示和自定义 Markdown 文本的开源库。


其主要特性如下:


  1. 强大的 Markdown 支持


它兼容 GitHub 风格的 Markdown 规范(GitHub Flavored Markdown Spec),基本支持所有类型 Markdown 元素,如:普通文本、标题(H1、H2 等)、图片、链接、有序无序列表、任务(Task)、引用、代码块、表格、分割线、加粗斜体等文本样式


  1. 强大的自定义能力


提供了强大的主题(Theming)功能,让开发者可以精细的自定义 Markdown 样式,支持针对特定标签样式(如代码块、链接等)进行覆盖和修改


  1. 易用性


可以直接通过一个 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 的目录结构如上,主要分为四大块:


  1. DSL​:Markdown 构建器,提供 MarkdownContentBuilder,支持声明式语法构造 Markdown

  2. Parser​:解析器,调用 cmark-gfm 将 Markdown 字符串解析成 BlockNode、InlineNode 节点

  3. Renderer & Views​:渲染器,根据解析的节点类型渲染成对应的样式

  4. 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】专集,欢迎订阅 & 投稿!

发布于: 1 小时前阅读数: 6
用户头像

还未添加个人签名 2019-03-12 加入

还未添加个人简介

评论

发布
暂无评论
精读GitHub - swift-markdown-ui_swift_非专业程序员Ping_InfoQ写作社区