精读GitHub - swift-markdown-ui

一、项目介绍

项目地址: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]数组

BlockNode解析
InlineNode解析

4.2 Markdown 渲染

渲染过程分为 Block 节点处理和 Inline 节点处理。

BlockNode 处理流程如下:
在这里插入图片描述

InlineNode 处理流程如下:
在这里插入图片描述

关键代码:
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 实现的
无序列表序号
任务(Task)

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)    收藏  举报

导航