|
|
@ -2,12 +2,43 @@ |
|
|
|
|
|
|
|
# pylint: disable=too-few-public-methods |
|
|
|
|
|
|
|
import functools |
|
|
|
import logging |
|
|
|
import os |
|
|
|
|
|
|
|
LOGGER = logging.getLogger() |
|
|
|
|
|
|
|
class OrderedLifoDict: |
|
|
|
"""Ordered (LIFO) dictionary. |
|
|
|
|
|
|
|
Mimics the :class:`dict` dictionary, with: |
|
|
|
- dictionary is ordered: the order the keys are kept (as with |
|
|
|
:class:`collections.OrderedDict`), excepted that: |
|
|
|
- LIFO: the last item is reterned first when iterating. |
|
|
|
""" |
|
|
|
|
|
|
|
def __init__(self, default=None): |
|
|
|
if default is None: |
|
|
|
self._keys = [] |
|
|
|
self._values = {} |
|
|
|
else: |
|
|
|
self._keys = list(default.keys()) |
|
|
|
self._values = default.copy() |
|
|
|
|
|
|
|
def values(self): |
|
|
|
"""Same as :meth:`dict.values`.""" |
|
|
|
for key in self: |
|
|
|
yield self._values[key] |
|
|
|
|
|
|
|
def __iter__(self): |
|
|
|
yield from self._keys |
|
|
|
|
|
|
|
def __setitem__(self, key, value): |
|
|
|
if key not in self._keys: |
|
|
|
self._keys.insert(0, key) |
|
|
|
self._values[key] = value |
|
|
|
|
|
|
|
def __getitem__(self, key): |
|
|
|
return self._values[key] |
|
|
|
|
|
|
|
def _indent(string): |
|
|
|
"""Return and indented version of argument.""" |
|
|
|
return "\n".join([" {}".format(line) for line in string.split('\n')]) |
|
|
@ -21,15 +52,6 @@ INLINE_PROPERTIES = { |
|
|
|
"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 = { |
|
|
@ -40,7 +62,6 @@ DIRECTIVE_SHORTCUTS = { |
|
|
|
"c": "comment", |
|
|
|
"gc": "guitar_comment", |
|
|
|
"cover": "cov", |
|
|
|
"vcover": "vcov", |
|
|
|
} |
|
|
|
|
|
|
|
def directive_name(text): |
|
|
@ -53,14 +74,17 @@ class AST: |
|
|
|
_template = None |
|
|
|
inline = False |
|
|
|
|
|
|
|
def template(self, extension): |
|
|
|
def template(self): |
|
|
|
"""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) |
|
|
|
return "content_{}".format(base) |
|
|
|
|
|
|
|
class Error(AST): |
|
|
|
"""Parsing error. To be ignored.""" |
|
|
|
|
|
|
|
class Line(AST): |
|
|
|
"""A line is a sequence of (possibly truncated) words, spaces and chords.""" |
|
|
@ -76,9 +100,6 @@ class Line(AST): |
|
|
|
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: |
|
|
@ -94,6 +115,7 @@ class Line(AST): |
|
|
|
|
|
|
|
class LineElement(AST): |
|
|
|
"""Something present on a line.""" |
|
|
|
# pylint: disable=abstract-method |
|
|
|
pass |
|
|
|
|
|
|
|
class Word(LineElement): |
|
|
@ -104,9 +126,6 @@ class Word(LineElement): |
|
|
|
super().__init__() |
|
|
|
self.value = value |
|
|
|
|
|
|
|
def __str__(self): |
|
|
|
return self.value |
|
|
|
|
|
|
|
class Space(LineElement): |
|
|
|
"""A space between words""" |
|
|
|
_template = "space" |
|
|
@ -114,20 +133,21 @@ class Space(LineElement): |
|
|
|
def __init__(self): |
|
|
|
super().__init__() |
|
|
|
|
|
|
|
def __str__(self): |
|
|
|
return " " |
|
|
|
class ChordList(LineElement): |
|
|
|
"""A list of chords.""" |
|
|
|
_template = "chordlist" |
|
|
|
|
|
|
|
def __init__(self, *chords): |
|
|
|
self.chords = chords |
|
|
|
|
|
|
|
class Chord(LineElement): |
|
|
|
class Chord(AST): |
|
|
|
"""A chord.""" |
|
|
|
|
|
|
|
_template = "chord" |
|
|
|
|
|
|
|
def __init__(self, value): |
|
|
|
super().__init__() |
|
|
|
self.value = value |
|
|
|
|
|
|
|
def __str__(self): |
|
|
|
return "[{}]".format(self.value) |
|
|
|
def __init__(self, chord): |
|
|
|
# pylint: disable=too-many-arguments |
|
|
|
self.chord = chord |
|
|
|
|
|
|
|
class Verse(AST): |
|
|
|
"""A verse (or bridge, or chorus)""" |
|
|
@ -144,12 +164,6 @@ class Verse(AST): |
|
|
|
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' |
|
|
@ -167,30 +181,23 @@ class Song(AST): |
|
|
|
- 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 = { |
|
|
|
METADATA_ADD = { |
|
|
|
"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", |
|
|
|
"define": "add_cumulative", |
|
|
|
"language": "add_cumulative", |
|
|
|
} |
|
|
|
|
|
|
|
def __init__(self, filename): |
|
|
|
super().__init__() |
|
|
|
self.content = [] |
|
|
|
self.meta = [] |
|
|
|
self.meta = OrderedLifoDict() |
|
|
|
self._authors = [] |
|
|
|
self._titles = [] |
|
|
|
self._subtitles = [] |
|
|
@ -199,13 +206,9 @@ class Song(AST): |
|
|
|
|
|
|
|
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: |
|
|
|
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()) |
|
|
@ -219,127 +222,78 @@ class Song(AST): |
|
|
|
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) |
|
|
|
# methods listed in ``METADATA_ADD``. |
|
|
|
if data.keyword in self.METADATA_ADD: |
|
|
|
getattr(self, self.METADATA_ADD[data.keyword])(data) |
|
|
|
else: |
|
|
|
self.meta.append(data) |
|
|
|
self.meta[data.keyword] = 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 add_title(self, data): |
|
|
|
"""Add a title""" |
|
|
|
self._titles.insert(0, data.argument) |
|
|
|
|
|
|
|
def add_cumulative(self, data): |
|
|
|
"""Add a cumulative argument into metadata""" |
|
|
|
if data.keyword not in self.meta: |
|
|
|
self.meta[data.keyword] = [] |
|
|
|
self.meta[data.keyword].insert(0, data) |
|
|
|
|
|
|
|
def __str__(self): |
|
|
|
return ( |
|
|
|
"\n".join(self.str_meta()).strip() |
|
|
|
+ |
|
|
|
"\n========\n" |
|
|
|
+ |
|
|
|
"\n".join([str(item) for item in self.content]).strip() |
|
|
|
) |
|
|
|
def get_data_argument(self, keyword, default): |
|
|
|
"""Return `self.meta[keyword].argument`. |
|
|
|
|
|
|
|
Return `default` if `self.meta[keyword]` does not exist. |
|
|
|
|
|
|
|
def add_title(self, __ignored, title): |
|
|
|
"""Add a title""" |
|
|
|
self._titles.insert(0, title) |
|
|
|
If `self.meta[keyword]` is a list, return the list of `item.argument` |
|
|
|
for each item in the list. |
|
|
|
""" |
|
|
|
if keyword not in self.meta: |
|
|
|
return default |
|
|
|
if isinstance(self.meta[keyword], list): |
|
|
|
return [item.argument for item in self.meta[keyword]] |
|
|
|
else: |
|
|
|
return self.meta[keyword].argument |
|
|
|
|
|
|
|
def add_subtitle(self, __ignored, title): |
|
|
|
def add_subtitle(self, data): |
|
|
|
"""Add a subtitle""" |
|
|
|
self._subtitles.insert(0, title) |
|
|
|
self._subtitles.insert(0, data.argument) |
|
|
|
|
|
|
|
@property |
|
|
|
def titles(self): |
|
|
|
"""Return the list of titles (and subtitles).""" |
|
|
|
return self._titles + self._subtitles |
|
|
|
|
|
|
|
def add_author(self, __ignored, title): |
|
|
|
def add_author(self, data): |
|
|
|
"""Add an auhor.""" |
|
|
|
self._authors.insert(0, title) |
|
|
|
self._authors.insert(0, data.argument) |
|
|
|
|
|
|
|
@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): |
|
|
|
def add_key(self, data): |
|
|
|
"""Add a new {key: foo: bar} directive.""" |
|
|
|
key, *argument = argument.split(":") |
|
|
|
self._keys.append(Directive( |
|
|
|
key, *argument = data.argument.split(":") |
|
|
|
if 'keys' not in self.meta: |
|
|
|
self.meta['keys'] = [] |
|
|
|
self.meta['keys'].insert(0, 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): |
|
|
|
def __init__(self, keyword, argument=None): |
|
|
|
super().__init__() |
|
|
|
self._keyword = None |
|
|
|
self.keyword = keyword |
|
|
|
self.keyword = directive_name(keyword.strip()) |
|
|
|
self.argument = argument |
|
|
|
|
|
|
|
@property |
|
|
@ -350,49 +304,47 @@ class Directive(AST): |
|
|
|
""" |
|
|
|
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) |
|
|
|
return self.argument |
|
|
|
|
|
|
|
@property |
|
|
|
def as_tuple(self): |
|
|
|
"""Return the directive as a tuple.""" |
|
|
|
return (self.keyword, self.argument) |
|
|
|
class Define(Directive): |
|
|
|
"""A chord definition. |
|
|
|
|
|
|
|
Attributes: |
|
|
|
|
|
|
|
def __eq__(self, other): |
|
|
|
return self.as_tuple == other.as_tuple |
|
|
|
.. attribute:: key |
|
|
|
The key, as a :class:`Chord` object. |
|
|
|
.. attribute:: basefret |
|
|
|
The base fret, as an integer. Can be `None` if no base fret is defined. |
|
|
|
.. attribute:: frets |
|
|
|
The list of frets, as a list of integers, or `None`, if this fret is not to be played. |
|
|
|
.. attribute:: fingers |
|
|
|
The list of fingers to use on frets, as a list of integers, or `None` |
|
|
|
if no information is given (this string is not played, or is played |
|
|
|
open). Can be `None` if not defined. |
|
|
|
""" |
|
|
|
|
|
|
|
def __init__(self, key, basefret, frets, fingers): |
|
|
|
self.key = key |
|
|
|
self.basefret = basefret # Can be None |
|
|
|
self.frets = frets |
|
|
|
self.fingers = fingers # Can be None |
|
|
|
super().__init__("define", None) |
|
|
|
|
|
|
|
def __lt__(self, other): |
|
|
|
return self.as_tuple < other.as_tuple |
|
|
|
def __str__(self): |
|
|
|
return None |
|
|
|
|
|
|
|
class Tab(AST): |
|
|
|
"""Tablature""" |
|
|
|
|
|
|
|
inline = True |
|
|
|
_template = "tablature" |
|
|
|
|
|
|
|
def __init__(self): |
|
|
|
super().__init__() |
|
|
@ -402,9 +354,3 @@ class Tab(AST): |
|
|
|
"""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)), |
|
|
|
) |
|
|
|
|
|
|
|