Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save florianpircher/7434569461b529f6f1197f651ff02e38 to your computer and use it in GitHub Desktop.
Save florianpircher/7434569461b529f6f1197f651ff02e38 to your computer and use it in GitHub Desktop.
import Foundation
public enum PropertyList {
case string(String)
case data(ContiguousArray<UInt8>)
case array([PropertyList])
case dictionary([String: PropertyList])
}
let data = try Data(contentsOf: URL(fileURLWithPath: "/Users/Florian/Desktop/File.plist"))
let plist = try data.withUnsafeBytes {
try PropertyList(decoding: $0.assumingMemoryBound(to: UInt8.self))
}
extension PropertyList {
// MARK: - Parsing Helpers
@usableFromInline
enum Parsing {
@usableFromInline
static func decodeHexadecimalValue(_ digit: UInt8) -> UInt8? {
if digit >= 0x30 && digit <= 0x39 {
digit - 0x30
}
else if digit >= 0x41 && digit <= 0x46 {
10 + (digit - 0x41)
}
else if digit >= 0x61 && digit <= 0x66 {
10 + (digit - 0x61)
}
else {
nil
}
}
@usableFromInline
static func skipTrivia(parser: inout Parser<UnsafeBufferPointer<UInt8>>) throws(ContentError) {
while let c = parser.peek() {
if (c >= 0x09 && c <= 0x0D) || c == 0x20 {
// ASCII whitespace: \t, \n, vertical tab, form feed, \r, or space
parser.advance()
}
else if c == 0xE2, let (_, a, b) = parser.peek(), a == 0x80, b == 0xA8 || b == 0xA9 {
// Unicode whitespace: U+2028 LINE SEPARATOR or U+2029 PARAGRAPH SEPARATOR
parser.advance(by: 3)
}
else if c == 0x2F {
// comment
parser.advance()
switch parser.pop() {
case 0x2F:
// single-line comment
parser.advance { c, parser in
if c == 0x0A || c == 0x0D {
// ASCII line break
false
}
else if c == 0xE2, let (_, a, b) = parser.peek(), a == 0x80, b == 0xA8 || b == 0xA9 {
// Unicode line break: U+2028 LINE SEPARATOR or U+2029 PARAGRAPH SEPARATOR
false
}
else {
true
}
}
case 0x2A:
// multi-line comment
parser.advance { c, parser in
if c == 0x2A, let (_, b) = parser.peek(), b == 0x2F {
false
}
else {
true
}
}
guard parser.pop(0x2A) && parser.pop(0x2F) else {
throw PropertyList.ContentError.missingCommentEnd
}
case let char?:
throw PropertyList.ContentError.illegalCommentStart(char)
case nil:
throw PropertyList.ContentError.incompleteCommentStart
}
}
else {
break
}
}
}
@usableFromInline
static func skipWhitespace(parser: inout Parser<UnsafeBufferPointer<UInt8>>) {
while let c = parser.peek() {
if (c >= 0x09 && c <= 0x0D) || c == 0x20 {
// ASCII whitespace: \t, \n, vertical tab, form feed, \r, or space
parser.advance()
}
else if c == 0xE2, let (_, a, b) = parser.peek(), a == 0x80, b == 0xA8 || b == 0xA9 {
// Unicode whitespace: U+2028 LINE SEPARATOR or U+2029 PARAGRAPH SEPARATOR
parser.advance(by: 3)
}
else {
break
}
}
}
/// Returns whether a code point is valid in an unquoted string literal.
///
/// This implementation differs from the standard property list format by also allowing the plus symbol.
@inlinable
public static func isUnquotedStringCharacter(codePoint c: UInt8) -> Bool {
// a-z, A-Z, - . / 0-9 :, _, $, +
(c >= 0x61 && c <= 0x7A) || (c >= 0x41 && c <= 0x5A) || (c >= 0x2D && c <= 0x3A) || c == 0x5F || c == 0x24 || c == 0x2B
}
@inlinable
public static func parsePropertyList(
parser: inout Parser<UnsafeBufferPointer<UInt8>>,
keySubset: Set<String>?,
isSkipping: Bool,
) throws(PropertyList.ContentError) -> PropertyList {
try PropertyList.Parsing.skipTrivia(parser: &parser)
guard let head = parser.peek() else {
throw PropertyList.ContentError.missingContent
}
switch head {
case 0x28:
parser.advance()
var array: [PropertyList]? = isSkipping ? nil : []
try PropertyList.Parsing.skipTrivia(parser: &parser)
while parser.peek() != 0x29 {
let value = try parsePropertyList(parser: &parser, keySubset: nil, isSkipping: isSkipping)
array?.append(value)
try PropertyList.Parsing.skipTrivia(parser: &parser)
if parser.pop(0x2C) {
try PropertyList.Parsing.skipTrivia(parser: &parser)
}
else {
break
}
}
guard parser.pop(0x29) else {
throw PropertyList.ContentError.missingClosingParenthesis
}
try PropertyList.Parsing.skipTrivia(parser: &parser)
if let array {
return .array(array)
}
else {
return .string("")
}
case 0x7B:
parser.advance()
var dictionary: [String: PropertyList]? = isSkipping ? nil : [:]
try PropertyList.Parsing.skipTrivia(parser: &parser)
while parser.peek() != 0x7D {
let key = try parsePropertyList(parser: &parser, keySubset: nil, isSkipping: isSkipping)
guard case .string(let keyString) = key else {
throw PropertyList.ContentError.nonStringKey
}
let nestedIsSkipping = isSkipping || (keySubset.map { !$0.contains(keyString) } ?? false)
try PropertyList.Parsing.skipTrivia(parser: &parser)
guard parser.pop(0x3D) else {
throw PropertyList.ContentError.missingEqualSignInDictionary
}
try PropertyList.Parsing.skipTrivia(parser: &parser)
let value = try parsePropertyList(parser: &parser, keySubset: nil, isSkipping: nestedIsSkipping)
try PropertyList.Parsing.skipTrivia(parser: &parser)
guard parser.pop(0x3B) else {
throw PropertyList.ContentError.missingSemicolonInDictionary
}
try PropertyList.Parsing.skipTrivia(parser: &parser)
if !nestedIsSkipping {
dictionary?[keyString] = value
}
}
guard parser.pop(0x7D) else {
throw PropertyList.ContentError.missingClosingBrace
}
try PropertyList.Parsing.skipTrivia(parser: &parser)
if let dictionary {
return .dictionary(dictionary)
}
else {
return .string("")
}
case 0x22, 0x27:
var buffer = ""
parser.advance()
while true {
let chunk = parser.read(while: { $0 != head && $0 != 0x5C })
if !isSkipping {
let stringChunk = String(decoding: chunk, as: UTF8.self)
buffer.append(stringChunk)
}
guard let stopChar = parser.pop() else {
throw PropertyList.ContentError.missingClosingQuote
}
if stopChar == head {
break
}
else if stopChar == 0x5C {
guard let specialChar = parser.pop() else {
throw PropertyList.ContentError.missingClosingQuote
}
let octalRange: ClosedRange<UInt8> = 0x30 ... 0x37
switch specialChar {
case 0x5C:
if !isSkipping { buffer.append("\\" as Character) }
case 0x61:
if !isSkipping { buffer.append("\u{07}" as Character) }
case 0x62:
if !isSkipping { buffer.append("\u{08}" as Character) }
case 0x65:
if !isSkipping { buffer.append("\u{1B}" as Character) }
case 0x66:
if !isSkipping { buffer.append("\u{0C}" as Character) }
case 0x6E:
if !isSkipping { buffer.append("\n" as Character) }
case 0x72:
if !isSkipping { buffer.append("\r" as Character) }
case 0x74:
if !isSkipping { buffer.append("\t" as Character) }
case 0x76:
if !isSkipping { buffer.append("\u{0B}" as Character) }
case 0x0A:
if !isSkipping { buffer.append("\n" as Character) }
case octalRange:
let a1 = specialChar - 0x30
// 0oX == 0bABC
var codePoint = a1
if let d2 = parser.pop(), octalRange.contains(d2) {
let a2 = d2 - 0x30
// 0oXY = 0bABC_DEF
codePoint = (codePoint << 3) | a2
if let d3 = parser.pop(), octalRange.contains(d3) {
let a3 = d3 - 0x30
if a1 >= 0b10 {
if a1 >= 0b100 {
// 'A' bit is 1 => 9 bits required => overflow
throw PropertyList.ContentError.octalCodeOverflowStringEscapeSequence(a1, a2, a3)
}
else {
// 'B' bit is 1 => 8 bits => not ASCII
throw PropertyList.ContentError.nonASCIIOctalCodeStringEscapeSequence(a1, a2, a3)
}
}
// 0oXYZ = 0b00C_DEF_GHI
codePoint = (codePoint << 3) | a3
}
}
if !isSkipping {
buffer.append(Character(Unicode.Scalar(codePoint)))
}
case 0x55:
guard let d1 = parser.pop(),
let a1 = Parsing.decodeHexadecimalValue(d1),
let d2 = parser.pop(),
let a2 = Parsing.decodeHexadecimalValue(d2),
let d3 = parser.pop(),
let a3 = Parsing.decodeHexadecimalValue(d3),
let d4 = parser.pop(),
let a4 = Parsing.decodeHexadecimalValue(d4) else {
throw PropertyList.ContentError.incompleteHexadecimalCodeStringEscapeSequence
}
let codePoint = UInt16(a1) << 12 | UInt16(a2) << 8 | UInt16(a3) << 4 | UInt16(a4)
guard let scalar = Unicode.Scalar(codePoint) else {
throw PropertyList.ContentError.nonUnicodeScalarHexadecimalCodeStringEscapeSequence(codePoint)
}
if !isSkipping {
buffer.append(Character(scalar))
}
default:
if !isSkipping {
buffer.append(Character(Unicode.Scalar(specialChar)))
}
}
}
}
return .string(buffer)
case 0x3C:
parser.advance()
PropertyList.Parsing.skipWhitespace(parser: &parser)
var buffer: ContiguousArray<UInt8>? = isSkipping ? nil : ContiguousArray<UInt8>()
while let d1 = parser.peek(), d1 != 0x3E {
parser.advance()
guard let a1 = Parsing.decodeHexadecimalValue(d1) else {
throw PropertyList.ContentError.nonHexadecimalHighByteData(d1)
}
PropertyList.Parsing.skipWhitespace(parser: &parser)
guard let d2 = parser.pop(), d2 != 0x3E else {
throw PropertyList.ContentError.missingHexadecimalLowByteData
}
guard let a2 = Parsing.decodeHexadecimalValue(d2) else {
throw PropertyList.ContentError.nonHexadecimalLowByteData(d2)
}
buffer?.append((a1 << 4) | a2)
PropertyList.Parsing.skipWhitespace(parser: &parser)
}
guard parser.pop() == 0x3E else {
throw PropertyList.ContentError.missingDataEnd
}
if let buffer {
return .data(buffer)
}
else {
return .string("")
}
default:
if Parsing.isUnquotedStringCharacter(codePoint: head) {
let stringBytes = parser.read(while: Parsing.isUnquotedStringCharacter(codePoint:))
if !isSkipping {
return .string(String(decoding: stringBytes, as: UTF8.self))
}
else {
return .string("")
}
}
else {
throw PropertyList.ContentError.illegalContent(head)
}
}
}
}
// MARK: - Main Parsing Code
/// Reads a property list value from the given bytes.
///
/// - Throws: `DecodingError` if parsing failed.
@inlinable
public init(decoding bytes: UnsafeBufferPointer<UInt8>) throws(DecodingError) {
var parser = Parser(subject: bytes)
do {
self = try Parsing.parsePropertyList(parser: &parser, keySubset: ["DisplayStrings"], isSkipping: false)
try Parsing.skipTrivia(parser: &parser)
guard parser.isAtEnd else {
throw ContentError.oversuppliedContent
}
}
catch let error as ContentError {
let subject = parser.subject
let headIndex = parser.position
let line = subject[subject.startIndex ..< headIndex].count { $0 == 0x0A } + 1
let lastLineFeed = subject[subject.startIndex ..< headIndex].lastIndex(of: 0x0A)
let column = if let lastLineFeed {
subject.distance(from: lastLineFeed, to: headIndex)
}
else {
subject.distance(from: subject.startIndex, to: headIndex) + 1
}
throw DecodingError(
contentError: error,
line: line,
column: column)
}
catch {
preconditionFailure("unreachable")
}
}
// MARK: - Errors
public enum ContentError: Error, Equatable {
case missingContent
case illegalContent(UInt8)
case oversuppliedContent
case nonUTF8StringContents
case octalCodeOverflowStringEscapeSequence(UInt8, UInt8, UInt8)
case nonASCIIOctalCodeStringEscapeSequence(UInt8, UInt8, UInt8)
case incompleteHexadecimalCodeStringEscapeSequence
case nonUnicodeScalarHexadecimalCodeStringEscapeSequence(UInt16)
case nonHexadecimalHighByteData(UInt8)
case missingHexadecimalLowByteData
case nonHexadecimalLowByteData(UInt8)
case missingDataEnd
case missingClosingQuote
case missingClosingParenthesis
case missingClosingBrace
case nonStringKey
case missingEqualSignInDictionary
case missingSemicolonInDictionary
case incompleteCommentStart
case illegalCommentStart(UInt8)
case missingCommentEnd
public var errorDescription: String? {
switch self {
case .missingContent:
"Missing content"
case .illegalContent(let byte):
"Illegal character 0x\(String(byte, radix: 16, uppercase: true))"
case .oversuppliedContent:
"Parsing ended before end of file"
case .nonUTF8StringContents:
"String value contains invalid UTF-8 data"
case .octalCodeOverflowStringEscapeSequence(let o1, let o2, let o3):
"Octal code overflow; 9 bits would be required to represent 0o\(String((UInt16(o1) << 6) | (UInt16(o2) << 3) | UInt16(o3), radix: 8))"
case .nonASCIIOctalCodeStringEscapeSequence(let o1, let o2, let o3):
"Non-ASCII octal code; 8 bits would be required to represent 0o\(String((UInt16(o1) << 6) | (UInt16(o2) << 3) | UInt16(o3), radix: 8))"
case .incompleteHexadecimalCodeStringEscapeSequence:
"Incomplete hexadecimal code; expected four hexadecimal digits to follow \\U"
case .nonUnicodeScalarHexadecimalCodeStringEscapeSequence(let value):
"Hexadecimal value does no represent Unicode scalar: 0x\(String(value, radix: 16, uppercase: true))"
case .nonHexadecimalHighByteData(let byte):
"Non-hexadecimal high byte in data value: 0x\(String(byte, radix: 16, uppercase: true))"
case .missingHexadecimalLowByteData:
"Missing low byte in hexadecimal data value"
case .nonHexadecimalLowByteData(let byte):
"Non-hexadecimal low byte in data value: 0x\(String(byte, radix: 16, uppercase: true))"
case .missingDataEnd:
"Missing closing angle bracket (greater-than sign); data value not properly terminated"
case .missingClosingQuote:
"Missing closing quote; string not properly terminated"
case .missingClosingParenthesis:
"Missing closing parenthesis; array not properly terminated (or: missing separator comma)"
case .missingClosingBrace:
"Missing closing curly brace; dictionary not properly terminated"
case .nonStringKey:
"Dictionary key is not a string value"
case .missingEqualSignInDictionary:
"Missing equals sign following key in dictionary"
case .missingSemicolonInDictionary:
"Missing semicolon following value in dictionary"
case .incompleteCommentStart:
"Incomplete comment start"
case .illegalCommentStart(let char):
"Comment requires `*` or `/` after first `/`, got 0x\(String(char, radix: 16, uppercase: true)) instead."
case .missingCommentEnd:
"Missing comment end; comment not properly terminated"
}
}
}
public struct DecodingError: Error, Equatable {
public let contentError: ContentError
public let line: Int
public let column: Int
@usableFromInline
init(contentError: ContentError, line: Int, column: Int) {
self.contentError = contentError
self.line = line
self.column = column
}
public var errorDescription: String? {
"[\(self.line):\(self.column)] \(self.contentError.errorDescription ?? String(describing: self.contentError))"
}
}
}
// MARK: - Parser Type
/// A parser for accessing the elements of a collection.
public struct Parser<Subject: Collection>: ~Copyable {
public typealias Index = Subject.Index
public typealias Element = Subject.Element
public typealias SubSequence = Subject.SubSequence
/// The collection that is parsed by the parser.
public let subject: Subject
/// The index of the current element, which is the next element to be parsed.
public var position: Index
/// Creates a parser for parsing the given collection.
///
/// The initial position is set to the start index of the collection.
@inlinable
public init(subject: Subject) {
self.subject = subject
self.position = subject.startIndex
}
/// Creates a parser for parsing the given collection from the given position.
@inlinable
public init(subject: Subject, position: Index) {
self.subject = subject
self.position = position
}
// MARK: State
/// Whether the parser is at the end of the subject and no more elements can be read.
@inlinable
public var isAtEnd: Bool {
self.position == self.subject.endIndex
}
/// Returns the currently unparsed part of the subject, or an empty subsequence if the parser is at the end of the subject.
@inlinable
public func remainder() -> SubSequence {
guard !self.isAtEnd else {
return self.subject[self.subject.endIndex ..< self.subject.endIndex] // at end: return empty subsequence
}
return self.subject[self.position ..< self.subject.endIndex]
}
// MARK: Advance
/// Advances the parser by one element.
///
/// > Important: Only call this method if the parser is not at the end.
/// > Otherwise, the call will result in a fatal error.
@inlinable
public mutating func advance() {
self.position = self.subject.index(after: self.position)
}
/// Advances the parser by the given number of elements.
///
/// > Important: Only call this method if advancing the parser by the given distance does not move past the start or end of the subject.
/// > Otherwise, the call will result in a fatal error.
@inlinable
public mutating func advance(by distance: Int) {
self.position = self.subject.index(self.position, offsetBy: distance)
}
/// Advances the parser while `predicate` returns `true`.
///
/// If the parser reaches the end of the subject, it will stop advancing.
///
/// ## Examples
///
/// Skip whitespace:
///
/// ```swift
/// parser.advance(while: \.isWhitespace)
/// ```
///
/// Skip letters until an `x` is encountered:
///
/// ```swift
/// parser.advance(while: { $0.isLetter && $0 != "x" })
/// ```
///
/// - Parameter predicate: A closure that takes the current element as its argument and returns whether the parser should advance past that element.
@inlinable
public mutating func advance<E>(while predicate: (Element) throws(E) -> Bool) throws(E) {
while let element = self.peek(), try predicate(element) {
self.advance()
}
}
/// Advances the parser while `predicate` returns `true`, providing the parser for inspection.
///
/// If the parser reaches the end of the subject, it will stop advancing.
///
/// - Parameter predicate: A closure that takes the current element as its argument and returns whether the parser should advance past that element. The second parameter is the parser itself, borrowed for further inspection of the subject.
@inlinable
public mutating func advance<E>(
while predicate: (
_ element: Element,
_ parser: borrowing Self
) throws(E) -> Bool
) throws(E) {
while let element = self.peek(), try predicate(element, self) {
self.advance()
}
}
/// Advances the parser by matching the given regex against the prefix of the remainder of the subject.
///
/// If the regex does not match the prefix of the remainder of the subject, the parser will not be advanced.
///
/// - Parameter regex: The regex to match.
/// - Throws: This method can throw an error if this regex includes a transformation closure that throws an error.
@available(macOS 13.0, *, iOS 16.0, *, tvOS 16.0, *, watchOS 9.0, *)
@inlinable
public mutating func advance(matching regex: some RegexComponent) throws where SubSequence == Substring {
guard let match = try regex.regex.prefixMatch(in: self.remainder()) else {
return
}
self.position = match.range.upperBound
}
// MARK: Peek
/// Returns the current element, or `nil` if the parser is at the end of the subject.
@inlinable
public func peek() -> Element? {
guard !self.isAtEnd else {
return nil
}
return self.subject[self.position]
}
/// Returns the next two elements, if available.
@_disfavoredOverload
@inlinable
public func peek() -> (Element, Element)? {
guard let a = self.peek() else {
return nil
}
let bIndex = self.subject.index(after: self.position)
guard bIndex < self.subject.endIndex else {
return nil
}
return (a, self.subject[bIndex])
}
/// Returns the next three elements, if available.
@_disfavoredOverload
@inlinable
public func peek() -> (Element, Element, Element)? {
guard let a = self.peek() else {
return nil
}
let bIndex = self.subject.index(after: self.position)
guard bIndex < self.subject.endIndex else {
return nil
}
let cIndex = self.subject.index(after: bIndex)
guard cIndex < self.subject.endIndex else {
return nil
}
return (a, self.subject[bIndex], self.subject[cIndex])
}
// MARK: Has Prefix
/// Returns whether the current element is equal to the given element.
@inlinable
public func hasPrefix(_ element: Element) -> Bool where Element: Equatable {
self.peek() == element
}
/// Returns whether the remainder of the subject starts with the given prefix.
@_disfavoredOverload
@inlinable
public func hasPrefix(_ prefix: some StringProtocol) -> Bool where Subject: StringProtocol {
self.remainder().hasPrefix(prefix)
}
/// Returns whether the remainder of the subject starts with the given prefix.
@_disfavoredOverload
@inlinable
public func hasPrefix(_ prefix: SubSequence) -> Bool where SubSequence: Equatable {
self.remainder().prefix(prefix.count) == prefix
}
/// Returns whether the prefix of the remainder of the subject matches the given regex.
///
/// If the regex includes a transformation closure that throws an error, the error will be ignored and `false` will be returned.
///
/// - Parameter regex: The regex to match.
@available(macOS 13.0, *, iOS 16.0, *, tvOS 16.0, *, watchOS 9.0, *)
@_disfavoredOverload
@inlinable
public func hasPrefix(_ regex: some RegexComponent) -> Bool where SubSequence == Substring {
self.remainder().prefixMatch(of: regex) != nil
}
// MARK: Pop
/// Returns the current element and advances the parser, or returns `nil` if the parser is at the end of the subject.
@inlinable
public mutating func pop() -> Element? {
guard let element = self.peek() else {
return nil
}
self.advance()
return element
}
/// Returns whether the current element is equal to the given element, and advances the parser by that element if so.
@inlinable
public mutating func pop(_ element: Element) -> Bool where Element: Equatable {
if self.hasPrefix(element) {
self.advance()
return true
}
return false
}
/// Returns the current element if it matches the given predicate, and advances the parser by that element if so.
///
/// - Returns: The current element if it matches the given predicate, or `nil` if the parser is at the end of the subject or the current element does not match the given predicate.
@inlinable
public mutating func pop<E>(where predicate: (Element) throws(E) -> Bool) throws(E) -> Element? {
guard let element = self.peek(), try predicate(element) else {
return nil
}
self.advance()
return element
}
// MARK: Read
/// Returns a prefix of the remainder of the subject matching the given element count and advances the parser, or `nil`, if the remainder is shorter then the count.
@inlinable
public mutating func read(count: UInt) -> SubSequence? {
let count = Int(count)
let sliceStartIndex = self.position
guard let sliceEndIndex = self.subject.index(sliceStartIndex, offsetBy: count, limitedBy: self.subject.endIndex) else {
return nil
}
self.advance(by: count)
return self.subject[sliceStartIndex ..< sliceEndIndex]
}
/// Returns whether the remainder of the subject starts with the given prefix, and advances the parser by that prefix if so.
@inlinable
public mutating func read(_ subsequence: SubSequence) -> Bool where SubSequence: Equatable {
if self.hasPrefix(subsequence) {
self.advance(by: subsequence.count)
return true
}
return false
}
/// Returns whether the remainder of the subject starts with the given prefix, and advances the parser by that prefix if so.
@_disfavoredOverload
@inlinable
public mutating func read(_ string: some StringProtocol) -> Bool where Subject: StringProtocol {
if self.hasPrefix(string) {
self.advance(by: string.count)
return true
}
return false
}
/// Returns a prefix containing the elements until `predicate` returns `false`, and advances the parser by that prefix if so.
///
/// If the parser reaches the end of the subject, it will stop reading.
///
/// - Parameter predicate: A closure that takes the current element as its argument and returns whether the parser should advance past that element.
@inlinable
public mutating func read<E>(while predicate: (Element) throws(E) -> Bool) throws(E) -> SubSequence {
let prefix: SubSequence
do {
prefix = try self.remainder().prefix(while: predicate)
}
catch let error as E {
throw error
}
catch {
preconditionFailure("unreachable")
}
self.advance(by: prefix.count)
return prefix
}
/// Returns the match of the given regex if it matches the prefix of the remainder of the subject, and advances the parser by the match if so.
///
/// - Parameter regex: The regex to match.
/// - Throws: This method can throw an error if this regex includes a transformation closure that throws an error.
@available(macOS 13.0, *, iOS 16.0, *, tvOS 16.0, *, watchOS 9.0, *)
@_disfavoredOverload
@inlinable
public mutating func read<R: RegexComponent>(_ regex: R) throws -> Regex<R.RegexOutput>.Match? where SubSequence == Substring {
guard let match = try regex.regex.prefixMatch(in: self.remainder()) else {
return nil
}
self.position = match.range.upperBound
return match
}
// MARK: View
/// Creates a nested parser that parsed a view of the subject.
///
/// The view must share indices with the subject.
/// Most importantly, the current parser position must be valid for the view as it will be used as the initial position of the nested parser.
///
/// The nested parser is provided to the given function.
/// The return value and thrown errors are returned/rethrown by this function.
/// After returning or throwing, the position of the nested parser is set as the position of the original parser.
///
/// ## Examples
///
/// ```swift
/// let string = "café."
/// var parser = Parser(subject: string.utf8)
/// let word = parser.withView(string.unicodeScalars) { parser in
/// String(parser.read(while: { $0.properties.isAlphabetic }))
/// }
/// assert(word == "café")
/// ```
@inlinable
public mutating func withView<E, R, View>(
_ view: View,
_ code: (inout Parser<View>) throws(E) -> R
) throws(E) -> R where View: Collection, View.Index == Index {
var subParser = Parser<View>(subject: view, position: self.position)
defer {
self.position = subParser.position
}
return try code(&subParser)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment