Browse Source

Tex rendere works

pull/70/head
Louis 10 years ago
committed by Luthaf
parent
commit
b9d459ec24
  1. 13
      patacrep/authors.py
  2. 4
      patacrep/build.py
  3. 1
      patacrep/data/examples/example-all.sb
  4. 11
      patacrep/data/examples/songs/greensleeves.sgc
  5. 45
      patacrep/songs/__init__.py
  6. 47
      patacrep/songs/chordpro/__init__.py
  7. 198
      patacrep/songs/chordpro/ast.py
  8. 27
      patacrep/songs/chordpro/data/latex/chordpro.tex
  9. 1
      patacrep/songs/chordpro/data/latex/content_chord.tex
  10. 1
      patacrep/songs/chordpro/data/latex/content_comment.tex
  11. 3
      patacrep/songs/chordpro/data/latex/content_error.tex
  12. 1
      patacrep/songs/chordpro/data/latex/content_guitar_comment.tex
  13. 1
      patacrep/songs/chordpro/data/latex/content_image.tex
  14. 1
      patacrep/songs/chordpro/data/latex/content_line.tex
  15. 2
      patacrep/songs/chordpro/data/latex/content_newline.tex
  16. 1
      patacrep/songs/chordpro/data/latex/content_partition.tex
  17. 1
      patacrep/songs/chordpro/data/latex/content_space.tex
  18. 7
      patacrep/songs/chordpro/data/latex/content_verse.tex
  19. 1
      patacrep/songs/chordpro/data/latex/content_word.tex
  20. 27
      patacrep/songs/chordpro/lexer.py
  21. 22
      patacrep/songs/chordpro/syntax.py
  22. 1
      patacrep/songs/chordpro/test/__init__.py
  23. 7
      patacrep/songs/chordpro/test/greensleeves.txt
  24. 4
      patacrep/songs/chordpro/test/metadata.sgc
  25. 14
      patacrep/songs/chordpro/test/metadata.txt
  26. 76
      patacrep/songs/chordpro/test/test_parser.py
  27. 2
      patacrep/songs/latex/__init__.py
  28. 40
      patacrep/templates.py
  29. 26
      patacrep/test.py
  30. 3
      setup.py

13
patacrep/authors.py

@ -225,9 +225,12 @@ def processauthors(authors_string, after=None, ignore=None, sep=None):
) )
] ]
def process_listauthors(authors_list): def process_listauthors(authors_list, after=None, ignore=None, sep=None):
"""Process a list of authors, and return the list of resulting authors.""" """Process a list of authors, and return the list of resulting authors."""
return sum([ authors = []
processauthors(string) for sublist in [
for string in authors_list processauthors(string, after, ignore, sep)
]) for string in authors_list
]:
authors.extend(sublist)
return authors

4
patacrep/build.py

@ -10,7 +10,7 @@ from subprocess import Popen, PIPE, call
from patacrep import __DATADIR__, authors, content, errors, files from patacrep import __DATADIR__, authors, content, errors, files
from patacrep.index import process_sxd from patacrep.index import process_sxd
from patacrep.templates import TexRenderer from patacrep.templates import TexBookRenderer
from patacrep.songs import DataSubpath from patacrep.songs import DataSubpath
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -87,7 +87,7 @@ class Songbook(object):
# Updating configuration # Updating configuration
config = DEFAULT_CONFIG.copy() config = DEFAULT_CONFIG.copy()
config.update(self.config) config.update(self.config)
renderer = TexRenderer( renderer = TexBookRenderer(
config['template'], config['template'],
config['datadir'], config['datadir'],
config['lang'], config['lang'],

1
patacrep/data/examples/example-all.sb

@ -7,6 +7,7 @@
], ],
"booktype" : "chorded", "booktype" : "chorded",
"lang" : "french", "lang" : "french",
"encoding": "utf8",
"authwords" : { "authwords" : {
"sep" : ["and", "et"] "sep" : ["and", "et"]
}, },

