diff options
Diffstat (limited to 'Sources/WmapParser')
| -rw-r--r-- | Sources/WmapParser/Component.swift | 6 | ||||
| -rw-r--r-- | Sources/WmapParser/ComponentParser.swift | 78 | ||||
| -rw-r--r-- | Sources/WmapParser/Constants.swift | 35 | ||||
| -rw-r--r-- | Sources/WmapParser/Dependency.swift | 6 | ||||
| -rw-r--r-- | Sources/WmapParser/DependencyParser.swift | 50 | ||||
| -rw-r--r-- | Sources/WmapParser/Evolution.swift | 5 | ||||
| -rw-r--r-- | Sources/WmapParser/Group.swift | 4 | ||||
| -rw-r--r-- | Sources/WmapParser/Inertia.swift | 4 | ||||
| -rw-r--r-- | Sources/WmapParser/KeywordParser.swift | 184 | ||||
| -rw-r--r-- | Sources/WmapParser/KeywordParserContext.swift | 7 | ||||
| -rw-r--r-- | Sources/WmapParser/Map.swift | 10 | ||||
| -rw-r--r-- | Sources/WmapParser/MapPoint.swift | 5 | ||||
| -rw-r--r-- | Sources/WmapParser/Note.swift | 5 | ||||
| -rw-r--r-- | Sources/WmapParser/Shape.swift | 8 | ||||
| -rw-r--r-- | Sources/WmapParser/Stage.swift | 5 | ||||
| -rw-r--r-- | Sources/WmapParser/StageNumber.swift | 8 | ||||
| -rw-r--r-- | Sources/WmapParser/Utils.swift | 136 | ||||
| -rw-r--r-- | Sources/WmapParser/WmapParser.swift | 79 |
18 files changed, 635 insertions, 0 deletions
diff --git a/Sources/WmapParser/Component.swift b/Sources/WmapParser/Component.swift new file mode 100644 index 0000000..027ac59 --- /dev/null +++ b/Sources/WmapParser/Component.swift @@ -0,0 +1,6 @@ +/// 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 new file mode 100644 index 0000000..c899637 --- /dev/null +++ b/Sources/WmapParser/ComponentParser.swift @@ -0,0 +1,78 @@ +/// Parses a component with its coordinates. +func parseComponent<T: Collection>(_ 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 + } + + guard + let (coordinates, endIndex) = parseCoordinates( + bytes, parenthesisIndex + 1, end) + else { + return nil + } + + let shape = extractComponentShape(bytes, endIndex, end) + + return Component(label: label, coordinates: coordinates, shape: shape) +} + +/// Looks for a component label by taking the string before the first open +/// parenthesis. +@inline(__always) +private func extractComponentLabel<T: Collection>(_ 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 +} + +/// 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<T: Collection>(_ bytes: T, _ start: Int, _ end: Int) -> Shape +where T.Element == UInt8, T.Index == Int { + var index = skipWhitespace(bytes, end, start) + + guard index < end && bytes[index] == kOpenSquareBracket else { return .circle } + + index += 1 + let shapeStart = index + while index < end && bytes[index] != kCloseSquareBracket { index += 1 } + + guard index > shapeStart && index < end else { return .circle } + + let shapeName = bytes[shapeStart..<index] + return parseShapeFromBytes(shapeName) +} + +/// Given the contents of a shape label, return the right type. +@inline(__always) +private func parseShapeFromBytes<T: Collection>(_ 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) + + let trimmedName = shapeName[trimStart..<trimEnd] + + // "x" + if matchesKeyword(trimmedName, [120]) { return .xMark } + // "square" + if matchesKeyword(trimmedName, [115, 113, 117, 97, 114, 101]) { return .square } + // "triangle" + if matchesKeyword(trimmedName, [116, 114, 105, 97, 110, 103, 108, 101]) { return .triangle } + + return .circle +} diff --git a/Sources/WmapParser/Constants.swift b/Sources/WmapParser/Constants.swift new file mode 100644 index 0000000..73340b6 --- /dev/null +++ b/Sources/WmapParser/Constants.swift @@ -0,0 +1,35 @@ +// Whitespace +let kNewline = 10 +let kCarriageReturn = 13 +let kTab = 9 +let kSpace = 32 + +// Punctuation +let kOpenParenthesis = 40 +let kCloseParenthesis = 41 +let kPlus = 43 +let kComma = 44 +let kDash = 45 +let kPeriod = 46 +let kGreaterThan = 62 +let kOpenSquareBracket = 91 +let kCloseSquareBracket = 93 + +// Letters +let kCapitalA = 65 +let kCapitalZ = 90 +let kCapsToLowercaseDistance: UInt8 = 32 + +// Numbers +let kZero = 48 +let kNine = 57 + +// Keywords. ASCII representations of the keywords. +let kInertiaKeyword: [UInt8] = [105, 110, 101, 114, 116, 105, 97] +let kEvolutionKeyword: [UInt8] = [101, 118, 111, 108, 117, 116, 105, 111, 110] +let kNoteKeyword: [UInt8] = [110, 111, 116, 101] +let kGroupKeyword: [UInt8] = [103, 114, 111, 117, 112] +let kIKeyword: [UInt8] = [105] +let kIIKeyword: [UInt8] = [105, 105] +let kIIIKeyword: [UInt8] = [105, 105, 105] +let kIVKeyword: [UInt8] = [105, 118] diff --git a/Sources/WmapParser/Dependency.swift b/Sources/WmapParser/Dependency.swift new file mode 100644 index 0000000..38f03be --- /dev/null +++ b/Sources/WmapParser/Dependency.swift @@ -0,0 +1,6 @@ +/// A dependency between two components. +public struct Dependency: Sendable, Equatable { + public let fromComponent: String + public let toComponent: String + public let isDirected: Bool +} diff --git a/Sources/WmapParser/DependencyParser.swift b/Sources/WmapParser/DependencyParser.swift new file mode 100644 index 0000000..9964d5a --- /dev/null +++ b/Sources/WmapParser/DependencyParser.swift @@ -0,0 +1,50 @@ +/// Looks for -- or ->. If found, returns where this arrow starts. +@inline(__always) +func hasDependency<T: Collection>(_ 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 +} + +/// 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<T: Collection>(_ 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 toComponent = extractTrimmedString(bytes, separatorIndex + 2, end) else { + return nil + } + + 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<T: Collection>(_ 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 new file mode 100644 index 0000000..df6dd33 --- /dev/null +++ b/Sources/WmapParser/Evolution.swift @@ -0,0 +1,5 @@ +/// 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 new file mode 100644 index 0000000..5204d45 --- /dev/null +++ b/Sources/WmapParser/Group.swift @@ -0,0 +1,4 @@ +/// 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 new file mode 100644 index 0000000..e9be9c0 --- /dev/null +++ b/Sources/WmapParser/Inertia.swift @@ -0,0 +1,4 @@ +/// 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 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) +} diff --git a/Sources/WmapParser/KeywordParserContext.swift b/Sources/WmapParser/KeywordParserContext.swift new file mode 100644 index 0000000..1b889b3 --- /dev/null +++ b/Sources/WmapParser/KeywordParserContext.swift @@ -0,0 +1,7 @@ +struct KeywordParserContext { + var stages: [Stage] + var notes: [Note] + var groups: [Group] + var inertias: [Inertia] + var evolutions: [Evolution] +} diff --git a/Sources/WmapParser/Map.swift b/Sources/WmapParser/Map.swift new file mode 100644 index 0000000..7a25686 --- /dev/null +++ b/Sources/WmapParser/Map.swift @@ -0,0 +1,10 @@ +/// A parsed wardley map. +public struct Map: Sendable { + public let components: [Component] + public let dependencies: [Dependency] + public let notes: [Note] + public let stages: [Stage] + public let groups: [Group] + public let inertias: [Inertia] + public let evolutions: [Evolution] +} diff --git a/Sources/WmapParser/MapPoint.swift b/Sources/WmapParser/MapPoint.swift new file mode 100644 index 0000000..718ecab --- /dev/null +++ b/Sources/WmapParser/MapPoint.swift @@ -0,0 +1,5 @@ +/// A point in the map +public struct MapPoint: Sendable, Equatable { + let xCoordinate: Double + let yCoordinate: Double +} diff --git a/Sources/WmapParser/Note.swift b/Sources/WmapParser/Note.swift new file mode 100644 index 0000000..0db3739 --- /dev/null +++ b/Sources/WmapParser/Note.swift @@ -0,0 +1,5 @@ +/// A note. +public struct Note: Sendable, Equatable { + public let text: String + public let coordinates: MapPoint +} diff --git a/Sources/WmapParser/Shape.swift b/Sources/WmapParser/Shape.swift new file mode 100644 index 0000000..ea1844e --- /dev/null +++ b/Sources/WmapParser/Shape.swift @@ -0,0 +1,8 @@ +/// Any of the potential shapes in a component. +@frozen +public enum Shape: Sendable, Equatable { + case circle + case xMark + case square + case triangle +} diff --git a/Sources/WmapParser/Stage.swift b/Sources/WmapParser/Stage.swift new file mode 100644 index 0000000..78429e0 --- /dev/null +++ b/Sources/WmapParser/Stage.swift @@ -0,0 +1,5 @@ +/// An override for the width of an evolution stage. +public struct Stage: Sendable, Equatable { + public let stage: StageNumber + public let value: Double +} diff --git a/Sources/WmapParser/StageNumber.swift b/Sources/WmapParser/StageNumber.swift new file mode 100644 index 0000000..2e8a2da --- /dev/null +++ b/Sources/WmapParser/StageNumber.swift @@ -0,0 +1,8 @@ +/// The potential stages of evolution. +@frozen +public enum StageNumber: Sendable, Equatable { + case stageI + case stageII + case stageIII + case stageIV +} diff --git a/Sources/WmapParser/Utils.swift b/Sources/WmapParser/Utils.swift new file mode 100644 index 0000000..8bea13a --- /dev/null +++ b/Sources/WmapParser/Utils.swift @@ -0,0 +1,136 @@ +/// Check if a byte is a digit +@inline(__always) +func isDigit(_ byte: UInt8) -> Bool { + byte >= kZero && byte <= kNine +} + +/// Looks for x and y separated by a comma. +func parseCoordinates<T: Collection>(_ 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<T: Collection>(_ 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<T: Collection>(_ 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<T: Collection>(_ 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<T: Collection>(_ 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..<index], as: UTF8.self) + return Double(str) +} + +/// Advances an index by looking for whitespace left-to-right +func skipWhitespace<T: Collection>(_ 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<T: Collection>(_ 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<T: Collection>(_ 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<T: Collection>(_ 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..<trimEnd], as: UTF8.self) +} diff --git a/Sources/WmapParser/WmapParser.swift b/Sources/WmapParser/WmapParser.swift new file mode 100644 index 0000000..f2bef8f --- /dev/null +++ b/Sources/WmapParser/WmapParser.swift @@ -0,0 +1,79 @@ +import Foundation + +// Public API ////////////////////////////////////////////////////////////////// + +/// Parses a wmap source string. +public func parse(_ source: String) -> Map { + source.utf8.withContiguousStorageIfAvailable { bytes in + parseBytes(bytes) + } ?? parseBytes(Array(source.utf8)) +} + +// Parsers ///////////////////////////////////////////////////////////////////// + +struct LineParserContext { + var components: [Component] + var dependencies: [Dependency] + var keywordContext: KeywordParserContext +} + +/// Main parsing function, we work on bytes as it's faster. +private func parseBytes<T: Collection>(_ bytes: T) -> Map where T.Element == UInt8, T.Index == Int { + let estimatedLines = bytes.count / 30 + + var context = LineParserContext( + components: [], + dependencies: [], + keywordContext: KeywordParserContext( + stages: [], + notes: [], + groups: [], + inertias: [], + evolutions: [] + ) + ) + + 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 index = 0 + let length = bytes.count + + while index < length { + guard let (start, end) = extractLine(bytes, &index, length) else { continue } + parseLine(bytes, start, end, &context) + } + + 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 + ) +} + +/// Given a trimmed line, checks if it's a keyword, dependency or component +/// and parses it. +@inline(__always) +private func parseLine<T: Collection>( + _ 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) + } +} |