diff --git a/patacrep/data/examples/songs/errors.sgc b/patacrep/data/examples/songs/errors.sgc new file mode 100644 index 00000000..0b006cab --- /dev/null +++ b/patacrep/data/examples/songs/errors.sgc @@ -0,0 +1,13 @@ +{language : english} +{columns : 2} +{ title : Error} +{subtitle: A chordpro file with many errors} +{artist: Traditionnel} + +{define: H4 base-fret 7 frets 2} +{define:} + +Bla []bla +Bla [H]bla + + diff --git a/patacrep/songs/chordpro/ast.py b/patacrep/songs/chordpro/ast.py index c74a628b..049a3b2f 100644 --- a/patacrep/songs/chordpro/ast.py +++ b/patacrep/songs/chordpro/ast.py @@ -86,6 +86,12 @@ class AST: """Return the chordpro string corresponding to this object.""" raise NotImplementedError() +class Error(AST): + """Parsing error. To be ignored.""" + + def chordpro(self): + return "" + class Line(AST): """A line is a sequence of (possibly truncated) words, spaces and chords.""" @@ -266,7 +272,9 @@ class Song(AST): if data.keyword in self.PROCESS_DIRECTIVE: data = getattr(self, self.PROCESS_DIRECTIVE[data.keyword])(data) - if data is None: + if isinstance(data, Error): + return self + elif data is None: # New line if not (self.content and isinstance(self.content[0], Newline)): self.content.insert(0, Newline()) diff --git a/patacrep/songs/chordpro/lexer.py b/patacrep/songs/chordpro/lexer.py index af8be763..813c42b0 100644 --- a/patacrep/songs/chordpro/lexer.py +++ b/patacrep/songs/chordpro/lexer.py @@ -133,28 +133,32 @@ class ChordProLexer: return token @staticmethod - def t_error(token): - """Manage errors""" - LOGGER.error("Illegal character '{}'".format(token.value[0])) + def error(token, more=""): + """Display error message, and skip illegal token.""" + LOGGER.error( + "Line {line}: Illegal character '{char}'{more}.".format( + line=token.lexer.lineno, + char=token.value[0], + more=more, + ) + ) token.lexer.skip(1) - @staticmethod - def t_chord_error(token): + def t_error(self, token): """Manage errors""" - LOGGER.error("Illegal character '{}' in chord..".format(token.value[0])) - token.lexer.skip(1) + self.error(token) - @staticmethod - def t_tablature_error(token): + def t_chord_error(self, token): """Manage errors""" - LOGGER.error("Illegal character '{}' in tablature..".format(token.value[0])) - token.lexer.skip(1) + self.error(token, more=" in chord") - @staticmethod - def t_directive_error(token): + def t_tablature_error(self, token): """Manage errors""" - LOGGER.error("Illegal character '{}' in directive..".format(token.value[0])) - token.lexer.skip(1) + self.error(token, more=" in tablature") + + def t_directive_error(self, token): + """Manage errors""" + self.error(token, more=" in directive") def t_directiveargument_error(self, token): """Manage errors""" diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py index ea50e267..1e1420bc 100644 --- a/patacrep/songs/chordpro/syntax.py +++ b/patacrep/songs/chordpro/syntax.py @@ -1,6 +1,5 @@ """ChordPro parser""" -import logging import ply.yacc as yacc import re @@ -8,10 +7,9 @@ from patacrep.songs.syntax import Parser from patacrep.songs.chordpro import ast from patacrep.songs.chordpro.lexer import tokens, ChordProLexer -LOGGER = logging.getLogger() - CHORD_RE = re.compile( r""" + ^ (?P[A-G]) (?P[b#])? (?P(maj|sus|dim|m))? @@ -21,65 +19,11 @@ CHORD_RE = re.compile( (?P[A-G]) (?P[b#])? )? + $ """, re.VERBOSE ) -def _parse_chords(string): - """Parse a list of chords. - - Iterate over :class:`ast.Chord` objects. - """ - for chord in string.split(): - match = CHORD_RE.match(chord) - if match is None: - TODO - yield ast.Chord(**match.groupdict()) - -def _parse_define(key, basefret, frets, fingers): - """Parse a `{define: KEY base-fret BASE frets FRETS fingers FINGERS}` directive - - Return a :class:`ast.Define` object. - """ - # pylint: disable=too-many-branches - key = list(_parse_chords(key)) - if len(key) != 1: - TODO - else: - processed_key = key[0] - - if basefret is None: - processed_basefret = None - else: - processed_basefret = int(basefret) - - if frets is None: - processed_frets = None - else: - processed_frets = [] - for fret in frets.split(): - if fret == "x": - processed_frets.append(None) - else: - processed_frets.append(int(fret)) - - if fingers is None: - processed_fingers = None - else: - processed_fingers = [] - for finger in fingers.split(): - if finger == '-': - processed_fingers.append(None) - else: - processed_fingers.append(int(finger)) - - return ast.Define( - key=processed_key, - basefret=processed_basefret, - frets=processed_frets, - fingers=processed_fingers, - ) - class ChordproParser(Parser): """ChordPro parser class""" # pylint: disable=too-many-public-methods @@ -124,8 +68,51 @@ class ChordproParser(Parser): """ symbols[0] = None - @staticmethod - def p_directive(symbols): + def _parse_define(self, groups, *, symbols): + """Parse a `{define: KEY base-fret BASE frets FRETS fingers FINGERS}` directive + + Return a :class:`ast.Define` object. + """ + # pylint: disable=too-many-branches + key = list(self._parse_chords(groups['key'], symbols=symbols)) + if len(key) != 1: + return None + else: + key = key[0] + + if groups['basefret'] is None: + basefret = None + else: + basefret = int(groups['basefret']) + + if groups['frets'] is None: + frets = None + else: + frets = [] + for fret in groups['frets'].split(): + if fret == "x": + frets.append(None) + else: + frets.append(int(fret)) + + if groups['fingers'] is None: + fingers = None + else: + fingers = [] + for finger in groups['fingers'].split(): + if finger == '-': + fingers.append(None) + else: + fingers.append(int(finger)) + + return ast.Define( + key=key, + basefret=basefret, + frets=frets, + fingers=fingers, + ) + + def p_directive(self, symbols): """directive : LBRACE KEYWORD directive_next RBRACE | LBRACE SPACE KEYWORD directive_next RBRACE """ @@ -139,18 +126,38 @@ class ChordproParser(Parser): if keyword == "define": match = re.compile( r""" + ^ (?P[^\ ]*)\ * (base-fret\ *(?P[2-9]))?\ * frets\ *(?P((\d+|x)\ *)+)\ * (fingers\ *(?P(([0-4-])\ *)*))? + $ """, re.VERBOSE ).match(argument) if match is None: - TODO + if argument.strip(): + self.error( + line=symbols.lexer.lineno, + message="Invalid chord definition '{}'.".format(argument), + ) + else: + self.error( + line=symbols.lexer.lineno, + message="Invalid empty chord definition.", + ) + symbols[0] = ast.Error() + return + + symbols[0] = self._parse_define(match.groupdict(), symbols=symbols) + if symbols[0] is None: + self.error( + line=symbols.lexer.lineno, + message="Invalid chord definition '{}'.".format(argument), + ) + symbols[0] = ast.Error() - symbols[0] = _parse_define(**match.groupdict()) else: symbols[0] = ast.Directive(keyword, argument) @@ -158,12 +165,15 @@ class ChordproParser(Parser): def p_directive_next(symbols): """directive_next : SPACE COLON TEXT | COLON TEXT + | COLON | empty """ if len(symbols) == 3: symbols[0] = symbols[2].strip() elif len(symbols) == 4: symbols[0] = symbols[3].strip() + elif len(symbols) == 2 and symbols[1] == ":": + symbols[0] = "" else: symbols[0] = None @@ -196,10 +206,24 @@ class ChordproParser(Parser): """space : SPACE""" symbols[0] = ast.Space() - @staticmethod - def p_chord(symbols): + def _parse_chords(self, string, *, symbols): + """Parse a list of chords. + + Iterate over :class:`ast.Chord` objects. + """ + for chord in string.split(): + match = CHORD_RE.match(chord) + if match is None: + self.error( + line=symbols.lexer.lineno, + message="Invalid chord '{}'.".format(chord), + ) + continue + yield ast.Chord(**match.groupdict()) + + def p_chord(self, symbols): """chord : CHORD""" - symbols[0] = ast.ChordList(*_parse_chords(symbols[1])) + symbols[0] = ast.ChordList(*self._parse_chords(symbols[1], symbols=symbols)) @staticmethod def p_chorus(symbols): diff --git a/patacrep/songs/chordpro/test/invalid_chord.sgc b/patacrep/songs/chordpro/test/invalid_chord.sgc new file mode 100644 index 00000000..872697b3 --- /dev/null +++ b/patacrep/songs/chordpro/test/invalid_chord.sgc @@ -0,0 +1,3 @@ +This is [H] invalid. +This [A+]too. +And [Amm]as well. diff --git a/patacrep/songs/chordpro/test/invalid_chord.txt b/patacrep/songs/chordpro/test/invalid_chord.txt new file mode 100644 index 00000000..e72a02d0 --- /dev/null +++ b/patacrep/songs/chordpro/test/invalid_chord.txt @@ -0,0 +1,6 @@ + +{start_of_verse} + This is invalid. + This [A]too. + And []as well. +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/invalid_customchord.sgc b/patacrep/songs/chordpro/test/invalid_customchord.sgc new file mode 100644 index 00000000..9754318f --- /dev/null +++ b/patacrep/songs/chordpro/test/invalid_customchord.sgc @@ -0,0 +1,5 @@ +{define : } +{define: H base-fret 7 frets 0 1 3 3 x x} +{define: E4 base-fret H frets 0 1 3 3 x x} +{define: E4 base-fret 7 frets 0 1 3 3 x A} +{define: E5 base-fret 7 frets 0 1 3 3 x x fingers - 1 2 3 - A} diff --git a/patacrep/songs/chordpro/test/invalid_customchord.txt b/patacrep/songs/chordpro/test/invalid_customchord.txt new file mode 100644 index 00000000..e69de29b diff --git a/patacrep/songs/syntax.py b/patacrep/songs/syntax.py index aa2075a2..00768019 100644 --- a/patacrep/songs/syntax.py +++ b/patacrep/songs/syntax.py @@ -20,14 +20,27 @@ class Parser: column = (token.lexpos - last_cr) + 1 return column + @staticmethod + def error(*, line, column=None, message=""): + """Display an error message""" + text = "Line {}".format(line) + if column is not None: + text += ", column {}".format(column) + if message: + text += ": " + message + else: + text += "." + LOGGER.error(text) + def p_error(self, token): """Manage parsing errors.""" - if token: - LOGGER.error( - "Error in file {}, line {}:{}.".format( - str(self.filename), - token.lineno, - self.__find_column(token), - ) + if token is None: + self.error( + line=token.lineno, + message="Unexpected end of file.", + ) + else: + self.error( + line=token.lineno, + column=self.__find_column(token), ) -