Engine for LaTeX songbooks http://www.patacrep.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

333 lines
9.6 KiB

"""ChordPro parser"""
import logging
import re
import ply.yacc as yacc
from patacrep.content import ContentError
from patacrep.songs.chordpro import ast
from patacrep.songs.chordpro.lexer import tokens, ChordProLexer
from patacrep.songs.syntax import Parser
LOGGER = logging.getLogger()
class ChordproParser(Parser):
"""ChordPro parser class"""
# pylint: disable=too-many-public-methods
start = "song"
def __init__(self, filename=None):
super().__init__()
self.tokens = tokens
self.filename = filename
self._directives = []
self.parser = yacc.yacc(
module=self,
debug=0,
write_tables=0,
)
def parse(self, content):
"""Parse file
This is a shortcut to `yacc.yacc(...).parse()`. The arguments are
transmitted to this method.
"""
lexer = ChordProLexer(filename=self.filename)
ast.AST.lexer = lexer.lexer
parsed = self.parser.parse(content, lexer=lexer.lexer)
parsed.error_builders.extend(lexer.error_builders)
return parsed
10 years ago
def p_song(self, symbols):
"""song : block song
| empty
"""
if len(symbols) == 2:
symbols[0] = ast.Song(
self.filename,
directives=self._directives,
errors=self._errors,
)
else:
symbols[0] = symbols[2].add(symbols[1])
@staticmethod
def p_block(symbols):
"""block : SPACE block
| line ENDOFLINE
| line_error ENDOFLINE
| chorus ENDOFLINE
| tab ENDOFLINE
| bridge ENDOFLINE
| ENDOFLINE
"""
if len(symbols) == 3 and isinstance(symbols[1], str):
symbols[0] = symbols[2]
elif (symbols[1] is None) or (len(symbols) == 2):
symbols[0] = None
else:
symbols[0] = symbols[1]
@staticmethod
def p_maybespace(symbols):
"""maybespace : SPACE
| empty
"""
symbols[0] = None
@staticmethod
def _parse_define(groups):
"""Parse a `{define: KEY base-fret BASE frets FRETS fingers FINGERS}` directive
Return a :class:`ast.Define` object.
"""
# pylint: disable=too-many-branches
if not groups['key'].strip():
return None
else:
key = ast.Chord(groups['key'].strip())
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 in "xX":
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
"""
if len(symbols) == 5:
keyword = symbols[2]
argument = symbols[3]
else:
keyword = symbols[3]
argument = symbols[4]
if keyword == "define":
match = re.compile(
r"""
^
(?P<key>[^\ ]*)\ *
(base-fret\ *(?P<basefret>\d{1,2}))?\ *
frets\ *(?P<frets>((\d+|x|X)\ *)+)\ *
(fingers\ *(?P<fingers>(([0-4-])\ *)*))?
$
""",
re.VERBOSE
).match(argument)
if match is None:
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
define = self._parse_define(match.groupdict())
if define is None:
self.error(
line=symbols.lexer.lineno,
message="Invalid chord definition '{}'.".format(argument),
)
symbols[0] = ast.Error()
return
self._directives.append(define)
else:
directive = ast.Directive(keyword, argument)
if directive.inline:
symbols[0] = directive
else:
self._directives.append(directive)
@staticmethod
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
def p_line_error(self, symbols):
"""line_error : error directive"""
self.error(
line=symbols.lexer.lineno,
message="Directive can only be preceded or followed by spaces",
)
symbols[0] = ast.Line()
@staticmethod
def p_line(symbols):
"""line : word line_next
| chord line_next
| directive maybespace
"""
if isinstance(symbols[2], ast.Line):
# Line with words, etc.
symbols[0] = symbols[2].prepend(symbols[1])
else:
# Directive
if symbols[1] is None:
# Meta directive. Nothing to do
symbols[0] = ast.Line()
else:
# Inline directive
symbols[0] = ast.Line(symbols[1])
@staticmethod
def p_line_next(symbols):
"""line_next : word line_next
| space line_next
| chord line_next
| empty
"""
if len(symbols) == 2:
symbols[0] = ast.Line()
else:
symbols[0] = symbols[2].prepend(symbols[1])
@staticmethod
def p_word(symbols):
"""word : WORD"""
symbols[0] = ast.Word(symbols[1])
@staticmethod
def p_space(symbols):
"""space : SPACE"""
symbols[0] = ast.Space()
@staticmethod
def p_chord(symbols):
"""chord : CHORD"""
symbols[0] = ast.ChordList(*[ast.Chord(chord) for chord in symbols[1].split()])
@staticmethod
def p_chorus(symbols):
"""chorus : SOC maybespace ENDOFLINE chorus_content EOC maybespace
"""
symbols[0] = symbols[4]
@staticmethod
def p_chorus_content(symbols):
"""chorus_content : line ENDOFLINE chorus_content
| line_error ENDOFLINE chorus_content
| SPACE chorus_content
| empty
"""
if len(symbols) == 2:
symbols[0] = ast.Chorus()
elif len(symbols) == 3:
symbols[0] = symbols[2]
else:
symbols[0] = symbols[3].prepend(symbols[1])
@staticmethod
def p_bridge(symbols):
"""bridge : SOB maybespace ENDOFLINE bridge_content EOB maybespace
"""
symbols[0] = symbols[4]
@staticmethod
def p_bridge_content(symbols):
"""bridge_content : line ENDOFLINE bridge_content
| line_error ENDOFLINE bridge_content
| SPACE bridge_content
| empty
"""
if len(symbols) == 2:
symbols[0] = ast.Bridge()
elif len(symbols) == 3:
symbols[0] = symbols[2]
else:
symbols[0] = symbols[3].prepend(symbols[1])
@staticmethod
def p_tab(symbols):
"""tab : SOT maybespace ENDOFLINE tab_content EOT maybespace
"""
symbols[0] = symbols[4]
@staticmethod
def p_tab_content(symbols):
"""tab_content : ENDOFLINE tab_content
| TEXT tab_content
| SPACE tab_content
| empty
"""
if len(symbols) == 2:
symbols[0] = ast.Tab()
else:
if symbols[1].strip():
symbols[2].prepend(symbols[1])
symbols[0] = symbols[2]
@staticmethod
def p_empty(symbols):
"""empty :"""
symbols[0] = None
def p_error(self, token):
super().p_error(token)
while True:
token = self.parser.token()
if not token or token.type == "ENDOFLINE":
break
if token:
self.parser.errok()
return token
def parse_song(content, filename=None):
"""Parse song and return its metadata."""
parser = ChordproParser(filename)
parsed_content = parser.parse(content)
if parsed_content is None:
raise ContentError(message='Fatal error during song parsing.')
return parsed_content