Browse Source

[chordpro] `{define: FOO}` directive are now meta information. Generated AST changed too.

pull/79/head
Louis 9 years ago
parent
commit
6afb5fd8e0
  1. 14
      patacrep/songs/chordpro/__init__.py
  2. 288
      patacrep/songs/chordpro/ast.py
  3. 10
      patacrep/songs/chordpro/data/latex/song.tex
  4. 22
      patacrep/songs/chordpro/syntax.py
  5. 2
      patacrep/songs/chordpro/test/00.txt
  6. 2
      patacrep/songs/chordpro/test/01.txt
  7. 2
      patacrep/songs/chordpro/test/02.txt
  8. 2
      patacrep/songs/chordpro/test/03.txt
  9. 2
      patacrep/songs/chordpro/test/04.txt
  10. 2
      patacrep/songs/chordpro/test/05.txt
  11. 2
      patacrep/songs/chordpro/test/06.txt
  12. 2
      patacrep/songs/chordpro/test/07.txt
  13. 2
      patacrep/songs/chordpro/test/08.txt
  14. 2
      patacrep/songs/chordpro/test/09.txt
  15. 2
      patacrep/songs/chordpro/test/10.txt
  16. 2
      patacrep/songs/chordpro/test/11.txt
  17. 2
      patacrep/songs/chordpro/test/12.txt
  18. 2
      patacrep/songs/chordpro/test/13.txt
  19. 2
      patacrep/songs/chordpro/test/21.txt
  20. 2
      patacrep/songs/chordpro/test/22.txt
  21. 2
      patacrep/songs/chordpro/test/23.txt
  22. 2
      patacrep/songs/chordpro/test/24.txt
  23. 2
      patacrep/songs/chordpro/test/25.txt
  24. 2
      patacrep/songs/chordpro/test/26.txt
  25. 2
      patacrep/songs/chordpro/test/27.txt
  26. 2
      patacrep/songs/chordpro/test/28.txt
  27. 2
      patacrep/songs/chordpro/test/29.txt
  28. 2
      patacrep/songs/chordpro/test/chords.txt
  29. 2
      patacrep/songs/chordpro/test/customchords.txt
  30. 6
      patacrep/songs/chordpro/test/greensleeves.txt
  31. 8
      patacrep/songs/chordpro/test/metadata.txt
  32. 4
      patacrep/songs/chordpro/test/test_parser.py

14
patacrep/songs/chordpro/__init__.py

@ -22,28 +22,30 @@ class ChordproSong(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.languages = song.get_data_argument('language', [self.config['lang']])
self.data = 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),
'language': self.config.get(
'lang',
self.cached['song'].get_data_argument('language', 'english'),
),
#'columns': self.cached['song'].get_data_argument('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(),
"render": self.render_tex,
}
self.texenv = Environment(loader=FileSystemLoader(os.path.join(
os.path.abspath(pkg_resources.resource_filename(__name__, 'data')),
'latex'
)))
return self.render_tex(context, self.cached['song'].content, template="chordpro.tex")
return self.render_tex(context, self.cached['song'].content, template="song.tex")
@contextfunction
def render_tex(self, context, content, template=None):

288
patacrep/songs/chordpro/ast.py

