SPM Linux

模式

Patterns 是一个用于解析表达式文法 (PEGs) 的 Swift 库。它可以用来创建类似于正则表达式(regex)和语法(用于解析器)的表达式。

有关 PEGs 的一般信息,请参阅原始论文维基百科

示例

let text = "This is a point: (43,7), so is (0, 5). But my final point is (3,-1)."

let number = ("+" / "-" / "")  digit+
let point = "("  Capture(name: "x", number)
	 ","  " "¿  Capture(name: "y", number)  ")"

struct Point: Codable {
	let x, y: Int
}

let points = try Parser(search: point).decode([Point].self, from: text)
// points == [Point(x: 43, y: 7), Point(x: 0, y: 5), Point(x: 3, y: -1)]

另请参阅

用法

模式直接在代码中定义,而不是在文本字符串中。

注意: 长模式可能会让 Swift 类型检查器思考很多,特别是长的 a / b / c / 等... 系列。为了提高构建时间,请尝试将长模式拆分为多个较短的模式。

标准 PEG

“文本”

双引号内的文本与该确切文本匹配,无需使用 \ 转义特殊字符。如果要将字符串变量 s 转换为模式,请使用 Literal(s)

OneOf(...)

这类似于正则表达式中的字符类 ([...]),并且匹配 1 个字符。OneOf("aeiouAEIOU") 匹配该字符串中的任何单个字符,OneOf("a"..."e") 匹配 "abcde" 中的任何字符。它们也可以组合使用,例如 OneOf("aeiou", punctuation, "x"..."z")。要匹配除了 ... 之外的任何字符,请使用 OneOf(not: ...)

您也可以自己实现一个

OneOf(description: "ten") { character in
	character.wholeNumberValue == 10
}

它接受一个闭包 @escaping (Character) -> Bool,并匹配闭包返回 true 的任何字符。description 参数仅在创建模式的文本表示时使用。

a • b • c

• 运算符(在美国键盘上为 Option-8,在挪威键盘上为 Option-Q)首先匹配 a,然后匹配 b,然后匹配 c。它用于从一系列其他模式创建模式。

a*

匹配 0 个或多个,尽可能多(它是贪婪的,类似于正则表达式 a*?)。因此,像 a* • a 这样的模式永远不会匹配任何内容,因为 a* 模式总是会匹配所有它可以匹配的内容,而没有为最后的 a 留下任何内容。

a+

匹配 1 个或多个,也尽可能多(类似于正则表达式 a+?)。

a¿

使 a 成为可选的,但如果可以匹配,它总是会匹配(¿ 字符在大多数键盘上是 Option-Shift-TheKeyWith?OnIt)。

a / b

这首先尝试左侧的模式。如果失败,则尝试右侧的模式。这是有序选择,一旦 a 匹配,如果表达式的后面部分失败,它将永远不会返回并尝试 b。这是 PEG 与大多数其他语法和正则表达式之间的主要区别。

&&a • b

“与谓词”首先验证 a 是否匹配,然后将输入中的位置移回 a 开始的位置,并继续执行 b。换句话说,它验证 ab 是否都从同一位置匹配。因此,要匹配一个 ASCII 字母,您可以使用 &&ascii • letter

!a • b

“非谓词”验证 a匹配,然后就像上面一样,它将输入中的位置移回 a 开始的位置,并继续执行 b。您可以将其理解为“b 且非 a”。

语法

PEG 优于正则表达式的主要优势在于它们支持递归表达式。这些表达式可以包含自身,或反过来包含它们的其他表达式。以下是如何解析简单算术表达式的方法

let arithmetic = Grammar { g in
	g.all     <- g.expr  !any
	g.expr    <- g.sum
	g.sum     <- g.product  (("+" / "-")  g.product)*
	g.product <- g.power  (("*" / "/")  g.power)*
	g.power   <- g.value  ("^"  g.power)¿
	g.value   <- digit+ / "("  g.expr  ")"
}

这将解析诸如 “1+2-3^(4*3)/2” 之类的表达式。