11
patacrep/data/examples/songs/greensleeves.sgc

@ -1,7 +1,9 @@
{language : english} {language : english}
{columns : 2} {columns : 2}
{ title : Greensleeves} { title : Greensleeves}
{ title : Un sous titre}
{artist: Traditionnel} {artist: Traditionnel}
{artist: Prénom Nom}
{cover : traditionnel } {cover : traditionnel }
{album :Angleterre} {album :Angleterre}
@ -10,7 +12,7 @@
A[Am]las, my love, ye [G]do me wrong A[Am]las, my love, ye [G]do me wrong
To [Am]cast me oft dis[E]curteously To [Am]cast me oft dis[E]curteously
And [Am]{I have} loved [G]you so long And [Am]I have loved [G]you so long
De[Am]lighting [E]in your [Am]companie De[Am]lighting [E]in your [Am]companie
{start_of_chorus} {start_of_chorus}
@ -35,8 +37,15 @@ The [Am]cloth so fine as [E]fine might be
I [Am]gave thee jewels [G]for thy chest I [Am]gave thee jewels [G]for thy chest
And [Am]all this [E]cost I [Am]spent on thee 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 Thy [Am]smock of silke, both [G]faire and white
With [Am]gold embrodered [E]gorgeously With [Am]gold embrodered [E]gorgeously
Thy [Am]peticote of [G]sendall right Thy [Am]peticote of [G]sendall right
And [Am]this I [E]bought thee [Am]gladly And [Am]this I [E]bought thee [Am]gladly

45
patacrep/songs/__init__.py

