Swift MarkdownKit

Platform: macOS | iOS | Linux Language: Swift 5.7 IDE: Xcode 14 License: Apache

概述

Swift MarkdownKit 是一个用于解析 Markdown 格式文本的框架。它支持基于 CommonMark Markdown 规范的语法。Swift MarkdownKit 还提供了一个扩展版本的解析器,能够处理 Markdown 表格。

Swift MarkdownKit 定义了 Markdown 的抽象语法,它提供了一个用于将字符串解析为抽象语法树的解析器,并且附带了用于创建 HTML 和 属性字符串 的生成器。

使用框架

解析 Markdown

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 指的是文档的根。它包含一系列的块。在上面的例子中,文档中出现了两种不同类型的块:headingparagraphheading Case 包含一个标题级别(作为其第一个参数)和标题文本(作为第二个参数)。paragraph Case 简单地包含文本。

文本使用结构体 Text 表示,它实际上是一个 TextFragment 值的序列。TextFragment 是另一个递归定义的枚举,包含关联值。上面的示例展示了两种不同的 TextFragment Case 的使用:textstrong。Case text 表示纯文本字符串。Case strong 包含一个 Text 对象,即它封装了一个 "强标记" 的 TextFragment 值序列。

解析“扩展”Markdown

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.

配置 Markdown 解析器

MarkdownParser 支持的 Markdown 方言由两个参数定义:一系列的块解析器(每个解析器都表示为 BlockParser 的子类)和一系列的内联转换器(每个转换器都表示为 InlineTransformer 的子类)。类 MarkdownParser 的初始化器可选地接受这两种组件。默认配置(初始化器既不提供块解析器,也不提供内联转换器)能够处理基于 CommonMark 规范 的 Markdown。

由于 MarkdownParser 对象是无状态的(除了块解析器和内联转换器的配置之外),因此可以通过静态属性 MarkdownParser.standard 访问预定义的默认 MarkdownParser 对象。上面的例子使用了这个默认的解析对象。

可以通过继承 MarkdownParser 并重写类属性 defaultBlockParsersdefaultInlineTransformers 来创建具有不同配置的新 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()
}

扩展 Markdown 解析器

在 MarkdownKit 框架的 1.1 版本中,现在也可以扩展 MarkdownKit 支持的抽象语法。BlockTextFragment 枚举现在都包含一个 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

使用抽象语法树表示 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"]

将 Markdown 转换为其他格式

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 的官方产品。