@ -2,12 +2,41 @@
# 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):
self._keys = []
self._values = {}
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 +50,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 = {
@ -62,6 +82,10 @@ class AST:
base = self._template
return "content_{}.{}".format(base, extension)
def chordpro(self):
"""Return the chordpro string corresponding to this object."""
raise NotImplementedError()
class Line(AST):
"""A line is a sequence of (possibly truncated) words, spaces and chords."""
@ -76,8 +100,8 @@ class Line(AST):
self.line.insert(0, data)
return self
def __str__(self):
return "".join([str(item) for item in self.line])
def chordpro(self):
return "".join([item.chordpro() for item in self.line])
def strip(self):
"""Remove spaces at the beginning and end of line."""
@ -94,6 +118,7 @@ class Line(AST):
class LineElement(AST):
"""Something present on a line."""
# pylint: disable=abstract-method
pass
class Word(LineElement):
@ -104,7 +129,7 @@ class Word(LineElement):
super().__init__()
self.value = value
def __str__(self):
def chordpro(self):
return self.value
class Space(LineElement):
@ -114,7 +139,7 @@ class Space(LineElement):
def __init__(self):
super().__init__()
def __str__(self):
def chordpro(self):
return " "
class ChordList(LineElement):
@ -124,9 +149,9 @@ class ChordList(LineElement):
def __init__(self, *chords):
self.chords = chords
def __str__(self):
def chordpro(self):
return "[{}]".format(" ".join(
[str(chord) for chord in self.chords]
[chord.chordpro() for chord in self.chords]
))
class Chord(AST):
@ -151,7 +176,7 @@ class Chord(AST):
self.basskey = basskey
self.bassalteration = bassalteration
def __str__(self):
def chordpro(self):
text = ""
text += self.key
if self.alteration is not None:
@ -166,50 +191,6 @@ class Chord(AST):
text += self.bassalteration
return text
class Define(AST):
"""A chord definition.
Attributes:
.. 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.
"""
_template = "define"
inline = True
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
def __str__(self):
text = str(self.key)
if self.basefret is not None:
text += " base-fret " + str(self.basefret)
text += " frets"
for fret in self.frets:
if fret is None:
text += " x"
else:
text += " " + str(fret)
if self.fingers:
text += " fingers"
for finger in self.fingers:
if finger is None:
text += " -"
else:
text += " " + str(finger)
return "{{define: {}}}".format(text)
class Verse(AST):
"""A verse (or bridge, or chorus)"""
_template = "verse"
@ -225,10 +206,10 @@ class Verse(AST):
self.lines.insert(0, data)
return self
def __str__(self):
def chordpro(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])),
content=_indent("\n".join([line.chordpro() for line in self.lines])),
)
class Chorus(Verse):
@ -248,17 +229,17 @@ 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",
"define": "add_cumulative",
"language": "add_cumulative",
}
#: Some directives have to be processed before being considered.
@ -271,7 +252,7 @@ class Song(AST):
def __init__(self, filename):
super().__init__()
self.content = []
self.meta = []
self.meta = OrderedLifoDict()
self._authors = []
self._titles = []
self._subtitles = []
@ -282,9 +263,8 @@ class Song(AST):
"""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.keyword in self.PROCESS_DIRECTIVE:
data = getattr(self, self.PROCESS_DIRECTIVE[data.keyword])(data)
if data is None:
# New line
@ -300,12 +280,11 @@ 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
@ -316,62 +295,71 @@ class Song(AST):
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)
for key in self.keys:
yield "{{key: {}}}".format(key.chordpro())
for value in self.meta.values():
if isinstance(value, list):
yield "\n".join([item.chordpro() for item in value])
else:
yield value.chordpro()
def __str__(self):
def chordpro(self):
return (
"\n".join(self.str_meta()).strip()
+
"\n========\n"
"\n\n"
+
"\n".join([str(item) for item in self.content]).strip()
"\n".join([item.chordpro() for item in self.content]).strip()
)
def add_title(self, __ignored, title):
def add_title(self, data):
"""Add a title"""
self._titles.insert(0, 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 get_data_argument(self, keyword, default):
"""Return `self.meta[keyword].argument`.
Return `default` if `self.meta[keyword]` does not exist.
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(":")
self._keys.insert(0, Directive(
key.strip(),
":".join(argument).strip(),
))
@ -384,15 +372,6 @@ class Song(AST):
"""
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
@ -410,17 +389,15 @@ class Newline(AST):
"""New line"""
_template = "newline"
def __str__(self):
def chordpro(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
@ -431,26 +408,13 @@ 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):
def chordpro(self):
if self.argument is not None:
return "{{{}: {}}}".format(
self.keyword,
@ -459,16 +423,54 @@ class Directive(AST):
else:
return "{{{}}}".format(self.keyword)
@property
def as_tuple(self):
"""Return the directive as a tuple."""
return (self.keyword, self.argument)
def __str__(self):
return self.argument
class Define(Directive):
"""A chord definition.
Attributes:
.. 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 __eq__(self, other):
return self.as_tuple == other.as_tuple
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 chordpro(self):
text = self.key.chordpro()
if self.basefret is not None:
text += " base-fret " + str(self.basefret)
text += " frets"
for fret in self.frets:
if fret is None:
text += " x"
else:
text += " " + str(fret)
if self.fingers:
text += " fingers"
for finger in self.fingers:
if finger is None:
text += " -"
else:
text += " " + str(finger)
return "{{define: {}}}".format(text)
def __str__(self):
return None
class Tab(AST):
"""Tablature"""
@ -484,7 +486,7 @@ class Tab(AST):
self.content.insert(0, data)
return self
def __str__(self):
def chordpro(self):
return '{{start_of_tab}}\n{}\n{{end_of_tab}}'.format(
_indent("\n".join(self.content)),
)

