Browse Source

Merge branch 'master' into test_compilation

pull/120/head
Louis 9 years ago
parent
commit
c99469d557
  1. 3
      examples/example-all.sb
  2. 2
      examples/songs/greensleeves.sgc
  3. 2
      examples/songs/tests/chords.sgc
  4. 2
      examples/songs/tests/errors.sgc
  5. 6
      patacrep/build.py
  6. 8
      patacrep/content/song.py
  7. 2
      patacrep/data/templates/patacrep.tex
  8. 12
      patacrep/data/templates/songs.tex
  9. 14
      patacrep/files.py
  10. 23
      patacrep/latex/__init__.py
  11. 6
      patacrep/latex/ast.py
  12. 2
      patacrep/songbook/__main__.py
  13. 66
      patacrep/songs/__init__.py
  14. 120
      patacrep/songs/chordpro/__init__.py
  15. 4
      patacrep/songs/chordpro/ast.py
  16. 4
      patacrep/songs/chordpro/data/chordpro/song_header
  17. 5
      patacrep/songs/chordpro/data/html/content_image
  18. 9
      patacrep/songs/chordpro/data/html/content_metadata_cover
  19. 7
      patacrep/songs/chordpro/data/html/content_partition
  20. 8
      patacrep/songs/chordpro/data/html/content_verse
  21. 4
      patacrep/songs/chordpro/data/html/song_header
  22. 7
      patacrep/songs/chordpro/data/latex/content_image
  23. 5
      patacrep/songs/chordpro/data/latex/content_partition
  24. 11
      patacrep/songs/chordpro/data/latex/song
  25. 19
      patacrep/songs/convert/__main__.py
  26. 39
      patacrep/songs/latex/__init__.py
  27. 4
      patacrep/templates.py
  28. 61
      patacrep/utils.py
  29. 2
      test/test_chordpro/00.sgc
  30. 2
      test/test_chordpro/01.sgc
  31. 2
      test/test_chordpro/02.sgc
  32. 2
      test/test_chordpro/03.sgc
  33. 2
      test/test_chordpro/04.sgc
  34. 2
      test/test_chordpro/05.sgc
  35. 2
      test/test_chordpro/06.sgc
  36. 2
      test/test_chordpro/07.sgc
  37. 2
      test/test_chordpro/08.sgc
  38. 2
      test/test_chordpro/09.sgc
  39. 2
      test/test_chordpro/10.sgc
  40. 2
      test/test_chordpro/11.sgc
  41. 2
      test/test_chordpro/12.sgc
  42. 2
      test/test_chordpro/13.sgc
  43. 2
      test/test_chordpro/21.sgc
  44. 2
      test/test_chordpro/22.sgc
  45. 2
      test/test_chordpro/23.sgc
  46. 2
      test/test_chordpro/24.sgc
  47. 2
      test/test_chordpro/25.sgc
  48. 2
      test/test_chordpro/26.sgc
  49. 2
      test/test_chordpro/27.sgc
  50. 2
      test/test_chordpro/28.sgc
  51. 2
      test/test_chordpro/29.sgc
  52. 2
      test/test_chordpro/author_names.sgc
  53. 2
      test/test_chordpro/chords.sgc
  54. 2
      test/test_chordpro/customchords.sgc
  55. 0
      test/test_chordpro/datadir/img/traditionnel.png
  56. 0
      test/test_chordpro/datadir/scores/greensleeves.ly
  57. 2
      test/test_chordpro/greensleeves.sgc
  58. 2
      test/test_chordpro/greensleeves.source
  59. 4
      test/test_chordpro/greensleeves.tex
  60. 2
      test/test_chordpro/invalid_chord.sgc
  61. 2
      test/test_chordpro/invalid_customchord.sgc
  62. 1
      test/test_chordpro/lang.sgc
  63. 1
      test/test_chordpro/lang.source
  64. 12
      test/test_chordpro/metadata.sgc
  65. 14
      test/test_chordpro/metadata.source
  66. 10
      test/test_chordpro/metadata.tex
  67. 0
      test/test_chordpro/metadata_cover.png
  68. 0
      test/test_chordpro/metadata_image.png
  69. 0
      test/test_chordpro/metadata_lilypond.ly
  70. 55
      test/test_chordpro/newline.html
  71. 2
      test/test_chordpro/newline.sgc
  72. 2
      test/test_chordpro/nolyrics.sgc
  73. 22
      test/test_chordpro/test_parser.py
  74. 2
      test/test_chordpro/ukulelechords.sgc
  75. 3
      test/test_compilation/subdir.sb
  76. 15
      test/test_compilation/subdir.tex.control
  77. 0
      test/test_compilation/subdir_datadir/scores/datadir.ly
  78. 6
      test/test_compilation/subdir_datadir/songs/datadir.sg
  79. 6
      test/test_compilation/subdir_datadir/songs/datadir2.sg
  80. 0
      test/test_compilation/subdir_datadir2/scores/datadir2.ly
  81. 10
      test/test_compilation/test_compilation.py

3
examples/example-all.sb

