aboutsummaryrefslogtreecommitdiff
path: root/Map/Business/WmapParser
diff options
context:
space:
mode:
Diffstat (limited to 'Map/Business/WmapParser')
-rw-r--r--Map/Business/WmapParser/Entity.swift73
-rw-r--r--Map/Business/WmapParser/Lexer.swift93
-rw-r--r--Map/Business/WmapParser/Parser.swift87
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,