10
patacrep/songs/chordpro/data/latex/chordpro.tex → patacrep/songs/chordpro/data/latex/song.tex

@ -10,8 +10,10 @@
\beginsong{((titles))}[
by={((authors))},
(* for (key, argument) in beginsong *)
((key))={((argument))},
(* for key in ['album', 'copyright', 'cov', 'vcov', 'tag'] *)
(* if key in metadata *)
(( key ))={(( metadata[key] ))},
(* endif *)
(* endfor *)
]
@ -19,6 +21,10 @@
\cover
(* endif -*)
(* for chord in metadata['define'] *)
(( render(chord) ))
(* endfor *)
(* for item in content -*)
(( render(item) ))
(* endfor *)

22
patacrep/songs/chordpro/syntax.py

@ -130,12 +130,13 @@ class ChordproParser(Parser):
| LBRACE SPACE KEYWORD directive_next RBRACE
"""
if len(symbols) == 5:
symbols[3].keyword = symbols[2]
symbols[0] = symbols[3]
keyword = symbols[2]
argument = symbols[3]
else:
symbols[4].keyword = symbols[3]
symbols[0] = symbols[4]
if symbols[0].keyword == 'define':
keyword = symbols[3]
argument = symbols[4]
if keyword == "define":
match = re.compile(
r"""
(?P<key>[^\ ]*)\ *
@ -144,12 +145,14 @@ class ChordproParser(Parser):
(fingers\ *(?P<fingers>(([0-4-])\ *)*))?
""",
re.VERBOSE
).match(symbols[0].argument)
).match(argument)
if match is None:
TODO
symbols[0] = _parse_define(**match.groupdict())
else:
symbols[0] = ast.Directive(keyword, argument)
@staticmethod
def p_directive_next(symbols):
@ -157,11 +160,12 @@ class ChordproParser(Parser):
| COLON TEXT
| empty
"""
symbols[0] = ast.Directive()
if len(symbols) == 3:
symbols[0].argument = symbols[2].strip()
symbols[0] = symbols[2].strip()
elif len(symbols) == 4:
symbols[0].argument = symbols[3].strip()
symbols[0] = symbols[3].strip()
else:
symbols[0] = None
@staticmethod
def p_line(symbols):

2
patacrep/songs/chordpro/test/00.txt

@ -1 +1 @@
========

2
patacrep/songs/chordpro/test/01.txt

@ -1,4 +1,4 @@
========
{start_of_verse}
A verse line
{end_of_verse}

2
patacrep/songs/chordpro/test/02.txt

@ -1,2 +1,2 @@
{title: A directive}
========

2
patacrep/songs/chordpro/test/03.txt

@ -1 +1 @@
========

2
patacrep/songs/chordpro/test/04.txt

@ -1,4 +1,4 @@
========
{start_of_chorus}
A one line chorus
{end_of_chorus}

2
patacrep/songs/chordpro/test/05.txt

@ -1,4 +1,4 @@
========
{start_of_bridge}
A one line bridge
{end_of_bridge}

2
patacrep/songs/chordpro/test/06.txt

@ -1 +1 @@
========

2
patacrep/songs/chordpro/test/07.txt

@ -1,4 +1,4 @@
========
{start_of_tab}
A tab
{end_of_tab}

2
patacrep/songs/chordpro/test/08.txt

@ -1,4 +1,4 @@
========
{start_of_verse}
A lot of new lines
{end_of_verse}

