Add the PureXML.Regex namespace: a recursive-descent parser for the XSD
regex grammar (literals, ., character classes with ranges and negation, the
\d \w \s family and negations, single-char escapes, grouping, alternation,
and the ?, *, +, {n}, {n,}, {n,m} quantifiers) compiled to a Thompson NFA.
matches(_:) is whole-string as xs:pattern requires, and runs linear in the
input with no backtracking blow-up. \p{...} category escapes are not yet
supported.
Closes #59
Closes #60
Closes #30
229 lines
8.2 KiB
Swift
229 lines
8.2 KiB
Swift
/// One member of a character class: a single scalar (a possible range endpoint)
|
|
/// or a named sub-class. File-scope and private.
|
|
private enum ClassMember {
|
|
case scalar(Unicode.Scalar)
|
|
case named(PureXML.Regex.NamedClass)
|
|
case negatedNamed(PureXML.Regex.NamedClass)
|
|
}
|
|
|
|
extension PureXML.Regex {
|
|
/// Recursive-descent parser for the XML Schema regular-expression grammar,
|
|
/// producing a ``Node`` tree. Anchoring is implicit (the whole string is
|
|
/// matched), so `^` and `$` are ordinary characters.
|
|
struct RegexParser {
|
|
private let chars: [Character]
|
|
private var index = 0
|
|
|
|
private init(_ pattern: String) {
|
|
chars = Array(pattern)
|
|
}
|
|
|
|
static func parse(_ pattern: String) throws -> Node {
|
|
guard !pattern.isEmpty else { throw RegexError.empty }
|
|
var parser = RegexParser(pattern)
|
|
let node = try parser.parseAlternation()
|
|
guard parser.isAtEnd else { throw RegexError.unbalanced }
|
|
return node
|
|
}
|
|
|
|
private mutating func parseAlternation() throws -> Node {
|
|
var branches = try [parseBranch()]
|
|
while peek() == "|" {
|
|
advance()
|
|
try branches.append(parseBranch())
|
|
}
|
|
return branches.count == 1 ? branches[0] : .alternate(branches)
|
|
}
|
|
|
|
private mutating func parseBranch() throws -> Node {
|
|
var pieces: [Node] = []
|
|
while let character = peek(), character != "|", character != ")" {
|
|
try pieces.append(parsePiece())
|
|
}
|
|
if pieces.isEmpty { return .empty }
|
|
return pieces.count == 1 ? pieces[0] : .concat(pieces)
|
|
}
|
|
|
|
private mutating func parsePiece() throws -> Node {
|
|
if let character = peek(), "?*+".contains(character) || character == "{" {
|
|
throw RegexError.danglingQuantifier
|
|
}
|
|
let atom = try parseAtom()
|
|
return try applyQuantifier(to: atom)
|
|
}
|
|
|
|
private mutating func applyQuantifier(to atom: Node) throws -> Node {
|
|
switch peek() {
|
|
case "?": advance()
|
|
return .repeated(atom, min: 0, max: 1)
|
|
case "*": advance()
|
|
return .repeated(atom, min: 0, max: nil)
|
|
case "+": advance()
|
|
return .repeated(atom, min: 1, max: nil)
|
|
case "{": return try parseBoundedQuantifier(atom)
|
|
default: return atom
|
|
}
|
|
}
|
|
|
|
private mutating func parseBoundedQuantifier(_ atom: Node) throws -> Node {
|
|
advance()
|
|
let minimum = parseInteger()
|
|
var maximum: Int? = minimum
|
|
if peek() == "," {
|
|
advance()
|
|
maximum = peek() == "}" ? nil : parseInteger()
|
|
}
|
|
guard peek() == "}", let minimum else { throw RegexError.badQuantifier }
|
|
advance()
|
|
return .repeated(atom, min: minimum, max: maximum)
|
|
}
|
|
|
|
private mutating func parseAtom() throws -> Node {
|
|
guard let character = peek() else { throw RegexError.danglingQuantifier }
|
|
switch character {
|
|
case "(":
|
|
advance()
|
|
let inner = try parseAlternation()
|
|
guard peek() == ")" else { throw RegexError.unbalanced }
|
|
advance()
|
|
return inner
|
|
case "[":
|
|
return try .characters(parseClass())
|
|
case ".":
|
|
advance()
|
|
return .characters(.named(.anyButLineBreak))
|
|
case "\\":
|
|
advance()
|
|
return try .characters(parseEscape())
|
|
default:
|
|
advance()
|
|
return .characters(.single(scalar(of: character)))
|
|
}
|
|
}
|
|
|
|
// MARK: Escapes
|
|
|
|
private mutating func parseEscape() throws -> CharClass {
|
|
guard let character = peek() else { throw RegexError.badEscape("") }
|
|
advance()
|
|
if let single = Self.singleEscape(character) {
|
|
return .single(single)
|
|
}
|
|
if let member = Self.classEscape(character) {
|
|
return classFrom(member)
|
|
}
|
|
throw RegexError.badEscape(String(character))
|
|
}
|
|
|
|
private func classFrom(_ member: ClassMember) -> CharClass {
|
|
switch member {
|
|
case let .scalar(value): .single(value)
|
|
case let .named(named): CharClass(named: [named])
|
|
case let .negatedNamed(named): CharClass(negatedNamed: [named])
|
|
}
|
|
}
|
|
|
|
private static func singleEscape(_ character: Character) -> Unicode.Scalar? {
|
|
switch character {
|
|
case "n": "\n"
|
|
case "r": "\r"
|
|
case "t": "\t"
|
|
case "\\", ".", "|", "-", "^", "?", "*", "+", "{", "}", "(", ")", "[", "]":
|
|
character.unicodeScalars.first
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
private static let classEscapes: [Character: ClassMember] = [
|
|
"d": .named(.digit), "D": .negatedNamed(.digit),
|
|
"w": .named(.word), "W": .negatedNamed(.word),
|
|
"s": .named(.space), "S": .negatedNamed(.space),
|
|
"i": .named(.nameStart), "I": .negatedNamed(.nameStart),
|
|
"c": .named(.nameChar), "C": .negatedNamed(.nameChar),
|
|
]
|
|
|
|
private static func classEscape(_ character: Character) -> ClassMember? {
|
|
classEscapes[character]
|
|
}
|
|
|
|
// MARK: Character classes
|
|
|
|
private mutating func parseClass() throws -> CharClass {
|
|
advance()
|
|
var cls = CharClass()
|
|
if peek() == "^" {
|
|
advance()
|
|
cls.negated = true
|
|
}
|
|
while let character = peek(), character != "]" {
|
|
try parseClassMember(into: &cls)
|
|
}
|
|
guard peek() == "]" else { throw RegexError.badClass }
|
|
advance()
|
|
return cls
|
|
}
|
|
|
|
private mutating func parseClassMember(into cls: inout CharClass) throws {
|
|
let member = try nextClassMember()
|
|
guard case let .scalar(low) = member else {
|
|
add(member, to: &cls)
|
|
return
|
|
}
|
|
if peek() == "-", peek(1) != "]" {
|
|
advance()
|
|
guard case let .scalar(high) = try nextClassMember() else { throw RegexError.badClass }
|
|
cls.ranges.append(low ... high)
|
|
} else {
|
|
cls.ranges.append(low ... low)
|
|
}
|
|
}
|
|
|
|
private func add(_ member: ClassMember, to cls: inout CharClass) {
|
|
switch member {
|
|
case let .scalar(value): cls.ranges.append(value ... value)
|
|
case let .named(named): cls.named.append(named)
|
|
case let .negatedNamed(named): cls.negatedNamed.append(named)
|
|
}
|
|
}
|
|
|
|
private mutating func nextClassMember() throws -> ClassMember {
|
|
guard let character = peek() else { throw RegexError.badClass }
|
|
advance()
|
|
guard character == "\\" else { return .scalar(scalar(of: character)) }
|
|
guard let escaped = peek() else { throw RegexError.badClass }
|
|
advance()
|
|
if let single = Self.singleEscape(escaped) { return .scalar(single) }
|
|
if let member = Self.classEscape(escaped) { return member }
|
|
throw RegexError.badEscape(String(escaped))
|
|
}
|
|
|
|
// MARK: Cursor
|
|
|
|
private func scalar(of character: Character) -> Unicode.Scalar {
|
|
character.unicodeScalars.first ?? Unicode.Scalar(0)
|
|
}
|
|
|
|
private mutating func parseInteger() -> Int? {
|
|
var digits = ""
|
|
while let character = peek(), character.isNumber {
|
|
digits.append(character)
|
|
advance()
|
|
}
|
|
return Int(digits)
|
|
}
|
|
|
|
private var isAtEnd: Bool {
|
|
index >= chars.count
|
|
}
|
|
|
|
private func peek(_ ahead: Int = 0) -> Character? {
|
|
let target = index + ahead
|
|
return target < chars.count ? chars[target] : nil
|
|
}
|
|
|
|
private mutating func advance() {
|
|
if index < chars.count { index += 1 }
|
|
}
|
|
}
|
|
}
|