告别 Scanner 和抽象语法树吧。语法会将文本转换为你真正需要的模型。
Syntax 是一种类似 SwiftUI 的数据驱动的解析器构建 DSL。 你可以使用组合和函数式编程,以最小的努力实现自顶向下的 LL(n) 解析器。 最终的结果是一个为你期望的输出模型量身定制的解析器 😉
你可以通过 Swift Package Manager 安装 Syntax,只需将以下行添加到你的 Package.swift
文件中
import PackageDescription
let package = Package(
[...]
dependencies: [
.package(url: "https://github.com/nerdsupremacist/Syntax.git", from: "3.0.0"), // for Swift 5.7
.package(url: "https://github.com/nerdsupremacist/Syntax.git", from: "2.0.0"), // for Swift 5.4
.package(url: "https://github.com/nerdsupremacist/Syntax.git", from: "1.0.0"), // for Swift 5.3
]
)
Syntax 允许你编写完美契合你想要的模型的解析器。 例如,假设你想解析 FizzBuzz 的输出。 使用 Syntax,你可以从编写你的模型开始
enum FizzBuzzValue {
case number(Int)
case fizz
case buzz
case fizzBuzz
}
然后你可以直接编写一个解析器。 Syntax 中的解析器是结构体,它们返回一个 body
,就像 SwiftUI 中一样。
import Syntax
struct FizzBuzzParser: Parser {
var body: any Parser<[FizzBuzzValue]> {
Repeat {
Either {
IntLiteral().map { FizzBuzzValue.number($0) }
Word("FizzBuzz").map(to: FizzBuzzValue.fizzBuzz)
Word("Fizz").map(to: FizzBuzzValue.fizz)
Word("Buzz").map(to: FizzBuzzValue.buzz)
}
}
}
}
让我们分解一下
Repeat
表示它应该解析多个值。Either
表示你期望以下选项中的任何一个。IntLiteral
将匹配它看到的下一个整数文字。Word
将匹配你给定的单词,并且仅当它单独存在时才匹配。map
将解析器的值映射到其他内容。map(to:)
将值映射到一个常量,对于匹配关键字之类的东西很有用。要使用这个解析器,你可以调用 parse
函数
let text = "1 2 Fizz"
let values = try FizzBuzzParser().parse(text) // [.number(1), .number(2), .fizz]
Syntax 支持输出抽象语法树。 语法树中的所有节点都用一种类型(kind)注释。 类型将从解析器的名称自动派生,但你也可以自己指定它
import Syntax
struct FizzBuzzParser: Parser {
var body: any Parser<[FizzBuzzValue]> {
Repeat {
Either {
IntLiteral().map { FizzBuzzValue.number($0) }
Word("FizzBuzz").map(to: FizzBuzzValue.fizzBuzz).kind("keyword.fizzbuzz")
Word("Fizz").map(to: FizzBuzzValue.fizz).kind("keyword.fizz")
Word("Buzz").map(to: FizzBuzzValue.buzz).kind("keyword.buzz")
}
}
}
}
要获取 AST,你可以通过 syntaxTree
函数请求它
let text = "1 2 Fizz"
let tree = FizzBuzzParser().syntaxTree(text)
AST 是可编码的,因此你可以将其编码为 JSON。 例如
{
"startLocation": { "line": 0, "column": 0 },
"kind": "fizz.buzz",
"startOffset": 0,
"endLocation": { "line": 0, "column": 8 },
"endOffset": 8,
"children": [
{
"startLocation": { "line": 0, "column": 0 },
"kind": "int.literal",
"startOffset": 0,
"endLocation": { "line": 0, "column": 1 },
"endOffset": 1,
"value": 1
},
{
"startLocation": { "line": 0, "column": 2 },
"kind": "int.literal",
"startOffset": 2,
"endLocation": { "line": 0, "column": 3 },
"endOffset": 3,
"value": 2
},
{
"match": "Fizz",
"startLocation": { "line": 0, "column": 4 },
"kind": "keyword.fizz",
"startOffset": 4,
"endLocation": { "line": 0, "column": 8 },
"endOffset": 8
}
]
}
你可以在 Publish 站点上使用你的解析器来高亮代码,使用这个插件。
import SyntaxHighlightPublishPlugin
extension Grammar {
// define Fizz Buzz Grammar
static let fizzBuzz = Grammar(name: "FizzBuzz") {
FizzBuzzParser()
}
}
try MyPublishSite().publish(using: [
...
// use plugin and include your Grammar
.installPlugin(.syntaxHighlighting(.fizzbuzz)),
])
好的。 我听到了。 FizzBuzz 确实不算是一个挑战。 那么让我们提升一个档次,解析 JSON 吧。 为了能够解析 JSON,我们必须了解 JSON 到底是什么。 JSON 由以下内容组成:
a) 原始值,如字符串、数字、布尔值,以及 b) 对象(字典)和数组的任意组合。
因此,JSON 的一个可能的模型是
enum JSON {
case object([String : JSON])
case array([JSON])
case int(Int)
case double(Double)
case bool(Bool)
case string(String)
case null
}
Syntax 自带了大多数这些结构的构建块,例如:StringLiteral
和 IntLiteral
。 因此,我们可以依赖这些。 我们可以将我们的大部分情况放在 Either
中,它将尝试解析任何适用的情况
struct JSONParser: Parser {
var body: any Parser<JSON> {
Either {
/// TODO: Arrays and Objects
StringLiteral().map(JSON.string)
IntLiteral().map(JSON.int)
DoubleLiteral().map(JSON.double)
BooleanLiteral().map(JSON.bool)
Word("null").map(to: JSON.null)
}
}
}
你会注意到我们在每一行的末尾都放了一个 map
。 这是因为像 StringLiteral
这样的解析器将返回一个 String 而不是 JSON。 因此,我们需要将该字符串映射到 JSON。
因此,我们其余的工作将是解析对象和字面量。 然而,我们注意到的第一件事是数组和对象需要再次解析 JSON。 这种递归需要明确声明。 要递归地使用解析器,我们实现一个不同的协议,称为 RecursiveParser
struct JSONParser: RecursiveParser {
var body: any Parser<JSON> {
Either {
/// TODO: Arrays and Objects
StringLiteral().map(JSON.string)
IntLiteral().map(JSON.int)
DoubleLiteral().map(JSON.double)
BooleanLiteral().map(JSON.bool)
Word("null").map(to: JSON.null)
}
}
}
名称 RecursiveParser
非常准确地描述了它的作用。 它是一个 Parser
,其中可以包含循环。 注意: 协议 RecursiveParser
期望你的 Parser 类型也符合 Hashable
。 如果你的类型只有 Hashable
属性,则此一致性将由编译器合成。
现在,让我们开始解析这些递归定义。 我们可以从数组开始。 我们可以创建一个数组解析器,它将解析由逗号分隔的多个 JSON
值,这些值位于 [
和 ]
内。 在 Syntax 中,它看起来像这样
struct JSONArrayParser: Parser {
var body: any Parser<[JSON]> {
"["
// we can just reuse our JSON Parser here.
JSONParser()
.separated(by: ",")
"]"
}
}
很简单,对吧? 这几乎就是我们用语言描述的内容。 字典非常相似,只是我们有由逗号分隔的键值对
struct JSONDictionaryParser: Parser {
var body: any Parser<[String : JSON]> {
"{"
// Group acts kinda like parenthesis here.
// It groups the key-value pair into one parser
Group {
StringLiteral()
":"
JSONParser()
}
.separated(by: ",")
.map { values in
// put the pairs in a dictionary
return Dictionary(values) { $1 }
}
"}"
}
}
最后,我们将这两者添加到我们的 JSON 的 Either
中
struct JSONParser: RecursiveParser {
var body: any Parser<JSON> {
Either {
JSONDictionaryParser().map(JSON.object)
JSONArrayParser().map(JSON.array)
StringLiteral().map(JSON.string)
IntLiteral().map(JSON.int)
DoubleLiteral().map(JSON.double)
BooleanLiteral().map(JSON.bool)
Word("null").map(to: JSON.null)
}
}
}
let text = "[42, 1337]"
let json = try JSONParser().parse(text) // .array([.int(42), .int(1337)])
欢迎并鼓励贡献!
Syntax 在 MIT 许可证下可用。 有关更多信息,请参阅 LICENSE 文件。