From 0b90075f76b6f2df77c01c303322323b1395641a Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Sun, 14 Dec 2025 16:16:22 +0100 Subject: Use WmapParser namespace --- Sources/WmapParser/Component.swift | 6 - Sources/WmapParser/ComponentParser.swift | 127 +++---- Sources/WmapParser/Constants.swift | 64 ++-- Sources/WmapParser/DataStructures/Component.swift | 8 + Sources/WmapParser/DataStructures/Dependency.swift | 8 + Sources/WmapParser/DataStructures/Evolution.swift | 7 + Sources/WmapParser/DataStructures/Group.swift | 6 + Sources/WmapParser/DataStructures/Inertia.swift | 6 + Sources/WmapParser/DataStructures/Map.swift | 12 + Sources/WmapParser/DataStructures/MapPoint.swift | 7 + Sources/WmapParser/DataStructures/Note.swift | 7 + Sources/WmapParser/DataStructures/Shape.swift | 10 + Sources/WmapParser/DataStructures/Stage.swift | 7 + .../WmapParser/DataStructures/StageNumber.swift | 10 + Sources/WmapParser/Dependency.swift | 6 - Sources/WmapParser/DependencyParser.swift | 88 ++--- Sources/WmapParser/Evolution.swift | 5 - Sources/WmapParser/Group.swift | 4 - Sources/WmapParser/Inertia.swift | 4 - Sources/WmapParser/KeywordParser.swift | 364 +++++++++++---------- Sources/WmapParser/KeywordParserContext.swift | 7 - Sources/WmapParser/Map.swift | 10 - Sources/WmapParser/MapPoint.swift | 5 - Sources/WmapParser/Note.swift | 5 - .../ParserContext/KeywordParserContext.swift | 9 + .../ParserContext/LineParserContext.swift | 7 + Sources/WmapParser/Shape.swift | 8 - Sources/WmapParser/Stage.swift | 5 - Sources/WmapParser/StageNumber.swift | 8 - Sources/WmapParser/Utils.swift | 272 +++++++-------- Sources/WmapParser/WmapParser.swift | 129 ++++---- 31 files changed, 630 insertions(+), 591 deletions(-) delete mode 100644 Sources/WmapParser/Component.swift create mode 100644 Sources/WmapParser/DataStructures/Component.swift create mode 100644 Sources/WmapParser/DataStructures/Dependency.swift create mode 100644 Sources/WmapParser/DataStructures/Evolution.swift create mode 100644 Sources/WmapParser/DataStructures/Group.swift create mode 100644 Sources/WmapParser/DataStructures/Inertia.swift create mode 100644 Sources/WmapParser/DataStructures/Map.swift create mode 100644 Sources/WmapParser/DataStructures/MapPoint.swift create mode 100644 Sources/WmapParser/DataStructures/Note.swift create mode 100644 Sources/WmapParser/DataStructures/Shape.swift create mode 100644 Sources/WmapParser/DataStructures/Stage.swift create mode 100644 Sources/WmapParser/DataStructures/StageNumber.swift delete mode 100644 Sources/WmapParser/Dependency.swift delete mode 100644 Sources/WmapParser/Evolution.swift delete mode 100644 Sources/WmapParser/Group.swift delete mode 100644 Sources/WmapParser/Inertia.swift delete mode 100644 Sources/WmapParser/KeywordParserContext.swift delete mode 100644 Sources/WmapParser/Map.swift delete mode 100644 Sources/WmapParser/MapPoint.swift delete mode 100644 Sources/WmapParser/Note.swift create mode 100644 Sources/WmapParser/ParserContext/KeywordParserContext.swift create mode 100644 Sources/WmapParser/ParserContext/LineParserContext.swift delete mode 100644 Sources/WmapParser/Shape.swift delete mode 100644 Sources/WmapParser/Stage.swift delete mode 100644 Sources/WmapParser/StageNumber.swift (limited to 'Sources/WmapParser') diff --git a/Sources/WmapParser/Component.swift b/Sources/WmapParser/Component.swift deleted file mode 100644 index 027ac59..0000000 --- a/Sources/WmapParser/Component.swift +++ /dev/null @@ -1,6 +0,0 @@ -/// A component in the map. -public struct Component: Sendable, Equatable { - public let label: String - public let coordinates: MapPoint - public let shape: Shape -} diff --git a/Sources/WmapParser/ComponentParser.swift b/Sources/WmapParser/ComponentParser.swift index c899637..66979eb 100644 --- a/Sources/WmapParser/ComponentParser.swift +++ b/Sources/WmapParser/ComponentParser.swift @@ -1,78 +1,81 @@ -/// Parses a component with its coordinates. -func parseComponent(_ bytes: T, _ start: Int, _ end: Int) -> Component? -where T.Element == UInt8, T.Index == Int { - guard let (label, parenthesisIndex) = extractComponentLabel(bytes, start, end) else { - return nil - } +extension WmapParser { - guard - let (coordinates, endIndex) = parseCoordinates( - bytes, parenthesisIndex + 1, end) - else { - return nil - } + /// Parses a component with its coordinates. + static func parseComponent(_ bytes: T, _ start: Int, _ end: Int) -> Component? + where T.Element == UInt8, T.Index == Int { + guard let (label, parenthesisIndex) = extractComponentLabel(bytes, start, end) else { + return nil + } - let shape = extractComponentShape(bytes, endIndex, end) + guard + let (coordinates, endIndex) = parseCoordinates( + bytes, parenthesisIndex + 1, end) + else { + return nil + } - return Component(label: label, coordinates: coordinates, shape: shape) -} + let shape = extractComponentShape(bytes, endIndex, end) -/// Looks for a component label by taking the string before the first open -/// parenthesis. -@inline(__always) -private func extractComponentLabel(_ bytes: T, _ start: Int, _ end: Int) -> ( - String, Int -)? -where T.Element == UInt8, T.Index == Int { - var index = start - while index < end { - if bytes[index] == kOpenParenthesis { - guard index > start else { return nil } - guard let label = extractTrimmedString(bytes, start, index) else { return nil } - return (label, index) + return Component(label: label, coordinates: coordinates, shape: shape) } - index += 1 - } - return nil -} -/// Extracts a shape by looking for a shape label in square brackets. -/// If missing, incomplete or invalid, it returns a circle -@inline(__always) -private func extractComponentShape(_ bytes: T, _ start: Int, _ end: Int) -> Shape -where T.Element == UInt8, T.Index == Int { - var index = skipWhitespace(bytes, end, start) + /// Looks for a component label by taking the string before the first open + /// parenthesis. + @inline(__always) + private static func extractComponentLabel(_ bytes: T, _ start: Int, _ end: Int) -> ( + String, Int + )? + where T.Element == UInt8, T.Index == Int { + var index = start + while index < end { + if bytes[index] == kOpenParenthesis { + guard index > start else { return nil } + guard let label = extractTrimmedString(bytes, start, index) else { return nil } + return (label, index) + } + index += 1 + } + return nil + } - guard index < end && bytes[index] == kOpenSquareBracket else { return .circle } + /// Extracts a shape by looking for a shape label in square brackets. + /// If missing, incomplete or invalid, it returns a circle + @inline(__always) + private static func extractComponentShape(_ bytes: T, _ start: Int, _ end: Int) -> Shape + where T.Element == UInt8, T.Index == Int { + var index = skipWhitespace(bytes, end, start) - index += 1 - let shapeStart = index - while index < end && bytes[index] != kCloseSquareBracket { index += 1 } + guard index < end && bytes[index] == kOpenSquareBracket else { return .circle } - guard index > shapeStart && index < end else { return .circle } + index += 1 + let shapeStart = index + while index < end && bytes[index] != kCloseSquareBracket { index += 1 } - let shapeName = bytes[shapeStart.. shapeStart && index < end else { return .circle } -/// Given the contents of a shape label, return the right type. -@inline(__always) -private func parseShapeFromBytes(_ shapeName: T) -> Shape -where T.Element == UInt8, T.Index == Int { - var trimStart = shapeName.startIndex - var trimEnd = shapeName.endIndex + let shapeName = bytes[shapeStart..(_ shapeName: T) -> Shape + where T.Element == UInt8, T.Index == Int { + var trimStart = shapeName.startIndex + var trimEnd = shapeName.endIndex - trimStart = skipWhitespace(shapeName, trimEnd, trimStart) - trimEnd = shaveWhitespace(shapeName, trimStart, trimEnd) + trimStart = skipWhitespace(shapeName, trimEnd, trimStart) + trimEnd = shaveWhitespace(shapeName, trimStart, trimEnd) - let trimmedName = shapeName[trimStart... If found, returns where this arrow starts. -@inline(__always) -func hasDependency(_ bytes: T, _ start: Int, _ end: Int) -> Int? -where T.Element == UInt8, T.Index == Int { - var index = start - while index < end { - if bytes[index] == kOpenParenthesis { return nil } - if bytes[index] == kDash && index + 1 < end { - let next = bytes[index + 1] - if next == kGreaterThan || next == kDash { return index } +extension WmapParser { + /// Looks for -- or ->. If found, returns where this arrow starts. + @inline(__always) + static func hasDependency(_ bytes: T, _ start: Int, _ end: Int) -> Int? + where T.Element == UInt8, T.Index == Int { + var index = start + while index < end { + if bytes[index] == kOpenParenthesis { return nil } + if bytes[index] == kDash && index + 1 < end { + let next = bytes[index + 1] + if next == kGreaterThan || next == kDash { return index } + } + index += 1 + } + return nil } - index += 1 - } - return nil -} -/// Finds a dependency and whether it's directed. It assumes we previously -/// checked for the location of our separator to speed things up. -func parseDependency(_ bytes: T, _ start: Int, _ end: Int, _ separatorIndex: Int) - -> Dependency? -where T.Element == UInt8, T.Index == Int { - guard let isDirected = extractIsDirected(bytes, separatorIndex, end) else { - return nil - } + /// Finds a dependency and whether it's directed. It assumes we previously + /// checked for the location of our separator to speed things up. + static func parseDependency(_ bytes: T, _ start: Int, _ end: Int, _ separatorIndex: Int) + -> Dependency? + where T.Element == UInt8, T.Index == Int { + guard let isDirected = extractIsDirected(bytes, separatorIndex, end) else { + return nil + } - guard let fromComponent = extractTrimmedString(bytes, start, separatorIndex) else { - return nil - } + guard let fromComponent = extractTrimmedString(bytes, start, separatorIndex) else { + return nil + } - guard let toComponent = extractTrimmedString(bytes, separatorIndex + 2, end) else { - return nil - } + guard let toComponent = extractTrimmedString(bytes, separatorIndex + 2, end) else { + return nil + } - return Dependency(fromComponent: fromComponent, toComponent: toComponent, isDirected: isDirected) -} + return Dependency(fromComponent: fromComponent, toComponent: toComponent, isDirected: isDirected) + } -/// Checks the second character of the separator to see if it's directed. -/// It assumes we already checked for -- or ->, so it only checks for -/// Whether it's greaterThan or not. -@inline(__always) -private func extractIsDirected(_ bytes: T, _ separatorIndex: Int, _ end: Int) - -> Bool? -where T.Element == UInt8, T.Index == Int { - let followingCharacter = separatorIndex + 1 - guard followingCharacter < end else { return nil } - if bytes[followingCharacter] == kGreaterThan { - return true - } - return false + /// Checks the second character of the separator to see if it's directed. + /// It assumes we already checked for -- or ->, so it only checks for + /// Whether it's greaterThan or not. + @inline(__always) + private static func extractIsDirected(_ bytes: T, _ separatorIndex: Int, _ end: Int) + -> Bool? + where T.Element == UInt8, T.Index == Int { + let followingCharacter = separatorIndex + 1 + guard followingCharacter < end else { return nil } + if bytes[followingCharacter] == kGreaterThan { + return true + } + return false + } } diff --git a/Sources/WmapParser/Evolution.swift b/Sources/WmapParser/Evolution.swift deleted file mode 100644 index df6dd33..0000000 --- a/Sources/WmapParser/Evolution.swift +++ /dev/null @@ -1,5 +0,0 @@ -/// Evolution associated with a component. -public struct Evolution: Sendable, Equatable { - public let component: String - public let value: Double -} diff --git a/Sources/WmapParser/Group.swift b/Sources/WmapParser/Group.swift deleted file mode 100644 index 5204d45..0000000 --- a/Sources/WmapParser/Group.swift +++ /dev/null @@ -1,4 +0,0 @@ -/// A group of components. -public struct Group: Sendable, Equatable { - public let components: [String] -} diff --git a/Sources/WmapParser/Inertia.swift b/Sources/WmapParser/Inertia.swift deleted file mode 100644 index e9be9c0..0000000 --- a/Sources/WmapParser/Inertia.swift +++ /dev/null @@ -1,4 +0,0 @@ -/// Inertia associated with a component. -public struct Inertia: Sendable, Equatable { - public let component: String -} diff --git a/Sources/WmapParser/KeywordParser.swift b/Sources/WmapParser/KeywordParser.swift index 8db1164..b560d6a 100644 --- a/Sources/WmapParser/KeywordParser.swift +++ b/Sources/WmapParser/KeywordParser.swift @@ -1,184 +1,186 @@ -/// Checks which keyword we're dealing with and routes it to the right parser. -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) -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) -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. -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. -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 +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) + } } - 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..(_ 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.. Bool { - byte >= kZero && byte <= kNine -} - -/// Looks for x and y separated by a comma. -func parseCoordinates(_ bytes: T, _ start: Int, _ end: Int) -> ( - MapPoint, Int -)? -where T.Element == UInt8, T.Index == Int { - var index = skipWhitespace(bytes, end, start) - - guard let (xCoordinate, nextIndex) = parseSingleCoordinate(bytes, index, end) else { - return nil - } - index = nextIndex - - guard expectComma(bytes, &index, end) else { return nil } - - guard let (yCoordinate, nextIndex) = parseSingleCoordinate(bytes, index, end) else { - return nil - } - index = nextIndex - - guard expectClosingParen(bytes, &index, end) else { return nil } - - return (MapPoint(xCoordinate: xCoordinate, yCoordinate: yCoordinate), index) -} - -/// Checks if a candidate for a coordinate is a digit, and parses it. -@inline(__always) -func parseSingleCoordinate(_ bytes: T, _ start: Int, _ end: Int) -> (Double, Int)? -where T.Element == UInt8, T.Index == Int { - var index = start - while index < end && (isDigit(bytes[index]) || bytes[index] == 46) { index += 1 } - guard index > start else { return nil } - guard let value = parseDouble(bytes, start, index) else { return nil } - return (value, index) -} - -/// Advances the index until we find a comma. -@inline(__always) -func expectComma(_ bytes: T, _ index: inout Int, _ end: Int) -> Bool -where T.Element == UInt8, T.Index == Int { - index = skipWhitespace(bytes, end, index) - guard index < end && bytes[index] == kComma else { return false } - index += 1 - index = skipWhitespace(bytes, end, index) - return true -} - -/// Advances the index until we find a closing parenthesis -@inline(__always) -func expectClosingParen(_ bytes: T, _ index: inout Int, _ end: Int) -> Bool -where T.Element == UInt8, T.Index == Int { - index = skipWhitespace(bytes, end, index) - guard index < end && bytes[index] == kCloseParenthesis else { return false } - index += 1 - return true -} - -/// Parses a double -@inline(__always) -func parseDouble(_ bytes: T, _ start: Int, _ end: Int) -> Double? -where T.Element == UInt8, T.Index == Int { - var index = skipWhitespace(bytes, end, start) - - let numStart = index - while index < end && (isDigit(bytes[index]) || bytes[index] == kPeriod) { index += 1 } - - guard index > numStart else { return nil } - - // Why not String(data:encoding:)? This is much faster. - // swiftlint:disable:next optional_data_string_conversion - let str = String(decoding: bytes[numStart..(_ bytes: T, _ end: Int, _ start: Int) -> Int -where T.Element == UInt8, T.Index == Int { - var index = start - while index < end && (bytes[index] == kSpace || bytes[index] == kTab) { index += 1 } - return index -} - -/// Decreases an index by looking for whitespace right-to-left -func shaveWhitespace(_ bytes: T, _ start: Int, _ end: Int) -> Int -where T.Element == UInt8, T.Index == Int { - var index = end - while index > start && (bytes[index - 1] == kSpace || bytes[index - 1] == kTab) { index -= 1 } - return index -} - -/// Given the bytes buffer, finds the next non-empty line. -@inline(__always) -func extractLine(_ bytes: T, _ index: inout Int, _ length: Int) -> ( - Int, Int -)? -where T.Element == UInt8, T.Index == Int { - while index < length && (bytes[index] == kNewline || bytes[index] == kCarriageReturn) { - index += 1 - } - let lineStart = index - - while index < length && bytes[index] != kNewline && bytes[index] != kCarriageReturn { index += 1 } - if lineStart == index { return nil } - - var start = lineStart - var end = index - - start = skipWhitespace(bytes, end, start) - end = shaveWhitespace(bytes, start, end) - - if start == end { return nil } - - return (start, end) -} - -/// Trims a byte collection and returns a string -@inline(__always) -func extractTrimmedString(_ bytes: T, _ start: Int, _ end: Int) -> String? -where T.Element == UInt8, T.Index == Int { - var trimStart = start - var trimEnd = end - - trimStart = skipWhitespace(bytes, trimEnd, trimStart) - trimEnd = shaveWhitespace(bytes, trimStart, trimEnd) - - guard trimStart < trimEnd else { return nil } - - // Why not String(data:encoding:)? This is much faster. - // swiftlint:disable:next optional_data_string_conversion - return String(decoding: bytes[trimStart.. Bool { + byte >= kZero && byte <= kNine + } + + /// Looks for x and y separated by a comma. + static func parseCoordinates(_ bytes: T, _ start: Int, _ end: Int) -> ( + MapPoint, Int + )? + where T.Element == UInt8, T.Index == Int { + var index = skipWhitespace(bytes, end, start) + + guard let (xCoordinate, nextIndex) = parseSingleCoordinate(bytes, index, end) else { + return nil + } + index = nextIndex + + guard expectComma(bytes, &index, end) else { return nil } + + guard let (yCoordinate, nextIndex) = parseSingleCoordinate(bytes, index, end) else { + return nil + } + index = nextIndex + + guard expectClosingParen(bytes, &index, end) else { return nil } + + return (MapPoint(xCoordinate: xCoordinate, yCoordinate: yCoordinate), index) + } + + /// Checks if a candidate for a coordinate is a digit, and parses it. + @inline(__always) + static func parseSingleCoordinate(_ bytes: T, _ start: Int, _ end: Int) -> (Double, Int)? + where T.Element == UInt8, T.Index == Int { + var index = start + while index < end && (isDigit(bytes[index]) || bytes[index] == 46) { index += 1 } + guard index > start else { return nil } + guard let value = parseDouble(bytes, start, index) else { return nil } + return (value, index) + } + + /// Advances the index until we find a comma. + @inline(__always) + static func expectComma(_ bytes: T, _ index: inout Int, _ end: Int) -> Bool + where T.Element == UInt8, T.Index == Int { + index = skipWhitespace(bytes, end, index) + guard index < end && bytes[index] == kComma else { return false } + index += 1 + index = skipWhitespace(bytes, end, index) + return true + } + + /// Advances the index until we find a closing parenthesis + @inline(__always) + static func expectClosingParen(_ bytes: T, _ index: inout Int, _ end: Int) -> Bool + where T.Element == UInt8, T.Index == Int { + index = skipWhitespace(bytes, end, index) + guard index < end && bytes[index] == kCloseParenthesis else { return false } + index += 1 + return true + } + + /// Parses a double + @inline(__always) + static func parseDouble(_ bytes: T, _ start: Int, _ end: Int) -> Double? + where T.Element == UInt8, T.Index == Int { + var index = skipWhitespace(bytes, end, start) + + let numStart = index + while index < end && (isDigit(bytes[index]) || bytes[index] == kPeriod) { index += 1 } + + guard index > numStart else { return nil } + + // Why not String(data:encoding:)? This is much faster. + // swiftlint:disable:next optional_data_string_conversion + let str = String(decoding: bytes[numStart..(_ bytes: T, _ end: Int, _ start: Int) -> Int + where T.Element == UInt8, T.Index == Int { + var index = start + while index < end && (bytes[index] == kSpace || bytes[index] == kTab) { index += 1 } + return index + } + + /// Decreases an index by looking for whitespace right-to-left + static func shaveWhitespace(_ bytes: T, _ start: Int, _ end: Int) -> Int + where T.Element == UInt8, T.Index == Int { + var index = end + while index > start && (bytes[index - 1] == kSpace || bytes[index - 1] == kTab) { index -= 1 } + return index + } + + /// Given the bytes buffer, finds the next non-empty line. + @inline(__always) + static func extractLine(_ bytes: T, _ index: inout Int, _ length: Int) -> ( + Int, Int + )? + where T.Element == UInt8, T.Index == Int { + while index < length && (bytes[index] == kNewline || bytes[index] == kCarriageReturn) { + index += 1 + } + let lineStart = index + + while index < length && bytes[index] != kNewline && bytes[index] != kCarriageReturn { index += 1 } + if lineStart == index { return nil } + + var start = lineStart + var end = index + + start = skipWhitespace(bytes, end, start) + end = shaveWhitespace(bytes, start, end) + + if start == end { return nil } + + return (start, end) + } + + /// Trims a byte collection and returns a string + @inline(__always) + static func extractTrimmedString(_ bytes: T, _ start: Int, _ end: Int) -> String? + where T.Element == UInt8, T.Index == Int { + var trimStart = start + var trimEnd = end + + trimStart = skipWhitespace(bytes, trimEnd, trimStart) + trimEnd = shaveWhitespace(bytes, trimStart, trimEnd) + + guard trimStart < trimEnd else { return nil } + + // Why not String(data:encoding:)? This is much faster. + // swiftlint:disable:next optional_data_string_conversion + return String(decoding: bytes[trimStart.. Map { - source.utf8.withContiguousStorageIfAvailable { bytes in - parseBytes(bytes) - } ?? parseBytes(Array(source.utf8)) -} - -// Parsers ///////////////////////////////////////////////////////////////////// + // Public API ////////////////////////////////////////////////////////////////// -struct LineParserContext { - var components: [Component] - var dependencies: [Dependency] - var keywordContext: KeywordParserContext -} + /// Parses a wmap source string. + public static func parse(_ source: String) -> Map { + source.utf8.withContiguousStorageIfAvailable { bytes in + parseBytes(bytes) + } ?? parseBytes(Array(source.utf8)) + } -/// Main parsing function, we work on bytes as it's faster. -private func parseBytes(_ bytes: T) -> Map where T.Element == UInt8, T.Index == Int { - let estimatedLines = bytes.count / 30 + // Parsers ///////////////////////////////////////////////////////////////////// - var context = LineParserContext( - components: [], - dependencies: [], - keywordContext: KeywordParserContext( - stages: [], - notes: [], - groups: [], - inertias: [], - evolutions: [] - ) - ) + /// Main parsing function, we work on bytes as it's faster. + private static func parseBytes(_ bytes: T) -> Map where T.Element == UInt8, T.Index == Int { + let estimatedLines = bytes.count / 30 - context.components.reserveCapacity(estimatedLines) - context.dependencies.reserveCapacity(estimatedLines) - context.keywordContext.notes.reserveCapacity(estimatedLines / 10) - context.keywordContext.stages.reserveCapacity(4) - context.keywordContext.groups.reserveCapacity(estimatedLines / 20) - context.keywordContext.inertias.reserveCapacity(estimatedLines / 20) - context.keywordContext.evolutions.reserveCapacity(estimatedLines / 20) + var context = LineParserContext( + components: [], + dependencies: [], + keywordContext: KeywordParserContext( + stages: [], + notes: [], + groups: [], + inertias: [], + evolutions: [] + ) + ) - var index = 0 - let length = bytes.count + context.components.reserveCapacity(estimatedLines) + context.dependencies.reserveCapacity(estimatedLines) + context.keywordContext.notes.reserveCapacity(estimatedLines / 10) + context.keywordContext.stages.reserveCapacity(4) + context.keywordContext.groups.reserveCapacity(estimatedLines / 20) + context.keywordContext.inertias.reserveCapacity(estimatedLines / 20) + context.keywordContext.evolutions.reserveCapacity(estimatedLines / 20) - while index < length { - guard let (start, end) = extractLine(bytes, &index, length) else { continue } - parseLine(bytes, start, end, &context) - } + var index = 0 + let length = bytes.count - return Map( - components: context.components, - dependencies: context.dependencies, - notes: context.keywordContext.notes, - stages: context.keywordContext.stages, - groups: context.keywordContext.groups, - inertias: context.keywordContext.inertias, - evolutions: context.keywordContext.evolutions - ) -} + while index < length { + guard let (start, end) = extractLine(bytes, &index, length) else { continue } + parseLine(bytes, start, end, &context) + } -/// Given a trimmed line, checks if it's a keyword, dependency or component -/// and parses it. -@inline(__always) -private func parseLine( - _ bytes: T, _ start: Int, _ end: Int, - _ context: inout LineParserContext -) where T.Element == UInt8, T.Index == Int { - if bytes[start] == kOpenSquareBracket { - parseKeywordLine(bytes, start, end, &context.keywordContext) - } else if let separatorIndex = hasDependency(bytes, start, end) { - if let dependency = parseDependency(bytes, start, end, separatorIndex) { - context.dependencies.append(dependency) + return Map( + components: context.components, + dependencies: context.dependencies, + notes: context.keywordContext.notes, + stages: context.keywordContext.stages, + groups: context.keywordContext.groups, + inertias: context.keywordContext.inertias, + evolutions: context.keywordContext.evolutions + ) } - } else if let component = parseComponent(bytes, start, end) { - context.components.append(component) - } + + /// Given a trimmed line, checks if it's a keyword, dependency or component + /// and parses it. + @inline(__always) + private static func parseLine( + _ bytes: T, _ start: Int, _ end: Int, + _ context: inout LineParserContext + ) where T.Element == UInt8, T.Index == Int { + if bytes[start] == kOpenSquareBracket { + parseKeywordLine(bytes, start, end, &context.keywordContext) + } else if let separatorIndex = hasDependency(bytes, start, end) { + if let dependency = parseDependency(bytes, start, end, separatorIndex) { + context.dependencies.append(dependency) + } + } else if let component = parseComponent(bytes, start, end) { + context.components.append(component) + } + } } -- cgit