Browse Source

[WIP] Chordpro format

pull/70/head
Luthaf 10 years ago
parent
commit
2c79570ceb
  1. 71
      patacrep/chordpro/ast.py
  2. 10
      patacrep/chordpro/lexer.py
  3. 88
      patacrep/chordpro/parser.py

71
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 = ""

10
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)
token.lexer.skip(1)

88
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,
)
Loading…
Cancel
Save