diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-07-09 08:34:19 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-07-09 08:34:19 +0200 |
| commit | 1ec7f2c6deb64f5b13b32fae2bf74ae2d8aaaabb (patch) | |
| tree | 47b6addedf0bdb18d35774e9b027d07074929d4a /Map/Business/WmapParser | |
| parent | 6926feead1a7fadb7f5f974996bef8c30b3bfdbe (diff) | |
Account for CRLF/CR/LF. Lex note text. Use Wmap Parser for renderer.
Diffstat (limited to 'Map/Business/WmapParser')
| -rw-r--r-- | Map/Business/WmapParser/Entity.swift | 73 | ||||
| -rw-r--r-- | Map/Business/WmapParser/Lexer.swift | 93 | ||||
| -rw-r--r-- | Map/Business/WmapParser/Parser.swift | 87 |
3 files changed, 181 insertions, 72 deletions
diff --git a/Map/Business/WmapParser/Entity.swift b/Map/Business/WmapParser/Entity.swift index 40dd7ca..52f6e93 100644 --- a/Map/Business/WmapParser/Entity.swift +++ b/Map/Business/WmapParser/Entity.swift @@ -1,11 +1,51 @@ struct Wmap { - protocol Entity { - var position: SourcePosition { get } - var range: SourceRange { get } - var syntaxElements: [SyntaxElement] { get } + enum Entity: Equatable { + case vertex(Vertex) + case edge(Edge) + case note(Note) + case stage(Stage) + case group(Group) + case inertia(Inertia) + case evolution(Evolution) + + var position: SourcePosition { + switch self { + case .vertex(let vertex): return vertex.position + case .edge(let edge): return edge.position + case .note(let note): return note.position + case .stage(let stage): return stage.position + case .group(let group): return group.position + case .inertia(let inertia): return inertia.position + case .evolution(let evolution): return evolution.position + } + } + + var range: SourceRange { + switch self { + case .vertex(let vertex): return vertex.range + case .edge(let edge): return edge.range + case .note(let note): return note.range + case .stage(let stage): return stage.range + case .group(let group): return group.range + case .inertia(let inertia): return inertia.range + case .evolution(let evolution): return evolution.range + } + } + + var syntaxElements: [SyntaxElement] { + switch self { + case .vertex(let vertex): return vertex.syntaxElements + case .edge(let edge): return edge.syntaxElements + case .note(let note): return note.syntaxElements + case .stage(let stage): return stage.syntaxElements + case .group(let group): return group.syntaxElements + case .inertia(let inertia): return inertia.syntaxElements + case .evolution(let evolution): return evolution.syntaxElements + } + } } - struct Vertex: Entity { + struct Vertex: Equatable { let label: String let position: SourcePosition let coordinates: (Double, Double) @@ -21,6 +61,12 @@ struct Wmap { let rightParenRange: SourceRange let shapeRange: SourceRange? + static func == (lhs: Vertex, rhs: Vertex) -> Bool { + return lhs.label == rhs.label && lhs.position == rhs.position + && lhs.coordinates.0 == rhs.coordinates.0 && lhs.coordinates.1 == rhs.coordinates.1 + && lhs.shape == rhs.shape && lhs.range == rhs.range + } + var syntaxElements: [SyntaxElement] { var elements: [SyntaxElement] = [] @@ -86,7 +132,7 @@ struct Wmap { } } - struct Edge: Entity { + struct Edge: Equatable { let from: String let to: String let isDirected: Bool @@ -129,7 +175,7 @@ struct Wmap { } } - struct Note: Entity { + struct Note: Equatable { let coordinates: (Double, Double) let text: String let position: SourcePosition @@ -144,6 +190,11 @@ struct Wmap { let rightParenRange: SourceRange let textRange: SourceRange + static func == (lhs: Note, rhs: Note) -> Bool { + return lhs.coordinates.0 == rhs.coordinates.0 && lhs.coordinates.1 == rhs.coordinates.1 + && lhs.text == rhs.text && lhs.position == rhs.position && lhs.range == rhs.range + } + var syntaxElements: [SyntaxElement] { var elements: [SyntaxElement] = [] @@ -207,7 +258,7 @@ struct Wmap { } } - struct Stage: Entity { + struct Stage: Equatable { let number: String // i, ii, iii, iv let value: Double let position: SourcePosition @@ -258,7 +309,7 @@ struct Wmap { } } - struct Group: Entity { + struct Group: Equatable { let vertices: [String] let position: SourcePosition let range: SourceRange @@ -303,7 +354,7 @@ struct Wmap { } } - struct Inertia: Entity { + struct Inertia: Equatable { let vertex: String let position: SourcePosition let range: SourceRange @@ -335,7 +386,7 @@ struct Wmap { } } - struct Evolution: Entity { + struct Evolution: Equatable { let vertex: String let isPositive: Bool let value: Double diff --git a/Map/Business/WmapParser/Lexer.swift b/Map/Business/WmapParser/Lexer.swift index dc789bb..c79879f 100644 --- a/Map/Business/WmapParser/Lexer.swift +++ b/Map/Business/WmapParser/Lexer.swift @@ -18,6 +18,7 @@ extension Wmap { private var current: String.Index private var line = 1 private var column = 1 + private var expectingText = false init(_ input: String) { self.input = input @@ -41,17 +42,9 @@ extension Wmap { let tokenStart = current switch char { - case "\n": - advance() - let token = Token( - type: .newline, - value: "\n", - position: pos, - stringRange: tokenStart..<current - ) - line += 1 - column = 1 - return token + case "\n", "\r": + expectingText = false // Reset expectingText on newline + return scanLineEnding() case "(": advance() @@ -128,6 +121,12 @@ extension Wmap { return scanStageOrLabel() default: + // If we're expecting text, scan as text instead of vertex label + if expectingText { + expectingText = false // Reset after scanning text + return scanText() + } + // If we encounter an unexpected character, try to scan it as a vertex label // This provides more graceful degradation than failing immediately return scanVertexLabel() @@ -136,13 +135,17 @@ extension Wmap { func scanText() -> Token { let pos = currentPosition() + + // Skip any leading whitespace + skipWhitespace() + let tokenStart = current - while current < input.endIndex && input[current] != "\n" { + while current < input.endIndex && !isLineEnding(input[current]) { advance() } - let originalValue = String(input[tokenStart..<current]) + let originalValue = String(input[tokenStart..<current]).trimmingCharacters(in: .whitespaces) return Token( type: .text, value: originalValue, @@ -151,19 +154,69 @@ extension Wmap { ) } + private func scanLineEnding() -> Token { + let pos = currentPosition() + let tokenStart = current + + if input[current] == "\r" { + advance() + // Check for CRLF + if current < input.endIndex && input[current] == "\n" { + advance() + let token = Token( + type: .newline, + value: "\r\n", + position: pos, + stringRange: tokenStart..<current + ) + line += 1 + column = 1 + return token + } else { + // Just CR + let token = Token( + type: .newline, + value: "\r", + position: pos, + stringRange: tokenStart..<current + ) + line += 1 + column = 1 + return token + } + } else if input[current] == "\n" { + advance() + let token = Token( + type: .newline, + value: "\n", + position: pos, + stringRange: tokenStart..<current + ) + line += 1 + column = 1 + return token + } else { + fatalError("scanLineEnding called on non-line-ending character") + } + } + + private func isLineEnding(_ char: Character) -> Bool { + return char == "\n" || char == "\r" + } + private func scanBracketedToken() -> Token { let pos = currentPosition() let tokenStart = current advance() // consume '[' let contentStart = current - while current < input.endIndex && input[current] != "]" && input[current] != "\n" { + while current < input.endIndex && input[current] != "]" && !isLineEnding(input[current]) { advance() } - // If we hit a newline or EOF before finding the closing bracket, it's an error + // If we hit a line ending or EOF before finding the closing bracket, it's an error guard current < input.endIndex && input[current] == "]" else { - // Return error token for unclosed bracket (don't consume past newline) + // Return error token for unclosed bracket (don't consume past line ending) return Token( type: .error, value: String(input[tokenStart..<current]), @@ -180,6 +233,7 @@ extension Wmap { switch lowercaseContent { case "note": + expectingText = true // Set expectingText when we return a note keyword return Token( type: .noteKeyword, value: "[Note]", @@ -311,16 +365,17 @@ extension Wmap { private func isVertexLabelDelimiter(_ char: Character) -> Bool { return char == "-" || char == "+" || char == "," || char == "[" || char == "]" || char == "(" - || char == ")" || char == "\n" + || char == ")" || isLineEnding(char) } private func isDelimiter(_ char: Character) -> Bool { - return char.isWhitespace || char == "\n" || char == "(" || char == ")" || char == "[" + return char.isWhitespace || isLineEnding(char) || char == "(" || char == ")" || char == "[" || char == "]" || char == "," } private func skipWhitespace() { - while current < input.endIndex && input[current].isWhitespace && input[current] != "\n" { + while current < input.endIndex && input[current].isWhitespace && !isLineEnding(input[current]) + { advance() } } diff --git a/Map/Business/WmapParser/Parser.swift b/Map/Business/WmapParser/Parser.swift index f04998c..9418263 100644 --- a/Map/Business/WmapParser/Parser.swift +++ b/Map/Business/WmapParser/Parser.swift @@ -56,15 +56,15 @@ extension Wmap { case .vertexLabel: return try parseVertexOrEdge() case .noteKeyword: - return try parseNote() + return .note(try parseNote()) case .stageNumber: - return try parseStage() + return .stage(try parseStage()) case .groupKeyword: - return try parseGroup() + return .group(try parseGroup()) case .inertiaKeyword: - return try parseInertia() + return .inertia(try parseInertia()) case .evolutionKeyword: - return try parseEvolution() + return .evolution(try parseEvolution()) case .error: // Error tokens should cause the line to be ignored throw ParseError.invalidLine @@ -97,19 +97,20 @@ extension Wmap { vertexLabelMap[secondLabel.lowercased()] = secondLabel advance() - return Edge( - from: firstLabel, - to: secondLabel, - isDirected: isDirected, - position: startPos, - range: SourceRange( - start: startPos, end: endPos, - stringRange: firstLabelRange.stringRange - .lowerBound..<secondLabelRange.stringRange.upperBound), - fromLabelRange: firstLabelRange, - edgeTypeRange: edgeTypeRange, - toLabelRange: secondLabelRange - ) + return .edge( + Edge( + from: firstLabel, + to: secondLabel, + isDirected: isDirected, + position: startPos, + range: SourceRange( + start: startPos, end: endPos, + stringRange: firstLabelRange.stringRange + .lowerBound..<secondLabelRange.stringRange.upperBound), + fromLabelRange: firstLabelRange, + edgeTypeRange: edgeTypeRange, + toLabelRange: secondLabelRange + )) } else { // Parse vertex let (coordinates, coordRanges) = try parsePositionWithRanges() @@ -117,24 +118,25 @@ extension Wmap { let endPos = shapeRange?.end ?? coordRanges.rightParen.end - return Vertex( - label: firstLabel, - position: startPos, - coordinates: coordinates, - shape: shape, - range: SourceRange( - start: startPos, end: endPos, - stringRange: firstLabelRange.stringRange - .lowerBound..<(shapeRange?.stringRange.upperBound - ?? coordRanges.rightParen.stringRange.upperBound)), - labelRange: firstLabelRange, - leftParenRange: coordRanges.leftParen, - xCoordRange: coordRanges.xCoord, - commaRange: coordRanges.comma, - yCoordRange: coordRanges.yCoord, - rightParenRange: coordRanges.rightParen, - shapeRange: shapeRange - ) + return .vertex( + Vertex( + label: firstLabel, + position: startPos, + coordinates: coordinates, + shape: shape, + range: SourceRange( + start: startPos, end: endPos, + stringRange: firstLabelRange.stringRange + .lowerBound..<(shapeRange?.stringRange.upperBound + ?? coordRanges.rightParen.stringRange.upperBound)), + labelRange: firstLabelRange, + leftParenRange: coordRanges.leftParen, + xCoordRange: coordRanges.xCoord, + commaRange: coordRanges.comma, + yCoordRange: coordRanges.yCoord, + rightParenRange: coordRanges.rightParen, + shapeRange: shapeRange + )) } } @@ -145,12 +147,13 @@ extension Wmap { let (coordinates, coordRanges) = try parsePositionWithRanges() - // For notes, we need to scan the remaining text on the line - let textToken = lexer.scanText() - let text = textToken.value - let textRange = makeSourceRange(from: textToken) - let endPos = textToken.position - currentToken = lexer.nextToken() // Update current token after scanning text + guard currentToken.type == .text else { + throw ParseError.expectedText(actual: currentToken) + } + let text = currentToken.value + let textRange = makeSourceRange(from: currentToken) + let endPos = currentToken.position + advance() return Note( coordinates: coordinates, |