@ -6,8 +6,9 @@
"pictures"
],
"booktype" : "chorded",
"datadir": ["datadir2"],
"template" : "patacrep.tex",
"lang" : "french",
"lang" : "fr",
"encoding": "utf8",
"authwords" : {
"sep" : ["and", "et"]

2
examples/songs/greensleeves.sgc

@ -1,4 +1,4 @@
{language : english}
{lang : en}
{columns : 2}
{ title : Greensleeves}
{subtitle: Test of the chordpro format}

2
examples/songs/tests/chords.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{columns: 1}
{title: Chords testing}
{subtitle: Test of the chords specification and LaTeX translation}

2
examples/songs/tests/errors.sgc

@ -1,4 +1,4 @@
{language : english}
{lang : en}
{columns : 2}
{ title : Error}
{subtitle: A chordpro file with many errors}

6
patacrep/build.py

@ -29,7 +29,7 @@ GENERATED_EXTENSIONS = [
]
DEFAULT_CONFIG = {
'template': "default.tex",
'lang': 'english',
'lang': 'en',
'content': [],
'titleprefixwords': [],
'encoding': None,
@ -110,8 +110,8 @@ class Songbook(object):
config['_song_plugins'] = files.load_plugins(
datadirs=config.get('datadir', []),
root_modules=['songs'],
keyword='SONG_PARSERS',
)
keyword='SONG_RENDERERS',
)['latex']
# Configuration set
config['render'] = content.render

8
patacrep/content/song.py

@ -43,7 +43,7 @@ class SongRenderer(Content):
""").format(
separator="%"*80,
path=self.song.subpath,
song=self.song.render(output=context['filename'], output_format="latex"),
song=self.song.render(output=context['filename']),
)
#pylint: disable=unused-argument
@ -60,8 +60,8 @@ def parse(keyword, argument, contentlist, config):
Return a list of Song() instances.
"""
plugins = config['_song_plugins']
if '_languages' not in config:
config['_languages'] = set()
if '_langs' not in config:
config['_langs'] = set()
songlist = []
for songdir in config['_songdir']:
if contentlist:
@ -92,7 +92,7 @@ def parse(keyword, argument, contentlist, config):
datadir=songdir.datadir,
))
songlist.append(renderer)
config["_languages"].update(renderer.song.languages)
config["_langs"].add(renderer.song.lang)
if len(songlist) > before:
break
if len(songlist) == before:

2
patacrep/data/templates/patacrep.tex

