mirror of https://github.com/patacrep/patacrep.git
Luthaf
10 years ago
86 changed files with 1541 additions and 188 deletions
@ -0,0 +1,51 @@ |
|||||
|
{language : english} |
||||
|
{columns : 2} |
||||
|
{ title : Greensleeves} |
||||
|
{ title : Un sous titre} |
||||
|
{artist: Traditionnel} |
||||
|
{artist: Prénom Nom} |
||||
|
{cover : traditionnel } |
||||
|
{album :Angleterre} |
||||
|
|
||||
|
{partition : greensleeves.ly} |
||||
|
|
||||
|
|
||||
|
A[Am]las, my love, ye [G]do me wrong |
||||
|
To [Am]cast me oft dis[E]curteously |
||||
|
And [Am]I have loved [G]you so long |
||||
|
De[Am]lighting [E]in your [Am]companie |
||||
|
|
||||
|
{start_of_chorus} |
||||
|
[C]Greensleeves was [G]all my joy |
||||
|
[Am]Greensleeves was [E]my delight |
||||
|
[C]Greensleeves was my [G]heart of gold |
||||
|
And [Am]who but [E]Ladie [Am]Greensleeves |
||||
|
{end_of_chorus} |
||||
|
|
||||
|
I [Am]have been ready [G]at your hand |
||||
|
To [Am]grant what ever [E]you would crave |
||||
|
I [Am]have both waged [G]life and land |
||||
|
Your [Am]love and [E]good will [Am]for to have |
||||
|
|
||||
|
I [Am]bought thee kerchers [G]to thy head |
||||
|
That [Am]were wrought fine and [E]gallantly |
||||
|
I [Am]kept thee both at [G]boord and bed |
||||
|
Which [Am]cost my [E]purse well [Am]favouredly |
||||
|
|
||||
|
I [Am]bought thee peticotes [G]of the best |
||||
|
The [Am]cloth so fine as [E]fine might be |
||||
|
I [Am]gave thee jewels [G]for thy chest |
||||
|
And [Am]all this [E]cost I [Am]spent on thee |
||||
|
|
||||
|
{c:test of comment} |
||||
|
|
||||
|
{gc: test of guitar comment} |
||||
|
|
||||
|
{image: traditionnel} |
||||
|
|
||||
|
Thy [Am]smock of silke, both [G]faire and white |
||||
|
With [Am]gold embrodered [E]gorgeously |
||||
|
Thy [Am]peticote of [G]sendall right |
||||
|
And [Am]this I [E]bought thee [Am]gladly |
||||
|
|
||||
|
|
@ -1,19 +1,8 @@ |
|||||
"""Very simple LaTeX parser |
"""Dumb and very very incomplete LaTeX parser. |
||||
|
|
||||
This module uses an LALR parser to try to parse LaTeX code. LaTeX language |
This module uses an LALR parser to try to parse LaTeX code. LaTeX language |
||||
*cannot* be parsed by an LALR parser, so this is a very simple attemps, which |
*cannot* be parsed by an LALR parser, so this is a very simple attemps, which |
||||
will work on simple cases, but not on complex ones. |
will work on simple cases, but not on complex ones. |
||||
""" |
""" |
||||
|
|
||||
from patacrep.latex.syntax import tex2plain |
from patacrep.latex.syntax import tex2plain, parse_song |
||||
from patacrep.latex.syntax import parsesong as syntax_parsesong |
|
||||
from patacrep import encoding |
|
||||
|
|
||||
def parsesong(path, fileencoding=None): |
|
||||
"""Return a dictonary of data read from the latex file `path`. |
|
||||
|
|
||||
""" |
|
||||
with encoding.open_read(path, encoding=fileencoding) as songfile: |
|
||||
data = syntax_parsesong(songfile.read(), path) |
|
||||
data['@path'] = path |
|
||||
return data |
|
||||
|
@ -0,0 +1,49 @@ |
|||||
|
"""Chordpro parser""" |
||||
|
|
||||
|
from jinja2 import Environment, FileSystemLoader |
||||
|
import pkg_resources |
||||
|
import os |
||||
|
|
||||
|
from patacrep import encoding, files |
||||
|
from patacrep.songs import Song |
||||
|
from patacrep.songs.chordpro.syntax import parse_song |
||||
|
from patacrep.templates import TexRenderer |
||||
|
|
||||
|
class ChordproSong(Song): |
||||
|
"""Chordpros song parser.""" |
||||
|
|
||||
|
def parse(self, config): |
||||
|
"""Parse content, and return the dictinory of song data.""" |
||||
|
with encoding.open_read(self.fullpath, encoding=self.encoding) as song: |
||||
|
song = parse_song(song.read(), self.fullpath) |
||||
|
self.authors = song.authors |
||||
|
self.titles = song.titles |
||||
|
self.languages = song.get_directives('language') |
||||
|
self.data = dict([meta.as_tuple for meta in song.meta]) |
||||
|
self.cached = { |
||||
|
'song': song, |
||||
|
} |
||||
|
|
||||
|
def tex(self, output): |
||||
|
context = { |
||||
|
'language': self.cached['song'].get_directive('language', self.config['lang']), |
||||
|
'columns': self.cached['song'].get_directive('columns', 1), |
||||
|
"path": files.relpath(self.fullpath, os.path.dirname(output)), |
||||
|
"titles": r"\\".join(self.titles), |
||||
|
"authors": ", ".join(["{} {}".format(name[1], name[0]) for name in self.authors]), |
||||
|
"metadata": self.data, |
||||
|
"beginsong": self.cached['song'].meta_beginsong(), |
||||
|
"content": self.cached['song'].content, |
||||
|
} |
||||
|
return TexRenderer( |
||||
|
template="chordpro.tex", |
||||
|
encoding='utf8', |
||||
|
texenv=Environment(loader=FileSystemLoader(os.path.join( |
||||
|
os.path.abspath(pkg_resources.resource_filename(__name__, 'data')), |
||||
|
'latex' |
||||
|
))), |
||||
|
).template.render(context) |
||||
|
|
||||
|
SONG_PARSERS = { |
||||
|
'sgc': ChordproSong, |
||||
|
} |
@ -0,0 +1,410 @@ |
|||||
|
"""Abstract Syntax Tree for ChordPro code.""" |
||||
|
|
||||
|
# pylint: disable=too-few-public-methods |
||||
|
|
||||
|
import functools |
||||
|
import logging |
||||
|
import os |
||||
|
|
||||
|
LOGGER = logging.getLogger() |
||||
|
|
||||
|
def _indent(string): |
||||
|
"""Return and indented version of argument.""" |
||||
|
return "\n".join([" {}".format(line) for line in string.split('\n')]) |
||||
|
|
||||
|
#: List of properties that are to be displayed in the flow of the song (not as |
||||
|
#: metadata at the beginning or end of song. |
||||
|
INLINE_PROPERTIES = { |
||||
|
"partition", |
||||
|
"comment", |
||||
|
"guitar_comment", |
||||
|
"image", |
||||
|
} |
||||
|
|
||||
|
#: List of properties that are listed in the `\beginsong` LaTeX directive. |
||||
|
BEGINSONG_PROPERTIES = { |
||||
|
"album", |
||||
|
"copyright", |
||||
|
"cov", |
||||
|
"vcov", |
||||
|
"tag", |
||||
|
} |
||||
|
|
||||
|
#: Some directive have alternative names. For instance `{title: Foo}` and `{t: |
||||
|
#: Foo}` are equivalent. |
||||
|
DIRECTIVE_SHORTCUTS = { |
||||
|
"t": "title", |
||||
|
"st": "subtitle", |
||||
|
"a": "album", |
||||
|
"by": "artist", |
||||
|
"c": "comment", |
||||
|
"gc": "guitar_comment", |
||||
|
"cover": "cov", |
||||
|
"vcover": "vcov", |
||||
|
} |
||||
|
|
||||
|
def directive_name(text): |
||||
|
"""Return name of the directive, considering eventual shortcuts.""" |
||||
|
return DIRECTIVE_SHORTCUTS.get(text, text) |
||||
|
|
||||
|
|
||||
|
class AST: |
||||
|
"""Generic object representing elements of the song.""" |
||||
|
_template = None |
||||
|
inline = False |
||||
|
|
||||
|
def template(self, extension): |
||||
|
"""Return the template to be used to render this object.""" |
||||
|
if self._template is None: |
||||
|
LOGGER.warning("No template defined for {}.".format(self.__class__)) |
||||
|
base = "error" |
||||
|
else: |
||||
|
base = self._template |
||||
|
return "content_{}.{}".format(base, extension) |
||||
|
|
||||
|
class Line(AST): |
||||
|
"""A line is a sequence of (possibly truncated) words, spaces and chords.""" |
||||
|
|
||||
|
_template = "line" |
||||
|
|
||||
|
def __init__(self): |
||||
|
super().__init__() |
||||
|
self.line = [] |
||||
|
|
||||
|
def prepend(self, data): |
||||
|
"""Add an object at the beginning of line.""" |
||||
|
self.line.insert(0, data) |
||||
|
return self |
||||
|
|
||||
|
def __str__(self): |
||||
|
return "".join([str(item) for item in self.line]) |
||||
|
|
||||
|
def strip(self): |
||||
|
"""Remove spaces at the beginning and end of line.""" |
||||
|
while True: |
||||
|
if not self.line: |
||||
|
return self |
||||
|
if isinstance(self.line[0], Space): |
||||
|
del self.line[0] |
||||
|
continue |
||||
|
if isinstance(self.line[-1], Space): |
||||
|
del self.line[-1] |
||||
|
continue |
||||
|
return self |
||||
|
|
||||
|
class LineElement(AST): |
||||
|
"""Something present on a line.""" |
||||
|
pass |
||||
|
|
||||
|
class Word(LineElement): |
||||
|
"""A chunk of word.""" |
||||
|
_template = "word" |
||||
|
|
||||
|
def __init__(self, value): |
||||
|
super().__init__() |
||||
|
self.value = value |
||||
|
|
||||
|
def __str__(self): |
||||
|
return self.value |
||||
|
|
||||
|
class Space(LineElement): |
||||
|
"""A space between words""" |
||||
|
_template = "space" |
||||
|
|
||||
|
def __init__(self): |
||||
|
super().__init__() |
||||
|
|
||||
|
def __str__(self): |
||||
|
return " " |
||||
|
|
||||
|
class Chord(LineElement): |
||||
|
"""A chord.""" |
||||
|
|
||||
|
_template = "chord" |
||||
|
|
||||
|
def __init__(self, value): |
||||
|
super().__init__() |
||||
|
self.value = value |
||||
|
|
||||
|
def __str__(self): |
||||
|
return "[{}]".format(self.value) |
||||
|
|
||||
|
class Verse(AST): |
||||
|
"""A verse (or bridge, or chorus)""" |
||||
|
_template = "verse" |
||||
|
type = "verse" |
||||
|
inline = True |
||||
|
|
||||
|
def __init__(self): |
||||
|
super().__init__() |
||||
|
self.lines = [] |
||||
|
|
||||
|
def prepend(self, data): |
||||
|
"""Add data at the beginning of verse.""" |
||||
|
self.lines.insert(0, data) |
||||
|
return self |
||||
|
|
||||
|
def __str__(self): |
||||
|
return '{{start_of_{type}}}\n{content}\n{{end_of_{type}}}'.format( |
||||
|
type=self.type, |
||||
|
content=_indent("\n".join([str(line) for line in self.lines])), |
||||
|
) |
||||
|
|
||||
|
class Chorus(Verse): |
||||
|
"""Chorus""" |
||||
|
type = 'chorus' |
||||
|
|
||||
|
class Bridge(Verse): |
||||
|
"""Bridge""" |
||||
|
type = 'bridge' |
||||
|
|
||||
|
class Song(AST): |
||||
|
r"""A song |
||||
|
|
||||
|
Attributes: |
||||
|
- content: the song content, as a list of objects `foo` such that |
||||
|
`foo.inline` is True. |
||||
|
- titles: The list of titles |
||||
|
- language: The language (if set), None otherwise |
||||
|
- authors: The list of authors |
||||
|
- meta_beginsong: The list of directives that are to be set in the |
||||
|
`\beginsong{}` LaTeX directive. |
||||
|
- meta: Every other metadata. |
||||
|
""" |
||||
|
|
||||
|
#: Some directives are added to the song using special methods. |
||||
|
METADATA_TYPE = { |
||||
|
"title": "add_title", |
||||
|
"subtitle": "add_subtitle", |
||||
|
"artist": "add_author", |
||||
|
"key": "add_key", |
||||
|
} |
||||
|
|
||||
|
#: Some directives have to be processed before being considered. |
||||
|
PROCESS_DIRECTIVE = { |
||||
|
"cov": "_process_relative", |
||||
|
"partition": "_process_relative", |
||||
|
"image": "_process_relative", |
||||
|
} |
||||
|
|
||||
|
def __init__(self, filename): |
||||
|
super().__init__() |
||||
|
self.content = [] |
||||
|
self.meta = [] |
||||
|
self._authors = [] |
||||
|
self._titles = [] |
||||
|
self._subtitles = [] |
||||
|
self._keys = [] |
||||
|
self.filename = filename |
||||
|
|
||||
|
def add(self, data): |
||||
|
"""Add an element to the song""" |
||||
|
if isinstance(data, Directive): |
||||
|
# Some directives are preprocessed |
||||
|
name = directive_name(data.keyword) |
||||
|
if name in self.PROCESS_DIRECTIVE: |
||||
|
data = getattr(self, self.PROCESS_DIRECTIVE[name])(data) |
||||
|
|
||||
|
if data is None: |
||||
|
# New line |
||||
|
if not (self.content and isinstance(self.content[0], Newline)): |
||||
|
self.content.insert(0, Newline()) |
||||
|
elif isinstance(data, Line): |
||||
|
# Add a new line, maybe in the current verse. |
||||
|
if not (self.content and isinstance(self.content[0], Verse)): |
||||
|
self.content.insert(0, Verse()) |
||||
|
self.content[0].prepend(data.strip()) |
||||
|
elif data.inline: |
||||
|
# Add an object in the content of the song. |
||||
|
self.content.insert(0, data) |
||||
|
elif isinstance(data, Directive): |
||||
|
# Add a metadata directive. Some of them are added using special |
||||
|
# methods listed in ``METADATA_TYPE``. |
||||
|
name = directive_name(data.keyword) |
||||
|
if name in self.METADATA_TYPE: |
||||
|
getattr(self, self.METADATA_TYPE[name])(*data.as_tuple) |
||||
|
else: |
||||
|
self.meta.append(data) |
||||
|
else: |
||||
|
raise Exception() |
||||
|
return self |
||||
|
|
||||
|
def str_meta(self): |
||||
|
"""Return an iterator over *all* metadata, as strings.""" |
||||
|
for title in self.titles: |
||||
|
yield "{{title: {}}}".format(title) |
||||
|
for author in self.authors: |
||||
|
yield "{{by: {}}}".format(author) |
||||
|
for key in sorted(self.keys): |
||||
|
yield "{{key: {}}}".format(str(key)) |
||||
|
for key in sorted(self.meta): |
||||
|
yield str(key) |
||||
|
|
||||
|
def __str__(self): |
||||
|
return ( |
||||
|
"\n".join(self.str_meta()).strip() |
||||
|
+ |
||||
|
"\n========\n" |
||||
|
+ |
||||
|
"\n".join([str(item) for item in self.content]).strip() |
||||
|
) |
||||
|
|
||||
|
|
||||
|
def add_title(self, __ignored, title): |
||||
|
"""Add a title""" |
||||
|
self._titles.insert(0, title) |
||||
|
|
||||
|
def add_subtitle(self, __ignored, title): |
||||
|
"""Add a subtitle""" |
||||
|
self._subtitles.insert(0, title) |
||||
|
|
||||
|
@property |
||||
|
def titles(self): |
||||
|
"""Return the list of titles (and subtitles).""" |
||||
|
return self._titles + self._subtitles |
||||
|
|
||||
|
def add_author(self, __ignored, title): |
||||
|
"""Add an auhor.""" |
||||
|
self._authors.insert(0, title) |
||||
|
|
||||
|
@property |
||||
|
def authors(self): |
||||
|
"""Return the list of (raw) authors.""" |
||||
|
return self._authors |
||||
|
|
||||
|
def get_directive(self, key, default=None): |
||||
|
"""Return the first directive with a given key.""" |
||||
|
for directive in self.meta: |
||||
|
if directive.keyword == directive_name(key): |
||||
|
return directive.argument |
||||
|
return default |
||||
|
|
||||
|
def get_directives(self, key): |
||||
|
"""Return the list of directives with a given key.""" |
||||
|
values = [] |
||||
|
for directive in self.meta: |
||||
|
if directive.keyword == directive_name(key): |
||||
|
values.append(directive.argument) |
||||
|
return values |
||||
|
|
||||
|
def add_key(self, __ignored, argument): |
||||
|
"""Add a new {key: foo: bar} directive.""" |
||||
|
key, *argument = argument.split(":") |
||||
|
self._keys.append(Directive( |
||||
|
key.strip(), |
||||
|
":".join(argument).strip(), |
||||
|
)) |
||||
|
|
||||
|
@property |
||||
|
def keys(self): |
||||
|
"""Return the list of keys. |
||||
|
|
||||
|
That is, directive that where given of the form ``{key: foo: bar}``. |
||||
|
""" |
||||
|
return self._keys |
||||
|
|
||||
|
def meta_beginsong(self): |
||||
|
r"""Return the meta information to be put in \beginsong.""" |
||||
|
for directive in BEGINSONG_PROPERTIES: |
||||
|
if self.get_directive(directive) is not None: |
||||
|
yield (directive, self.get_directive(directive)) |
||||
|
for (key, value) in self.keys: |
||||
|
yield (key, value) |
||||
|
|
||||
|
|
||||
|
def _process_relative(self, directive): |
||||
|
"""Return the directive, in which the argument is given relative to file |
||||
|
|
||||
|
This argument is expected to be a path (as a string). |
||||
|
""" |
||||
|
return Directive( |
||||
|
directive.keyword, |
||||
|
os.path.join( |
||||
|
os.path.dirname(self.filename), |
||||
|
directive.argument, |
||||
|
), |
||||
|
) |
||||
|
|
||||
|
class Newline(AST): |
||||
|
"""New line""" |
||||
|
_template = "newline" |
||||
|
|
||||
|
def __str__(self): |
||||
|
return "" |
||||
|
|
||||
|
@functools.total_ordering |
||||
|
class Directive(AST): |
||||
|
"""A directive""" |
||||
|
|
||||
|
def __init__(self, keyword="", argument=None): |
||||
|
super().__init__() |
||||
|
self._keyword = None |
||||
|
self.keyword = keyword |
||||
|
self.argument = argument |
||||
|
|
||||
|
@property |
||||
|
def _template(self): |
||||
|
"""Name of the template to use to render this keyword. |
||||
|
|
||||
|
This only applies if ``self.inline == True`` |
||||
|
""" |
||||
|
return self.keyword |
||||
|
|
||||
|
@property |
||||
|
def keyword(self): |
||||
|
"""Keyword of the directive.""" |
||||
|
return self._keyword |
||||
|
|
||||
|
@property |
||||
|
def inline(self): |
||||
|
"""True iff this directive is to be rendered in the flow on the song. |
||||
|
""" |
||||
|
return self.keyword in INLINE_PROPERTIES |
||||
|
|
||||
|
@keyword.setter |
||||
|
def keyword(self, value): |
||||
|
"""self.keyword setter |
||||
|
|
||||
|
Replace keyword by its canonical name if it is a shortcut. |
||||
|
""" |
||||
|
self._keyword = directive_name(value.strip()) |
||||
|
|
||||
|
def __str__(self): |
||||
|
if self.argument is not None: |
||||
|
return "{{{}: {}}}".format( |
||||
|
self.keyword, |
||||
|
self.argument, |
||||
|
) |
||||
|
else: |
||||
|
return "{{{}}}".format(self.keyword) |
||||
|
|
||||
|
@property |
||||
|
def as_tuple(self): |
||||
|
"""Return the directive as a tuple.""" |
||||
|
return (self.keyword, self.argument) |
||||
|
|
||||
|
def __eq__(self, other): |
||||
|
return self.as_tuple == other.as_tuple |
||||
|
|
||||
|
def __lt__(self, other): |
||||
|
return self.as_tuple < other.as_tuple |
||||
|
|
||||
|
class Tab(AST): |
||||
|
"""Tablature""" |
||||
|
|
||||
|
inline = True |
||||
|
|
||||
|
def __init__(self): |
||||
|
super().__init__() |
||||
|
self.content = [] |
||||
|
|
||||
|
def prepend(self, data): |
||||
|
"""Add an element at the beginning of content.""" |
||||
|
self.content.insert(0, data) |
||||
|
return self |
||||
|
|
||||
|
def __str__(self): |
||||
|
return '{{start_of_tab}}\n{}\n{{end_of_tab}}'.format( |
||||
|
_indent("\n".join(self.content)), |
||||
|
) |
||||
|
|
@ -0,0 +1,27 @@ |
|||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
||||
|
% ((path)) |
||||
|
|
||||
|
(* if language is defined *) |
||||
|
\selectlanguage{((language))} |
||||
|
(* endif *) |
||||
|
\songcolumns{((metadata.columns))} |
||||
|
|
||||
|
\beginsong{((titles))}[ |
||||
|
by={((authors))}, |
||||
|
(* for (key, argument) in beginsong *) |
||||
|
((key))={((argument))}, |
||||
|
(* endfor *) |
||||
|
] |
||||
|
|
||||
|
(* if (metadata.cov is defined) or (metadata.vcov is defined) *) |
||||
|
\cover |
||||
|
(* endif *) |
||||
|
|
||||
|
(* for content in content *) |
||||
|
(* include content.template("tex") *) |
||||
|
(* endfor *) |
||||
|
|
||||
|
\endsong |
||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
||||
|
|
@ -0,0 +1 @@ |
|||||
|
\[(( content.value ))] |
@ -0,0 +1 @@ |
|||||
|
\textnote{(( content.argument ))} |
@ -0,0 +1,3 @@ |
|||||
|
|
||||
|
ERROR : Template not found for \verb+(( content.__class__))+. See the logs for details. |
||||
|
|
@ -0,0 +1 @@ |
|||||
|
\musicnote{(( content.argument ))} |
@ -0,0 +1 @@ |
|||||
|
\image{(( content.argument ))} |
@ -0,0 +1 @@ |
|||||
|
(* for content in content.line *)(* include content.template("tex") *)(* endfor *) |
@ -0,0 +1,2 @@ |
|||||
|
|
||||
|
|
@ -0,0 +1 @@ |
|||||
|
\lilypond{((content.argument))} |
@ -0,0 +1 @@ |
|||||
|
|
@ -0,0 +1,7 @@ |
|||||
|
\begin{(( content.type ))} |
||||
|
(* for content in content.lines *) |
||||
|
(* include content.template("tex") *) |
||||
|
|
||||
|
(* endfor *) |
||||
|
\end{(( content.type ))} |
||||
|
|
@ -0,0 +1 @@ |
|||||
|
(( content.value )) |
@ -0,0 +1,161 @@ |
|||||
|
"""ChordPro lexer""" |
||||
|
|
||||
|
import logging |
||||
|
import ply.lex as lex |
||||
|
|
||||
|
LOGGER = logging.getLogger() |
||||
|
|
||||
|
#pylint: disable=invalid-name |
||||
|
tokens = ( |
||||
|
'LBRACE', |
||||
|
'RBRACE', |
||||
|
'CHORD', |
||||
|
'NEWLINE', |
||||
|
'COLON', |
||||
|
'WORD', |
||||
|
'SPACE', |
||||
|
'TEXT', |
||||
|
'KEYWORD', |
||||
|
'SOC', |
||||
|
'EOC', |
||||
|
'SOB', |
||||
|
'EOB', |
||||
|
'SOT', |
||||
|
'EOT', |
||||
|
) |
||||
|
|
||||
|
class ChordProLexer: |
||||
|
"""ChordPro Lexer class""" |
||||
|
# pylint: disable=too-many-public-methods |
||||
|
|
||||
|
tokens = tokens |
||||
|
|
||||
|
states = ( |
||||
|
('chord', 'exclusive'), |
||||
|
('directive', 'exclusive'), |
||||
|
('directiveargument', 'exclusive'), |
||||
|
('tablature', 'exclusive'), |
||||
|
) |
||||
|
|
||||
|
t_SPACE = r'[ \t]+' |
||||
|
|
||||
|
t_chord_CHORD = r'[A-G7#m]+' # TODO This can be refined |
||||
|
|
||||
|
t_directive_SPACE = r'[ \t]+' |
||||
|
t_directive_KEYWORD = r'[a-zA-Z_]+' |
||||
|
t_directiveargument_TEXT = r'[^}]+' |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_SOC(token): |
||||
|
r'{(soc|start_of_chorus)}' |
||||
|
return token |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_EOC(token): |
||||
|
r'{(eoc|end_of_chorus)}' |
||||
|
return token |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_SOB(token): |
||||
|
r'{(sob|start_of_bridge)}' |
||||
|
return token |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_EOB(token): |
||||
|
r'{(eob|end_of_bridge)}' |
||||
|
return token |
||||
|
|
||||
|
def t_SOT(self, token): |
||||
|
r'{(sot|start_of_tab)}' |
||||
|
self.lexer.push_state('tablature') |
||||
|
return token |
||||
|
|
||||
|
def t_tablature_EOT(self, token): |
||||
|
r'{(eot|end_of_tab)}' |
||||
|
self.lexer.pop_state() |
||||
|
return token |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_tablature_SPACE(token): |
||||
|
r'[ \t]+' |
||||
|
return token |
||||
|
|
||||
|
t_tablature_TEXT = r'[^\n]+' |
||||
|
t_tablature_NEWLINE = r'\n' |
||||
|
|
||||
|
def __init__(self): |
||||
|
self.__class__.lexer = lex.lex(module=self) |
||||
|
|
||||
|
# Define a rule so we can track line numbers |
||||
|
@staticmethod |
||||
|
def t_NEWLINE(token): |
||||
|
r'[\n\r]' |
||||
|
token.lexer.lineno += 1 |
||||
|
return token |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_COMMENT(token): |
||||
|
r'\#.*' |
||||
|
pass |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_WORD(token): |
||||
|
r'[^{}\n\][\t ]+' |
||||
|
return token |
||||
|
|
||||
|
def t_LBRACKET(self, __token): |
||||
|
r'\[' |
||||
|
self.lexer.push_state('chord') |
||||
|
|
||||
|
def t_chord_RBRACKET(self, __token): |
||||
|
r'\]' |
||||
|
self.lexer.pop_state() |
||||
|
|
||||
|
def t_LBRACE(self, token): |
||||
|
r'{' |
||||
|
self.lexer.push_state('directive') |
||||
|
return token |
||||
|
|
||||
|
def t_directive_RBRACE(self, token): |
||||
|
r'}' |
||||
|
self.lexer.pop_state() |
||||
|
return token |
||||
|
|
||||
|
def t_directiveargument_RBRACE(self, token): |
||||
|
r'}' |
||||
|
self.lexer.pop_state() |
||||
|
self.lexer.pop_state() |
||||
|
return token |
||||
|
|
||||
|
def t_directive_COLON(self, token): |
||||
|
r':' |
||||
|
self.lexer.push_state('directiveargument') |
||||
|
return token |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_error(token): |
||||
|
"""Manage errors""" |
||||
|
LOGGER.error("Illegal character '{}'".format(token.value[0])) |
||||
|
token.lexer.skip(1) |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_chord_error(token): |
||||
|
"""Manage errors""" |
||||
|
LOGGER.error("Illegal character '{}' in chord..".format(token.value[0])) |
||||
|
token.lexer.skip(1) |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_tablature_error(token): |
||||
|
"""Manage errors""" |
||||
|
LOGGER.error("Illegal character '{}' in tablature..".format(token.value[0])) |
||||
|
token.lexer.skip(1) |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_directive_error(token): |
||||
|
"""Manage errors""" |
||||
|
LOGGER.error("Illegal character '{}' in directive..".format(token.value[0])) |
||||
|
token.lexer.skip(1) |
||||
|
|
||||
|
def t_directiveargument_error(self, token): |
||||
|
"""Manage errors""" |
||||
|
return self.t_directive_error(token) |
@ -0,0 +1,217 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
"""ChordPro parser""" |
||||
|
|
||||
|
import logging |
||||
|
import ply.yacc as yacc |
||||
|
|
||||
|
from patacrep.errors import SongbookError |
||||
|
from patacrep.songs.chordpro import ast |
||||
|
from patacrep.songs.chordpro.lexer import tokens, ChordProLexer |
||||
|
|
||||
|
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""" |
||||
|
|
||||
|
start = "song" |
||||
|
|
||||
|
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.""" |
||||
|
if token: |
||||
|
LOGGER.error("Error in file {}, line {}:{}.".format( |
||||
|
str(self.filename), |
||||
|
token.lineno, |
||||
|
self.__find_column(token), |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
def p_song(self, symbols): |
||||
|
"""song : block song |
||||
|
| empty |
||||
|
""" |
||||
|
#if isinstance(symbols[1], str): |
||||
|
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 |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_directive(symbols): |
||||
|
"""directive : LBRACE KEYWORD directive_next RBRACE |
||||
|
| LBRACE SPACE KEYWORD directive_next RBRACE |
||||
|
""" |
||||
|
if len(symbols) == 5: |
||||
|
symbols[3].keyword = symbols[2] |
||||
|
symbols[0] = symbols[3] |
||||
|
else: |
||||
|
symbols[4].keyword = symbols[3] |
||||
|
symbols[0] = symbols[4] |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_directive_next(symbols): |
||||
|
"""directive_next : SPACE COLON TEXT |
||||
|
| COLON TEXT |
||||
|
| empty |
||||
|
""" |
||||
|
symbols[0] = ast.Directive() |
||||
|
if len(symbols) == 3: |
||||
|
symbols[0].argument = symbols[2].strip() |
||||
|
elif len(symbols) == 4: |
||||
|
symbols[0].argument = symbols[3].strip() |
||||
|
|
||||
|
@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() |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_chord(symbols): |
||||
|
"""chord : CHORD""" |
||||
|
symbols[0] = ast.Chord(symbols[1]) |
||||
|
|
||||
|
@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=Parser(filename), |
||||
|
debug=0, |
||||
|
write_tables=0, |
||||
|
).parse( |
||||
|
content, |
||||
|
lexer=ChordProLexer().lexer, |
||||
|
) |
@ -0,0 +1 @@ |
|||||
|
======== |
@ -0,0 +1 @@ |
|||||
|
A verse line |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_verse} |
||||
|
A verse line |
||||
|
{end_of_verse} |
@ -0,0 +1 @@ |
|||||
|
{title : A directive} |
@ -0,0 +1,2 @@ |
|||||
|
{title: A directive} |
||||
|
======== |
@ -0,0 +1,4 @@ |
|||||
|
|
||||
|
|
||||
|
|
||||
|
|
@ -0,0 +1 @@ |
|||||
|
======== |
@ -0,0 +1,3 @@ |
|||||
|
{soc} |
||||
|
A one line chorus |
||||
|
{eoc} |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_chorus} |
||||
|
A one line chorus |
||||
|
{end_of_chorus} |
@ -0,0 +1,3 @@ |
|||||
|
{sob} |
||||
|
A one line bridge |
||||
|
{eob} |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_bridge} |
||||
|
A one line bridge |
||||
|
{end_of_bridge} |
@ -0,0 +1 @@ |
|||||
|
# A comment |
@ -0,0 +1 @@ |
|||||
|
======== |
@ -0,0 +1,3 @@ |
|||||
|
{sot} |
||||
|
A tab |
||||
|
{eot} |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_tab} |
||||
|
A tab |
||||
|
{end_of_tab} |
@ -0,0 +1,10 @@ |
|||||
|
|
||||
|
|
||||
|
|
||||
|
# comment |
||||
|
# comment |
||||
|
|
||||
|
|
||||
|
A lot of new lines |
||||
|
|
||||
|
# comment |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_verse} |
||||
|
A lot of new lines |
||||
|
{end_of_verse} |
@ -0,0 +1,15 @@ |
|||||
|
|
||||
|
|
||||
|
|
||||
|
# comment |
||||
|
# comment |
||||
|
|
||||
|
|
||||
|
A lot of new lines |
||||
|
|
||||
|
# comment |
||||
|
|
||||
|
{title: and a directive} |
||||
|
|
||||
|
# comment |
||||
|
|
@ -0,0 +1,5 @@ |
|||||
|
{title: and a directive} |
||||
|
======== |
||||
|
{start_of_verse} |
||||
|
A lot of new lines |
||||
|
{end_of_verse} |
@ -0,0 +1 @@ |
|||||
|
A line[A] with a chord |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_verse} |
||||
|
A line[A] with a chord |
||||
|
{end_of_verse} |
@ -0,0 +1 @@ |
|||||
|
A line ending with a chord[A] |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_verse} |
||||
|
A line ending with a chord[A] |
||||
|
{end_of_verse} |
@ -0,0 +1 @@ |
|||||
|
[A]A line starting with a chord |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_verse} |
||||
|
[A]A line starting with a chord |
||||
|
{end_of_verse} |
@ -0,0 +1,5 @@ |
|||||
|
{sot} |
||||
|
A table |
||||
|
wit many # weir [ |
||||
|
[ symbols |
||||
|
{eot} |
@ -0,0 +1,6 @@ |
|||||
|
======== |
||||
|
{start_of_tab} |
||||
|
A table |
||||
|
wit many # weir [ |
||||
|
[ symbols |
||||
|
{end_of_tab} |
@ -0,0 +1 @@ |
|||||
|
A verse line |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_verse} |
||||
|
A verse line |
||||
|
{end_of_verse} |
@ -0,0 +1 @@ |
|||||
|
{title : A directive} |
@ -0,0 +1,2 @@ |
|||||
|
{title: A directive} |
||||
|
======== |
@ -0,0 +1,4 @@ |
|||||
|
|
||||
|
|
||||
|
|
||||
|
|
@ -0,0 +1 @@ |
|||||
|
======== |
@ -0,0 +1,3 @@ |
|||||
|
{soc} |
||||
|
A one line chorus |
||||
|
{eoc} |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_chorus} |
||||
|
A one line chorus |
||||
|
{end_of_chorus} |
@ -0,0 +1,3 @@ |
|||||
|
{sob} |
||||
|
A one line bridge |
||||
|
{eob} |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_bridge} |
||||
|
A one line bridge |
||||
|
{end_of_bridge} |
@ -0,0 +1 @@ |
|||||
|
# A comment |
@ -0,0 +1 @@ |
|||||
|
======== |
@ -0,0 +1,3 @@ |
|||||
|
{sot} |
||||
|
A tab |
||||
|
{eot} |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_tab} |
||||
|
A tab |
||||
|
{end_of_tab} |
@ -0,0 +1,10 @@ |
|||||
|
|
||||
|
|
||||
|
|
||||
|
# comment |
||||
|
# comment |
||||
|
|
||||
|
|
||||
|
A lot of new lines |
||||
|
|
||||
|
# comment |
@ -0,0 +1,4 @@ |
|||||
|
======== |
||||
|
{start_of_verse} |
||||
|
A lot of new lines |
||||
|
{end_of_verse} |
@ -0,0 +1,15 @@ |
|||||
|
|
||||
|
|
||||
|
|
||||
|
# comment |
||||
|
# comment |
||||
|
|
||||
|
|
||||
|
A lot of new lines |
||||
|
|
||||
|
# comment |
||||
|
|
||||
|
{title: and a directive} |
||||
|
|
||||
|
# comment |
||||
|
|
@ -0,0 +1,5 @@ |
|||||
|
{title: and a directive} |
||||
|
======== |
||||
|
{start_of_verse} |
||||
|
A lot of new lines |
||||
|
{end_of_verse} |
@ -0,0 +1 @@ |
|||||
|
[A]A line starting with a chord |
@ -0,0 +1 @@ |
|||||
|
"""Test for chordpro parser""" |
@ -0,0 +1,44 @@ |
|||||
|
{language : english} |
||||
|
{columns : 2} |
||||
|
{subtitle : Un sous titre} |
||||
|
{ title : Greensleeves} |
||||
|
{title : Un autre sous-titre} |
||||
|
{artist: Traditionnel} |
||||
|
{cover : traditionnel } |
||||
|
{album :Angleterre} |
||||
|
|
||||
|
{partition : greensleeves.ly} |
||||
|
|
||||
|
|
||||
|
A[Am]las, my love, ye [G]do me wrong |
||||
|
To [Am]cast me oft dis[E]curteously |
||||
|
And [Am]I have loved [G]you so long |
||||
|
De[Am]lighting [E]in your [Am]companie |
||||
|
|
||||
|
{start_of_chorus} |
||||
|
[C]Green[B]sleeves was [G]all my joy |
||||
|
[Am]Greensleeves was [E]my delight |
||||
|
[C]Greensleeves was my [G]heart of gold |
||||
|
And [Am]who but [E]Ladie [Am]Greensleeves |
||||
|
{end_of_chorus} |
||||
|
|
||||
|
I [Am]have been ready [G]at your hand |
||||
|
To [Am]grant what ever [E]you would crave |
||||
|
I [Am]have both waged [G]life and land |
||||
|
Your [Am]love and [E]good will [Am]for to have |
||||
|
|
||||
|
I [Am]bought thee kerchers [G]to thy head |
||||
|
That [Am]were wrought fine and [E]gallantly |
||||
|
I [Am]kept thee both at [G]boord and bed |
||||
|
Which [Am]cost my [E]purse well [Am]favouredly |
||||
|
|
||||
|
I [Am]bought thee peticotes [G]of the best |
||||
|
The [Am]cloth so fine as [E]fine might be |
||||
|
I [Am]gave thee jewels [G]for thy chest |
||||
|
And [Am]all this [E]cost I [Am]spent on thee |
||||
|
|
||||
|
|
||||
|
Thy [Am]smock of silke, both [G]faire and white |
||||
|
With [Am]gold embrodered [E]gorgeously |
||||
|
Thy [Am]peticote of [G]sendall right |
||||
|
And [Am]this I [E]bought thee [Am]gladly |
@ -0,0 +1,52 @@ |
|||||
|
{title: Greensleeves} |
||||
|
{title: Un autre sous-titre} |
||||
|
{title: Un sous titre} |
||||
|
{by: Traditionnel} |
||||
|
{album: Angleterre} |
||||
|
{columns: 2} |
||||
|
{cov: DIRNAME/traditionnel} |
||||
|
{language: english} |
||||
|
======== |
||||
|
{partition: DIRNAME/greensleeves.ly} |
||||
|
|
||||
|
{start_of_verse} |
||||
|
A[Am]las, my love, ye [G]do me wrong |
||||
|
To [Am]cast me oft dis[E]curteously |
||||
|
And [Am]I have loved [G]you so long |
||||
|
De[Am]lighting [E]in your [Am]companie |
||||
|
{end_of_verse} |
||||
|
|
||||
|
{start_of_chorus} |
||||
|
[C]Green[B]sleeves was [G]all my joy |
||||
|
[Am]Greensleeves was [E]my delight |
||||
|
[C]Greensleeves was my [G]heart of gold |
||||
|
And [Am]who but [E]Ladie [Am]Greensleeves |
||||
|
{end_of_chorus} |
||||
|
|
||||
|
{start_of_verse} |
||||
|
I [Am]have been ready [G]at your hand |
||||
|
To [Am]grant what ever [E]you would crave |
||||
|
I [Am]have both waged [G]life and land |
||||
|
Your [Am]love and [E]good will [Am]for to have |
||||
|
{end_of_verse} |
||||
|
|
||||
|
{start_of_verse} |
||||
|
I [Am]bought thee kerchers [G]to thy head |
||||
|
That [Am]were wrought fine and [E]gallantly |
||||
|
I [Am]kept thee both at [G]boord and bed |
||||
|
Which [Am]cost my [E]purse well [Am]favouredly |
||||
|
{end_of_verse} |
||||
|
|
||||
|
{start_of_verse} |
||||
|
I [Am]bought thee peticotes [G]of the best |
||||
|
The [Am]cloth so fine as [E]fine might be |
||||
|
I [Am]gave thee jewels [G]for thy chest |
||||
|
And [Am]all this [E]cost I [Am]spent on thee |
||||
|
{end_of_verse} |
||||
|
|
||||
|
{start_of_verse} |
||||
|
Thy [Am]smock of silke, both [G]faire and white |
||||
|
With [Am]gold embrodered [E]gorgeously |
||||
|
Thy [Am]peticote of [G]sendall right |
||||
|
And [Am]this I [E]bought thee [Am]gladly |
||||
|
{end_of_verse} |
@ -0,0 +1,20 @@ |
|||||
|
{subtitle: Subtitle3} |
||||
|
{title: Title} |
||||
|
{title: Subtitle1} |
||||
|
{subtitle: Subtitle4} |
||||
|
{t: Subtitle2} |
||||
|
{st: Subtitle5} |
||||
|
{language: french} |
||||
|
{language: english} |
||||
|
{by: Author1} |
||||
|
{artist: Author2} |
||||
|
{album: Albom} |
||||
|
{copyright: Copyright} |
||||
|
{cover: Cover} |
||||
|
{vcover: VCover} |
||||
|
{capo: Capo} |
||||
|
{key: foo: Foo} |
||||
|
{comment: Comment} |
||||
|
{guitar_comment: GuitarComment} |
||||
|
{image: Image} |
||||
|
{partition: Lilypond} |
@ -0,0 +1,21 @@ |
|||||
|
{title: Title} |
||||
|
{title: Subtitle1} |
||||
|
{title: Subtitle2} |
||||
|
{title: Subtitle3} |
||||
|
{title: Subtitle4} |
||||
|
{title: Subtitle5} |
||||
|
{by: Author1} |
||||
|
{by: Author2} |
||||
|
{key: {foo: Foo}} |
||||
|
{album: Albom} |
||||
|
{capo: Capo} |
||||
|
{copyright: Copyright} |
||||
|
{cov: DIRNAME/Cover} |
||||
|
{language: english} |
||||
|
{language: french} |
||||
|
{vcov: VCover} |
||||
|
======== |
||||
|
{comment: Comment} |
||||
|
{guitar_comment: GuitarComment} |
||||
|
{image: DIRNAME/Image} |
||||
|
{partition: DIRNAME/Lilypond} |
@ -0,0 +1,56 @@ |
|||||
|
"""Tests for the chordpro parser.""" |
||||
|
|
||||
|
# pylint: disable=too-few-public-methods |
||||
|
|
||||
|
import glob |
||||
|
import os |
||||
|
import unittest |
||||
|
|
||||
|
from patacrep.songs.chordpro import syntax as chordpro |
||||
|
|
||||
|
class ParserTxtRenderer(unittest.TestCase): |
||||
|
"""Test parser, and renderer as a txt file.""" |
||||
|
|
||||
|
maxDiff = None |
||||
|
|
||||
|
def __init__(self, methodname="runTest", basename=None): |
||||
|
super().__init__(methodname) |
||||
|
self.basename = basename |
||||
|
|
||||
|
def shortDescription(self): |
||||
|
return "Parsing file '{}.txt'.".format(self.basename) |
||||
|
|
||||
|
def runTest(self): |
||||
|
"""Test txt output (default, debug output).""" |
||||
|
# pylint: disable=invalid-name |
||||
|
|
||||
|
if self.basename is None: |
||||
|
return |
||||
|
with open("{}.sgc".format(self.basename), 'r', encoding='utf8') as sourcefile: |
||||
|
with open("{}.txt".format(self.basename), 'r', encoding='utf8') as expectfile: |
||||
|
#print(os.path.basename(sourcefile.name)) |
||||
|
#with open("{}.txt.diff".format(self.basename), 'w', encoding='utf8') as difffile: |
||||
|
# difffile.write( |
||||
|
# str(chordpro.parse_song( |
||||
|
# sourcefile.read(), |
||||
|
# os.path.basename(sourcefile.name), |
||||
|
# )).strip() |
||||
|
# ) |
||||
|
# sourcefile.seek(0) |
||||
|
self.assertMultiLineEqual( |
||||
|
str(chordpro.parse_song( |
||||
|
sourcefile.read(), |
||||
|
os.path.abspath(sourcefile.name), |
||||
|
)).strip(), |
||||
|
expectfile.read().strip().replace("DIRNAME", os.path.dirname(self.basename)), |
||||
|
) |
||||
|
|
||||
|
def load_tests(__loader, tests, __pattern): |
||||
|
"""Load several tests given test files present in the directory.""" |
||||
|
# Load all txt files as tests |
||||
|
for txt in sorted(glob.glob(os.path.join( |
||||
|
os.path.dirname(__file__), |
||||
|
'*.txt', |
||||
|
))): |
||||
|
tests.addTest(ParserTxtRenderer(basename=txt[:-len('.txt')])) |
||||
|
return tests |
@ -0,0 +1,39 @@ |
|||||
|
"""Very simple LaTeX parser |
||||
|
|
||||
|
This module uses an LALR parser to try to parse LaTeX code. LaTeX language |
||||
|
*cannot* be parsed by an LALR parser, so this is a very simple attemps, which |
||||
|
will work on simple cases, but not on complex ones. |
||||
|
""" |
||||
|
|
||||
|
import os |
||||
|
|
||||
|
from patacrep import files, encoding |
||||
|
from patacrep.latex import parse_song |
||||
|
from patacrep.songs import Song |
||||
|
|
||||
|
class LatexSong(Song): |
||||
|
"""LaTeX song parser.""" |
||||
|
|
||||
|
def parse(self, __config): |
||||
|
"""Parse content, and return the dictinory of song data.""" |
||||
|
with encoding.open_read(self.fullpath, encoding=self.encoding) as song: |
||||
|
self.data = parse_song(song.read(), self.fullpath) |
||||
|
self.titles = self.data['@titles'] |
||||
|
del self.data['@titles'] |
||||
|
self.languages = self.data['@languages'] |
||||
|
del self.data['@languages'] |
||||
|
self.authors = [self.data['by']] |
||||
|
del self.data['by'] |
||||
|
|
||||
|
def tex(self, output): |
||||
|
"""Return the LaTeX code rendering the song.""" |
||||
|
return r'\input{{{}}}'.format(files.path2posix( |
||||
|
files.relpath( |
||||
|
self.fullpath, |
||||
|
os.path.dirname(output) |
||||
|
))) |
||||
|
|
||||
|
SONG_PARSERS = { |
||||
|
'is': LatexSong, |
||||
|
'sg': LatexSong, |
||||
|
} |
@ -1,30 +0,0 @@ |
|||||
"""Very simple LaTeX parsing.""" |
|
||||
|
|
||||
import os |
|
||||
|
|
||||
from patacrep import files |
|
||||
from patacrep.latex import parsesong |
|
||||
from patacrep.songs import Song |
|
||||
|
|
||||
class TexRenderer(Song): |
|
||||
"""Renderer for song and intersong files.""" |
|
||||
|
|
||||
def parse(self): |
|
||||
"""Parse song and set metadata.""" |
|
||||
self.data = parsesong(self.fullpath, self.encoding) |
|
||||
self.titles = self.data['@titles'] |
|
||||
self.languages = self.data['@languages'] |
|
||||
self.authors = self.data['by'] |
|
||||
|
|
||||
def render(self, context): |
|
||||
"""Return the string that will render the song.""" |
|
||||
return r'\input{{{}}}'.format(files.path2posix( |
|
||||
files.relpath( |
|
||||
self.fullpath, |
|
||||
os.path.dirname(context['filename']) |
|
||||
))) |
|
||||
|
|
||||
FILE_PLUGINS = { |
|
||||
'sg': TexRenderer, |
|
||||
'is': TexRenderer, |
|
||||
} |
|
@ -0,0 +1,26 @@ |
|||||
|
"""Tests""" |
||||
|
|
||||
|
import doctest |
||||
|
import os |
||||
|
import unittest |
||||
|
|
||||
|
import patacrep |
||||
|
|
||||
|
def suite(): |
||||
|
"""Return a TestSuite object, to test whole `patacrep` package. |
||||
|
|
||||
|
Both unittest and doctest are tested. |
||||
|
""" |
||||
|
test_loader = unittest.defaultTestLoader |
||||
|
return test_loader.discover(os.path.dirname(__file__)) |
||||
|
|
||||
|
def load_tests(__loader, tests, __pattern): |
||||
|
"""Load tests (unittests and doctests).""" |
||||
|
# Loading doctests |
||||
|
tests.addTests(doctest.DocTestSuite(patacrep)) |
||||
|
|
||||
|
# Unittests are loaded by default |
||||
|
return tests |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
unittest.TextTestRunner().run(suite()) |
Loading…
Reference in new issue