@ -8,7 +8,7 @@ import os
import pickle import pickle
import re import re
from patacrep.authors import processauthors from patacrep.authors import process_listauthors
from patacrep import files, encoding from patacrep import files, encoding
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -130,13 +130,12 @@ class Song(Content):
)) ))
# Data extraction from the latex song # Data extraction from the latex song
self.data = self.DEFAULT_DATA self.titles = []
self.data['@path'] = self.fullpath self.data = {}
self.data.update(self.parse( self.cached = None
encoding.open_read(self.fullpath).read() self.parse(config)
))
self.titles = self.data['@titles'] # Post processing of data
self.languages = self.data['@languages']
self.datadir = datadir self.datadir = datadir
self.unprefixed_titles = [ self.unprefixed_titles = [
unprefixed_title( unprefixed_title(
@ -146,17 +145,12 @@ class Song(Content):
for title for title
in self.titles in self.titles
] ]
self.subpath = subpath self.authors = process_listauthors(
self.authors = processauthors(
self.authors, self.authors,
**config["_compiled_authwords"] **config["_compiled_authwords"]
) )
# Cache management # Cache management
#: Special attribute to allow plugins to store cached data
self.cached = None
self._version = self.CACHE_VERSION self._version = self.CACHE_VERSION
self._write_cache() self._write_cache()
@ -181,11 +175,24 @@ class Song(Content):
Arguments: Arguments:
- output: Name of the output file. - output: Name of the output file.
""" """
return NotImplementedError() raise NotImplementedError()
def parse(self, content): # pylint: disable=no-self-use, unused-argument def parse(self, config): # pylint: disable=no-self-use
"""Parse song, and return a dictionary of its data.""" """Parse song.
return NotImplementedError()
It set the following attributes:
- titles: the list of (raw) titles. This list will be processed to
remove prefixes.
- languages: the list of languages used in the song, as languages
recognized by the LaTeX babel package.
- authors: the list of (raw) authors. This list will be processed to
'clean' it (see function :func:`patacrep.authors.processauthors`).
- data: song metadata. Used (among others) to sort the songs.
- cached: additional data that will be cached. Thus, data stored in
this attribute must be picklable.
"""
raise NotImplementedError()
def unprefixed_title(title, prefixes): def unprefixed_title(title, prefixes):
"""Remove the first prefix of the list in the beginning of title (if any). """Remove the first prefix of the list in the beginning of title (if any).

47
patacrep/songs/chordpro/__init__.py

@ -1,21 +1,50 @@
"""Chordpro parser"""
from patacrep import encoding from jinja2 import Environment, FileSystemLoader
import pkg_resources
import os
from patacrep import encoding, files
from patacrep.songs import Song from patacrep.songs import Song
from patacrep.songs.chordpro.syntax import parse_song from patacrep.songs.chordpro.syntax import parse_song
from patacrep.templates import TexRenderer
class ChordproSong(Song): class ChordproSong(Song):
"""Chordpros song parser.""" """Chordpros song parser."""
def parse(self): def parse(self, config):
"""Parse content, and return the dictinory of song data.""" """Parse content, and return the dictinory of song data."""
with encoding.open_read(self.fullpath, encoding=self.encoding) as song: with encoding.open_read(self.fullpath, encoding=self.encoding) as song:
self.data = parse_song(song.read(), self.fullpath) song = parse_song(song.read(), self.fullpath)
print(type(self.data), self.data) self.authors = song.authors
import sys; sys.exit(1) self.titles = song.titles
self.languages = self.data['@languages'] self.languages = song.get_directives('language')
del self.data['@languages'] self.data = dict([meta.as_tuple for meta in song.meta])
self.authors = self.data['by'] self.cached = {
del self.data['by'] 'song': song,
}
#: Main language
self.language = song.get_directive('language', config['lang'])
def tex(self, output):
context = {
'language': self.language,
'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 = { SONG_PARSERS = {
'sgc': ChordproSong, 'sgc': ChordproSong,

198
patacrep/songs/chordpro/ast.py

@ -1,18 +1,37 @@
# -*- coding: utf-8 -*-
"""Abstract Syntax Tree for ChordPro code.""" """Abstract Syntax Tree for ChordPro code."""
# pylint: disable=too-few-public-methods
import functools import functools
import logging
import os
LOGGER = logging.getLogger()
def _indent(string): def _indent(string):
"""Return and indented version of argument."""
return "\n".join([" {}".format(line) for line in string.split('\n')]) 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 = { INLINE_PROPERTIES = {
"lilypond", "partition",
"comment", "comment",
"guitar_comment", "guitar_comment",
"image", "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 = { DIRECTIVE_SHORTCUTS = {
"t": "title", "t": "title",
"st": "subtitle", "st": "subtitle",
@ -20,25 +39,40 @@ DIRECTIVE_SHORTCUTS = {
"by": "artist", "by": "artist",
"c": "comment", "c": "comment",
"gc": "guitar_comment", "gc": "guitar_comment",
"cover": "cov",
"vcover": "vcov",
} }
def directive_name(text): def directive_name(text):
if text in DIRECTIVE_SHORTCUTS: """Return name of the directive, considering eventual shortcuts."""
return DIRECTIVE_SHORTCUTS[text] return DIRECTIVE_SHORTCUTS.get(text, text)
return text
class AST: class AST:
"""Generic object representing elements of the song."""
_template = None
inline = False 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): class Line(AST):
"""A line is a sequence of (possibly truncated) words, spaces and chords.""" """A line is a sequence of (possibly truncated) words, spaces and chords."""
_template = "line"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.line = [] self.line = []
def prepend(self, data): def prepend(self, data):
"""Add an object at the beginning of line."""
self.line.insert(0, data) self.line.insert(0, data)
return self return self
@ -46,6 +80,7 @@ class Line(AST):
return "".join([str(item) for item in self.line]) return "".join([str(item) for item in self.line])
def strip(self): def strip(self):
"""Remove spaces at the beginning and end of line."""
while True: while True:
if not self.line: if not self.line:
return self return self
@ -63,6 +98,7 @@ class LineElement(AST):
class Word(LineElement): class Word(LineElement):
"""A chunk of word.""" """A chunk of word."""
_template = "word"
def __init__(self, value): def __init__(self, value):
super().__init__() super().__init__()
@ -73,6 +109,7 @@ class Word(LineElement):
class Space(LineElement): class Space(LineElement):
"""A space between words""" """A space between words"""
_template = "space"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -83,6 +120,8 @@ class Space(LineElement):
class Chord(LineElement): class Chord(LineElement):
"""A chord.""" """A chord."""
_template = "chord"
def __init__(self, value): def __init__(self, value):
super().__init__() super().__init__()
self.value = value self.value = value
@ -92,59 +131,95 @@ class Chord(LineElement):
class Verse(AST): class Verse(AST):
"""A verse (or bridge, or chorus)""" """A verse (or bridge, or chorus)"""
_template = "verse"
type = "verse" type = "verse"
inline = True inline = True
def __init__(self, block=None): def __init__(self):
super().__init__() super().__init__()
self.lines = [] # TODO check block self.lines = []
def prepend(self, data): def prepend(self, data):
"""Add data at the beginning of verse."""
self.lines.insert(0, data) self.lines.insert(0, data)
return self return self
def __str__(self): def __str__(self):
return '{{start_of_{type}}}\n{content}\n{{end_of_{type}}}'.format( return '{{start_of_{type}}}\n{content}\n{{end_of_{type}}}'.format(
type = self.type, type=self.type,
content = _indent("\n".join([str(line) for line in self.lines])), content=_indent("\n".join([str(line) for line in self.lines])),
) )
class Chorus(Verse): class Chorus(Verse):
"""Chorus"""
type = 'chorus' type = 'chorus'
class Bridge(Verse): class Bridge(Verse):
"""Bridge"""
type = 'bridge' type = 'bridge'
class Song(AST): class Song(AST):
"""A song""" 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 = { METADATA_TYPE = {
"title": "add_title", "title": "add_title",
"subtitle": "add_subtitle", "subtitle": "add_subtitle",
"language": "add_language",
"artist": "add_author", "artist": "add_author",
"key": "add_key",
} }
def __init__(self): #: 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__() super().__init__()
self.content = [] self.content = []
self.meta = [] self.meta = []
self._authors = [] self._authors = []
self._titles = [] self._titles = []
self._subtitles = [] self._subtitles = []
self._languages = set() self._keys = []
self.filename = filename
def add(self, data): 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 data is None:
# New line
if not (self.content and isinstance(self.content[0], Newline)): if not (self.content and isinstance(self.content[0], Newline)):
self.content.insert(0, Newline()) self.content.insert(0, Newline())
elif isinstance(data, Line): elif isinstance(data, Line):
# Add a new line, maybe in the current verse.
if not (self.content and isinstance(self.content[0], Verse)): if not (self.content and isinstance(self.content[0], Verse)):
self.content.insert(0, Verse()) self.content.insert(0, Verse())
self.content[0].prepend(data.strip()) self.content[0].prepend(data.strip())
elif data.inline: elif data.inline:
# Add an object in the content of the song.
self.content.insert(0, data) self.content.insert(0, data)
elif isinstance(data, Directive): 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) name = directive_name(data.keyword)
if name in self.METADATA_TYPE: if name in self.METADATA_TYPE:
getattr(self, self.METADATA_TYPE[name])(*data.as_tuple) getattr(self, self.METADATA_TYPE[name])(*data.as_tuple)
@ -155,12 +230,13 @@ class Song(AST):
return self return self
def str_meta(self): def str_meta(self):
"""Return an iterator over *all* metadata, as strings."""
for title in self.titles: for title in self.titles:
yield "{{title: {}}}".format(title) yield "{{title: {}}}".format(title)
for language in sorted(self.languages):
yield "{{language: {}}}".format(language)
for author in self.authors: for author in self.authors:
yield "{{by: {}}}".format(author) yield "{{by: {}}}".format(author)
for key in sorted(self.keys):
yield "{{key: {}}}".format(str(key))
for key in sorted(self.meta): for key in sorted(self.meta):
yield str(key) yield str(key)
@ -175,32 +251,84 @@ class Song(AST):
def add_title(self, __ignored, title): def add_title(self, __ignored, title):
"""Add a title"""
self._titles.insert(0, title) self._titles.insert(0, title)
def add_subtitle(self, __ignored, title): def add_subtitle(self, __ignored, title):
"""Add a subtitle"""
self._subtitles.insert(0, title) self._subtitles.insert(0, title)
@property @property
def titles(self): def titles(self):
"""Return the list of titles (and subtitles)."""
return self._titles + self._subtitles return self._titles + self._subtitles
def add_author(self, __ignored, title): def add_author(self, __ignored, title):
"""Add an auhor."""
self._authors.insert(0, title) self._authors.insert(0, title)
@property @property
def authors(self): def authors(self):
"""Return the list of (raw) authors."""
return self._authors return self._authors
def add_language(self, __ignored, language): def get_directive(self, key, default=None):
self._languages.add(language) """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 @property
def languages(self): def keys(self):
return self._languages """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): class Newline(AST):
"""New line"""
_template = "newline"
def __str__(self): def __str__(self):
return "" return ""
@ -208,22 +336,38 @@ class Newline(AST):
class Directive(AST): class Directive(AST):
"""A directive""" """A directive"""
def __init__(self): def __init__(self, keyword="", argument=None):
super().__init__() super().__init__()
self.keyword = "" self._keyword = None
self.argument = 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 @property
def keyword(self): def keyword(self):
"""Keyword of the directive."""
return self._keyword return self._keyword
@property @property
def inline(self): def inline(self):
"""True iff this directive is to be rendered in the flow on the song.
"""
return self.keyword in INLINE_PROPERTIES return self.keyword in INLINE_PROPERTIES
@keyword.setter @keyword.setter
def keyword(self, value): def keyword(self, value):
self._keyword = value.strip() """self.keyword setter
Replace keyword by its canonical name if it is a shortcut.
"""
self._keyword = directive_name(value.strip())
def __str__(self): def __str__(self):
if self.argument is not None: if self.argument is not None:
@ -236,6 +380,7 @@ class Directive(AST):
@property @property
def as_tuple(self): def as_tuple(self):
"""Return the directive as a tuple."""
return (self.keyword, self.argument) return (self.keyword, self.argument)
def __eq__(self, other): def __eq__(self, other):
@ -254,6 +399,7 @@ class Tab(AST):
self.content = [] self.content = []
def prepend(self, data): def prepend(self, data):
"""Add an element at the beginning of content."""
self.content.insert(0, data) self.content.insert(0, data)
return self return self

27
patacrep/songs/chordpro/data/latex/chordpro.tex

@ -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
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

1
patacrep/songs/chordpro/data/latex/content_chord.tex

@ -0,0 +1 @@
\[(( content.value ))]

1
patacrep/songs/chordpro/data/latex/content_comment.tex

@ -0,0 +1 @@
\textnote{(( content.argument ))}

3
patacrep/songs/chordpro/data/latex/content_error.tex

@ -0,0 +1,3 @@
ERROR : Template not found for \verb+(( content.__class__))+. See the logs for details.

1
patacrep/songs/chordpro/data/latex/content_guitar_comment.tex

@ -0,0 +1 @@
\musicnote{(( content.argument ))}

1
patacrep/songs/chordpro/data/latex/content_image.tex

@ -0,0 +1 @@
\image{(( content.argument ))}

1
patacrep/songs/chordpro/data/latex/content_line.tex

@ -0,0 +1 @@
(* for content in content.line *)(* include content.template("tex") *)(* endfor *)

2
patacrep/songs/chordpro/data/latex/content_newline.tex

@ -0,0 +1,2 @@

1
patacrep/songs/chordpro/data/latex/content_partition.tex

@ -0,0 +1 @@
\lilypond{((content.argument))}

1
patacrep/songs/chordpro/data/latex/content_space.tex

@ -0,0 +1 @@

7
patacrep/songs/chordpro/data/latex/content_verse.tex

@ -0,0 +1,7 @@
\begin{(( content.type ))}
(* for content in content.lines *)
(* include content.template("tex") *)
(* endfor *)
\end{(( content.type ))}

1
patacrep/songs/chordpro/data/latex/content_word.tex

@ -0,0 +1 @@
(( content.value ))

27
patacrep/songs/chordpro/lexer.py

@ -26,6 +26,7 @@ tokens = (
class ChordProLexer: class ChordProLexer:
"""ChordPro Lexer class""" """ChordPro Lexer class"""
# pylint: disable=too-many-public-methods
tokens = tokens tokens = tokens
@ -44,18 +45,23 @@ class ChordProLexer:
t_directive_KEYWORD = r'[a-zA-Z_]+' t_directive_KEYWORD = r'[a-zA-Z_]+'
t_directiveargument_TEXT = r'[^}]+' t_directiveargument_TEXT = r'[^}]+'
def t_SOC(self, token): @staticmethod
def t_SOC(token):
r'{(soc|start_of_chorus)}' r'{(soc|start_of_chorus)}'
return token return token
def t_EOC(self, token):
@staticmethod
def t_EOC(token):
r'{(eoc|end_of_chorus)}' r'{(eoc|end_of_chorus)}'
return token return token
def t_SOB(self, token): @staticmethod
def t_SOB(token):
r'{(sob|start_of_bridge)}' r'{(sob|start_of_bridge)}'
return token return token
def t_EOB(self, token): @staticmethod
def t_EOB(token):
r'{(eob|end_of_bridge)}' r'{(eob|end_of_bridge)}'
return token return token
@ -69,7 +75,8 @@ class ChordProLexer:
self.lexer.pop_state() self.lexer.pop_state()
return token return token
def t_tablature_SPACE(self, token): @staticmethod
def t_tablature_SPACE(token):
r'[ \t]+' r'[ \t]+'
return token return token
@ -96,11 +103,11 @@ class ChordProLexer:
r'[^{}\n\][\t ]+' r'[^{}\n\][\t ]+'
return token return token
def t_LBRACKET(self, token): def t_LBRACKET(self, __token):
r'\[' r'\['
self.lexer.push_state('chord') self.lexer.push_state('chord')
def t_chord_RBRACKET(self, token): def t_chord_RBRACKET(self, __token):
r'\]' r'\]'
self.lexer.pop_state() self.lexer.pop_state()
@ -149,6 +156,6 @@ class ChordProLexer:
LOGGER.error("Illegal character '{}' in directive..".format(token.value[0])) LOGGER.error("Illegal character '{}' in directive..".format(token.value[0]))
token.lexer.skip(1) token.lexer.skip(1)
@staticmethod def t_directiveargument_error(self, token):
def t_directiveargument_error(token): """Manage errors"""
return t_directive_error(token) return self.t_directive_error(token)

22
patacrep/songs/chordpro/syntax.py

@ -6,7 +6,6 @@ import ply.yacc as yacc
from patacrep.errors import SongbookError from patacrep.errors import SongbookError
from patacrep.songs.chordpro import ast from patacrep.songs.chordpro import ast
from patacrep.songs.chordpro import ast
from patacrep.songs.chordpro.lexer import tokens, ChordProLexer from patacrep.songs.chordpro.lexer import tokens, ChordProLexer
LOGGER = logging.getLogger() LOGGER = logging.getLogger()
@ -50,14 +49,13 @@ class Parser:
) )
) )
@staticmethod def p_song(self, symbols):
def p_song(symbols):
"""song : block song """song : block song
| empty | empty
""" """
#if isinstance(symbols[1], str): #if isinstance(symbols[1], str):
if len(symbols) == 2: if len(symbols) == 2:
symbols[0] = ast.Song() symbols[0] = ast.Song(self.filename)
else: else:
symbols[0] = symbols[2].add(symbols[1]) symbols[0] = symbols[2].add(symbols[1])
@ -249,19 +247,13 @@ class Parser:
"""empty :""" """empty :"""
symbols[0] = None symbols[0] = None
def lex_song(content):
# TODO delete
lex = ChordProLexer().lexer
lex.input(content)
while 1:
tok = lex.token()
if not tok: break
print(tok)
def parse_song(content, filename=None): def parse_song(content, filename=None):
"""Parse song and return its metadata.""" """Parse song and return its metadata."""
return yacc.yacc(module=Parser(filename)).parse( return yacc.yacc(
content, module=Parser(filename),
debug=0, debug=0,
write_tables=0,
).parse(
content,
lexer=ChordProLexer().lexer, lexer=ChordProLexer().lexer,
) )

