From 0d5e6636405dbe332c33c0fb82c7ccccb20f96cb Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Sun, 14 Dec 2025 20:21:24 +0100 Subject: Use wmap-parser-swift and tree-sitter-wmap for parsing and highlighting. --- Map/Business/RenderableMap.swift | 198 ++++---- Map/Business/WmapParser/Entity.swift | 440 ------------------ Map/Business/WmapParser/Lexer.swift | 463 ------------------- Map/Business/WmapParser/Parser.swift | 504 --------------------- Map/Business/WmapParser/SourcePosition.swift | 35 -- Map/Business/WmapParser/Syntax.swift | 32 -- Map/Business/WmapParser/Token.swift | 22 - Map/Business/WmapParser/TokenType.swift | 36 -- .../WmapSyntaxHighlighter/SyntaxHighlighter.swift | 102 ----- 9 files changed, 83 insertions(+), 1749 deletions(-) delete mode 100644 Map/Business/WmapParser/Entity.swift delete mode 100644 Map/Business/WmapParser/Lexer.swift delete mode 100644 Map/Business/WmapParser/Parser.swift delete mode 100644 Map/Business/WmapParser/SourcePosition.swift delete mode 100644 Map/Business/WmapParser/Syntax.swift delete mode 100644 Map/Business/WmapParser/Token.swift delete mode 100644 Map/Business/WmapParser/TokenType.swift delete mode 100644 Map/Business/WmapSyntaxHighlighter/SyntaxHighlighter.swift (limited to 'Map/Business') 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.." { - advance() - advance() - return Token( - type: .edgeDirected, - value: "->", - position: pos, - stringRange: tokenStart.. 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.. 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.. 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.. 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.. 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.. Token { - let pos = currentPosition() - let tokenStart = current - - while current < input.endIndex && !isVertexLabelDelimiter(input[current]) { - advance() - } - - let originalValue = String(input[tokenStart.. 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 - } - - 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 = 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.. 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.. 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.. 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.. 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.. 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.. ( - (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 - - 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 - } -} 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 = 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 - } - } - } -} -- cgit