@ -34,7 +34,7 @@
},
"picture": {"description": {"english": "Cover picture", "french": "Image de couverture"},
"type": "file",
"default": {"default": "treble_a"}
"default": {"default": "img/treble_a"}
},
"picturecopyright": {"description": {"english": "Copyright for the cover picture", "french": "Copyright pour l'image de couverture"},
"default": {"default": "Dbolton \\url{http://commons.wikimedia.org/wiki/User:Dbolton}"}

12
patacrep/data/templates/songs.tex

@ -52,7 +52,7 @@
"mandatory": true
},
"lang": {"description": {"english": "Language", "french": "Langue"},
"default": {"english": "english", "french": "french"}
"default": {"english": "en", "french": "fr"}
},
"titleprefixwords": {"description": {"english": "Ignore some words in the beginning of song titles",
"french": "Ignore des mots dans le classement des chansons"},
@ -80,16 +80,16 @@
(* block songbookpreambule *)
(( super() ))
(* for lang in _languages -*)
\PassOptionsToPackage{((lang))}{babel}
(* for lang in _langs -*)
\PassOptionsToPackage{(( lang2babel(lang) ))}{babel}
(* endfor *)
\usepackage[((lang))]{babel}
\lang{((lang))}
\usepackage[(( lang2babel(lang) ))]{babel}
\lang{(( lang2babel(lang) ))}
\usepackage{graphicx}
\graphicspath{ %
(* for dir in datadir *)
{(( path2posix(dir) ))/img/} %
{(( path2posix(dir) ))/} %
(* endfor *)
}

14
patacrep/files.py

@ -8,6 +8,8 @@ import posixpath
import re
import sys
from patacrep import utils
LOGGER = logging.getLogger(__name__)
def recursive_find(root_directory, extensions):
@ -104,7 +106,7 @@ def load_plugins(datadirs, root_modules, keyword):
- keys are the keywords ;
- values are functions triggered when this keyword is met.
"""
plugins = {}
plugins = utils.DictOfDict()
datadir_path = [
os.path.join(datadir, "python", *root_modules)
@ -121,13 +123,5 @@ def load_plugins(datadirs, root_modules, keyword):
prefix="patacrep.{}.".format(".".join(root_modules))
):
if hasattr(module, keyword):
for (key, value) in getattr(module, keyword).items():
if key in plugins:
LOGGER.warning(
"File %s: Keyword '%s' is already used. Ignored.",
module.__file__,
key,
)
continue
plugins[key] = value
plugins.update(getattr(module, keyword))
return plugins

23
patacrep/latex/__init__.py

@ -5,4 +5,27 @@ This module uses an LALR parser to try to parse LaTeX code. LaTeX language
will work on simple cases, but not on complex ones.
"""
import logging
from collections import OrderedDict
from patacrep.latex.syntax import tex2plain, parse_song
LOGGER = logging.getLogger(__name__)
BABEL_LANGUAGES = OrderedDict((
('fr', 'french'),
('en', 'english'),
('de', 'german'),
('es', 'spanish'),
('it', 'italian'),
('pt', 'portuguese'),
))
def lang2babel(lang):
"""Return the language used by babel, corresponding to the language code"""
try:
return BABEL_LANGUAGES[lang]
except KeyError:
available = ", ".join(BABEL_LANGUAGES.keys())
LOGGER.error('Unknown lang code: ' + lang + '. Supported: ' + available)
return 'english'

6
patacrep/latex/ast.py

@ -2,6 +2,8 @@
# pylint: disable=too-few-public-methods
DEFAULT_LANGUAGE = "english"
class AST:
"""Base class for the tree."""
# pylint: disable=no-init
@ -16,7 +18,7 @@ class AST:
parsing.
"""
cls.metadata = {
'@languages': set(),
'@language': DEFAULT_LANGUAGE,
}
class Expression(AST):
@ -44,7 +46,7 @@ class Command(AST):
self.optional = optional
if name == r'\selectlanguage':
self.metadata['@languages'] |= set(self.mandatory)
self.metadata['@language'] = self.mandatory[0]
def __str__(self):
if self.name in [r'\emph']:

2
patacrep/songbook/__main__.py

@ -108,6 +108,8 @@ def main():
options = argument_parser(sys.argv[1:])
songbook_path = options.book[0]
if os.path.exists(songbook_path + ".sb") and not os.path.exists(songbook_path):
songbook_path += ".sb"
basename = os.path.basename(songbook_path)[:-3]

66
patacrep/songs/__init__.py

@ -89,7 +89,7 @@ class Song:
"cached",
"data",
"subpath",
"languages",
"lang",
"authors",
"_filehash",
"_version",
@ -131,6 +131,7 @@ class Song:
self.titles = []
self.data = {}
self.cached = None
self.lang = None
self._parse(config)
# Post processing of data
@ -167,16 +168,12 @@ class Song:
def __repr__(self):
return repr((self.titles, self.data, self.fullpath))
def render(self, output_format, output=None, *args, **kwargs):
def render(self, output=None, *args, **kwargs):
"""Return the code rendering this song.
Arguments:
- output_format: Format of the output file (latex, chordpro...)
- output: Name of the output file, or `None` if irrelevant.
"""
method = "render_{}".format(output_format)
if hasattr(self, method):
return getattr(self, method)(output, *args, **kwargs)
raise NotImplementedError()
def _parse(self, config): # pylint: disable=no-self-use
@ -186,8 +183,7 @@ class Song:
- 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.
- lang: the main language of the song, as language code..
- 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.
@ -204,15 +200,24 @@ class Song:
if os.path.isdir(fullpath):
yield fullpath
def search_file(self, filename, extensions=None, directories=None):
def search_datadir_file(self, filename, extensions=None, directories=None):
"""Search for a file name.
:param str filename: The name, as provided in the chordpro file (with or without extension).
:param list extensions: Possible extensions (with '.'). Default is no extension.
:param iterator directories: Other directories where to search for the file
The directory where the Song file is stored is added to the list.
Returns None if nothing found.
:return: A tuple `(datadir, filename, extension)` if file has been
found. It is guaranteed that `os.path.join(datadir,
filename+extension)` is a (relative or absolute) valid path to an
existing filename.
* `datadir` is the datadir in which the file has been found. Can be
the empty string.
* `filename` is the filename, relative to the datadir.
* `extension` is the extension that is to be appended to the
filename to get the real filename. Can be the empty string.
Raise `FileNotFoundError` if nothing found.
This function can also be used as a preprocessor for a renderer: for
instance, it can compile a file, place it in a temporary folder, and
@ -224,27 +229,42 @@ class Song:
directories = self.config['datadir']
songdir = os.path.dirname(self.fullpath)
for extension in extensions:
if os.path.isfile(os.path.join(songdir, filename + extension)):
return "", os.path.join(songdir, filename), extension
for directory in [songdir] + list(directories):
for directory in directories:
for extension in extensions:
fullpath = os.path.join(directory, filename + extension)
if os.path.isfile(fullpath):
return os.path.abspath(fullpath)
return None
if os.path.isfile(os.path.join(directory, filename + extension)):
return directory, filename, extension
raise FileNotFoundError(filename)
def search_file(self, filename, extensions=None, *, datadirs=None):
"""Return the path to a file present in a datadir.
Implementation is specific to each renderer, as:
- some renderers can preprocess files;
- some renderers can return the absolute path, other can return something else;
- etc.
"""
raise NotImplementedError()
def search_image(self, filename, none_if_not_found=False):
def search_image(self, filename):
"""Search for an image file"""
filepath = self.search_file(
return self.search_file(
filename,
['', '.jpg', '.png'],
self.get_datadirs('img'),
datadirs=self.get_datadirs('img'),
)
return filepath if none_if_not_found or filepath else filename
def search_partition(self, filename, none_if_not_found=False):
def search_partition(self, filename):
"""Search for a lilypond file"""
filepath = self.search_file(filename, ['', '.ly'])
return filepath if none_if_not_found or filepath else filename
return self.search_file(
filename,
['', '.ly'],
datadirs=self.get_datadirs('scores'),
)
def unprefixed_title(title, prefixes):
"""Remove the first prefix of the list in the beginning of title (if any).

120
patacrep/songs/chordpro/__init__.py

@ -1,33 +1,24 @@
"""Chordpro parser"""
from jinja2 import Environment, FileSystemLoader, contextfunction
from jinja2 import Environment, FileSystemLoader, contextfunction, ChoiceLoader
import jinja2
import logging
import os
import pkg_resources
from pkg_resources import resource_filename
from patacrep import encoding, files
from patacrep.songs import Song
from patacrep.songs.chordpro.syntax import parse_song
from patacrep.templates import Renderer
from patacrep.latex import lang2babel
LOGGER = logging.getLogger(__name__)
class ChordproSong(Song):
"""Chordpros song parser."""
"""Chordpro song parser"""
# pylint: disable=abstract-method
@staticmethod
def iter_template_paths(templatedirs, output_format):
"""Iterate over paths in which templates are to be searched.
:param iterator templatedirs: Iterators of additional directories (the
default hard-coded template directory is returned last).
:param str output_format: Song output format, which is appended to
each directory.
"""
for directory in templatedirs:
yield os.path.join(directory, output_format)
yield os.path.join(
os.path.abspath(pkg_resources.resource_filename(__name__, 'data')),
output_format,
)
output_language = None
def _parse(self, config):
"""Parse content, and return the dictionary of song data."""
@ -35,18 +26,15 @@ class ChordproSong(Song):
song = parse_song(song.read(), self.fullpath)
self.authors = song.authors
self.titles = song.titles
self.languages = song.get_data_argument('language', [self.config['lang']])
self.lang = song.get_data_argument('lang', self.config['lang'])
self.data = song.meta
self.cached = {
'song': song,
}
def render(self, output_format, output=None, template="song", templatedirs=None): # pylint: disable=arguments-differ
if templatedirs is None:
templatedirs = []
def render(self, output=None, template="song"): # pylint: disable=arguments-differ
context = {
'language': self.languages[0],
'lang': self.lang,
"titles": self.titles,
"authors": self.authors,
"metadata": self.data,
@ -55,11 +43,17 @@ class ChordproSong(Song):
"content": self.cached['song'].content,
}
jinjaenv = Environment(loader=FileSystemLoader(
self.iter_template_paths(templatedirs, output_format)
))
jinjaenv = Environment(loader=ChoiceLoader([
FileSystemLoader(
self.get_datadirs(os.path.join("templates", self.output_language))
),
FileSystemLoader(
os.path.join(resource_filename(__name__, 'data'), self.output_language)
),
]))
jinjaenv.filters['search_image'] = self.search_image
jinjaenv.filters['search_partition'] = self.search_partition
jinjaenv.filters['lang2babel'] = lang2babel
try:
return Renderer(
@ -68,7 +62,7 @@ class ChordproSong(Song):
jinjaenv=jinjaenv,
).template.render(context)
except jinja2.exceptions.TemplateNotFound:
raise NotImplementedError("Cannot convert to format '{}'.".format(output_format))
raise NotImplementedError("Cannot convert to format '{}'.".format(self.output_language))
@staticmethod
@contextfunction
@ -77,6 +71,72 @@ class ChordproSong(Song):
context.vars['content'] = content
return context.environment.get_template(content.template()).render(context)
SONG_PARSERS = {
'sgc': ChordproSong,
class Chordpro2HtmlSong(ChordproSong):
"""Render chordpro song to html code"""
output_language = "html"
def search_file(self, filename, extensions=None, *, datadirs=None):
try:
datadir, filename, extension = self.search_datadir_file(filename, extensions, datadirs)
return os.path.join(datadir, filename + extension)
except FileNotFoundError:
LOGGER.warning(
"Song '%s' (datadir '%s'): File '%s' not found.",
self.subpath, self.datadir, filename,
)
return None
class Chordpro2LatexSong(ChordproSong):
"""Render chordpro song to latex code"""
output_language = "latex"
def search_file(self, filename, extensions=None, *, datadirs=None):
_datadir, filename, _extension = self.search_datadir_file(
filename,
extensions,
datadirs,
)
return filename
def search_partition(self, filename):
try:
return os.path.join("scores", super().search_partition(filename))
except FileNotFoundError:
LOGGER.warning(
"Song '%s' (datadir '%s'): Score '%s' not found.",
self.subpath, self.datadir, filename,
)
return None
def search_image(self, filename):
try:
return os.path.join("img", super().search_image(filename))
except FileNotFoundError:
LOGGER.warning(
"Song '%s' (datadir '%s'): Image '%s' not found.",
self.subpath, self.datadir, filename,
)
return None
class Chordpro2ChordproSong(ChordproSong):
"""Render chordpro song to chordpro code"""
output_language = "chordpro"
def search_file(self, filename, extensions=None, *, datadirs=None):
# pylint: disable=unused-variable
return filename
SONG_RENDERERS = {
"latex": {
'sgc': Chordpro2LatexSong,
},
"html": {
'sgc': Chordpro2HtmlSong,
},
"chordpro": {
'sgc': Chordpro2ChordproSong,
},
}

4
patacrep/songs/chordpro/ast.py

@ -31,6 +31,7 @@ DIRECTIVE_SHORTCUTS = {
"c": "comment",
"gc": "guitar_comment",
"cover": "cov",
"language": "lang",
}
def directive_name(text):
@ -181,7 +182,7 @@ class Song(AST):
- 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
- lang: The language code (if set), None otherwise
- authors: The list of authors
- meta: Every other metadata.
"""
@ -193,7 +194,6 @@ class Song(AST):
"artist": "add_author",
"key": "add_key",
"define": "add_cumulative",
"language": "add_cumulative",
}
def __init__(self, filename):

4
patacrep/songs/chordpro/data/chordpro/song_header

@ -1,5 +1,5 @@
(* if language is defined -*)
{language: (( language ))}
(* if lang is defined -*)
{lang: (( lang ))}
(* endif *)
(* if metadata.columns is defined -*)
{columns: (( metadata.columns ))}

5
patacrep/songs/chordpro/data/html/content_image

@ -1 +1,6 @@
(* block image *)
(* set image = content.argument|search_image *)
(* if image *)
<img src="(( content.argument|search_image ))">
(* endif *)
(* endblock *)

9
patacrep/songs/chordpro/data/html/content_metadata_cover

@ -1,3 +1,8 @@
(* block cov *)
(* if 'cov' in metadata -*)
<img src="(( metadata['cov'].argument|search_image ))"><br>
(* endif *)
(* set cov = metadata['cov'].argument|search_image *)
(* if cov *)
<img src="(( cov ))"><br>
(* endif *)
(* endif *)
(* endblock *)

7
patacrep/songs/chordpro/data/html/content_partition

@ -1 +1,6 @@
<a class="song-partition" href="(( content.argument|search_partition ))">((content.argument))</a>
(* block partition *)
(* set partition = content.argument|search_partition *)
(* if partition *)
<a class="song-partition" href="(( partition ))">((content.argument))</a>
(* endif *)
(* endblock *)

8
patacrep/songs/chordpro/data/html/content_verse

@ -1,5 +1,11 @@
(* if content.directive() *)
<p class="directives">
(*- elif content.nolyrics -*)
<p class="nolyrics">
(*- else *)
<p class="(( content.type ))">
(* for line in content.lines *)
(*- endif *)
(* for line in content.lines -*)
(( render(line) ))
(*- if not loop.last *)<br>
(* endif *)

4
patacrep/songs/chordpro/data/html/song_header

@ -17,8 +17,8 @@
(* endif *)
(* endfor *)
(* if language is defined -*)
<span class="song-language">Language: (( language ))</span><br>
(* if lang is defined -*)
<span class="song-language">Lang: (( lang ))</span><br>
(* endif *)
(* include 'content_metadata_cover' *)

7
patacrep/songs/chordpro/data/latex/content_image

@ -1 +1,6 @@
\image{(( content.argument|search_image ))}
(* block image *)
(* set image = content.argument|search_image *)
(* if image *)
\image{(( image ))}
(*- endif *)
(*- endblock *)

5
patacrep/songs/chordpro/data/latex/content_partition

@ -1 +1,6 @@
(* block partition *)
(* set partition = content.argument|search_partition *)
(* if partition *)
\lilypond{ ((- content.argument|search_partition -)) }
(*- endif -*)
(*- endblock -*)

11
patacrep/songs/chordpro/data/latex/song

@ -1,5 +1,5 @@
(* if language is defined -*)
\selectlanguage{((language))}
(* if lang is defined -*)
\selectlanguage{(( lang2babel(lang) ))}
(* endif *)
(*- if metadata.columns is defined *)
@ -28,7 +28,12 @@
(* endif *)
(* endfor *)
(* if 'cov' in metadata *)
cov={(( metadata["cov"].argument|search_image ))},
(* block cov *)
(* set cov = metadata["cov"].argument|search_image *)
(* if cov *)
cov={(( cov ))},
(* endif *)
(* endblock *)
(* endif *)
(* for key in metadata.morekeys *)
(( key.keyword ))={(( key.argument ))},

19
patacrep/songs/convert/__main__.py

@ -36,22 +36,29 @@ if __name__ == "__main__":
dest = sys.argv[2]
song_files = sys.argv[3:]
song_parsers = files.load_plugins(
renderers = files.load_plugins(
datadirs=DEFAULT_CONFIG.get('datadir', []),
root_modules=['songs'],
keyword='SONG_PARSERS',
keyword='SONG_RENDERERS',
)
if source not in song_parsers:
if dest not in renderers:
LOGGER.error(
"Unknown file format '%s'. Available ones are %s.",
"Unknown destination file format '%s'. Available ones are %s.",
source,
", ".join(["'{}'".format(key) for key in song_parsers.keys()])
", ".join(["'{}'".format(key) for key in renderers.keys()])
)
sys.exit(1)
if source not in renderers[dest]:
LOGGER.error(
"Unknown source file format '%s'. Available ones are %s.",
source,
", ".join(["'{}'".format(key) for key in renderers[dest].keys()])
)
sys.exit(1)
for file in song_files:
song = song_parsers[source](file, DEFAULT_CONFIG)
song = renderers[dest][source](file, DEFAULT_CONFIG)
try:
destname = "{}.{}".format(".".join(file.split(".")[:-1]), dest)
if os.path.exists(destname):

39
patacrep/songs/latex/__init__.py

@ -8,28 +8,30 @@ 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.latex import parse_song, BABEL_LANGUAGES
from patacrep.songs import Song
class LatexSong(Song):
"""LaTeX song parser."""
class Latex2LatexSong(Song):
"""Song written in LaTeX, rendered in LaTeX"""
# pylint: disable=abstract-method
def _parse(self, __config):
"""Parse content, and return the dictinory of song data."""
"""Parse content, and return the dictionary 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.set_lang(self.data['@language'])
del self.data['@language']
if "by" in self.data:
self.authors = [self.data['by']]
del self.data['by']
else:
self.authors = []
def render_latex(self, output):
def render(self, output):
"""Return the code rendering the song."""
# pylint: disable=signature-differs
if output is None:
raise ValueError(output)
path = files.path2posix(files.relpath(
@ -38,7 +40,22 @@ class LatexSong(Song):
))
return r'\import{{{}/}}{{{}}}'.format(os.path.dirname(path), os.path.basename(path))
SONG_PARSERS = {
'is': LatexSong,
'sg': LatexSong,
}
def set_lang(self, language):
"""Set the language code"""
for lang, babel_language in BABEL_LANGUAGES.items():
if language == babel_language:
self.lang = lang
return
# Add a custom language to the babel dictionary (language is not officially supported)
custom_lang = '_' + language
BABEL_LANGUAGES[custom_lang] = language
self.lang = custom_lang
SONG_RENDERERS = {
"latex": {
'is': Latex2LatexSong,
'sg': Latex2LatexSong,
},
}

4
patacrep/templates.py

@ -9,6 +9,7 @@ import re
import json
from patacrep import errors, files
from patacrep.latex import lang2babel
import patacrep.encoding
_LATEX_SUBS = (
@ -84,6 +85,7 @@ class Renderer:
self.jinjaenv.trim_blocks = True
self.jinjaenv.lstrip_blocks = True
self.jinjaenv.globals["path2posix"] = files.path2posix
self.jinjaenv.globals["lang2babel"] = lang2babel
self.template = self.jinjaenv.get_template(template)
@ -153,7 +155,7 @@ class TexBookRenderer(Renderer):
variable = default["default"]
elif "en" in default:
variable = default["en"]
elif len(default > 0):
elif len(default):
variable = default.popitem()[1]
else:
variable = None

61
patacrep/utils.py

@ -0,0 +1,61 @@
"""Some utility functions"""
from collections import UserDict
class DictOfDict(UserDict):
"""Dictionary, with a recursive :meth:`update` method.
By "recursive", we mean: if `self.update(other)` is called, and for some
key both `self[key]` and `other[key]` are dictionary, then `self[key]` is
not replaced by `other[key]`, but instead is updated. This is done
recursively (that is, `self[foo][bar][baz]` is updated with
`other[foo][bar][baz]`, if the corresponding objects are dictionaries).
>>> ordinal = DictOfDict({
... "francais": {
... 1: "premier",
... 2: "deuxieme",
... },
... "english": {
... 1: "first",
... },
... })
>>> ordinal.update({
... "francais": {
... 2: "second",
... 3: "troisieme",
... },
... "espanol": {
... 1: "primero",
... },
... })
>>> ordinal == {
... "francais": {
... 1: "premier",
... 2: "second",
... 3: "troisieme",
... },
... "english": {
... 1: "first",
... },
... "espanol": {
... 1: "primero",
... },
... }
True
"""
def update(self, other):
# pylint: disable=arguments-differ
self._update(self, other)
@staticmethod
def _update(left, right):
"""Equivalent to `left.update(right)`, with recursive update."""
for key in right:
if key not in left:
left[key] = right[key]
elif isinstance(left[key], dict) and isinstance(right[key], dict):
DictOfDict._update(left[key], right[key])
else:
left[key] = right[key]

2
test/test_chordpro/00.sgc

@ -1 +1 @@
{language: english}
{lang: en}

2
test/test_chordpro/01.sgc

@ -1,3 +1,3 @@
{language: english}
{lang: en}
A verse line

2
test/test_chordpro/02.sgc

@ -1,3 +1,3 @@
{language: english}
{lang: en}
{title: A directive}

2
test/test_chordpro/03.sgc

@ -1,2 +1,2 @@
{language: english}
{lang: en}

2
test/test_chordpro/04.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{start_of_chorus}
A one line chorus

2
test/test_chordpro/05.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{start_of_bridge}
A one line bridge

2
test/test_chordpro/06.sgc

@ -1,2 +1,2 @@
{language: english}
{lang: en}

2
test/test_chordpro/07.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{start_of_tab}
A tab

2
test/test_chordpro/08.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
A lot of new lines

2
test/test_chordpro/09.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{title: and a directive}

2
test/test_chordpro/10.sgc

@ -1,3 +1,3 @@
{language: english}
{lang: en}
A line[A] with a chord

2
test/test_chordpro/11.sgc

@ -1,3 +1,3 @@
{language: english}
{lang: en}
A line ending with a chord[A]

2
test/test_chordpro/12.sgc

@ -1,3 +1,3 @@
{language: english}
{lang: en}
[A]A line starting with a chord

2
test/test_chordpro/13.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{start_of_tab}
A table

2
test/test_chordpro/21.sgc

@ -1,3 +1,3 @@
{language: english}
{lang: en}
A verse line

2
test/test_chordpro/22.sgc

@ -1,3 +1,3 @@
{language: english}
{lang: en}
{title: A directive}

2
test/test_chordpro/23.sgc

@ -1 +1 @@
{language: english}
{lang: en}

2
test/test_chordpro/24.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{start_of_chorus}
A one line chorus

2
test/test_chordpro/25.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{start_of_bridge}
A one line bridge

2
test/test_chordpro/26.sgc

@ -1,2 +1,2 @@
{language: english}
{lang: en}

2
test/test_chordpro/27.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{start_of_tab}
A tab

2
test/test_chordpro/28.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
A lot of new lines

2
test/test_chordpro/29.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{title: and a directive}

2
test/test_chordpro/author_names.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{title: Title}
{artist: The Beatles}
{artist: Oasis}

2
test/test_chordpro/chords.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
[A]Simple
[Bb]Bémol

2
test/test_chordpro/customchords.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{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 - -}
{define: E5/A* base-fret 7 frets 0 1 3 3 x x fingers - 1 2 3 - -}

0
test/test_chordpro/datadir/img/traditionnel.png

0
test/test_chordpro/datadir/scores/greensleeves.ly

2
test/test_chordpro/greensleeves.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{columns: 2}
{title: Greensleeves}
{title: Un autre sous-titre}

2
test/test_chordpro/greensleeves.source

@ -1,4 +1,4 @@
{language : english}
{lang : en}
{columns : 2}
{subtitle : Un sous titre}
{ title : Greensleeves}

4
test/test_chordpro/greensleeves.tex

@ -7,14 +7,14 @@ Un sous titre}[
by={
Traditionnel },
album={Angleterre},
cov={traditionnel},
cov={img/traditionnel},
]
\cover
\lilypond{greensleeves.ly}
\lilypond{scores/greensleeves.ly}

2
test/test_chordpro/invalid_chord.sgc

@ -1,3 +1,3 @@
{language: english}
{lang: en}
This is invalid.

2
test/test_chordpro/invalid_customchord.sgc

@ -1,2 +1,2 @@
{language: english}
{lang: en}

1
test/test_chordpro/lang.sgc

@ -0,0 +1 @@
{lang: fr}

1
test/test_chordpro/lang.source

@ -0,0 +1 @@
{language: fr}

12
test/test_chordpro/metadata.sgc

@ -1,22 +1,22 @@
{language: french}
{lang: fr}
{capo: Capo}
{title: Title}
{title: Je t'ai Manqué}
{title: Subtitle1}
{title: Subtitle2}
{title: Subtitle3}
{title: Subtitle4}
{title: Subtitle5}
{artist: Author1}
{artist: Author2}
{artist: Texte de Jean Richepin, chanté par Georges Brassens}
{album: Album}
{copyright: Copyright}
{cov: Cover}
{cov: metadata_cover}
{key: foo: Foo}
{comment: Comment}
{guitar_comment: GuitarComment}
{partition: Lilypond}
{image: Image}
{partition: metadata_lilypond}
{image: metadata_image}
Foo

14
test/test_chordpro/metadata.source

@ -1,21 +1,21 @@
{subtitle: Subtitle3}
{title: Title}
{title: Je t'ai Manqué}
{title: Subtitle1}
{subtitle: Subtitle4}
{t: Subtitle2}
{st: Subtitle5}
{language: french}
{language: english}
{lang: en}
{lang: fr}
{by: Author1}
{artist: Author2}
{artist: Texte de Jean Richepin, chanté par Georges Brassens}
{album: Album}
{copyright: Copyright}
{cover: Cover}
{cover: metadata_cover}
{capo: Capo}
{key: foo: Foo}
{comment: Comment}
{guitar_comment: GuitarComment}
{partition: Lilypond}
{image: Image}
{partition: metadata_lilypond}
{image: metadata_image}
Foo

10
test/test_chordpro/metadata.tex

@ -1,6 +1,6 @@
\selectlanguage{french}
\beginsong{Title\\
\beginsong{Je t'ai Manqué\\
Subtitle1\\
Subtitle2\\
Subtitle3\\
@ -8,10 +8,10 @@ Subtitle4\\
Subtitle5}[
by={
Author1,
Author2 },
Texte de Jean Richepin, chanté par Georges Brassens },
album={Album},
copyright={Copyright},
cov={Cover},
cov={img/test/test_chordpro/metadata_cover},
foo={Foo},
]
@ -19,8 +19,8 @@ Subtitle5}[
\textnote{Comment}
\musicnote{GuitarComment}
\lilypond{Lilypond}
\image{Image}
\lilypond{scores/test/test_chordpro/metadata_lilypond}
\image{img/test/test_chordpro/metadata_image}

0
test/test_chordpro/metadata_cover.png

0
test/test_chordpro/metadata_image.png

0
test/test_chordpro/metadata_lilypond.ly

55
test/test_chordpro/newline.html

@ -1,50 +1,43 @@
<span class="song-language">Language: english</span><br>
<span class="song-language">Lang: en</span><br>
<div class="song_content">
<p class="verse">
This is a verse<br>
With a new line<br>
<br>
The second part of the verse<br>
Is this line
<p class="verse">This is a verse<br>
With a new line<br>
<br>
The second part of the verse<br>
Is this line
</p>
<p class="verse">
Here is a new line at the end<br>
<p class="verse">Here is a new line at the end<br>
</p>
<p class="verse">
Foo bar
<p class="verse">Foo bar
</p>
<p class="verse">
<br>
And a new line<br>
At the beginning
<p class="verse"><br>
And a new line<br>
At the beginning
</p>
<p class="chorus">
New lines can also<br>
<br>
Be in chorus
<p class="chorus">New lines can also<br>
<br>
Be in chorus
</p>
<p class="bridge">
New lines can also<br>
<br>
Be in bridges
<p class="bridge">New lines can also<br>
<br>
Be in bridges
</p>
<p class="verse">
New lines can also<br>
<br>
Be surrounded by spaces
<p class="verse">New lines can also<br>
<br>
Be surrounded by spaces
</p>
<p class="verse">
New lines cannot
<p class="verse">New lines cannot
</p>
</div>
</div>

2
test/test_chordpro/newline.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
This is a verse
With a new line

2
test/test_chordpro/nolyrics.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
A chorus [A]with lyrics
[Emaj3]maj et nombre

22
test/test_chordpro/test_parser.py

@ -5,9 +5,11 @@
import glob
import os
import unittest
from pkg_resources import resource_filename
from patacrep import files
from patacrep.build import DEFAULT_CONFIG
from patacrep.songs.chordpro import ChordproSong
from patacrep.encoding import open_read
from .. import disable_logging
from .. import dynamic # pylint: disable=unused-import
@ -34,11 +36,22 @@ class FileTest(unittest.TestCase, metaclass=dynamic.DynamicTest):
@classmethod
def _iter_testmethods(cls):
"""Iterate over song files to test."""
# Setting datadir
cls.config = DEFAULT_CONFIG
if 'datadir' not in cls.config:
cls.config['datadir'] = []
cls.config['datadir'].append(resource_filename(__name__, 'datadir'))
cls.song_plugins = files.load_plugins(
datadirs=cls.config['datadir'],
root_modules=['songs'],
keyword='SONG_RENDERERS',
)
for source in sorted(glob.glob(os.path.join(
os.path.dirname(__file__),
'*.source',
))):
base = source[:-len(".source")]
base = os.path.relpath(source, os.getcwd())[:-len(".source")]
for dest in LANGUAGES:
destname = "{}.{}".format(base, dest)
if not os.path.exists(destname):
@ -58,13 +71,12 @@ class FileTest(unittest.TestCase, metaclass=dynamic.DynamicTest):
if base is None or dest is None:
return
destname = "{}.{}".format(base, dest)
with open(destname, 'r', encoding='utf8') as expectfile:
with open_read(destname) as expectfile:
chordproname = "{}.source".format(base)
with disable_logging():
self.assertMultiLineEqual(
ChordproSong(chordproname, DEFAULT_CONFIG).render(
self.song_plugins[LANGUAGES[dest]]['sgc'](chordproname, self.config).render(
output=chordproname,
output_format=LANGUAGES[dest],
).strip(),
expectfile.read().strip(),
)

2
test/test_chordpro/ukulelechords.sgc

@ -1,4 +1,4 @@
{language: english}
{lang: en}
{define: G frets 0 2 3 2}
{define: D7 frets 2 2 2 3 fingers 1 1 1 2}
{define: G frets 3 2 0 0 0 3}

3
test/test_compilation/subdir.sb

@ -3,5 +3,6 @@
"lilypond",
"pictures"
],
"datadir": ["subdir_datadir", "subdir_datadir2"]
"datadir": ["subdir_datadir", "subdir_datadir2"],
"lang": "en"
}

15
test/test_compilation/subdir.tex.control

@ -36,6 +36,7 @@ guitar,
\PassOptionsToPackage{french}{babel}
\PassOptionsToPackage{english}{babel}
\usepackage[english]{babel}
\lang{english}
@ -100,15 +101,15 @@ guitar,
Chordpro}[
by={
},
cov={/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir2/img/datadir2.png},
cov={img/datadir2.png},
]
\cover
\lilypond{datadir2.ly}
\image{/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir2/img/datadir2.png}
\lilypond{scores/datadir2.ly}
\image{img/datadir2.png}
\endsong
@ -149,15 +150,15 @@ Chordpro}[
Chordpro}[
by={
},
cov={/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir/img/datadir.png},
cov={img/datadir.png},
]
\cover
\lilypond{datadir.ly}
\image{/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir/img/datadir.png}
\lilypond{scores/datadir.ly}
\image{img/datadir.png}
\endsong
@ -177,4 +178,4 @@ Chordpro}[
\end{document}
\end{document}

0
test/test_compilation/subdir_datadir/img/datadir.ly → test/test_compilation/subdir_datadir/scores/datadir.ly

6
test/test_compilation/subdir_datadir/songs/datadir.sg

@ -1,10 +1,10 @@
\beginsong{Image included from datadir\\\LaTeX}
[cov={datadir}]
[cov={img/datadir}]
\cover
\lilypond{datadir.ly}
\lilypond{scores/datadir.ly}
\image{datadir}
\image{img/datadir}
\endsong

6
test/test_compilation/subdir_datadir/songs/datadir2.sg

@ -1,10 +1,10 @@
\beginsong{Image included from a different datadir\\\LaTeX}
[cov={datadir2}]
[cov={img/datadir2}]
\cover
\lilypond{datadir2.ly}
\lilypond{scores/datadir2.ly}
\image{datadir2}
\image{img/datadir2}
\endsong

0
test/test_compilation/subdir_datadir2/img/datadir2.ly → test/test_compilation/subdir_datadir2/scores/datadir2.ly

10
test/test_compilation/test_compilation.py

@ -7,6 +7,8 @@ import os
import subprocess
import unittest
from patacrep.encoding import open_read
from .. import dynamic # pylint: disable=unused-import
@ -57,11 +59,11 @@ class FileTest(unittest.TestCase, metaclass=dynamic.DynamicTest):
# Check generated tex
control = "{}.tex.control".format(base)
tex = "{}.tex".format(base)
with open(control, 'r', encoding='utf8') as expectfile:
with open(tex, 'r', encoding='utf8') as latexfile:
with open_read(control) as expectfile:
with open_read(tex) as latexfile:
self.assertMultiLineEqual(
latexfile.read(),
expectfile.read(),
latexfile.read().strip(),
expectfile.read().strip(),
)
# Check compilation

Loading…
Cancel
Save