1
patacrep/songs/chordpro/test/__init__.py

@ -0,0 +1 @@
"""Test for chordpro parser"""

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

@ -1,13 +1,14 @@
{title: Greensleeves} {title: Greensleeves}
{title: Un autre sous-titre} {title: Un autre sous-titre}
{title: Un sous titre} {title: Un sous titre}
{language: english}
{by: Traditionnel} {by: Traditionnel}
{album: Angleterre} {album: Angleterre}
{columns: 2} {columns: 2}
{cover: traditionnel} {cov: DIRNAME/traditionnel}
{partition: greensleeves.ly} {language: english}
======== ========
{partition: DIRNAME/greensleeves.ly}
{start_of_verse} {start_of_verse}
A[Am]las, my love, ye [G]do me wrong A[Am]las, my love, ye [G]do me wrong
To [Am]cast me oft dis[E]curteously To [Am]cast me oft dis[E]curteously

4
patacrep/songs/chordpro/test/metadata.sgc

@ -13,8 +13,8 @@
{cover: Cover} {cover: Cover}
{vcover: VCover} {vcover: VCover}
{capo: Capo} {capo: Capo}
{foo: Foo} {key: foo: Foo}
{comment: Comment} {comment: Comment}
{guitar_comment: GuitarComment} {guitar_comment: GuitarComment}
{image: Image} {image: Image}
{lilypond: Lilypond} {partition: Lilypond}

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

