aboutsummaryrefslogtreecommitdiff
path: root/Map/Business
diff options
context:
space:
mode:
authorRuben Beltran del Rio <jj@r.bdr.sh>2025-12-14 20:21:24 +0100
committerRuben Beltran del Rio <jj@r.bdr.sh>2025-12-15 00:10:39 +0100
commit0d5e6636405dbe332c33c0fb82c7ccccb20f96cb (patch)
treea9cda2b711bbfc9277d66310440dd965b16682fc /Map/Business
parenta5ae7aedc840a38505b3181f4e5ba92e90d94fa1 (diff)
Use wmap-parser-swift and tree-sitter-wmap for parsing and highlighting.
Diffstat (limited to 'Map/Business')
-rw-r--r--Map/Business/RenderableMap.swift198
-rw-r--r--Map/Business/WmapParser/Entity.swift440
-rw-r--r--Map/Business/WmapParser/Lexer.swift463
-rw-r--r--Map/Business/WmapParser/Parser.swift504
-rw-r--r--Map/Business/WmapParser/SourcePosition.swift35
-rw-r--r--Map/Business/WmapParser/Syntax.swift32
-rw-r--r--Map/Business/WmapParser/Token.swift22
-rw-r--r--Map/Business/WmapParser/TokenType.swift36
-rw-r--r--Map/Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift102
9 files changed, 83 insertions, 1749 deletions
diff --git a/Map/Business/RenderableMap.swift b/Map/Business/RenderableMap.swift
index d43b584..82acaff 100644
--- a/Map/Business/RenderableMap.swift
+++ b/Map/Business/RenderableMap.swift
@@ -13,6 +13,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see https://map.tranquil.systems.
import Foundation
+import WmapParser
struct RenderableMap {
let vertices: [Vertex]
@@ -20,111 +21,101 @@ struct RenderableMap {
let inertias: [Inertia]
let opportunities: [Opportunity]
let notes: [Note]
- let stages: [CGFloat]
+ let stages: [Double]
let groups: [[Vertex]]
static let empty: RenderableMap = RenderableMap(
vertices: [], edges: [], inertias: [], opportunities: [], notes: [], stages: defaultDimensions,
groups: [])
- static func from(_ entities: [Wmap.Entity]) -> RenderableMap {
+ static func from(_ map: WmapParser.Map) -> RenderableMap {
var vertices: [Vertex] = []
var edges: [MapEdge] = []
var inertias: [Inertia] = []
var opportunities: [Opportunity] = []
var notes: [Note] = []
- var stages: [CGFloat] = [25.0, 50.0, 75.0] // default dimensions
+ var stages: [Double] = [25.0, 50.0, 75.0] // default dimensions
var groups: [[Vertex]] = []
// Build a vertex lookup for edges, groups, etc.
var vertexMap: [String: Vertex] = [:]
- var vertexIdCounter = 0
-
- for entity in entities {
- switch entity {
- case .vertex(let vertex):
- let newVertex = Vertex(
- id: vertexIdCounter,
- label: vertex.label,
- position: CGPoint(x: vertex.coordinates.0, y: vertex.coordinates.1),
- shape: convertShape(vertex.shape)
- )
- vertices.append(newVertex)
- vertexMap[vertex.label.lowercased()] = newVertex
- vertexIdCounter += 1
-
- case .note(let note):
- notes.append(
- Note(
- id: notes.count,
- position: CGPoint(x: note.coordinates.0, y: note.coordinates.1),
- text: note.text
- ))
-
- case .stage(let stage):
- let stageIndex = convertStageNumber(stage.number) - 1
- if stageIndex >= 0 && stageIndex < 3 {
- stages[stageIndex] = stage.value
- }
+
+ for (index, component) in map.components.enumerated() {
+ let newVertex = Vertex(
+ id: index,
+ label: component.label,
+ position: CGPoint(x: component.coordinates.xCoordinate, y: component.coordinates.yCoordinate),
+ shape: component.shape
+ )
+ vertices.append(newVertex)
+ vertexMap[component.label.lowercased()] = newVertex
+ }
+
+ for (index, note) in map.notes.enumerated() {
+ notes.append(
+ Note(
+ id: index,
+ position: CGPoint(x: note.coordinates.xCoordinate, y: note.coordinates.yCoordinate),
+ text: note.text
+ ))
+ }
- default:
- continue
+ for stage in map.stages {
+ let stageIndex = stageToIndex(stage.stage)
+ if stageIndex >= 0 && stageIndex < 3 {
+ stages[stageIndex] = stage.value
}
}
// Second pass for entities that depend on vertices
- for entity in entities {
- switch entity {
- case .edge(let edge):
- if let fromVertex = vertexMap[edge.from.lowercased()],
- let toVertex = vertexMap[edge.to.lowercased()]
- {
- edges.append(
- MapEdge(
- id: edges.count,
- origin: fromVertex.position,
- destination: toVertex.position,
- arrowhead: edge.isDirected
- ))
- }
-
- case .inertia(let inertia):
- if let vertex = vertexMap[inertia.vertex.lowercased()] {
- inertias.append(
- Inertia(
- id: inertias.count,
- position: vertex.position
- ))
- }
-
- case .evolution(let evolution):
- if let vertex = vertexMap[evolution.vertex.lowercased()] {
- let direction: CGFloat = evolution.isPositive ? 1.0 : -1.0
- let destination = CGPoint(
- x: vertex.position.x + (direction * evolution.value),
- y: vertex.position.y
- )
- opportunities.append(
- Opportunity(
- id: opportunities.count,
- origin: vertex.position,
- destination: destination
- ))
- }
-
- case .group(let group):
- var groupVertices: [Vertex] = []
- for vertexName in group.vertices {
- if let vertex = vertexMap[vertexName.lowercased()] {
- groupVertices.append(vertex)
- }
- }
- if !groupVertices.isEmpty {
- groups.append(groupVertices)
+ for (index, dependency) in map.dependencies.enumerated() {
+ if let fromVertex = vertexMap[dependency.fromComponent.lowercased()],
+ let toVertex = vertexMap[dependency.toComponent.lowercased()]
+ {
+ edges.append(
+ MapEdge(
+ id: index,
+ origin: fromVertex.position,
+ destination: toVertex.position,
+ arrowhead: dependency.isDirected
+ ))
+ }
+ }
+
+ for (index, inertia) in map.inertias.enumerated() {
+ if let vertex = vertexMap[inertia.component.lowercased()] {
+ inertias.append(
+ Inertia(
+ id: index,
+ position: vertex.position
+ ))
+ }
+ }
+
+ for (index, evolution) in map.evolutions.enumerated() {
+ if let vertex = vertexMap[evolution.component.lowercased()] {
+ let destination = CGPoint(
+ x: vertex.position.x + evolution.value,
+ y: vertex.position.y
+ )
+ opportunities.append(
+ Opportunity(
+ id: index,
+ origin: vertex.position,
+ destination: destination
+ ))
+ }
+ }
+
+ for group in map.groups {
+ var groupVertices: [Vertex] = []
+ for componentName in group.components {
+ if let vertex = vertexMap[componentName.lowercased()] {
+ groupVertices.append(vertex)
}
-
- default:
- continue
+ }
+ if !groupVertices.isEmpty {
+ groups.append(groupVertices)
}
}
@@ -139,23 +130,12 @@ struct RenderableMap {
)
}
- private static func convertShape(_ shape: String?) -> VertexShape {
- guard let shape = shape?.lowercased() else { return .circle }
- switch shape {
- case "square": return .square
- case "triangle": return .triangle
- case "x": return .x
- default: return .circle
- }
- }
-
- private static func convertStageNumber(_ stageNumber: String) -> Int {
- switch stageNumber.lowercased() {
- case "i": return 1
- case "ii": return 2
- case "iii": return 3
- case "iv": return 4
- default: return 1
+ private static func stageToIndex(_ stageNumber: WmapParser.StageNumber) -> Int {
+ switch stageNumber {
+ case .stageI: return 0
+ case .stageII: return 1
+ case .stageIII: return 2
+ case .stageIV: return 3
}
}
}
@@ -164,7 +144,7 @@ struct Vertex: Identifiable, Hashable {
let id: Int
let label: String
let position: CGPoint
- var shape: VertexShape = .circle
+ var shape: WmapParser.Shape = .circle
}
struct Note {
@@ -173,13 +153,6 @@ struct Note {
let text: String
}
-enum VertexShape: String {
- case circle
- case square
- case triangle
- case x
-}
-
struct MapEdge {
let id: Int
let origin: CGPoint
@@ -198,12 +171,7 @@ struct Opportunity {
let destination: CGPoint
}
-struct StageDimensions {
- let index: Int
- let dimensions: CGFloat
-}
-
-private let defaultDimensions: [CGFloat] = [
+private let defaultDimensions: [Double] = [
25.0,
50.0,
75.0,
diff --git a/Map/Business/WmapParser/Entity.swift b/Map/Business/WmapParser/Entity.swift
deleted file mode 100644
index 52f6e93..0000000
--- a/Map/Business/WmapParser/Entity.swift
+++ /dev/null
@@ -1,440 +0,0 @@
-struct Wmap {
- 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: Equatable {
- let label: String
- let position: SourcePosition
- let coordinates: (Double, Double)
- let shape: String?
- let range: SourceRange
-
- // Syntax element ranges (populated during parsing)
- let labelRange: SourceRange
- let leftParenRange: SourceRange
- let xCoordRange: SourceRange
- let commaRange: SourceRange
- let yCoordRange: SourceRange
- 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] = []
-
- // Vertex label
- elements.append(
- SyntaxElement(
- type: .vertexLabel,
- range: labelRange,
- value: label
- ))
-
- // Left parenthesis
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: leftParenRange,
- value: "("
- ))
-
- // X coordinate
- elements.append(
- SyntaxElement(
- type: .number,
- range: xCoordRange,
- value: String(coordinates.0)
- ))
-
- // Comma
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: commaRange,
- value: ","
- ))
-
- // Y coordinate
- elements.append(
- SyntaxElement(
- type: .number,
- range: yCoordRange,
- value: String(coordinates.1)
- ))
-
- // Right parenthesis
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: rightParenRange,
- value: ")"
- ))
-
- // Shape if present
- if let shape = shape, let shapeRange = shapeRange {
- elements.append(
- SyntaxElement(
- type: .shape,
- range: shapeRange,
- value: "[\(shape)]"
- ))
- }
-
- return elements
- }
- }
-
- struct Edge: Equatable {
- let from: String
- let to: String
- let isDirected: Bool
- let position: SourcePosition
- let range: SourceRange
-
- // Syntax element ranges
- let fromLabelRange: SourceRange
- let edgeTypeRange: SourceRange
- let toLabelRange: SourceRange
-
- var syntaxElements: [SyntaxElement] {
- var elements: [SyntaxElement] = []
-
- // From vertex label
- elements.append(
- SyntaxElement(
- type: .vertexLabel,
- range: fromLabelRange,
- value: from
- ))
-
- // Edge symbol
- elements.append(
- SyntaxElement(
- type: .symbol,
- range: edgeTypeRange,
- value: isDirected ? "->" : "--"
- ))
-
- // To vertex label
- elements.append(
- SyntaxElement(
- type: .vertexLabel,
- range: toLabelRange,
- value: to
- ))
-
- return elements
- }
- }
-
- struct Note: Equatable {
- let coordinates: (Double, Double)
- let text: String
- let position: SourcePosition
- let range: SourceRange
-
- // Syntax element ranges
- let keywordRange: SourceRange
- let leftParenRange: SourceRange
- let xCoordRange: SourceRange
- let commaRange: SourceRange
- let yCoordRange: SourceRange
- 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] = []
-
- // [Note] keyword
- elements.append(
- SyntaxElement(
- type: .keyword,
- range: keywordRange,
- value: "[Note]"
- ))
-
- // Left parenthesis
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: leftParenRange,
- value: "("
- ))
-
- // X coordinate
- elements.append(
- SyntaxElement(
- type: .number,
- range: xCoordRange,
- value: String(coordinates.0)
- ))
-
- // Comma
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: commaRange,
- value: ","
- ))
-
- // Y coordinate
- elements.append(
- SyntaxElement(
- type: .number,
- range: yCoordRange,
- value: String(coordinates.1)
- ))
-
- // Right parenthesis
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: rightParenRange,
- value: ")"
- ))
-
- // Text content
- elements.append(
- SyntaxElement(
- type: .text,
- range: textRange,
- value: text
- ))
-
- return elements
- }
- }
-
- struct Stage: Equatable {
- let number: String // i, ii, iii, iv
- let value: Double
- let position: SourcePosition
- let range: SourceRange
-
- // Syntax element ranges
- let leftBracketRange: SourceRange
- let stageNumberRange: SourceRange
- let rightBracketRange: SourceRange
- let valueRange: SourceRange
-
- var syntaxElements: [SyntaxElement] {
- var elements: [SyntaxElement] = []
-
- // Left bracket
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: leftBracketRange,
- value: "["
- ))
-
- // Stage number
- elements.append(
- SyntaxElement(
- type: .stageNumber,
- range: stageNumberRange,
- value: number
- ))
-
- // Right bracket
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: rightBracketRange,
- value: "]"
- ))
-
- // Value
- elements.append(
- SyntaxElement(
- type: .number,
- range: valueRange,
- value: String(value)
- ))
-
- return elements
- }
- }
-
- struct Group: Equatable {
- let vertices: [String]
- let position: SourcePosition
- let range: SourceRange
-
- // Syntax element ranges
- let keywordRange: SourceRange
- let vertexRanges: [SourceRange]
- let commaRanges: [SourceRange]
-
- var syntaxElements: [SyntaxElement] {
- var elements: [SyntaxElement] = []
-
- // [Group] keyword
- elements.append(
- SyntaxElement(
- type: .keyword,
- range: keywordRange,
- value: "[Group]"
- ))
-
- // Vertex labels and commas
- for (index, vertex) in vertices.enumerated() {
- elements.append(
- SyntaxElement(
- type: .vertexLabel,
- range: vertexRanges[index],
- value: vertex
- ))
-
- // Add comma if not the last vertex
- if index < vertices.count - 1 {
- elements.append(
- SyntaxElement(
- type: .punctuation,
- range: commaRanges[index],
- value: ","
- ))
- }
- }
-
- return elements
- }
- }
-
- struct Inertia: Equatable {
- let vertex: String
- let position: SourcePosition
- let range: SourceRange
-
- // Syntax element ranges
- let keywordRange: SourceRange
- let vertexRange: SourceRange
-
- var syntaxElements: [SyntaxElement] {
- var elements: [SyntaxElement] = []
-
- // [Inertia] keyword
- elements.append(
- SyntaxElement(
- type: .keyword,
- range: keywordRange,
- value: "[Inertia]"
- ))
-
- // Vertex label
- elements.append(
- SyntaxElement(
- type: .vertexLabel,
- range: vertexRange,
- value: vertex
- ))
-
- return elements
- }
- }
-
- struct Evolution: Equatable {
- let vertex: String
- let isPositive: Bool
- let value: Double
- let position: SourcePosition
- let range: SourceRange
-
- // Syntax element ranges
- let keywordRange: SourceRange
- let vertexRange: SourceRange
- let signRange: SourceRange
- let valueRange: SourceRange
-
- var syntaxElements: [SyntaxElement] {
- var elements: [SyntaxElement] = []
-
- // [Evolution] keyword
- elements.append(
- SyntaxElement(
- type: .keyword,
- range: keywordRange,
- value: "[Evolution]"
- ))
-
- // Vertex label
- elements.append(
- SyntaxElement(
- type: .vertexLabel,
- range: vertexRange,
- value: vertex
- ))
-
- // Sign
- elements.append(
- SyntaxElement(
- type: .symbol,
- range: signRange,
- value: isPositive ? "+" : "-"
- ))
-
- // Value
- elements.append(
- SyntaxElement(
- type: .number,
- range: valueRange,
- value: String(value)
- ))
-
- return elements
- }
- }
-}
diff --git a/Map/Business/WmapParser/Lexer.swift b/Map/Business/WmapParser/Lexer.swift
deleted file mode 100644
index 93ba712..0000000
--- a/Map/Business/WmapParser/Lexer.swift
+++ /dev/null
@@ -1,463 +0,0 @@
-// Copyright (C) 2024 Rubén Beltrán del Río
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see https://map.tranquil.systems.
-extension Wmap {
- class Lexer {
- private enum LexerState {
- case normal
- case afterNoteKeyword
- case inNotePosition(tokensNeeded: Int) // tracks: leftParen(1), number(2), comma(3), number(4), rightParen(5)
- case expectingText
- }
-
- let input: String
- private var current: String.Index
- private var line = 1
- private var column = 1
- private var state: LexerState = .normal
-
- init(_ input: String) {
- self.input = input
- self.current = input.startIndex
- }
-
- func nextToken() -> Token {
- skipWhitespace()
-
- guard current < input.endIndex else {
- return Token(
- type: .eof,
- value: "",
- position: currentPosition(),
- stringRange: current..<current
- )
- }
-
- // Handle text scanning before character-specific parsing
- if case .expectingText = state {
- state = .normal // Reset after scanning text
- return scanText()
- }
-
- let char = input[current].lowercased().first!
- let pos = currentPosition()
- let tokenStart = current
-
- switch char {
- case "\n", "\r":
- state = .normal // Reset state on newline
- return scanLineEnding()
-
- case "(":
- updateStateForToken(.leftParen)
- advance()
- return Token(
- type: .leftParen,
- value: "(",
- position: pos,
- stringRange: tokenStart..<current
- )
-
- case ")":
- updateStateForToken(.rightParen)
- advance()
- return Token(
- type: .rightParen,
- value: ")",
- position: pos,
- stringRange: tokenStart..<current
- )
-
- case "[":
- return scanBracketedToken()
-
- case ",":
- updateStateForToken(.comma)
- advance()
- return Token(
- type: .comma,
- value: ",",
- position: pos,
- stringRange: tokenStart..<current
- )
-
- case "+":
- advance()
- return Token(
- type: .plus,
- value: "+",
- position: pos,
- stringRange: tokenStart..<current
- )
-
- case "-":
- if peek() == "-" {
- advance()
- advance()
- return Token(
- type: .edgeUndirected,
- value: "--",
- position: pos,
- stringRange: tokenStart..<current
- )
- } else if peek() == ">" {
- advance()
- advance()
- return Token(
- type: .edgeDirected,
- value: "->",
- position: pos,
- stringRange: tokenStart..<current
- )
- } else {
- advance()
- return Token(
- type: .minus,
- value: "-",
- position: pos,
- stringRange: tokenStart..<current
- )
- }
-
- case "0"..."9", ".":
- let numberToken = scanNumber()
- updateStateForToken(.realNumber)
- return numberToken
-
- default:
- // If we're expecting text, scan as text instead of vertex label
- if case .expectingText = state {
- state = .normal // 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()
- }
- }
-
- private func updateStateForToken(_ tokenType: TokenType) {
- switch state {
- case .normal:
- // No state changes needed for normal parsing
- break
-
- case .afterNoteKeyword:
- if tokenType == .leftParen {
- state = .inNotePosition(tokensNeeded: 1) // Expecting: number, comma, number, rightParen
- } else {
- state = .normal // Invalid sequence, reset
- }
-
- case .inNotePosition(let tokensNeeded):
- switch tokensNeeded {
- case 1: // Expecting first number
- if tokenType == .realNumber {
- state = .inNotePosition(tokensNeeded: 2)
- } else {
- state = .normal
- }
- case 2: // Expecting comma
- if tokenType == .comma {
- state = .inNotePosition(tokensNeeded: 3)
- } else {
- state = .normal
- }
- case 3: // Expecting second number
- if tokenType == .realNumber {
- state = .inNotePosition(tokensNeeded: 4)
- } else {
- state = .normal
- }
- case 4: // Expecting right paren
- if tokenType == .rightParen {
- state = .expectingText // Complete position sequence, now expect text
- } else {
- state = .normal
- }
- default:
- state = .normal
- }
-
- case .expectingText:
- // Text has been consumed, reset to normal
- state = .normal
- }
- }
-
- func scanText() -> Token {
- let pos = currentPosition()
-
- // Skip any leading whitespace
- skipWhitespace()
-
- let tokenStart = current
-
- while current < input.endIndex && !isLineEnding(input[current]) {
- advance()
- }
-
- let originalValue = String(input[tokenStart..<current]).trimmingCharacters(in: .whitespaces)
- return Token(
- type: .text,
- value: originalValue,
- position: pos,
- stringRange: tokenStart..<current
- )
- }
-
- 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] != "]" && !isLineEnding(input[current]) {
- advance()
- }
-
- // 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 line ending)
- return Token(
- type: .error,
- value: String(input[tokenStart..<current]),
- position: pos,
- stringRange: tokenStart..<current
- )
- }
-
- let originalContent = String(input[contentStart..<current])
- let lowercaseContent = originalContent.lowercased()
- advance() // consume ']'
-
- let tokenEnd = current
-
- switch lowercaseContent {
- case "note":
- state = .afterNoteKeyword // Set state to track note parsing sequence
- return Token(
- type: .noteKeyword,
- value: "[Note]",
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- case "group":
- return Token(
- type: .groupKeyword,
- value: "[Group]",
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- case "inertia":
- return Token(
- type: .inertiaKeyword,
- value: "[Inertia]",
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- case "evolution":
- return Token(
- type: .evolutionKeyword,
- value: "[Evolution]",
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- case "x", "square", "triangle", "circle":
- return Token(
- type: .shapeLabel,
- value: originalContent, // Use original case, not lowercase
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- default:
- if lowercaseContent.allSatisfy({ $0 == "i" }) && lowercaseContent.count <= 4 {
- return Token(
- type: .stageNumber,
- value: lowercaseContent,
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- }
- // Return error token for invalid bracketed content
- return Token(
- type: .error,
- value: String(input[tokenStart..<tokenEnd]),
- position: pos,
- stringRange: tokenStart..<tokenEnd
- )
- }
- }
-
- private func scanNumber() -> Token {
- let pos = currentPosition()
- let tokenStart = current
-
- // Handle leading decimal point
- if input[current] == "." {
- advance()
- }
-
- while current < input.endIndex && input[current].isNumber {
- advance()
- }
-
- // Handle decimal point if we haven't seen one
- if current < input.endIndex && input[current] == "." && input[tokenStart] != "." {
- advance()
- while current < input.endIndex && input[current].isNumber {
- advance()
- }
- }
-
- let value = String(input[tokenStart..<current])
- return Token(
- type: .realNumber,
- value: value,
- position: pos,
- stringRange: tokenStart..<current
- )
- }
-
- private func scanStageOrLabel() -> Token {
- let pos = currentPosition()
- let tokenStart = current
-
- // Check if it's a stage number (i, ii, iii, iv)
- var tempIndex = current
- var iCount = 0
-
- while tempIndex < input.endIndex && input[tempIndex].lowercased() == "i" && iCount < 4 {
- tempIndex = input.index(after: tempIndex)
- iCount += 1
- }
-
- // If we have 1-4 i's and next char is whitespace/delimiter, it's a stage
- if iCount > 0 && iCount <= 4 && (tempIndex >= input.endIndex || isDelimiter(input[tempIndex]))
- {
- current = tempIndex
- return Token(
- type: .stageNumber,
- value: String(repeating: "i", count: iCount),
- position: pos,
- stringRange: tokenStart..<current
- )
- }
-
- // Otherwise, scan as vertex label
- return scanVertexLabel()
- }
-
- private func scanVertexLabel() -> Token {
- let pos = currentPosition()
- let tokenStart = current
-
- while current < input.endIndex && !isVertexLabelDelimiter(input[current]) {
- advance()
- }
-
- let originalValue = String(input[tokenStart..<current]).trimmingCharacters(in: .whitespaces)
- return Token(
- type: .vertexLabel,
- value: originalValue,
- position: pos,
- stringRange: tokenStart..<current
- )
- }
-
- private func isVertexLabelDelimiter(_ char: Character) -> Bool {
- return char == "-" || char == "+" || char == "," || char == "[" || char == "]" || char == "("
- || char == ")" || isLineEnding(char)
- }
-
- private func isDelimiter(_ char: Character) -> Bool {
- return char.isWhitespace || isLineEnding(char) || char == "(" || char == ")" || char == "["
- || char == "]" || char == ","
- }
-
- private func skipWhitespace() {
- while current < input.endIndex && input[current].isWhitespace && !isLineEnding(input[current])
- {
- advance()
- }
- }
-
- private func advance() {
- if current < input.endIndex {
- current = input.index(after: current)
- column += 1
- }
- }
-
- private func peek() -> Character? {
- let next = input.index(after: current)
- return next < input.endIndex ? input[next] : nil
- }
-
- private func currentPosition() -> SourcePosition {
- return SourcePosition(line: line, column: column, stringIndex: current)
- }
- }
-}
diff --git a/Map/Business/WmapParser/Parser.swift b/Map/Business/WmapParser/Parser.swift
deleted file mode 100644
index 7c87150..0000000
--- a/Map/Business/WmapParser/Parser.swift
+++ /dev/null
@@ -1,504 +0,0 @@
-// Copyright (C) 2024 Rubén Beltrán del Río
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see https://map.tranquil.systems.
-import Foundation
-
-extension Wmap {
-
- struct ParsedMap {
- let entities: [Entity]
- let vertexLabels: Set<String>
- }
-
- class Parser {
- private let lexer: Lexer
- private var currentToken: Token
- private var vertexLabelMap: [String: String] = [:]
-
- init(lexer: Lexer) {
- self.lexer = lexer
- self.currentToken = lexer.nextToken()
- }
-
- func parse() -> ParsedMap {
- var entities: [Entity] = []
- var vertexLabels: Set<String> = Set()
-
- while currentToken.type != .eof {
- if currentToken.type == .newline {
- advance()
- continue
- }
-
- do {
- let entity = try parseEntity()
- entities.append(entity)
- switch entity {
- case .vertex(let vertex):
- vertexLabels.insert(vertex.label.lowercased())
- default:
- break
- }
-
- if currentToken.type == .newline {
- advance()
- } else if currentToken.type != .eof {
- throw ParseError.expectedNewline(actual: currentToken)
- }
- } catch {
- skipToNextLine()
- }
- }
-
- return ParsedMap(entities: entities, vertexLabels: vertexLabels)
- }
-
- private func parseEntity() throws -> Entity {
- switch currentToken.type {
- case .vertexLabel:
- return try parseVertexOrEdge()
- case .noteKeyword:
- return .note(try parseNote())
- case .stageNumber:
- return .stage(try parseStage())
- case .groupKeyword:
- return .group(try parseGroup())
- case .inertiaKeyword:
- return .inertia(try parseInertia())
- case .evolutionKeyword:
- return .evolution(try parseEvolution())
- case .error:
- // Error tokens should cause the line to be ignored
- throw ParseError.invalidLine
- default:
- throw ParseError.invalidLine
- }
- }
-
- private func parseVertexOrEdge() throws -> Entity {
- let startPos = currentToken.position
- let firstLabel = currentToken.value
- let firstLabelRange = makeSourceRange(from: currentToken)
-
- vertexLabelMap[firstLabel.lowercased()] = firstLabel
- advance()
-
- if currentToken.type == .edgeUndirected || currentToken.type == .edgeDirected {
- let isDirected = currentToken.type == .edgeDirected
- let edgeTypeRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .vertexLabel else {
- throw ParseError.expectedVertexLabel(actual: currentToken)
- }
-
- let secondLabel = currentToken.value
- let secondLabelRange = makeSourceRange(from: currentToken)
- let endPos = currentToken.position
-
- vertexLabelMap[secondLabel.lowercased()] = secondLabel
- advance()
-
- 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()
- let (shape, shapeRange) = try parseOptionalShapeWithRange()
-
- let endPos = shapeRange?.end ?? coordRanges.rightParen.end
-
- 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
- ))
- }
- }
-
- private func parseNote() throws -> Note {
- let startPos = currentToken.position
- let keywordRange = makeSourceRange(from: currentToken)
- advance()
-
- let (coordinates, coordRanges) = try parsePositionWithRanges()
-
- 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,
- text: text,
- position: startPos,
- range: SourceRange(
- start: startPos, end: endPos,
- stringRange: keywordRange.stringRange.lowerBound..<textRange.stringRange.upperBound),
- keywordRange: keywordRange,
- leftParenRange: coordRanges.leftParen,
- xCoordRange: coordRanges.xCoord,
- commaRange: coordRanges.comma,
- yCoordRange: coordRanges.yCoord,
- rightParenRange: coordRanges.rightParen,
- textRange: textRange
- )
- }
-
- private func parseStage() throws -> Stage {
- let startPos = currentToken.position
- let stageToken = currentToken
- let stageValue = stageToken.value
-
- // For bracketed stage numbers like [ii], we need to parse the components
- // The lexer gives us the full token, but we need to break it down
- let tokenRange = stageToken.stringRange
-
- // Find the bracket positions within the token
- let leftBracketRange = SourceRange(
- start: stageToken.position,
- end: SourcePosition(
- line: stageToken.position.line,
- column: stageToken.position.column + 1,
- stringIndex: lexer.input.index(after: tokenRange.lowerBound)
- ),
- stringRange: tokenRange.lowerBound..<lexer.input.index(after: tokenRange.lowerBound)
- )
-
- let stageNumberStart = lexer.input.index(after: tokenRange.lowerBound)
- let stageNumberEnd = lexer.input.index(tokenRange.upperBound, offsetBy: -1)
- let stageNumberRange = SourceRange(
- start: SourcePosition(
- line: stageToken.position.line,
- column: stageToken.position.column + 1,
- stringIndex: stageNumberStart
- ),
- end: SourcePosition(
- line: stageToken.position.line,
- column: stageToken.position.column + 1 + stageValue.count,
- stringIndex: stageNumberEnd
- ),
- stringRange: stageNumberStart..<stageNumberEnd
- )
-
- let rightBracketRange = SourceRange(
- start: SourcePosition(
- line: stageToken.position.line,
- column: stageToken.position.column + 1 + stageValue.count,
- stringIndex: stageNumberEnd
- ),
- end: SourcePosition(
- line: stageToken.position.line,
- column: stageToken.position.column + 2 + stageValue.count,
- stringIndex: tokenRange.upperBound
- ),
- stringRange: stageNumberEnd..<tokenRange.upperBound
- )
-
- advance()
-
- guard currentToken.type == .realNumber else {
- throw ParseError.expectedNumber(actual: currentToken)
- }
-
- guard let value = Double(currentToken.value) else {
- throw ParseError.invalidNumber(currentToken.value)
- }
-
- let valueRange = makeSourceRange(from: currentToken)
- let endPos = currentToken.position
- advance()
-
- return Stage(
- number: stageValue,
- value: value,
- position: startPos,
- range: SourceRange(
- start: startPos,
- end: endPos,
- stringRange: leftBracketRange.stringRange.lowerBound..<valueRange.stringRange.upperBound
- ),
- leftBracketRange: leftBracketRange,
- stageNumberRange: stageNumberRange,
- rightBracketRange: rightBracketRange,
- valueRange: valueRange
- )
- }
-
- private func parseGroup() throws -> Group {
- let startPos = currentToken.position
- let keywordRange = makeSourceRange(from: currentToken)
- advance()
-
- var vertices: [String] = []
- var vertexRanges: [SourceRange] = []
- var commaRanges: [SourceRange] = []
-
- guard currentToken.type == .vertexLabel else {
- throw ParseError.expectedVertexLabel(actual: currentToken)
- }
-
- vertices.append(currentToken.value)
- vertexRanges.append(makeSourceRange(from: currentToken))
- advance()
-
- while currentToken.type == .comma {
- commaRanges.append(makeSourceRange(from: currentToken))
- advance()
-
- guard currentToken.type == .vertexLabel else {
- throw ParseError.expectedVertexLabel(actual: currentToken)
- }
-
- vertices.append(currentToken.value)
- vertexRanges.append(makeSourceRange(from: currentToken))
- advance()
- }
-
- let endPos = vertexRanges.last?.end ?? startPos
-
- return Group(
- vertices: vertices,
- position: startPos,
- range: SourceRange(
- start: startPos, end: endPos,
- stringRange: keywordRange.stringRange
- .lowerBound..<vertexRanges.last!.stringRange.upperBound),
- keywordRange: keywordRange,
- vertexRanges: vertexRanges,
- commaRanges: commaRanges
- )
- }
-
- private func parseInertia() throws -> Inertia {
- let startPos = currentToken.position
- let keywordRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .vertexLabel else {
- throw ParseError.expectedVertexLabel(actual: currentToken)
- }
-
- let vertex = currentToken.value
- let vertexRange = makeSourceRange(from: currentToken)
- let endPos = currentToken.position
- advance()
-
- return Inertia(
- vertex: vertex,
- position: startPos,
- range: SourceRange(
- start: startPos, end: endPos,
- stringRange: keywordRange.stringRange.lowerBound..<vertexRange.stringRange.upperBound),
- keywordRange: keywordRange,
- vertexRange: vertexRange
- )
- }
-
- private func parseEvolution() throws -> Evolution {
- let startPos = currentToken.position
- let keywordRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .vertexLabel else {
- throw ParseError.expectedVertexLabel(actual: currentToken)
- }
-
- let vertex = currentToken.value
- let vertexRange = makeSourceRange(from: currentToken)
- advance()
-
- let isPositive: Bool
- let signRange: SourceRange
- if currentToken.type == .plus {
- isPositive = true
- signRange = makeSourceRange(from: currentToken)
- advance()
- } else if currentToken.type == .minus {
- isPositive = false
- signRange = makeSourceRange(from: currentToken)
- advance()
- } else {
- throw ParseError.expectedSign(actual: currentToken)
- }
-
- guard currentToken.type == .realNumber else {
- throw ParseError.expectedNumber(actual: currentToken)
- }
-
- guard let value = Double(currentToken.value) else {
- throw ParseError.invalidNumber(currentToken.value)
- }
-
- let valueRange = makeSourceRange(from: currentToken)
- let endPos = currentToken.position
- advance()
-
- return Evolution(
- vertex: vertex,
- isPositive: isPositive,
- value: value,
- position: startPos,
- range: SourceRange(
- start: startPos, end: endPos,
- stringRange: keywordRange.stringRange.lowerBound..<valueRange.stringRange.upperBound),
- keywordRange: keywordRange,
- vertexRange: vertexRange,
- signRange: signRange,
- valueRange: valueRange
- )
- }
-
- // Helper methods
- private func parsePositionWithRanges() throws -> (
- (Double, Double),
- (
- leftParen: SourceRange, xCoord: SourceRange, comma: SourceRange, yCoord: SourceRange,
- rightParen: SourceRange
- )
- ) {
- guard currentToken.type == .leftParen else {
- throw ParseError.expectedLeftParen(actual: currentToken)
- }
- let leftParenRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .realNumber else {
- throw ParseError.expectedNumber(actual: currentToken)
- }
- guard let x = Double(currentToken.value) else {
- throw ParseError.invalidNumber(currentToken.value)
- }
- let xCoordRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .comma else {
- throw ParseError.expectedComma(actual: currentToken)
- }
- let commaRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .realNumber else {
- throw ParseError.expectedNumber(actual: currentToken)
- }
- guard let y = Double(currentToken.value) else {
- throw ParseError.invalidNumber(currentToken.value)
- }
- let yCoordRange = makeSourceRange(from: currentToken)
- advance()
-
- guard currentToken.type == .rightParen else {
- throw ParseError.expectedRightParen(actual: currentToken)
- }
- let rightParenRange = makeSourceRange(from: currentToken)
- advance()
-
- return ((x, y), (leftParenRange, xCoordRange, commaRange, yCoordRange, rightParenRange))
- }
-
- private func parseOptionalShapeWithRange() throws -> (String?, SourceRange?) {
- guard currentToken.type == .shapeLabel else {
- return (nil, nil)
- }
-
- let shape = currentToken.value
- let shapeRange = makeSourceRange(from: currentToken)
- advance()
- return (shape, shapeRange)
- }
-
- private func makeSourceRange(from token: Token) -> SourceRange {
- return SourceRange(
- start: token.position,
- end: SourcePosition(
- line: token.position.line,
- column: token.position.column + token.value.count,
- stringIndex: token.stringRange.upperBound
- ),
- stringRange: token.stringRange
- )
- }
-
- private func advance() {
- currentToken = lexer.nextToken()
- }
-
- private func skipToNextLine() {
- while currentToken.type != .newline && currentToken.type != .eof {
- advance()
- }
- if currentToken.type == .newline {
- advance()
- }
- }
- }
-
- enum ParseError: Error, LocalizedError {
- case unexpectedToken(expected: String, actual: Token)
- case expectedVertexLabel(actual: Token)
- case expectedNumber(actual: Token)
- case expectedText(actual: Token)
- case expectedSign(actual: Token)
- case expectedComma(actual: Token)
- case expectedLeftParen(actual: Token)
- case expectedRightParen(actual: Token)
- case expectedNewline(actual: Token)
- case invalidNumber(String)
- case invalidLine
-
- var errorDescription: String? {
- switch self {
- case .unexpectedToken(let expected, let actual):
- return
- "Expected \(expected), got \(actual.type) '\(actual.value)' at line \(actual.position.line)"
- case .expectedVertexLabel(let actual):
- return "Expected vertex label, got \(actual.type) at line \(actual.position.line)"
- case .invalidNumber(let value):
- return "Invalid number format: \(value)"
- // ... other cases
- default:
- return "Parse error"
- }
- }
- }
-}
diff --git a/Map/Business/WmapParser/SourcePosition.swift b/Map/Business/WmapParser/SourcePosition.swift
deleted file mode 100644
index e83c07a..0000000
--- a/Map/Business/WmapParser/SourcePosition.swift
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2024 Rubén Beltrán del Río
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see https://map.tranquil.systems.
-extension Wmap {
- struct SourcePosition: Equatable {
- let line: Int
- let column: Int
- let stringIndex: String.Index
-
- static func == (lhs: SourcePosition, rhs: SourcePosition) -> Bool {
- return lhs.line == rhs.line && lhs.column == rhs.column && lhs.stringIndex == rhs.stringIndex
- }
- }
-
- struct SourceRange: Equatable {
- let start: SourcePosition
- let end: SourcePosition
- let stringRange: Range<String.Index>
-
- static func == (lhs: SourceRange, rhs: SourceRange) -> Bool {
- return lhs.start == rhs.start && lhs.end == rhs.end && lhs.stringRange == rhs.stringRange
- }
- }
-}
diff --git a/Map/Business/WmapParser/Syntax.swift b/Map/Business/WmapParser/Syntax.swift
deleted file mode 100644
index 551ebcd..0000000
--- a/Map/Business/WmapParser/Syntax.swift
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2024 Rubén Beltrán del Río
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see https://map.tranquil.systems.
-extension Wmap {
- struct SyntaxElement {
- let type: SyntaxType
- let range: SourceRange
- let value: String
- }
-
- enum SyntaxType {
- case keyword // [Note], [Group], etc.
- case vertexLabel // vertex names
- case number // coordinates, values
- case symbol // ->, --, +, -
- case shape // [square], [circle]
- case text // note text
- case punctuation // (, ), [, ], ,
- case stageNumber // i, ii, iii, iv
- }
-}
diff --git a/Map/Business/WmapParser/Token.swift b/Map/Business/WmapParser/Token.swift
deleted file mode 100644
index 86bab60..0000000
--- a/Map/Business/WmapParser/Token.swift
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) 2024 Rubén Beltrán del Río
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see https://map.tranquil.systems.
-extension Wmap {
- struct Token {
- let type: TokenType
- let value: String
- let position: SourcePosition
- let stringRange: Range<String.Index>
- }
-}
diff --git a/Map/Business/WmapParser/TokenType.swift b/Map/Business/WmapParser/TokenType.swift
deleted file mode 100644
index 2e316bd..0000000
--- a/Map/Business/WmapParser/TokenType.swift
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2024 Rubén Beltrán del Río
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see https://map.tranquil.systems.
-extension Wmap {
- enum TokenType: CaseIterable {
- case vertexLabel
- case edgeUndirected // --
- case edgeDirected // ->
- case leftParen, rightParen
- case leftBracket, rightBracket
- case comma
- case plus, minus
- case realNumber
- case stageNumber // i, ii, iii, iv
- case shapeLabel // x, square, triangle, circle
- case noteKeyword // [Note]
- case groupKeyword // [Group]
- case inertiaKeyword // [Inertia]
- case evolutionKeyword // [Evolution]
- case text // free text until newline
- case newline
- case eof
- case error // invalid token that should cause line to be ignored
- }
-}
diff --git a/Map/Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift b/Map/Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift
deleted file mode 100644
index 8b31547..0000000
--- a/Map/Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright (C) 2024 Rubén Beltrán del Río
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see https://map.tranquil.systems.
-import AppKit
-
-extension Wmap {
- class SyntaxHighlighter {
- var cachedSyntaxElements: [SyntaxElement] = []
- private var lineToElementsMap: [Int: [SyntaxElement]] = [:]
- private var cachedVertexLabels: Set<String> = Set()
-
- func updateSyntaxElements(from parsedMap: Wmap.ParsedMap) {
- cachedSyntaxElements = parsedMap.entities.flatMap { $0.syntaxElements }
- cachedVertexLabels = parsedMap.vertexLabels
-
- // Build line-to-elements mapping for fast lookup
- lineToElementsMap.removeAll()
- for element in cachedSyntaxElements {
- let line = element.range.start.line
- lineToElementsMap[line, default: []].append(element)
- }
- }
-
- func applySyntaxHighlighting(textStorage: NSTextStorage, range: NSRange) {
- // Get the line range that encompasses the edited range
- let string = textStorage.string as NSString
- let lineRange = string.lineRange(for: range)
-
- // Apply highlighting to elements that intersect with the line range
- for element in cachedSyntaxElements {
- // Safely convert string range to NSRange with bounds checking
- guard element.range.stringRange.lowerBound <= element.range.stringRange.upperBound,
- element.range.stringRange.upperBound <= textStorage.string.endIndex
- else {
- continue
- }
-
- let nsRange = NSRange(element.range.stringRange, in: textStorage.string)
-
- // Only process elements that intersect with our update area
- guard nsRange.location != NSNotFound,
- nsRange.location + nsRange.length <= textStorage.length,
- NSIntersectionRange(nsRange, lineRange).length > 0
- else {
- continue
- }
-
- let color = colorForSyntaxType(element.type)
- var attributes: [NSAttributedString.Key: Any] = [
- .foregroundColor: color
- ]
-
- if UserDefaults.standard.bool(forKey: "useSmartEditor") {
- if element.type == .vertexLabel
- && !cachedVertexLabels.contains(element.value.lowercased())
- {
- if UserDefaults.standard.bool(forKey: "highlightMissingComponents") {
- attributes[.underlineStyle] =
- NSUnderlineStyle.thick.rawValue | NSUnderlineStyle.patternDot.rawValue
- attributes[.underlineColor] = NSColor.Theme.Syntax.error
- attributes[.init("MissingComponent")] = element.value
- }
- }
- }
-
- textStorage.addAttributes(attributes, range: nsRange)
- }
- }
-
- private func colorForSyntaxType(_ type: SyntaxType) -> NSColor {
- switch type {
- case .keyword:
- return NSColor.Theme.Syntax.option
- case .vertexLabel:
- return NSColor.Theme.Syntax.vertex
- case .number:
- return NSColor.Theme.Syntax.number
- case .symbol:
- return NSColor.Theme.Syntax.symbol
- case .shape:
- return NSColor.Theme.Syntax.option
- case .text:
- return NSColor.Theme.Syntax.text
- case .punctuation:
- return NSColor.Theme.Syntax.punctuation
- case .stageNumber:
- return NSColor.Theme.Syntax.option
- }
- }
- }
-}