Swift MarkdownKit 是一个用于解析 Markdown 格式文本的框架。它支持基于 CommonMark Markdown 规范的语法。Swift MarkdownKit 还提供了一个扩展版本的解析器,能够处理 Markdown 表格。
Swift MarkdownKit 定义了 Markdown 的抽象语法,它提供了一个用于将字符串解析为抽象语法树的解析器,并且附带了用于创建 HTML 和 属性字符串 的生成器。
类 MarkdownParser
提供了一个简单的 API 用于解析字符串中的 Markdown。解析器返回一个抽象语法树,表示字符串中的 Markdown 结构。
let markdown = MarkdownParser.standard.parse("""
# Header
## Sub-header
And this is a **paragraph**.
""")
print(markdown)
执行此代码将导致打印类型为 Block
的以下数据结构
document(heading(1, text("Header")),
heading(2, text("Sub-header")),
paragraph(text("And this is a "),
strong(text("paragraph")),
text("."))))
Block
是一个递归定义的枚举,包含关联值(也称为代数数据类型)。Case document
指的是文档的根。它包含一系列的块。在上面的例子中,文档中出现了两种不同类型的块:heading
和 paragraph
。heading
Case 包含一个标题级别(作为其第一个参数)和标题文本(作为第二个参数)。paragraph
Case 简单地包含文本。
文本使用结构体 Text
表示,它实际上是一个 TextFragment
值的序列。TextFragment
是另一个递归定义的枚举,包含关联值。上面的示例展示了两种不同的 TextFragment
Case 的使用:text
和 strong
。Case text
表示纯文本字符串。Case strong
包含一个 Text
对象,即它封装了一个 "强标记" 的 TextFragment
值序列。
类 ExtendedMarkdownParser
具有与 MarkdownParser
相同的接口,但除了 CommonMark 规范定义的块类型之外,还支持表格和定义列表。表格 基于 GitHub Flavored Markdown 规范,但有一个扩展:在表格块中,可以转义换行符,以便可以在多行上编写单元格文本。这是一个例子
| Column 1 | Column 2 |
| ------------ | -------------- |
| This text \
is very long | More cell text |
| Last line | Last cell |
定义列表 以一种特殊的方式实现。一个定义包含术语及其对应的定义。这是一个包含两个定义的例子
Apple
: Pomaceous fruit of plants of the genus Malus in the family Rosaceae.
Orange
: The fruit of an evergreen tree of the genus Citrus.
: A large round juicy citrus fruit with a tough bright reddish-yellow rind.
MarkdownParser
支持的 Markdown 方言由两个参数定义:一系列的块解析器(每个解析器都表示为 BlockParser
的子类)和一系列的内联转换器(每个转换器都表示为 InlineTransformer
的子类)。类 MarkdownParser
的初始化器可选地接受这两种组件。默认配置(初始化器既不提供块解析器,也不提供内联转换器)能够处理基于 CommonMark 规范 的 Markdown。
由于 MarkdownParser
对象是无状态的(除了块解析器和内联转换器的配置之外),因此可以通过静态属性 MarkdownParser.standard
访问预定义的默认 MarkdownParser
对象。上面的例子使用了这个默认的解析对象。
可以通过继承 MarkdownParser
并重写类属性 defaultBlockParsers
和 defaultInlineTransformers
来创建具有不同配置的新 markdown 解析器。这是一个例子,展示了如何通过简单地重写 defaultBlockParsers
并在协变的方式中专门化 standard
来从 MarkdownParser
派生类 ExtendedMarkdownParser
。
open class ExtendedMarkdownParser: MarkdownParser {
override open class var defaultBlockParsers: [BlockParser.Type] {
return self.blockParsers
}
private static let blockParsers: [BlockParser.Type] =
MarkdownParser.defaultBlockParsers + [TableParser.self]
override open class var standard: ExtendedMarkdownParser {
return self.singleton
}
private static let singleton: ExtendedMarkdownParser = ExtendedMarkdownParser()
}
在 MarkdownKit 框架的 1.1 版本中,现在也可以扩展 MarkdownKit 支持的抽象语法。Block
和 TextFragment
枚举现在都包含一个 custom
Case,它引用表示扩展语法的对象。这些对象必须为块实现协议 CustomBlock
,为文本片段实现协议 CustomTextFragment
。
这是一个简单的例子,展示了如何通过继承现有的内联转换器来添加对“下划线”(例如,this is ~underlined~ text
)和“删除线”(例如,this is using ~~strike-through~~
)的支持。
首先,必须实现一个新的自定义文本片段类型,用于表示带下划线和删除线的文本。这可以通过实现 CustomTextFragment
协议的枚举来完成
enum LineEmphasis: CustomTextFragment {
case underline(Text)
case strikethrough(Text)
func equals(to other: CustomTextFragment) -> Bool {
guard let that = other as? LineEmphasis else {
return false
}
switch (self, that) {
case (.underline(let lhs), .underline(let rhs)):
return lhs == rhs
case (.strikethrough(let lhs), .strikethrough(let rhs)):
return lhs == rhs
default:
return false
}
}
func transform(via transformer: InlineTransformer) -> TextFragment {
switch self {
case .underline(let text):
return .custom(LineEmphasis.underline(transformer.transform(text)))
case .strikethrough(let text):
return .custom(LineEmphasis.strikethrough(transformer.transform(text)))
}
}
func generateHtml(via htmlGen: HtmlGenerator) -> String {
switch self {
case .underline(let text):
return "<u>" + htmlGen.generate(text: text) + "</u>"
case .strikethrough(let text):
return "<s>" + htmlGen.generate(text: text) + "</s>"
}
}
func generateHtml(via htmlGen: HtmlGenerator,
and attrGen: AttributedStringGenerator?) -> String {
return self.generateHtml(via: htmlGen)
}
var rawDescription: String {
switch self {
case .underline(let text):
return text.rawDescription
case .strikethrough(let text):
return text.rawDescription
}
}
var description: String {
switch self {
case .underline(let text):
return "~\(text.description)~"
case .strikethrough(let text):
return "~~\(text.description)~~"
}
}
var debugDescription: String {
switch self {
case .underline(let text):
return "underline(\(text.debugDescription))"
case .strikethrough(let text):
return "strikethrough(\(text.debugDescription))"
}
}
}
接下来,需要扩展两个内联转换器以识别新的强调分隔符 ~
final class EmphasisTestTransformer: EmphasisTransformer {
override public class var supportedEmphasis: [Emphasis] {
return super.supportedEmphasis + [
Emphasis(ch: "~", special: false, factory: { double, text in
return .custom(double ? LineEmphasis.strikethrough(text)
: LineEmphasis.underline(text))
})]
}
}
final class DelimiterTestTransformer: DelimiterTransformer {
override public class var emphasisChars: [Character] {
return super.emphasisChars + ["~"]
}
}
最后,可以创建一个新的扩展 markdown 解析器
final class EmphasisTestMarkdownParser: MarkdownParser {
override public class var defaultInlineTransformers: [InlineTransformer.Type] {
return [DelimiterTestTransformer.self,
CodeLinkHtmlTransformer.self,
LinkTransformer.self,
EmphasisTestTransformer.self,
EscapeTransformer.self]
}
override public class var standard: EmphasisTestMarkdownParser {
return self.singleton
}
private static let singleton: EmphasisTestMarkdownParser = EmphasisTestMarkdownParser()
}
使用抽象语法树表示 Markdown 文本的优势在于,它可以非常容易地处理这些数据,特别是转换和提取信息。下面是一个简短的 Swift 代码片段,演示了如何处理抽象语法树,目的是提取所有顶级标题(即,此代码打印 Markdown 格式文本的顶级大纲)。
let markdown = MarkdownParser.standard.parse("""
# First *Header*
## Sub-header
And this is a **paragraph**.
# Second **Header**
And this is another paragraph.
""")
func topLevelHeaders(doc: Block) -> [String] {
guard case .document(let topLevelBlocks) = doc else {
preconditionFailure("markdown block does not represent a document")
}
var outline: [String] = []
for block in topLevelBlocks {
if case .heading(1, let text) = block {
outline.append(text.rawDescription)
}
}
return outline
}
let headers = topLevelHeaders(doc: markdown)
print(headers)
这将打印一个包含以下两个条目的数组
["First Header", "Second Header"]
Swift MarkdownKit 目前提供两种不同的生成器,即 Markdown 处理器,它为给定的 Markdown 文档输出相应格式的表示。
HtmlGenerator
定义了从 Markdown 到 HTML 的简单映射。这是一个生成器使用的示例
let html = HtmlGenerator.standard.generate(doc: markdown)
目前没有办法在继承之外自定义 HtmlGenerator
。这是一个示例,它定义了一个自定义的 HTML 生成器,该生成器使用 HTML 表格格式化 blockquote
Markdown 块
open class CustomizedHtmlGenerator: HtmlGenerator {
open override func generate(block: Block, tight: Bool = false) -> String {
switch block {
case .blockquote(let blocks):
return "<table><tbody><tr><td style=\"background: #bbb; width: 0.2em;\" />" +
"<td style=\"width: 0.2em;\" /><td>\n" +
self.generate(blocks: blocks) +
"</td></tr></tbody></table>\n"
default:
return super.generate(block: block, tight: tight)
}
}
}
Swift MarkdownKit 还附带一个属性字符串生成器。AttributedStringGenerator
在内部使用自定义的 HTML 生成器来定义从 Markdown 到 NSAttributedString
的转换。AttributedStringGenerator
的初始化器提供了许多参数,用于自定义生成的属性字符串的样式。
let generator = AttributedStringGenerator(fontSize: 12,
fontFamily: "Helvetica, sans-serif",
fontColor: "#33C",
h1Color: "#000")
let attributedStr = generator.generate(doc: markdown)
Swift MarkdownKit Xcode 项目还实现了一个 非常简单的命令行工具,用于将单个 Markdown 文本文件转换为 HTML,或者将给定目录中的所有 Markdown 文件转换为 HTML。
该工具旨在作为定制特定用例的基础。构建二进制文件的最简单方法是使用 Swift Package Manager (SPM)
> git clone https://github.com/objecthub/swift-markdownkit.git
Cloning into 'swift-markdownkit'...
remote: Enumerating objects: 70, done.
remote: Counting objects: 100% (70/70), done.
remote: Compressing objects: 100% (54/54), done.
remote: Total 70 (delta 13), reused 65 (delta 11), pack-reused 0
Unpacking objects: 100% (70/70), done.
> cd swift-markdownkit
> swift build -c release
[1/3] Compiling Swift Module 'MarkdownKit' (25 sources)
[2/3] Compiling Swift Module 'MarkdownKitProcess' (1 sources)
[3/3] Linking ./.build/x86_64-apple-macosx/release/MarkdownKitProcess
> ./.build/x86_64-apple-macosx/release/MarkdownKitProcess
usage: mdkitprocess <source> [<target>]
where: <source> is either a Markdown file or a directory containing Markdown files
<target> is either an HTML file or a directory in which HTML files are written
存在许多限制和已知问题
构建 Swift MarkdownKit 框架的组件需要以下技术。命令行工具可以使用 Swift Package Manager 编译,因此并非严格需要 Xcode。类似地,仅用于编译框架并在 Xcode 中尝试命令行工具,则不需要 Swift Package Manager。
作者:Matthias Zenger (matthias@objecthub.net)
版权 © 2019-2024 Google LLC。
请注意:这不是 Google 的官方产品。