diff --git a/patacrep/build.py b/patacrep/build.py
index 492f30fc..8151f797 100644
--- a/patacrep/build.py
+++ b/patacrep/build.py
@@ -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
diff --git a/patacrep/content/song.py b/patacrep/content/song.py
index 9101425d..d6bff200 100755
--- a/patacrep/content/song.py
+++ b/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
diff --git a/patacrep/files.py b/patacrep/files.py
index e77c3ab2..1116c8b0 100644
--- a/patacrep/files.py
+++ b/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
diff --git a/patacrep/songs/__init__.py b/patacrep/songs/__init__.py
index 68d5a960..05862287 100644
--- a/patacrep/songs/__init__.py
+++ b/patacrep/songs/__init__.py
@@ -167,16 +167,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
@@ -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
@@ -223,28 +228,43 @@ class Song:
if directories is None:
directories = self.config['datadir']
+ for directory in directories:
+ for extension in extensions:
+ if os.path.isfile(os.path.join(directory, filename + extension)):
+ return directory, filename, extension
+
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 extension in extensions:
- fullpath = os.path.join(directory, filename + extension)
- if os.path.isfile(fullpath):
- return os.path.abspath(fullpath)
- return None
+ raise FileNotFoundError(filename)
+
+ def search_file(self, filename, extensions=None, *, datadirs=None):
+ """Return the path to a file present in a datadir.
- def search_image(self, filename, none_if_not_found=False):
+ 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):
"""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('img'),
+ )
def unprefixed_title(title, prefixes):
"""Remove the first prefix of the list in the beginning of title (if any).
diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py
index a874628c..4f33a084 100644
--- a/patacrep/songs/chordpro/__init__.py
+++ b/patacrep/songs/chordpro/__init__.py
@@ -1,9 +1,9 @@
"""Chordpro parser"""
-from jinja2 import Environment, FileSystemLoader, contextfunction
+from jinja2 import Environment, FileSystemLoader, contextfunction, ChoiceLoader
import jinja2
import os
-import pkg_resources
+from pkg_resources import resource_filename
from patacrep import encoding, files
from patacrep.songs import Song
@@ -11,23 +11,10 @@ from patacrep.songs.chordpro.syntax import parse_song
from patacrep.templates import Renderer
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."""
@@ -41,10 +28,7 @@ class ChordproSong(Song):
'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],
"titles": self.titles,
@@ -55,9 +39,14 @@ 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
@@ -68,7 +57,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 +66,51 @@ 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, extensions = self.search_datadir_file(filename, extensions, datadirs)
+ return os.path.join(datadir, filename + extensions)
+ except FileNotFoundError:
+ return None
+
+class Chordpro2LatexSong(ChordproSong):
+ """Render chordpro song to latex code"""
+
+ output_language = "latex"
+
+ def search_file(self, filename, extensions=None, *, datadirs=None):
+ try:
+ _datadir, filename, _extension = self.search_datadir_file(
+ filename,
+ extensions,
+ datadirs,
+ )
+ return filename
+ except FileNotFoundError:
+ 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,
+ },
}
diff --git a/patacrep/songs/chordpro/data/html/content_image b/patacrep/songs/chordpro/data/html/content_image
index 0da4bcfe..bf46a95f 100644
--- a/patacrep/songs/chordpro/data/html/content_image
+++ b/patacrep/songs/chordpro/data/html/content_image
@@ -1 +1,6 @@
+(* block image *)
+(* set image = content.argument|search_image *)
+(* if image *)
+(* endif *)
+(* endblock *)
diff --git a/patacrep/songs/chordpro/data/html/content_metadata_cover b/patacrep/songs/chordpro/data/html/content_metadata_cover
index 96fc7718..0da23623 100644
--- a/patacrep/songs/chordpro/data/html/content_metadata_cover
+++ b/patacrep/songs/chordpro/data/html/content_metadata_cover
@@ -1,3 +1,8 @@
+(* block cov *)
(* if 'cov' in metadata -*)
-
-(* endif *)
\ No newline at end of file
+ (* set cov = metadatad['cov'].argument|search_image *)
+ (* if cov *)
+
+ (* endif *)
+(* endif *)
+(* endblock *)
diff --git a/patacrep/songs/chordpro/data/html/content_partition b/patacrep/songs/chordpro/data/html/content_partition
index 1075cced..b43318f8 100644
--- a/patacrep/songs/chordpro/data/html/content_partition
+++ b/patacrep/songs/chordpro/data/html/content_partition
@@ -1 +1,6 @@
-((content.argument))
+(* block partition *)
+(* set partition = content.argument|search_partition *)
+(* if partition *)
+((content.argument))
+(* endif *)
+(* endblock *)
diff --git a/patacrep/songs/chordpro/data/latex/content_image b/patacrep/songs/chordpro/data/latex/content_image
index e4c2befb..ac97404b 100644
--- a/patacrep/songs/chordpro/data/latex/content_image
+++ b/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 *)
diff --git a/patacrep/songs/chordpro/data/latex/content_partition b/patacrep/songs/chordpro/data/latex/content_partition
index 4ce134a1..a3c35a3e 100644
--- a/patacrep/songs/chordpro/data/latex/content_partition
+++ b/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 -*)
diff --git a/patacrep/songs/chordpro/data/latex/song b/patacrep/songs/chordpro/data/latex/song
index 6f51d673..050cf66b 100644
--- a/patacrep/songs/chordpro/data/latex/song
+++ b/patacrep/songs/chordpro/data/latex/song
@@ -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 ))},
diff --git a/patacrep/songs/convert/__main__.py b/patacrep/songs/convert/__main__.py
index 51786875..6b40dca4 100644
--- a/patacrep/songs/convert/__main__.py
+++ b/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):
diff --git a/patacrep/songs/latex/__init__.py b/patacrep/songs/latex/__init__.py
index 6ac54c81..565b4067 100644
--- a/patacrep/songs/latex/__init__.py
+++ b/patacrep/songs/latex/__init__.py
@@ -11,8 +11,9 @@ from patacrep import files, encoding
from patacrep.latex import parse_song
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."""
@@ -28,8 +29,9 @@ class LatexSong(Song):
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,9 @@ class LatexSong(Song):
))
return r'\import{{{}/}}{{{}}}'.format(os.path.dirname(path), os.path.basename(path))
-SONG_PARSERS = {
- 'is': LatexSong,
- 'sg': LatexSong,
- }
+SONG_RENDERERS = {
+ "latex": {
+ 'is': Latex2LatexSong,
+ 'sg': Latex2LatexSong,
+ },
+}
diff --git a/test/test_chordpro/datadir/img/greensleeves.ly b/test/test_chordpro/datadir/img/greensleeves.ly
new file mode 100644
index 00000000..e69de29b
diff --git a/test/test_chordpro/datadir/img/traditionnel.png b/test/test_chordpro/datadir/img/traditionnel.png
new file mode 100644
index 00000000..e69de29b
diff --git a/test/test_chordpro/greensleeves.tex b/test/test_chordpro/greensleeves.tex
index a0b602c0..b7186121 100644
--- a/test/test_chordpro/greensleeves.tex
+++ b/test/test_chordpro/greensleeves.tex
@@ -7,7 +7,7 @@ Un sous titre}[
by={
Traditionnel },
album={Angleterre},
- cov={traditionnel},
+ cov={traditionnel},
]
\cover
diff --git a/test/test_chordpro/metadata.sgc b/test/test_chordpro/metadata.sgc
index 979c1388..e270b729 100644
--- a/test/test_chordpro/metadata.sgc
+++ b/test/test_chordpro/metadata.sgc
@@ -10,13 +10,13 @@
{artist: Author2}
{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
diff --git a/test/test_chordpro/metadata.source b/test/test_chordpro/metadata.source
index 65a2359a..05031dd2 100644
--- a/test/test_chordpro/metadata.source
+++ b/test/test_chordpro/metadata.source
@@ -10,12 +10,12 @@
{artist: Author2}
{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
diff --git a/test/test_chordpro/metadata.tex b/test/test_chordpro/metadata.tex
index fa29ec3c..46368580 100644
--- a/test/test_chordpro/metadata.tex
+++ b/test/test_chordpro/metadata.tex
@@ -11,7 +11,7 @@ Subtitle5}[
Author2 },
album={Album},
copyright={Copyright},
- cov={Cover},
+ cov={test/test_chordpro/metadata_cover},
foo={Foo},
]
@@ -19,8 +19,8 @@ Subtitle5}[
\textnote{Comment}
\musicnote{GuitarComment}
-\lilypond{Lilypond}
-\image{Image}
+\lilypond{test/test_chordpro/metadata_lilypond}
+\image{test/test_chordpro/metadata_image}
diff --git a/test/test_chordpro/metadata_cover.png b/test/test_chordpro/metadata_cover.png
new file mode 100644
index 00000000..e69de29b
diff --git a/test/test_chordpro/metadata_image.png b/test/test_chordpro/metadata_image.png
new file mode 100644
index 00000000..e69de29b
diff --git a/test/test_chordpro/metadata_lilypond.ly b/test/test_chordpro/metadata_lilypond.ly
new file mode 100644
index 00000000..e69de29b
diff --git a/test/test_chordpro/test_parser.py b/test/test_chordpro/test_parser.py
index 20cafd38..6ce2a83a 100644
--- a/test/test_chordpro/test_parser.py
+++ b/test/test_chordpro/test_parser.py
@@ -5,9 +5,10 @@
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 .. import disable_logging
@@ -26,11 +27,22 @@ class FileTestMeta(type):
def __init__(cls, name, bases, nmspc):
super().__init__(name, bases, nmspc)
+ # 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):
@@ -55,9 +67,8 @@ class FileTestMeta(type):
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(),
)