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/Lexer.swift | |
| parent | c21a68c807dcb84b4ce910ee0f73238a8019ffe9 (diff) | |
Use AST based text editor
Diffstat (limited to 'Map/Business/WmapParser/Lexer.swift')
| -rw-r--r-- | Map/Business/WmapParser/Lexer.swift | 344 |
1 files changed, 344 insertions, 0 deletions
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) + } + } +} |