diff options
Diffstat (limited to 'Sources/WmapParser/KeywordParser.swift')
| -rw-r--r-- | Sources/WmapParser/KeywordParser.swift | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/Sources/WmapParser/KeywordParser.swift b/Sources/WmapParser/KeywordParser.swift new file mode 100644 index 0000000..8db1164 --- /dev/null +++ b/Sources/WmapParser/KeywordParser.swift @@ -0,0 +1,184 @@ +/// Checks which keyword we're dealing with and routes it to the right parser. +func parseKeywordLine<T: Collection>( + _ 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) +func extractKeyword<T: Collection>(_ 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..<keywordEnd], index) +} + +/// Checks the four potential values for the stage keyword. +@inline(__always) +func isStageKeyword<T: Collection>(_ 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) +func matchesKeyword<T: Collection>(_ 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..<expected.count { + let currentKeyword = keyword[keywordIndex] + let expectedKeyword = expected[index] + + let lowercasedKeyword = + (currentKeyword >= 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. +func parseStage<T: Collection>( + _ 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. +func parseNote<T: Collection>(_ 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..<textEnd], as: UTF8.self) + return Note(text: text, coordinates: coordinates) +} + +/// Parses the comma separated labels for a group +func parseGroup<T: Collection>(_ 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..<trimEnd], as: UTF8.self) + components.append(component) + } + + componentStart = index + 1 + } + index += 1 + } + + return components.isEmpty ? nil : Group(components: components) +} + +/// Parses the component label for inertia +func parseInertia<T: Collection>(_ 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..<trimEnd], as: UTF8.self) + return Inertia(component: component) +} + +/// Parses the component label and value for inertia +func parseEvolution<T: Collection>(_ 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..<trimEnd], as: UTF8.self) + guard let value = parseDouble(bytes, signIndex + 1, end) else { return nil } + + return Evolution(component: component, value: sign * value) +} |