aboutsummaryrefslogtreecommitdiff
path: root/Map/Business/WmapParser/Lexer.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Map/Business/WmapParser/Lexer.swift')
-rw-r--r--Map/Business/WmapParser/Lexer.swift463
1 files changed, 0 insertions, 463 deletions
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)
- }
- }
-}