From e7ea9e07d1fad0ffcfcf1c138f99b4f17593ed34 Mon Sep 17 00:00:00 2001 From: Luthaf Date: Mon, 19 Jan 2015 21:16:10 +0100 Subject: [PATCH] [WIP] Chordpro format --- patacrep/chordpro/ast.py | 71 ++++++++++++++++++++++++++++++ patacrep/chordpro/lexer.py | 10 +++-- patacrep/chordpro/parser.py | 88 +++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 patacrep/chordpro/ast.py create mode 100644 patacrep/chordpro/parser.py diff --git a/patacrep/chordpro/ast.py b/patacrep/chordpro/ast.py new file mode 100644 index 00000000..5eec6b1e --- /dev/null +++ b/patacrep/chordpro/ast.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +"""Abstract Syntax Tree for ChordPro code.""" + +class AST: + """Base class for the tree.""" + # pylint: disable=no-init + metadata = None + + @classmethod + def init_metadata(cls): + """Clear metadata + + As this attribute is a class attribute, it as to be reset at each new + parsing. + """ + cls.metadata = { + '@languages': set(), + } + +class Expression(AST): + """ChordPro expression""" + + def __init__(self, value): + super().__init__() + self.content = [value] + + def prepend(self, value): + """Add a value at the beginning of the content list.""" + if value is not None: + self.content.insert(0, value) + return self + + def __str__(self): + return "".join([str(item) for item in self.content]) + +class SongPart(AST): + """ChordPro start_of/end_of command + + {start_of_chorus}, {end_of_tab}, {eov} ... + """ + + class Type: + CHORUS = ("chorus", + "start_of_chorus", "end_of_chorus", + "soc", "eoc") + VERSE = ("verse", + "start_of_verse", "end_of_verse", + "sov", "eov") + BRIDGE = ("bridge", + "start_of_bridge", "end_of_bridge", + "sob", "eob") + TAB = ("tab", + "start_of_tab", "end_of_tab", + "sot", "eot") + + def __init__(self, name): + if "_" in name: + self.init_long_form(name) + else: + self.init_short_form(name) + + def __str__(self): + return self.name + + def init_short_form(self, name): + self.type = "" + + def init_long_form(self, name): + self.type = "" + + diff --git a/patacrep/chordpro/lexer.py b/patacrep/chordpro/lexer.py index 860e16d5..791d590e 100644 --- a/patacrep/chordpro/lexer.py +++ b/patacrep/chordpro/lexer.py @@ -29,7 +29,6 @@ class ChordProLexer: t_RBRACE = r'}' t_SPACE = r'[ \t]+' t_COLON = r':' - t_WORD = r'[a-zA-Z_]+' #TODO: handle unicode def __init__(self): self.__class__.lexer = lex.lex(module=self) @@ -45,7 +44,12 @@ class ChordProLexer: def t_comment(token): r'\#.*' pass - + + @staticmethod + def t_WORD(token): + r'[a-zA-Z_]+[.,;:!?]?' + return token + @staticmethod def t_NUMBER(token): r'[0-9]+' @@ -56,4 +60,4 @@ class ChordProLexer: def t_error(token): """Manage errors""" LOGGER.error("Illegal character '{}'".format(token.value[0])) - token.lexer.skip(1) \ No newline at end of file + token.lexer.skip(1) diff --git a/patacrep/chordpro/parser.py b/patacrep/chordpro/parser.py new file mode 100644 index 00000000..8c66af28 --- /dev/null +++ b/patacrep/chordpro/parser.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +"""ChordPro parser""" + +import logging +import ply.yacc as yacc + +from patacrep.chordpro.lexer import tokens, ChordProLexer +from patacrep.chordpro import ast +from patacrep.errors import SongbookError + +LOGGER = logging.getLogger() + +class ParsingError(SongbookError): + """Parsing error.""" + + def __init__(self, message): + super().__init__(self) + self.message = message + + def __str__(self): + return self.message + + +class Parser: + """ChordPro parser class""" + + def __init__(self, filename=None): + self.tokens = tokens + self.filename = filename + + @staticmethod + def __find_column(token): + """Return the column of ``token``.""" + last_cr = token.lexer.lexdata.rfind('\n', 0, token.lexpos) + if last_cr < 0: + last_cr = 0 + column = (token.lexpos - last_cr) + 1 + return column + + def p_error(self, token): + """Manage parsing errors.""" + LOGGER.error("Error in file {}, line {}:{}.".format( + str(self.filename), + token.lineno, + self.__find_column(token), + ) + ) + + @staticmethod + def p_expression(symbols): + """expression : brackets expression + | braces expression + | command expression + | NEWLINE expression + | word expression + | SPACE expression + | empty + """ + if len(symbols) == 3: + if symbols[2] is None: + symbols[0] = ast.Expression(symbols[1]) + else: + symbols[0] = symbols[2].prepend(symbols[1]) + else: + symbols[0] = None + + @staticmethod + def p_empty(__symbols): + """empty :""" + return None + + @staticmethod + def p_brackets(symbols): + """brackets : LBRACKET expression RBRACKET""" + symbols[0] = symbols[2] + + @staticmethod + def p_braces(symbols): + """braces : LBRACE expression COLON expression RBRACE""" + symbols[0] = symbols[2] + + +def parsesong(string, filename=None): + """Parse song and return its metadata.""" + return yacc.yacc(module=Parser(filename)).parse( + string, + lexer=ChordProLexer().lexer, + )