顶部表达式称为 first。• !any 意味着它必须匹配整个字符串,因为只有在字符串末尾才没有字符。如果要在字符串中匹配多个算术表达式,请注释掉第一个表达式。语法使用动态属性,因此表达式名称没有自动完成功能。

附加项

Swift 的 Character 的所有布尔 is... 属性都有预定义的 OneOf 模式:letterlowercaseuppercasepunctuationwhitespacenewlinehexDigitdigitasciisymbolmathSymbolcurrencySymbol

除了 wholeNumber 之外,它们都与属性的最后一部分名称相同,wholeNumber 重命名为 digit,因为 wholeNumber 听起来更像一个完整的数字而不是单个数字。

还有 alphanumeric,它是 letterdigit

any

匹配任何字符。!any 仅匹配文本的结尾。

Line()

匹配单行,不包括换行符。因此 Line() • Line() 永远不会匹配任何内容,但 Line() • "\n" • Line() 匹配 2 行。

Line.Start() 在文本的开头和任何换行符之后匹配。Line.End() 在文本的结尾和任何换行符之前匹配。它们都的长度为 0,这意味着下一个模式将从文本中的相同位置开始。

Word.Boundary()

匹配单词之前或之后的的位置。像 Line.Start()Line.End() 一样,它的长度也为 0。

a.repeat(...)

a.repeat(2) 连续匹配该模式 2 次。a.repeat(...2) 匹配 0 次、1 次或 2 次,a.repeat(2...) 匹配 2 次或更多次,a.repeat(3...6) 匹配 3 到 6 次。

Skip() • a • b

从当前位置查找 a • b 的第一个匹配项。

解析

要实际使用模式,请将其传递给 Parser

let parser = try Parser(search: a)
for match in parser.matches(in: text) {
	// ...
}

Parser(search: a) 搜索 a 的第一个匹配项。它与 Parser(Skip() • a) 相同。

.matches(in: String) 方法返回 Match 实例的惰性序列。

通常我们只对模式的部分感兴趣。您可以使用 Capture 模式为这些部分分配名称

let text = "This is a point: (43,7), so is (0, 5). But my final point is (3,-1)."

let number = ("+" / "-" / "")  digit+
let point = "("  Capture(name: "x", number)
	 ","  " "¿  Capture(name: "y", number)  ")"

struct Point: Codable {
	let x, y: Int
}

let parser = try Parser(search: point)
let points = try parser.decode([Point].self, from: text)

或者您可以使用下标

let pointsAsSubstrings = parser.matches(in: text).map { match in
	(text[match[one: "x"]!], text[match[one: "y"]!])
}

您还可以使用 match[multiple: name] 来获取数组(如果具有该名称的捕获可能会多次匹配)。match[one: name] 仅返回该名称的第一个捕获。

输入

默认情况下,模式的输入类型为 String。但是您可以将任何具有 Hashable 元素的 BidirectionalCollection 用于输入。只需显式指定第一个模式的输入类型,其余模式应该会自动获取它

let text = "This is a point: (43,7), so is (0, 5). But my final point is (3,-1).".utf8

let digit = OneOf<String.UTF8View>(UInt8(ascii: "0")...UInt8(ascii: "9"))
let number = ("+" / "-" / "")  digit+
let point = "("  Capture(name: "x", number)
	 ","  " "¿  Capture(name: "y", number)  ")"

struct Point: Codable {
	let x, y: Int
}

let parser = try Parser(search: point)
let pointsAsSubstrings = parser.matches(in: text).map { match in
	(text[match[one: "x"]!], text[match[one: "y"]!])
}

Parser.decode(目前)只能将 String 作为输入,但 .matches 处理所有类型。

设置

Swift Package Manager

将此添加到您的 Package.swift 文件中

dependencies: [
    .package(url: "https://github.com/kareman/Patterns.git", from: "0.1.0"),
]

或从 Xcode 中选择“Add Package Dependency”。

实现

Patterns 是使用虚拟解析机实现的,类似于 LPEG实现方式,以及 此处 描述的 backtrackingvm 函数。

贡献

非常欢迎贡献 🙌。

许可证

MIT

Patterns
Copyright © 2019

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.