@ -4,18 +4,18 @@
{title: Subtitle3} {title: Subtitle3}
{title: Subtitle4} {title: Subtitle4}
{title: Subtitle5} {title: Subtitle5}
{language: english}
{language: french}
{by: Author1} {by: Author1}
{by: Author2} {by: Author2}
{key: {foo: Foo}}
{album: Albom} {album: Albom}
{capo: Capo} {capo: Capo}
{copyright: Copyright} {copyright: Copyright}
{cover: Cover} {cov: DIRNAME/Cover}
{foo: Foo} {language: english}
{vcover: VCover} {language: french}
{vcov: VCover}
======== ========
{comment: Comment} {comment: Comment}
{guitar_comment: GuitarComment} {guitar_comment: GuitarComment}
{image: Image} {image: DIRNAME/Image}
{lilypond: Lilypond} {partition: DIRNAME/Lilypond}

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

@ -1,36 +1,56 @@
"""Tests for the chordpro parser."""
# pylint: disable=too-few-public-methods
import glob import glob
import os import os
import unittest import unittest
from patacrep.songs.chordpro import syntax as chordpro from patacrep.songs.chordpro import syntax as chordpro
class ParserTestCase(unittest.TestCase): 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 test_txt(self): def load_tests(__loader, tests, __pattern):
for txt in sorted(glob.glob(os.path.join( """Load several tests given test files present in the directory."""
os.path.dirname(__file__), # Load all txt files as tests
'*.txt', for txt in sorted(glob.glob(os.path.join(
os.path.dirname(__file__),
'*.txt',
))): ))):
basename = txt[:-len('.txt')] tests.addTest(ParserTxtRenderer(basename=txt[:-len('.txt')]))
with open("{}.sgc".format(basename), 'r', encoding='utf8') as sourcefile: return tests
with open("{}.txt".format(basename), 'r', encoding='utf8') as expectfile:
#print(os.path.basename(sourcefile.name))
#with open("{}.txt.diff".format(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.basename(sourcefile.name),
)).strip(),
expectfile.read().strip(),
)
def test_tex(self):
# TODO
pass

