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.

427 lines
12 KiB

"""Abstract Syntax Tree for ChordPro code."""
10 years ago
# pylint: disable=too-few-public-methods
import functools
10 years ago
import logging
import os
LOGGER = logging.getLogger()
def _indent(string):
10 years ago
"""Return and indented version of argument."""
return "\n".join([" {}".format(line) for line in string.split('\n')])
10 years ago
#: 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 = {
10 years ago
"partition",
"comment",
"guitar_comment",
"image",
}
10 years ago
#: 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",
10 years ago
"cover": "cov",
"vcover": "vcov",
}
def directive_name(text):
10 years ago
"""Return name of the directive, considering eventual shortcuts."""
return DIRECTIVE_SHORTCUTS.get(text, text)
class AST:
10 years ago
"""Generic object representing elements of the song."""
_template = None
inline = False
10 years ago
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."""
10 years ago
_template = "line"
def __init__(self):
super().__init__()
self.line = []
def prepend(self, data):
10 years ago
"""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):
10 years ago
"""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."""
10 years ago
_template = "word"
def __init__(self, value):
super().__init__()
self.value = value
def __str__(self):
return self.value
class Space(LineElement):
"""A space between words"""
10 years ago
_template = "space"
def __init__(self):
super().__init__()
def __str__(self):
return " "
class Chord(LineElement):
"""A chord."""
10 years ago
_template = "chord"
def __init__(self, key, alteration, modifier, add_note, bass):
super().__init__()
self.key = key
self.alteration = alteration
self.modifier = modifier
self.add_note = add_note
self.bass = bass
def __str__(self):
text = ""
text += self.key
if self.alteration is not None:
text += self.alteration
if self.modifier is not None:
text += self.modifier
if self.add_note is not None:
text += str(self.add_note)
if self.bass is not None:
text += "/" + self.bass[0]
if self.bass[1] is not None:
text += self.bass[1]
return "[{}]".format(text)
class Verse(AST):
"""A verse (or bridge, or chorus)"""
10 years ago
_template = "verse"
type = "verse"
inline = True
10 years ago
def __init__(self):
super().__init__()
10 years ago
self.lines = []
def prepend(self, data):
10 years ago
"""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):
10 years ago
"""Chorus"""
type = 'chorus'
class Bridge(Verse):
10 years ago
"""Bridge"""
type = 'bridge'
class Song(AST):
10 years ago
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",
10 years ago
"key": "add_key",
}
10 years ago
#: Some directives have to be processed before being considered.
PROCESS_DIRECTIVE = {
"cov": "_process_relative",
"partition": "_process_relative",
"image": "_process_relative",
}
10 years ago
def __init__(self, filename):
super().__init__()
self.content = []
self.meta = []
self._authors = []
self._titles = []
self._subtitles = []
10 years ago
self._keys = []
self.filename = filename
def add(self, data):
10 years ago
"""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:
10 years ago
# New line
if not (self.content and isinstance(self.content[0], Newline)):
self.content.insert(0, Newline())
elif isinstance(data, Line):
10 years ago
# 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:
10 years ago
# Add an object in the content of the song.
self.content.insert(0, data)
elif isinstance(data, Directive):
10 years ago
# 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):
10 years ago
"""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)
10 years ago
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):
10 years ago
"""Add a title"""
self._titles.insert(0, title)
def add_subtitle(self, __ignored, title):
10 years ago
"""Add a subtitle"""
self._subtitles.insert(0, title)
@property
def titles(self):
10 years ago
"""Return the list of titles (and subtitles)."""
return self._titles + self._subtitles
def add_author(self, __ignored, title):
10 years ago
"""Add an auhor."""
self._authors.insert(0, title)
@property
def authors(self):
10 years ago
"""Return the list of (raw) authors."""
return self._authors
10 years ago
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
10 years ago
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):
10 years ago
"""New line"""
_template = "newline"
def __str__(self):
return ""
@functools.total_ordering
class Directive(AST):
"""A directive"""
10 years ago
def __init__(self, keyword="", argument=None):
super().__init__()
10 years ago
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):
10 years ago
"""Keyword of the directive."""
return self._keyword
@property
def inline(self):
10 years ago
"""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):
10 years ago
"""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):
10 years ago
"""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):
10 years ago
"""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)),
)