Created
April 17, 2025 21:55
-
-
Save florianpircher/7434569461b529f6f1197f651ff02e38 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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