# SPDX-License-Identifier: LGPL-2.1-only # Copyright 2022 Jookia <contact@jookia.org> import enum from src.syntax import Syntax, SyntaxStream, SyntaxType # Errors that can happen when parsing class ParseError(enum.Enum): TEST_ERROR = enum.auto() # pragma: no mutate NO_TOKEN = enum.auto() # pragma: no mutate NOT_TOKEN = enum.auto() # pragma: no mutate WRONG_TOKEN = enum.auto() # pragma: no mutate FOUND_STARTTEXT = enum.auto() # pragma: no mutate FOUND_STARTNOTE = enum.auto() # pragma: no mutate NOT_BOOL = enum.auto() # pragma: no mutate FOUND_ENDNOTE = enum.auto() # pragma: no mutate # Exception thrown when a parse error is encountered class ParseErrorException(BaseException): def __init__(self, error, syntax, expected): self.error = error self.syntax = syntax self.expected = expected def __str__(self): return ( "ParseErrorException(error %s, syntax %s, expected %s)" # pragma: no mutate % ( # pragma: no mutate self.error, self.syntax, self.expected, ) ) def __eq__(self, other): return ( self.error == other.error and self.syntax == other.syntax and self.expected == other.expected ) # Reads a token, possibly of a certain value def read_token(stream, value): s = stream.pop() if s is None: raise ParseErrorException(ParseError.NO_TOKEN, None, None) elif s.type != SyntaxType.TOKEN: raise ParseErrorException(ParseError.NOT_TOKEN, s, None) elif value is not None and s.value != value: raise ParseErrorException(ParseError.WRONG_TOKEN, s, value) return s # The note skipper in a wrapper class for easy testing class NoteSkipper: # Skip a note def skip_note(self, stream): read_token(stream, "StartNote") while True: s = read_token(stream, None) # Don't allow StartNote in notes if s.value in ["StartNote"]: raise ParseErrorException(ParseError.FOUND_STARTNOTE, s, None) # EndNote found, end things elif s.value == "EndNote": break return None # Clear notes def clear_notes(self, stream): tokens = [] token = stream.peek() while token is not None: # Found a note, skip it if token.value == "StartNote": self.skip_note(stream) # EndNote found outside note elif token.value == "EndNote": raise ParseErrorException(ParseError.FOUND_ENDNOTE, token, None) # Add the token if it's not note related else: tokens.append(stream.pop()) token = stream.peek() return tokens # The recursive descent parser in a wrapper class for easy testing class Parser: # Parses a text syntax node def parse_text(self, stream): buffer = "" s = read_token(stream, "StartText") location = s.location # Parse following tokens while True: s = read_token(stream, None) # Don't allow StartText in text if s.value in ["StartText"]: raise ParseErrorException(ParseError.FOUND_STARTTEXT, s, None) # EndText found, end things elif s.value == "EndText": break else: buffer += s.value + " " type = SyntaxType.TEXT value = buffer[:-1] # Drop trailing space return Syntax(value, location, type) # Parses a boolean syntax node def parse_bool(self, stream): s = read_token(stream, None) if s.value == "True": return Syntax(True, s.location, SyntaxType.BOOL) elif s.value == "False": return Syntax(False, s.location, SyntaxType.BOOL) else: raise ParseErrorException(ParseError.NOT_BOOL, s, None) # Parses tokens def parse(tokens): stream = SyntaxStream(tokens) cleared = NoteSkipper().clear_notes(stream) return cleared