aboutsummaryrefslogtreecommitdiff
path: root/Sources/WmapParser/KeywordParser.swift
diff options
context:
space:
mode:
authorRuben Beltran del Rio <jj@r.bdr.sh>2025-12-13 01:17:52 +0100
committerRuben Beltran del Rio <jj@r.bdr.sh>2025-12-14 09:15:07 +0100
commit03f1a7389afeadf9aa2b1d62a38e5cb0a2a7e60d (patch)
tree2efefc34a61c62d55d8436180acfc3590287f5fd /Sources/WmapParser/KeywordParser.swift
Initial implementation
Diffstat (limited to 'Sources/WmapParser/KeywordParser.swift')
-rw-r--r--Sources/WmapParser/KeywordParser.swift184
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)
+}