// 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 private var expectingText = false 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) } } }