2
patacrep/songs/chordpro/test/09.txt

@ -1,5 +1,5 @@
{title: and a directive}
========
{start_of_verse}
A lot of new lines
{end_of_verse}

2
patacrep/songs/chordpro/test/10.txt

@ -1,4 +1,4 @@
========
{start_of_verse}
A line[A] with a chord
{end_of_verse}

2
patacrep/songs/chordpro/test/11.txt

@ -1,4 +1,4 @@
========
{start_of_verse}
A line ending with a chord[A]
{end_of_verse}

2
patacrep/songs/chordpro/test/12.txt

@ -1,4 +1,4 @@
========
{start_of_verse}
[A]A line starting with a chord
{end_of_verse}

2
patacrep/songs/chordpro/test/13.txt

@ -1,4 +1,4 @@
========
{start_of_tab}
A table
wit many # weir [

2
patacrep/songs/chordpro/test/21.txt

@ -1,4 +1,4 @@
========
{start_of_verse}
A verse line
{end_of_verse}

2
patacrep/songs/chordpro/test/22.txt

@ -1,2 +1,2 @@
{title: A directive}
========

2
patacrep/songs/chordpro/test/23.txt

@ -1 +1 @@
========

2
patacrep/songs/chordpro/test/24.txt

@ -1,4 +1,4 @@
========
{start_of_chorus}
A one line chorus
{end_of_chorus}

2
patacrep/songs/chordpro/test/25.txt

@ -1,4 +1,4 @@
========
{start_of_bridge}
A one line bridge
{end_of_bridge}

2
patacrep/songs/chordpro/test/26.txt

@ -1 +1 @@
========

2
patacrep/songs/chordpro/test/27.txt

@ -1,4 +1,4 @@
========
{start_of_tab}
A tab
{end_of_tab}

2
patacrep/songs/chordpro/test/28.txt

@ -1,4 +1,4 @@
========
{start_of_verse}
A lot of new lines
{end_of_verse}

2
patacrep/songs/chordpro/test/29.txt

@ -1,5 +1,5 @@
{title: and a directive}
========
{start_of_verse}
A lot of new lines
{end_of_verse}

2
patacrep/songs/chordpro/test/chords.txt

@ -1,4 +1,4 @@
========
{start_of_verse}
[A]Simple
[Bb]Bémol

2
patacrep/songs/chordpro/test/customchords.txt

@ -1,3 +1,3 @@
========
{define: E4 base-fret 7 frets 0 1 3 3 x x}
{define: E5 base-fret 7 frets 0 1 3 3 x x fingers - 1 2 3 - -}

6
patacrep/songs/chordpro/test/greensleeves.txt

@ -2,11 +2,11 @@
{title: Un autre sous-titre}
{title: Un sous titre}
{by: Traditionnel}
{album: Angleterre}
{language: english}
{columns: 2}
{cov: DIRNAME/traditionnel}
{language: english}
========
{album: Angleterre}
{partition: DIRNAME/greensleeves.ly}
{start_of_verse}

8
patacrep/songs/chordpro/test/metadata.txt

@ -7,14 +7,14 @@
{by: Author1}
{by: Author2}
{key: {foo: Foo}}
{language: french}
{language: english}
{album: Albom}
{capo: Capo}
{copyright: Copyright}
{cov: DIRNAME/Cover}
{language: english}
{language: french}
{vcov: VCover}
========
{capo: Capo}
{comment: Comment}
{guitar_comment: GuitarComment}
{image: DIRNAME/Image}

4
patacrep/songs/chordpro/test/test_parser.py

@ -29,10 +29,10 @@ class ParserTxtRenderer(unittest.TestCase):
with open("{}.sgc".format(self.basename), 'r', encoding='utf8') as sourcefile:
with open("{}.txt".format(self.basename), 'r', encoding='utf8') as expectfile:
self.assertMultiLineEqual(
str(chordpro.parse_song(
chordpro.parse_song(
sourcefile.read(),
os.path.abspath(sourcefile.name),
)).strip(),
).chordpro().strip(),
expectfile.read().strip().replace("DIRNAME", os.path.dirname(self.basename)),
)

Loading…
Cancel
Save