diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-07-09 01:55:50 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-07-09 01:55:50 +0200 |
| commit | 6926feead1a7fadb7f5f974996bef8c30b3bfdbe (patch) | |
| tree | 5a16e64f70bea3b9f33220cce5ca524e686dbedf /Map/Business/WmapParser | |
| parent | c21a68c807dcb84b4ce910ee0f73238a8019ffe9 (diff) | |
Use AST based text editor
Diffstat (limited to 'Map/Business/WmapParser')
| -rw-r--r-- | Map/Business/WmapParser/Entity.swift | 389 | ||||
| -rw-r--r-- | Map/Business/WmapParser/Lexer.swift | 344 | ||||
| -rw-r--r-- | Map/Business/WmapParser/Parser.swift | 488 | ||||
| -rw-r--r-- | Map/Business/WmapParser/SourcePosition.swift | 35 | ||||
| -rw-r--r-- | Map/Business/WmapParser/Syntax.swift | 32 | ||||
| -rw-r--r-- | Map/Business/WmapParser/Token.swift | 22 | ||||
| -rw-r--r-- | Map/Business/WmapParser/TokenType.swift | 36 |
7 files changed, 1346 insertions, 0 deletions
diff --git a/Map/Business/WmapParser/Entity.swift b/Map/Business/WmapParser/Entity.swift new file mode 100644 index 0000000..40dd7ca --- /dev/null +++ b/Map/Business/WmapParser/Entity.swift @@ -0,0 +1,389 @@ +struct Wmap { + protocol Entity { + var position: SourcePosition { get } + var range: SourceRange { get } + var syntaxElements: [SyntaxElement] { get } + } + + struct Vertex: Entity { + 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? + + 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: Entity { + 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: Entity { + 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 + + 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: Entity { + 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: Entity { + 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: Entity { + 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: Entity { + 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 new file mode 100644 index 0000000..dc789bb --- /dev/null +++ b/Map/Business/WmapParser/Lexer.swift @@ -0,0 +1,344 @@ +// 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 { + let input: String + private var current: String.Index + private var line = 1 + private var column = 1 + + 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 + ) + } + + let char = input[current].lowercased().first! + let pos = currentPosition() + let tokenStart = current + + switch char { + case "\n": + advance() + let token = Token( + type: .newline, + value: "\n", + position: pos, + stringRange: tokenStart..<current + ) + line += 1 + column = 1 + return token + + case "(": + advance() + return Token( + type: .leftParen, + value: "(", + position: pos, + stringRange: tokenStart..<current + ) + + case ")": + advance() + return Token( + type: .rightParen, + value: ")", + position: pos, + stringRange: tokenStart..<current + ) + + case "[": + return scanBracketedToken() + + case ",": + 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", ".": + return scanNumber() + + case "i": + return scanStageOrLabel() + + default: + // If we encounter an unexpected character, try to scan it as a vertex label + // This provides more graceful degradation than failing immediately + return scanVertexLabel() + } + } + + func scanText() -> Token { + let pos = currentPosition() + let tokenStart = current + + while current < input.endIndex && input[current] != "\n" { + advance() + } + + let originalValue = String(input[tokenStart..<current]) + return Token( + type: .text, + value: originalValue, + position: pos, + stringRange: tokenStart..<current + ) + } + + private func scanBracketedToken() -> Token { + let pos = currentPosition() + let tokenStart = current + advance() // consume '[' + + let contentStart = current + while current < input.endIndex && input[current] != "]" && input[current] != "\n" { + advance() + } + + // If we hit a newline or EOF before finding the closing bracket, it's an error + guard current < input.endIndex && input[current] == "]" else { + // Return error token for unclosed bracket (don't consume past newline) + return 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": + 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 == ")" || char == "\n" + } + + private func isDelimiter(_ char: Character) -> Bool { + return char.isWhitespace || char == "\n" || char == "(" || char == ")" || char == "[" + || char == "]" || char == "," + } + + private func skipWhitespace() { + while current < input.endIndex && input[current].isWhitespace && input[current] != "\n" { + 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 new file mode 100644 index 0000000..f04998c --- /dev/null +++ b/Map/Business/WmapParser/Parser.swift @@ -0,0 +1,488 @@ +// 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 { + 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() -> [Entity] { + var entities: [Entity] = [] + + while currentToken.type != .eof { + if currentToken.type == .newline { + advance() + continue + } + + do { + let entity = try parseEntity() + entities.append(entity) + + if currentToken.type == .newline { + advance() + } else if currentToken.type != .eof { + throw ParseError.expectedNewline(actual: currentToken) + } + } catch { + skipToNextLine() + } + } + + return entities + } + + private func parseEntity() throws -> Entity { + switch currentToken.type { + case .vertexLabel: + return try parseVertexOrEdge() + case .noteKeyword: + return try parseNote() + case .stageNumber: + return try parseStage() + case .groupKeyword: + return try parseGroup() + case .inertiaKeyword: + return try parseInertia() + case .evolutionKeyword: + return 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( + 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( + 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() + + // For notes, we need to scan the remaining text on the line + let textToken = lexer.scanText() + let text = textToken.value + let textRange = makeSourceRange(from: textToken) + let endPos = textToken.position + currentToken = lexer.nextToken() // Update current token after scanning text + + 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 new file mode 100644 index 0000000..e83c07a --- /dev/null +++ b/Map/Business/WmapParser/SourcePosition.swift @@ -0,0 +1,35 @@ +// 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 new file mode 100644 index 0000000..551ebcd --- /dev/null +++ b/Map/Business/WmapParser/Syntax.swift @@ -0,0 +1,32 @@ +// 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 new file mode 100644 index 0000000..86bab60 --- /dev/null +++ b/Map/Business/WmapParser/Token.swift @@ -0,0 +1,22 @@ +// 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 new file mode 100644 index 0000000..2e316bd --- /dev/null +++ b/Map/Business/WmapParser/TokenType.swift @@ -0,0 +1,36 @@ +// 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 + } +} |