extension WmapParser { /// Checks which keyword we're dealing with and routes it to the right parser. static func parseKeywordLine( _ bytes: T, _ start: Int, _ end: Int, _ context: inout KeywordParserContext ) where T.Element == UInt8, T.Index == Int { guard let (keyword, rest) = extractKeyword(bytes, start, end) else { return } if matchesKeyword(keyword, kInertiaKeyword), let inertia = parseInertia(bytes, rest, end) { context.inertias.append(inertia) } else if matchesKeyword(keyword, kEvolutionKeyword), let evolution = parseEvolution(bytes, rest, end) { context.evolutions.append(evolution) } else if matchesKeyword(keyword, kNoteKeyword), let note = parseNote(bytes, rest, end) { context.notes.append(note) } else if matchesKeyword(keyword, kGroupKeyword), let group = parseGroup(bytes, rest, end) { context.groups.append(group) } else if let stageNumber = isStageKeyword(keyword), let stage = parseStage(bytes, stageNumber, rest, end) { context.stages.append(stage) } } /// Extracts a keyword from inside brackets. + the index of the rest of the /// line. @inline(__always) static func extractKeyword(_ bytes: T, _ start: Int, _ end: Int) -> ( T.SubSequence, Int )? where T.Element == UInt8, T.Index == Int { var index = start + 1 // Skip '[' let keywordStart = index while index < end && bytes[index] != kCloseSquareBracket { index += 1 } // Find ']' if index >= end { return nil } let keywordEnd = index index += 1 // Skip whitespace after ']' index = skipWhitespace(bytes, end, index) return (bytes[keywordStart..(_ keyword: T) -> StageNumber? where T.Element == UInt8, T.Index == Int { if matchesKeyword(keyword, kIKeyword) { return .stageI } if matchesKeyword(keyword, kIIKeyword) { return .stageII } if matchesKeyword(keyword, kIIIKeyword) { return .stageIII } if matchesKeyword(keyword, kIVKeyword) { return .stageIV } return nil } /// Compares a collection of bytes against a list of bytes. /// Short-Circuits by length, only two have the same length (ii and iv) @inline(__always) static func matchesKeyword(_ keyword: T, _ expected: [UInt8]) -> Bool where T.Element == UInt8, T.Index == Int { guard keyword.count == expected.count else { return false } var keywordIndex = keyword.startIndex for index in 0..= kCapitalA && currentKeyword <= kCapitalZ) ? currentKeyword + kCapsToLowercaseDistance : currentKeyword if lowercasedKeyword != expectedKeyword { return false } keywordIndex += 1 } return true } /// Parses the stage. Assumes we got the stage number in the keyword check /// to avoid checking twice. static func parseStage( _ bytes: T, _ stageNumber: StageNumber, _ start: Int, _ end: Int ) -> Stage? where T.Element == UInt8, T.Index == Int { guard let value = parseDouble(bytes, start, end) else { return nil } return Stage(stage: stageNumber, value: value) } /// Parses the note coordinates and text. static func parseNote(_ bytes: T, _ start: Int, _ end: Int) -> Note? where T.Element == UInt8, T.Index == Int { var index = skipWhitespace(bytes, end, start) guard index < end && bytes[index] == kOpenParenthesis else { return nil } index += 1 guard let (coordinates, endIndex) = parseCoordinates(bytes, index, end) else { return nil } index = skipWhitespace(bytes, end, endIndex) let textEnd = shaveWhitespace(bytes, index, end) // Why not String(data:encoding:)? This is much faster. // swiftlint:disable:next optional_data_string_conversion let text = String(decoding: bytes[index..(_ bytes: T, _ start: Int, _ end: Int) -> Group? where T.Element == UInt8, T.Index == Int { var components = [String]() var index = start var componentStart = start while index <= end { if index == end || bytes[index] == kComma { let trimStart = skipWhitespace(bytes, index, componentStart) let trimEnd = shaveWhitespace(bytes, trimStart, index) if trimStart < trimEnd { // Why not String(data:encoding:)? This is much faster. // swiftlint:disable:next optional_data_string_conversion let component = String(decoding: bytes[trimStart..(_ bytes: T, _ start: Int, _ end: Int) -> Inertia? where T.Element == UInt8, T.Index == Int { let trimStart = skipWhitespace(bytes, end, start) let trimEnd = shaveWhitespace(bytes, trimStart, end) guard trimStart < trimEnd else { return nil } // Why not String(data:encoding:)? This is much faster. // swiftlint:disable:next optional_data_string_conversion let component = String(decoding: bytes[trimStart..(_ bytes: T, _ start: Int, _ end: Int) -> Evolution? where T.Element == UInt8, T.Index == Int { var signIndex: Int? var sign = 1.0 var index = start while index < end { if bytes[index] == kPlus || bytes[index] == kDash { signIndex = index sign = bytes[index] == 45 ? -1.0 : 1.0 break } index += 1 } guard let signIndex, signIndex > start else { return nil } let trimStart = skipWhitespace(bytes, signIndex, start) let trimEnd = shaveWhitespace(bytes, trimStart, signIndex) guard trimStart < trimEnd else { return nil } // Why not String(data:encoding:)? This is much faster. // swiftlint:disable:next optional_data_string_conversion let component = String(decoding: bytes[trimStart..