Syntax

Swift Package Manager Twitter: @nerdsupremacist

语法

告别 Scanner 和抽象语法树吧。语法会将文本转换为你真正需要的模型。

Syntax 是一种类似 SwiftUI 的数据驱动的解析器构建 DSL。 你可以使用组合和函数式编程,以最小的努力实现自顶向下的 LL(n) 解析器。 最终的结果是一个为你期望的输出模型量身定制的解析器 😉

安装

Swift Package Manager

你可以通过 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)
            }
        }
    }
}

让我们分解一下

要使用这个解析器,你可以调用 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 自带了大多数这些结构的构建块,例如:StringLiteralIntLiteral。 因此,我们可以依赖这些。 我们可以将我们的大部分情况放在 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 文件。