From 25314fd2aed81aa5281bc3d93dbcc7919368ebe1 Mon Sep 17 00:00:00 2001 From: Bicker Date: Wed, 22 May 2019 17:45:37 +0200 Subject: Initial commit --- qls/parser/lexer.py | 117 ++++++++++++++++++++++++++++ qls/parser/parser.py | 215 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 332 insertions(+) create mode 100644 qls/parser/lexer.py create mode 100644 qls/parser/parser.py (limited to 'qls') diff --git a/qls/parser/lexer.py b/qls/parser/lexer.py new file mode 100644 index 0000000..f7b8b98 --- /dev/null +++ b/qls/parser/lexer.py @@ -0,0 +1,117 @@ +from ply.lex import lex + + +class QLSLexer: + def __init__(self): + self.__errors = [] + self.__lexer = None + + @property + def lexer(self): + return self.__lexer + + @property + def errors(self): + return self.__errors + + def build(self): + self.__lexer = lex(module=self) + + def tokenize(self, data): + self.__errors = [] + self.lexer.input(data) + token = self.lexer.token() + tokens = [token] + + while token: + token = self.lexer.token() + tokens.append(token) + + return tokens + + tokens = [ + 'COLON', 'COMMA', + 'LEFT_BRACE', 'RIGHT_BRACE', + 'LEFT_BRACKET', 'RIGHT_BRACKET', + 'IDENTIFIER', + 'HEX_COLOR', + 'INTEGER_LITERAL', 'STRING_LITERAL', + ] + + # List of reserved keywords + reserved = { + 'stylesheet': 'STYLESHEET', + 'page': 'PAGE', + 'section': 'SECTION', + 'question': 'QUESTION', + 'default': 'DEFAULT', + + # Properties + 'height': 'HEIGHT', + 'width': 'WIDTH', + 'font': 'FONT', + 'fontsize': 'FONT_SIZE', + 'color': 'COLOR', + 'widget': 'WIDGET', + + # QL types + 'boolean': 'BOOLEAN', + 'date': 'DATE', + 'decimal': 'DECIMAL', + 'integer': 'INTEGER', + 'money': 'MONEY', + 'string': 'STRING', + + # Widgets + 'calendar': 'CALENDAR', + 'checkbox': 'CHECKBOX', + 'line_edit': 'LINE_EDIT', + 'spinbox': 'SPINBOX', + 'radio': 'RADIO', + } + + tokens += list(reserved.values()) + + # Regular expression rules for simple tokens + t_ignore = ' \t' + + t_COLON = r':' + t_COMMA = r',' + + t_LEFT_BRACE = r'\{' + t_RIGHT_BRACE = r'\}' + + t_LEFT_BRACKET = r'\(' + t_RIGHT_BRACKET = r'\)' + + t_HEX_COLOR = r'\#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})' + + @staticmethod + def t_newline(token): + r'\n+' + token.lexer.lineno += len(token.value) + + @staticmethod + def t_eof(token): + token.lexer.lineno = 1 + + @staticmethod + def t_INTEGER_LITERAL(token): + r'\d+' + token.value = int(token.value) + return token + + @staticmethod + def t_STRING_LITERAL(token): + r'\"(.+?)\"' + token.value = token.value[1:-1] + return token + + def t_IDENTIFIER(self, token): + r'[a-zA-Z][a-zA-Z_0-9]*' + token.type = self.reserved.get(token.value, 'IDENTIFIER') # Check for reserved words + return token + + def t_error(self, token): + self.errors.append("Illegal character '%s'" % token.value[0]) + token.lexer.skip(1) diff --git a/qls/parser/parser.py b/qls/parser/parser.py new file mode 100644 index 0000000..1637e6d --- /dev/null +++ b/qls/parser/parser.py @@ -0,0 +1,215 @@ +from ply.yacc import yacc + +from qls.parser.lexer import QLSLexer + + +class QLSParser: + def __init__(self): + self.__errors = [] + self.__tokens = QLSLexer.tokens + self.__parser = None + + @property + def errors(self): + return self.__errors + + @property + def tokens(self): + return self.__tokens + + @property + def parser(self): + return self.__parser + + def build(self): + self.__parser = yacc(module=self) + + def parse(self, data, lexer): + return self.parser.parse(data, lexer) + + @staticmethod + def p_stylesheet(production): + """stylesheet : STYLESHEET IDENTIFIER LEFT_BRACE pages RIGHT_BRACE""" + production[0] = ('STYLESHEET', production[2], production[4]) + + # Statements + @staticmethod + def p_pages(production): + """pages : page pages + | page""" + if len(production) == 3: + production[0] = [production[1]] + production[2] + elif len(production) == 2: + production[0] = [production[1]] + + @staticmethod + def p_page(production): + """page : PAGE IDENTIFIER LEFT_BRACE sections RIGHT_BRACE + | PAGE IDENTIFIER LEFT_BRACE sections default RIGHT_BRACE + | PAGE IDENTIFIER LEFT_BRACE default RIGHT_BRACE""" + if len(production) == 6: + production[0] = ('PAGE', production[2], production[4]) + elif len(production) == 7: + production[0] = ('PAGE', production[2], production[4], production[5]) + + @staticmethod + def p_sections(production): + """sections : section sections + | section""" + if len(production) == 3: + production[0] = [production[1]] + production[2] + elif len(production) == 2: + production[0] = [production[1]] + + @staticmethod + def p_statement(production): + """statement : section + | question""" + production[0] = production[1] + + @staticmethod + def p_statements(production): + """statements : statement statements + | statement""" + if len(production) == 3: + production[0] = [production[1]] + production[2] + elif len(production) == 2: + production[0] = [production[1]] + + @staticmethod + def p_section(production): + """section : SECTION STRING_LITERAL LEFT_BRACE statements RIGHT_BRACE + | SECTION STRING_LITERAL LEFT_BRACE statements default RIGHT_BRACE + | SECTION STRING_LITERAL question + | SECTION STRING_LITERAL default""" + if len(production) == 4: + production[0] = ('SECTION', production[2], [production[3]]) + elif len(production) == 6: + production[0] = ('SECTION', production[2], production[4]) + elif len(production) == 7: + production[0] = ('SECTION', production[2], production[4], production[5]) + + @staticmethod + def p_question(production): + """question : QUESTION IDENTIFIER""" + production[0] = ('QUESTION', production[2]) + + @staticmethod + def p_question_properties(production): + """question : QUESTION IDENTIFIER WIDGET widget""" + production[0] = ('QUESTION', production[2], production[4]) + + @staticmethod + def p_default(production): + """default : DEFAULT type LEFT_BRACE properties RIGHT_BRACE""" + production[0] = ('DEFAULT', production[2], production[4]) + + @staticmethod + def p_default_widget(production): + """default : DEFAULT type WIDGET widget""" + production[0] = ('DEFAULT', production[2], production[3]) + + # Properties + @staticmethod + def p_properties(production): + """properties : property properties + | property""" + if len(production) == 3: + production[0] = [production[1]] + production[2] + elif len(production) == 2: + production[0] = [production[1]] + + @staticmethod + def p_height(production): + """property : HEIGHT COLON INTEGER_LITERAL""" + production[0] = ('HEIGHT', production[3]) + + @staticmethod + def p_width(production): + """property : WIDTH COLON INTEGER_LITERAL""" + production[0] = ('WIDTH', production[3]) + + @staticmethod + def p_font(production): + """property : FONT COLON STRING_LITERAL""" + production[0] = ('FONT', production[3]) + + @staticmethod + def p_font_size(production): + """property : FONT_SIZE COLON INTEGER_LITERAL""" + production[0] = ('FONT_SIZE', production[3]) + + @staticmethod + def p_color(production): + """property : COLOR COLON HEX_COLOR""" + production[0] = ('COLOR', production[3]) + + @staticmethod + def p_widget(production): + """property : WIDGET widget""" + production[0] = ('WIDGET', production[2]) + + # Widgets + @staticmethod + def p_calendar(production): + """widget : CALENDAR""" + production[0] = 'CALENDAR' + + @staticmethod + def p_checkbox(production): + """widget : CHECKBOX""" + production[0] = 'CHECKBOX' + + @staticmethod + def p_line_edit(production): + """widget : LINE_EDIT""" + production[0] = 'LINE_EDIT' + + @staticmethod + def p_spinbox(production): + """widget : SPINBOX""" + production[0] = 'SPINBOX' + + @staticmethod + def p_radio_default(production): + """widget : RADIO""" + production[0] = 'RADIO' + + @staticmethod + def p_radio(production): + """widget : RADIO LEFT_BRACKET STRING_LITERAL COMMA STRING_LITERAL RIGHT_BRACKET""" + production[0] = ('RADIO', production[3], production[5]) + + # Types + @staticmethod + def p_boolean(production): + """type : BOOLEAN""" + production[0] = 'BOOLEAN' + + @staticmethod + def p_decimal(production): + """type : DECIMAL""" + production[0] = 'DECIMAL' + + @staticmethod + def p_string(production): + """type : STRING""" + production[0] = 'STRING' + + @staticmethod + def p_date(production): + """type : DATE""" + production[0] = 'DATE' + + @staticmethod + def p_money(production): + """type : MONEY""" + production[0] = 'MONEY' + + @staticmethod + def p_integer(production): + """type : INTEGER""" + production[0] = 'INTEGER' + + def p_error(self, production): + self.errors.append('Syntax error at line {}, token={}'.format(production.lineno, production.type)) -- cgit v1.2.3