mirror of https://github.com/patacrep/patacrep.git
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.
301 lines
8.4 KiB
301 lines
8.4 KiB
"""ChordPro parser"""
|
|
|
|
import ply.yacc as yacc
|
|
import re
|
|
|
|
from patacrep.songs.syntax import Parser
|
|
from patacrep.songs.chordpro import ast
|
|
from patacrep.songs.chordpro.lexer import tokens, ChordProLexer
|
|
|
|
CHORD_RE = re.compile(
|
|
r"""
|
|
^
|
|
(?P<key>[A-G])
|
|
(?P<alteration>[b#])?
|
|
(?P<modifier>(maj|sus|dim|m))?
|
|
(?P<addnote>[2-9])?
|
|
(
|
|
/
|
|
(?P<basskey>[A-G])
|
|
(?P<bassalteration>[b#])?
|
|
)?
|
|
$
|
|
""",
|
|
re.VERBOSE
|
|
)
|
|
|
|
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
|
|
|
|
def p_song(self, symbols):
|
|
"""song : block song
|
|
| empty
|
|
"""
|
|
if len(symbols) == 2:
|
|
symbols[0] = ast.Song(self.filename)
|
|
else:
|
|
symbols[0] = symbols[2].add(symbols[1])
|
|
|
|
@staticmethod
|
|
def p_block(symbols):
|
|
"""block : SPACE block
|
|
| directive NEWLINE
|
|
| line NEWLINE
|
|
| chorus NEWLINE
|
|
| tab NEWLINE
|
|
| bridge NEWLINE
|
|
| NEWLINE
|
|
"""
|
|
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
|
|
|
|
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
|
|
"""
|
|
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>[2-9]))?\ *
|
|
frets\ *(?P<frets>((\d+|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
|
|
|
|
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()
|
|
|
|
else:
|
|
symbols[0] = ast.Directive(keyword, argument)
|
|
|
|
@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
|
|
|
|
@staticmethod
|
|
def p_line(symbols):
|
|
"""line : word line_next
|
|
| chord line_next
|
|
"""
|
|
symbols[0] = symbols[2].prepend(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()
|
|
|
|
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(*self._parse_chords(symbols[1], symbols=symbols))
|
|
|
|
@staticmethod
|
|
def p_chorus(symbols):
|
|
"""chorus : SOC maybespace NEWLINE chorus_content EOC maybespace
|
|
"""
|
|
symbols[0] = symbols[4]
|
|
|
|
@staticmethod
|
|
def p_chorus_content(symbols):
|
|
"""chorus_content : line NEWLINE 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 NEWLINE bridge_content EOB maybespace
|
|
"""
|
|
symbols[0] = symbols[4]
|
|
|
|
@staticmethod
|
|
def p_bridge_content(symbols):
|
|
"""bridge_content : line NEWLINE 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 NEWLINE tab_content EOT maybespace
|
|
"""
|
|
symbols[0] = symbols[4]
|
|
|
|
@staticmethod
|
|
def p_tab_content(symbols):
|
|
"""tab_content : NEWLINE 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 parse_song(content, filename=None):
|
|
"""Parse song and return its metadata."""
|
|
return yacc.yacc(
|
|
module=ChordproParser(filename),
|
|
debug=0,
|
|
write_tables=0,
|
|
).parse(
|
|
content,
|
|
lexer=ChordProLexer().lexer,
|
|
)
|
|
|