// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers // // SPDX-License-Identifier: MPL-2.0 import Foundation /// This tokenises a string, without handling block layout at all. The tokens produced here should be fed into the ``BlockLayoutProcessor``. public struct Lexer { private let input: String private var index: String.Index private var tokens: [PterodactylSyntax.Token] public init(input: String) { self.input = input self.index = input.startIndex self.tokens = [] } private var isAtEnd: Bool { index >= input.endIndex } private var peek: Character? { guard index < input.endIndex else { return nil } return input[index] } private func lookahead() -> Character? { guard index < input.endIndex else { return nil } let next = input.index(after: index) guard next < input.endIndex else { return nil } return input[next] } private mutating func advance() -> Character { let c = input[index] index = input.index(after: index) return c } private mutating func consume(while predicate: (Character) -> Bool) { while let c = peek, predicate(c) { _ = advance() } } func text(from start: String.Index) -> String { let range = start.. (kind: TokenKind, text: String)? { guard let c = peek else { return nil } let start = index if c.isNewline { _ = advance() return (kind: .newline, text: text(from: start)) } if c.isWhitespace && !c.isNewline { consume { $0.isWhitespace && !$0.isNewline } return (kind: .whitespace, text: text(from: start)) } if c == "/" && lookahead() == "/" { _ = advance() _ = advance() consume { $0 != "\n" } return (kind: .lineComment, text: text(from: start)) } if c == "/" && lookahead() == "*" { _ = advance() // consume '/' _ = advance() // consume '*' var terminated = false while let ch = peek { if ch == "*" && lookahead() == "/" { _ = advance() // consume '*' _ = advance() // consume '/' terminated = true break } _ = advance() } return (kind: .blockComment(terminated: terminated), text: text(from: start)) } if c == "<" && lookahead() == "=" { _ = advance() _ = advance() return (kind: .punctuation(.doubleLeftArrow), text: text(from: start)) } if c == "=" && lookahead() == ">" { _ = advance() _ = advance() return (kind: .punctuation(.doubleRightArrow), text: text(from: start)) } if let punct = Punctuation(rawValue: String(c)) { _ = advance() return (.punctuation(punct), String(c)) } else if c.isLetter || c == "_" { _ = advance() consume { $0.isLetter || $0.isNumber || $0 == "_" } let text = text(from: start) if let keyword = Keyword(rawValue: text) { return (kind: .keyword(keyword), text: text) } return (kind: .identifier, text: text) } // Invalid single char (don’t drop input) let ch = advance() return (kind: .error, text: String(ch)) } public mutating func tokenize() -> [Token] { var tokens: [Token] = [] while !isAtEnd { guard let token = nextToken() else { break } tokens.append( Token(kind: token.kind, text: token.text) ) } let eofToken = Token(kind: .eof, text: "") tokens.append(eofToken) return tokens } }