2
patacrep/songs/latex/__init__.py

@ -14,7 +14,7 @@ from patacrep.songs import Song
class LatexSong(Song): class LatexSong(Song):
"""LaTeX song parser.""" """LaTeX song parser."""
def parse(self, content): def parse(self, __config):
"""Parse content, and return the dictinory of song data.""" """Parse content, and return the dictinory of song data."""
with encoding.open_read(self.fullpath, encoding=self.encoding) as song: with encoding.open_read(self.fullpath, encoding=self.encoding) as song:
self.data = parse_song(song.read(), self.fullpath) self.data = parse_song(song.read(), self.fullpath)

40
patacrep/templates.py

@ -64,9 +64,29 @@ def _escape_tex(value):
return newval return newval
class TexRenderer(object): class TexRenderer:
"""Render a template to a LaTeX file.""" """Render a template to a LaTeX file."""
def __init__(self, template, texenv, encoding=None):
self.encoding = encoding
self.texenv = texenv
self.texenv.block_start_string = '(*'
self.texenv.block_end_string = '*)'
self.texenv.variable_start_string = '(('
self.texenv.variable_end_string = '))'
self.texenv.comment_start_string = '(% comment %)'
self.texenv.comment_end_string = '(% endcomment %)'
self.texenv.line_comment_prefix = '%!'
self.texenv.filters['escape_tex'] = _escape_tex
self.texenv.trim_blocks = True
self.texenv.lstrip_blocks = True
self.texenv.globals["path2posix"] = files.path2posix
self.template = self.texenv.get_template(template)
class TexBookRenderer(TexRenderer):
"""Tex renderer for the whole songbook"""
def __init__(self, template, datadirs, lang, encoding=None): def __init__(self, template, datadirs, lang, encoding=None):
'''Start a new jinja2 environment for .tex creation. '''Start a new jinja2 environment for .tex creation.
@ -78,29 +98,15 @@ class TexRenderer(object):
- encoding: if set, encoding of the template. - encoding: if set, encoding of the template.
''' '''
self.lang = lang self.lang = lang
self.encoding = encoding
# Load templates in filesystem ... # Load templates in filesystem ...
loaders = [FileSystemLoader(os.path.join(datadir, 'templates')) loaders = [FileSystemLoader(os.path.join(datadir, 'templates'))
for datadir in datadirs] for datadir in datadirs]
self.texenv = Environment( texenv = Environment(
loader=ChoiceLoader(loaders), loader=ChoiceLoader(loaders),
extensions=[VariablesExtension], extensions=[VariablesExtension],
) )
self.texenv.block_start_string = '(*'
self.texenv.block_end_string = '*)'
self.texenv.variable_start_string = '(('
self.texenv.variable_end_string = '))'
self.texenv.comment_start_string = '(% comment %)'
self.texenv.comment_end_string = '(% endcomment %)'
self.texenv.line_comment_prefix = '%!'
self.texenv.filters['escape_tex'] = _escape_tex
self.texenv.trim_blocks = True
self.texenv.lstrip_blocks = True
self.texenv.globals["path2posix"] = files.path2posix
try: try:
self.template = self.texenv.get_template(template) super().__init__(template, texenv, encoding)
except TemplateNotFound as exception: except TemplateNotFound as exception:
# Only works if all loaders are FileSystemLoader(). # Only works if all loaders are FileSystemLoader().
paths = [ paths = [

26
patacrep/test.py

@ -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())

3
setup.py

@ -38,6 +38,7 @@ setup(
"Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.4",
"Topic :: Utilities", "Topic :: Utilities",
], ],
platforms=["GNU/Linux", "Windows", "MacOsX"] platforms=["GNU/Linux", "Windows", "MacOsX"],
test_suite="patacrep.test.suite",
long_description = open("README.rst", "r").read(), long_description = open("README.rst", "r").read(),
) )

Loading…
Cancel
Save