diff --git a/patacrep/encoding.py b/patacrep/encoding.py
index b8bdc8e6..fd58fc2f 100644
--- a/patacrep/encoding.py
+++ b/patacrep/encoding.py
@@ -16,7 +16,8 @@ def open_read(filename, mode='r', encoding=None):
If `encoding` is set, use it as the encoding (do not guess).
"""
if encoding is None:
- fileencoding = chardet.detect(open(filename, 'rb').read())['encoding']
+ with open(filename, 'rb') as file:
+ fileencoding = chardet.detect(file.read())['encoding']
else:
fileencoding = encoding
diff --git a/patacrep/songs/__init__.py b/patacrep/songs/__init__.py
index fbf02e36..297a993e 100644
--- a/patacrep/songs/__init__.py
+++ b/patacrep/songs/__init__.py
@@ -128,7 +128,7 @@ class Song:
self.titles = []
self.data = {}
self.cached = None
- self.parse(config)
+ self._parse(config)
# Post processing of data
self.datadir = datadir
@@ -165,19 +165,19 @@ class Song:
def __repr__(self):
return repr((self.titles, self.data, self.fullpath))
- def render(self, output, output_format):
+ def render(self, output_format, output=None, *args, **kwargs):
"""Return the code rendering this song.
Arguments:
- - output: Name of the output file.
- 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)
+ return getattr(self, method)(output, *args, **kwargs)
raise NotImplementedError()
- def parse(self, config): # pylint: disable=no-self-use
+ def _parse(self, config): # pylint: disable=no-self-use
"""Parse song.
It set the following attributes:
@@ -194,6 +194,56 @@ class Song:
"""
raise NotImplementedError()
+ def get_datadirs(self, subdir=None):
+ """Return an iterator of existing datadirs (with eventually a subdir)
+ """
+ for directory in self.config['datadir']:
+ fullpath = os.path.join(directory, subdir)
+ if os.path.isdir(fullpath):
+ yield fullpath
+
+ def search_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.
+
+ 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
+ return the path to the compiled file.
+ """
+ if extensions is None:
+ extensions = ['']
+ if directories is None:
+ directories = self.config['datadir']
+
+ songdir = os.path.dirname(self.fullpath)
+
+ 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
+
+ def search_image(self, filename, none_if_not_found=False):
+ """Search for an image file"""
+ filepath = self.search_file(
+ filename,
+ ['', '.jpg', '.png'],
+ 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):
+ """Search for a lilypond file"""
+ filepath = self.search_file(filename, ['', '.ly'])
+ return filepath if none_if_not_found or filepath else filename
+
def unprefixed_title(title, prefixes):
"""Remove the first prefix of the list in the beginning of title (if any).
"""
@@ -202,39 +252,3 @@ def unprefixed_title(title, prefixes):
if match:
return match.group(2)
return title
-
-def search_image(image, chordprofile, config):
- """Return the file name of an image, so that LaTeX will find it.
-
- :param str image: The name, as provided in the chordpro file.
- :param str chordprofile: The name of the file including this image.
- :param dict config: Songbook configuration dictionary.
-
- The image can be:
-
- - in the same directory as the including song file;
- - in the same directory as the main LaTeX file;
- - in some of the `DATADIR/img` directories.
-
- If image is not found, the `image` argument is returned.
- """
- # Image is in the same folder as its song
- texdir = os.path.dirname(chordprofile)
- if os.path.exists(os.path.join(texdir, image)):
- return os.path.join(texdir, image)
-
- # Image is in the same directory as the main tex file
- rootdir = os.path.dirname(os.path.join(
- os.getcwd(),
- config['filename'],
- ))
- if os.path.exists(os.path.join(rootdir, image)):
- return image
-
- # Image is in a datadir
- for directory in config['datadir']:
- if os.path.exists(os.path.join(directory, 'img', image)):
- return os.path.join(directory, 'img', image)
-
- # Could not find image
- return image
diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py
index ae37b79b..6d4c0e39 100644
--- a/patacrep/songs/chordpro/__init__.py
+++ b/patacrep/songs/chordpro/__init__.py
@@ -5,7 +5,7 @@ import pkg_resources
import os
from patacrep import encoding, files
-from patacrep.songs import Song, search_image
+from patacrep.songs import Song
from patacrep.songs.chordpro.syntax import parse_song
from patacrep.templates import Renderer
@@ -14,9 +14,24 @@ class ChordproSong(Song):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.jinjaenv = None
- def parse(self, config):
+ @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,
+ )
+
+ def _parse(self, config):
"""Parse content, and return the dictionary of song data."""
with encoding.open_read(self.fullpath, encoding=self.encoding) as song:
song = parse_song(song.read(), self.fullpath)
@@ -28,42 +43,39 @@ class ChordproSong(Song):
'song': song,
}
- def render(self, output, output_format):
+ def render(self, output_format, output=None, template="song", templatedirs=None): # pylint: disable=arguments-differ
+ if templatedirs is None:
+ templatedirs = []
+
context = {
'language': self.languages[0],
- "path": files.relpath(self.fullpath, os.path.dirname(output)),
"titles": self.titles,
"authors": self.authors,
"metadata": self.data,
"render": self._render_ast,
"config": self.config,
+ "content": self.cached['song'].content,
}
- self.jinjaenv = Environment(loader=FileSystemLoader(os.path.join(
- os.path.abspath(pkg_resources.resource_filename(__name__, 'data')),
- output_format,
- )))
- self.jinjaenv.filters['search_image'] = search_image
- return self._render_ast(
- context,
- self.cached['song'].content,
- template="song",
- )
- @contextfunction
- def _render_ast(self, context, content, template=None):
- """Render ``content``."""
- if isinstance(context, dict):
- context['content'] = content
- else:
- context.vars['content'] = content
- if template is None:
- template = content.template()
+ jinjaenv = Environment(loader=FileSystemLoader(
+ self.iter_template_paths(templatedirs, output_format)
+ ))
+ jinjaenv.filters['search_image'] = self.search_image
+ jinjaenv.filters['search_partition'] = self.search_partition
+
return Renderer(
template=template,
encoding='utf8',
- jinjaenv=self.jinjaenv,
+ jinjaenv=jinjaenv,
).template.render(context)
+ @staticmethod
+ @contextfunction
+ def _render_ast(context, content):
+ """Render ``content``."""
+ context.vars['content'] = content
+ return context.environment.get_template(content.template()).render(context)
+
SONG_PARSERS = {
'sgc': ChordproSong,
}
diff --git a/patacrep/songs/chordpro/ast.py b/patacrep/songs/chordpro/ast.py
index 5cffbb86..0227d457 100644
--- a/patacrep/songs/chordpro/ast.py
+++ b/patacrep/songs/chordpro/ast.py
@@ -39,6 +39,10 @@ class OrderedLifoDict:
def __getitem__(self, key):
return self._values[key]
+ def get(self, key, default=None):
+ """Same as :meth:`dict.get`."""
+ return self._values.get(key, default)
+
def _indent(string):
"""Return and indented version of argument."""
return "\n".join([" {}".format(line) for line in string.split('\n')])
@@ -149,6 +153,11 @@ class Chord(AST):
# pylint: disable=too-many-arguments
self.chord = chord
+ @property
+ def pretty_chord(self):
+ """Return the chord with nicer (utf8) alteration"""
+ return self.chord.replace('b', '♭').replace('#', '♯')
+
class Verse(AST):
"""A verse (or bridge, or chorus)"""
_template = "verse"
@@ -337,6 +346,11 @@ class Define(Directive):
self.fingers = fingers # Can be None
super().__init__("define", None)
+ @property
+ def pretty_key(self):
+ """Return the key with nicer (utf8) alteration"""
+ return self.key.chord.replace('&', '♭').replace('#', '♯')
+
def __str__(self):
return None
diff --git a/patacrep/songs/chordpro/data/chordpro/content_image b/patacrep/songs/chordpro/data/chordpro/content_image
index 58e7f904..4f9bf06c 100644
--- a/patacrep/songs/chordpro/data/chordpro/content_image
+++ b/patacrep/songs/chordpro/data/chordpro/content_image
@@ -1 +1 @@
-{image: (( content.argument ))}
+{image: (( content.argument|search_image ))}
diff --git a/patacrep/songs/chordpro/data/chordpro/content_newline b/patacrep/songs/chordpro/data/chordpro/content_newline
index 139597f9..8b137891 100644
--- a/patacrep/songs/chordpro/data/chordpro/content_newline
+++ b/patacrep/songs/chordpro/data/chordpro/content_newline
@@ -1,2 +1 @@
-
diff --git a/patacrep/songs/chordpro/data/chordpro/content_partition b/patacrep/songs/chordpro/data/chordpro/content_partition
index 362c4f64..43b8761e 100644
--- a/patacrep/songs/chordpro/data/chordpro/content_partition
+++ b/patacrep/songs/chordpro/data/chordpro/content_partition
@@ -1 +1 @@
-{partition: ((content.argument))}
+{partition: ((content.argument|search_partition))}
diff --git a/patacrep/songs/chordpro/data/chordpro/content_verse b/patacrep/songs/chordpro/data/chordpro/content_verse
index 465e7697..cd562cea 100644
--- a/patacrep/songs/chordpro/data/chordpro/content_verse
+++ b/patacrep/songs/chordpro/data/chordpro/content_verse
@@ -1,5 +1,11 @@
+(*- if content.type != 'verse' -*)
{start_of_(( content.type ))}
(* for line in content.lines *)
(( render(line) ))
(* endfor *)
{end_of_(( content.type ))}
+(* else -*)
+ (*- for line in content.lines -*)
+ (( render(line) ))
+ (* endfor -*)
+(*- endif -*)
diff --git a/patacrep/songs/chordpro/data/chordpro/song b/patacrep/songs/chordpro/data/chordpro/song
index 92f474ee..3baa8c22 100644
--- a/patacrep/songs/chordpro/data/chordpro/song
+++ b/patacrep/songs/chordpro/data/chordpro/song
@@ -1,35 +1,3 @@
-(* if language is defined -*)
- {language: (( language ))}
-(* endif *)
-(* if metadata.columns is defined -*)
- {columns: (( metadata.columns ))}
-(* endif *)
-(* if metadata.capo is defined -*)
- {capo: (( metadata.capo ))}
-(* endif *)
+(* include 'song_header' *)
-(*- for title in titles -*)
- {title: (( title ))}
-(* endfor -*)
-
-(*- for author in authors -*)
- {artist: (( author[1] )) (( author[0] ))}
-(* endfor *)
-
-(*- for key in ['album', 'copyright', 'cov', 'tag'] *)
- (* if key in metadata -*)
- {(( key )): (( metadata[key] ))}
- (* endif *)
-(* endfor *)
-
-(*- for key in metadata.keys -*)
- {key: (( key.keyword )): (( key.argument ))}
-(* endfor *)
-
-(*- for chord in metadata['define'] *)
- ((- render(chord) ))
-(* endfor *)
-
-(* for item in content -*)
- (( render(item) ))
-(* endfor *)
+(* include 'song_body' *)
diff --git a/patacrep/songs/chordpro/data/chordpro/song_body b/patacrep/songs/chordpro/data/chordpro/song_body
new file mode 100644
index 00000000..1b7159bf
--- /dev/null
+++ b/patacrep/songs/chordpro/data/chordpro/song_body
@@ -0,0 +1,3 @@
+(* for item in content -*)
+ (( render(item) ))
+(* endfor *)
diff --git a/patacrep/songs/chordpro/data/chordpro/song_header b/patacrep/songs/chordpro/data/chordpro/song_header
new file mode 100644
index 00000000..f111271f
--- /dev/null
+++ b/patacrep/songs/chordpro/data/chordpro/song_header
@@ -0,0 +1,34 @@
+(* if language is defined -*)
+ {language: (( language ))}
+(* endif *)
+(* if metadata.columns is defined -*)
+ {columns: (( metadata.columns ))}
+(* endif *)
+(* if metadata.capo is defined -*)
+ {capo: (( metadata.capo ))}
+(* endif *)
+
+(*- for title in titles -*)
+ {title: (( title ))}
+(* endfor -*)
+
+(*- for author in authors -*)
+ {artist: (( author[1] )) (( author[0] ))}
+(* endfor *)
+
+(*- for key in ['album', 'copyright', 'tag'] *)
+ (* if key in metadata -*)
+ {(( key )): (( metadata[key] ))}
+ (* endif *)
+(* endfor *)
+(* if 'cov' in metadata -*)
+ {(( 'cov' )): (( metadata['cov'].argument|search_image ))}
+(* endif *)
+
+(*- for key in metadata.keys -*)
+ {key: (( key.keyword )): (( key.argument ))}
+(* endfor *)
+
+(*- for chord in metadata['define'] *)
+ ((- render(chord) ))
+(* endfor *)
diff --git a/patacrep/songs/chordpro/data/html/content_chord b/patacrep/songs/chordpro/data/html/content_chord
new file mode 100644
index 00000000..baa97f7e
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_chord
@@ -0,0 +1 @@
+((- content.pretty_chord -))
diff --git a/patacrep/songs/chordpro/data/html/content_chordlist b/patacrep/songs/chordpro/data/html/content_chordlist
new file mode 100644
index 00000000..ad3fc4d0
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_chordlist
@@ -0,0 +1,6 @@
+
+ (*- for chord in content.chords -*)
+ (* if not loop.first *) (* endif -*)
+ (( render(chord) -))
+ (* endfor -*)
+
diff --git a/patacrep/songs/chordpro/data/html/content_comment b/patacrep/songs/chordpro/data/html/content_comment
new file mode 100644
index 00000000..d9dfd2f1
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_comment
@@ -0,0 +1 @@
+
diff --git a/patacrep/songs/chordpro/data/html/content_define b/patacrep/songs/chordpro/data/html/content_define
new file mode 100644
index 00000000..4998f0bd
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_define
@@ -0,0 +1,31 @@
+
\ No newline at end of file
diff --git a/patacrep/songs/chordpro/data/html/content_define_list b/patacrep/songs/chordpro/data/html/content_define_list
new file mode 100644
index 00000000..923f37c2
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_define_list
@@ -0,0 +1,3 @@
+(*- for chord in metadata['define'] *)
+ ((- render(chord) ))
+(* endfor *)
diff --git a/patacrep/songs/chordpro/data/html/content_error b/patacrep/songs/chordpro/data/html/content_error
new file mode 100644
index 00000000..bbdb95fd
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_error
@@ -0,0 +1,3 @@
+
+ERROR : Template not found for "(( content.__class__.__name__ ))". See the logs for details.
+
diff --git a/patacrep/songs/chordpro/data/html/content_guitar_comment b/patacrep/songs/chordpro/data/html/content_guitar_comment
new file mode 100644
index 00000000..770cc6b0
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_guitar_comment
@@ -0,0 +1 @@
+
diff --git a/patacrep/songs/chordpro/data/html/content_image b/patacrep/songs/chordpro/data/html/content_image
new file mode 100644
index 00000000..0da4bcfe
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_image
@@ -0,0 +1 @@
+
diff --git a/patacrep/songs/chordpro/data/html/content_line b/patacrep/songs/chordpro/data/html/content_line
new file mode 100644
index 00000000..03ca80b7
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_line
@@ -0,0 +1,3 @@
+(* for item in content.line -*)
+ (( render(item) ))
+(*- endfor *)
diff --git a/patacrep/songs/chordpro/data/html/content_metadata_cover b/patacrep/songs/chordpro/data/html/content_metadata_cover
new file mode 100644
index 00000000..96fc7718
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_metadata_cover
@@ -0,0 +1,3 @@
+(* if 'cov' in metadata -*)
+
+(* endif *)
\ No newline at end of file
diff --git a/patacrep/songs/chordpro/data/html/content_newline b/patacrep/songs/chordpro/data/html/content_newline
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_newline
@@ -0,0 +1 @@
+
diff --git a/patacrep/songs/chordpro/data/html/content_partition b/patacrep/songs/chordpro/data/html/content_partition
new file mode 100644
index 00000000..1075cced
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_partition
@@ -0,0 +1 @@
+((content.argument))
diff --git a/patacrep/songs/chordpro/data/html/content_space b/patacrep/songs/chordpro/data/html/content_space
new file mode 100644
index 00000000..8d1c8b69
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_space
@@ -0,0 +1 @@
+
diff --git a/patacrep/songs/chordpro/data/html/content_tablature b/patacrep/songs/chordpro/data/html/content_tablature
new file mode 100644
index 00000000..35cfd425
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_tablature
@@ -0,0 +1,5 @@
+
+ (* for content in content.content *)
+ ((- content ))
+ (* endfor *)
+
diff --git a/patacrep/songs/chordpro/data/html/content_verse b/patacrep/songs/chordpro/data/html/content_verse
new file mode 100644
index 00000000..eab4d361
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_verse
@@ -0,0 +1,6 @@
+
+ (*- for line in content.lines -*)
+ (* if not loop.first *)
(* endif -*)
+ (( render(line) ))
+ (* endfor -*)
+
diff --git a/patacrep/songs/chordpro/data/html/content_word b/patacrep/songs/chordpro/data/html/content_word
new file mode 100644
index 00000000..d9dd7a30
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_word
@@ -0,0 +1 @@
+(( content.value ))
diff --git a/patacrep/songs/chordpro/data/html/song b/patacrep/songs/chordpro/data/html/song
new file mode 100644
index 00000000..e774cdff
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/song
@@ -0,0 +1,5 @@
+(* include 'song_header' *)
+
+
+(* include 'song_body' *)
+
diff --git a/patacrep/songs/chordpro/data/html/song_body b/patacrep/songs/chordpro/data/html/song_body
new file mode 100644
index 00000000..1b7159bf
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/song_body
@@ -0,0 +1,3 @@
+(* for item in content -*)
+ (( render(item) ))
+(* endfor *)
diff --git a/patacrep/songs/chordpro/data/html/song_header b/patacrep/songs/chordpro/data/html/song_header
new file mode 100644
index 00000000..d85adbd8
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/song_header
@@ -0,0 +1,30 @@
+(*- for title in titles -*)
+ (* if loop.first *)
+ (( title ))
+ (* else *)
+ (( title ))
+ (* endif *)
+(* endfor -*)
+
+(*- for author in authors -*)
+ (( author[1] )) (( author[0] ))
+(* endfor *)
+
+
+(*- for key in ['album', 'copyright', 'tag', 'columns', 'capo'] *)
+ (* if key in metadata -*)
+ (( key|capitalize )): (( metadata[key] ))
+ (* endif *)
+(* endfor *)
+
+(* if language is defined -*)
+ Language: (( language ))
+(* endif *)
+
+(* include 'content_metadata_cover' *)
+
+(*- for key in metadata.keys -*)
+ {key: (( key.keyword )): (( key.argument ))}
+(* endfor *)
+
+(* include 'content_define_list' *)
diff --git a/patacrep/songs/chordpro/data/latex/content_define b/patacrep/songs/chordpro/data/latex/content_define
index ca899ff3..d1294b3a 100644
--- a/patacrep/songs/chordpro/data/latex/content_define
+++ b/patacrep/songs/chordpro/data/latex/content_define
@@ -1,4 +1,8 @@
+(*- if content.frets|length == 4 -*)
+\utab{
+(*- else -*)
\gtab{
+(*- endif -*)
((- render(content.key) -))
}{
(*- if content.basefret -*)
diff --git a/patacrep/songs/chordpro/data/latex/content_image b/patacrep/songs/chordpro/data/latex/content_image
index bd132ce6..e4c2befb 100644
--- a/patacrep/songs/chordpro/data/latex/content_image
+++ b/patacrep/songs/chordpro/data/latex/content_image
@@ -1 +1 @@
-\image{(( content.argument|search_image(path, config) ))}
+\image{(( content.argument|search_image ))}
diff --git a/patacrep/songs/chordpro/data/latex/content_partition b/patacrep/songs/chordpro/data/latex/content_partition
index bcb92a0b..4ce134a1 100644
--- a/patacrep/songs/chordpro/data/latex/content_partition
+++ b/patacrep/songs/chordpro/data/latex/content_partition
@@ -1 +1 @@
-\lilypond{ ((- content.argument|search_image(path, config) -)) }
+\lilypond{ ((- content.argument|search_partition -)) }
diff --git a/patacrep/songs/chordpro/data/latex/song b/patacrep/songs/chordpro/data/latex/song
index 4cb04412..2568f7f7 100644
--- a/patacrep/songs/chordpro/data/latex/song
+++ b/patacrep/songs/chordpro/data/latex/song
@@ -28,7 +28,7 @@
(* endif *)
(* endfor *)
(* if 'cov' in metadata *)
- cov={(( metadata["cov"].argument|search_image(path, config) ))},
+ cov={(( metadata["cov"].argument|search_image ))},
(* endif *)
(* for key in metadata.keys *)
(( key.keyword ))={(( key.argument ))},
@@ -43,8 +43,6 @@
(( render(chord) ))
(* endfor *)
-(* for item in content -*)
- (( render(item) ))
-(* endfor *)
+(* include 'song_body' *)
\endsong
diff --git a/patacrep/songs/chordpro/data/latex/song_body b/patacrep/songs/chordpro/data/latex/song_body
new file mode 100644
index 00000000..1b7159bf
--- /dev/null
+++ b/patacrep/songs/chordpro/data/latex/song_body
@@ -0,0 +1,3 @@
+(* for item in content -*)
+ (( render(item) ))
+(* endfor *)
diff --git a/patacrep/songs/latex/__init__.py b/patacrep/songs/latex/__init__.py
index a0d9c945..6ac54c81 100644
--- a/patacrep/songs/latex/__init__.py
+++ b/patacrep/songs/latex/__init__.py
@@ -14,7 +14,7 @@ from patacrep.songs import Song
class LatexSong(Song):
"""LaTeX song parser."""
- def parse(self, __config):
+ def _parse(self, __config):
"""Parse content, and return the dictinory of song data."""
with encoding.open_read(self.fullpath, encoding=self.encoding) as song:
self.data = parse_song(song.read(), self.fullpath)
@@ -30,6 +30,8 @@ class LatexSong(Song):
def render_latex(self, output):
"""Return the code rendering the song."""
+ if output is None:
+ raise ValueError(output)
path = files.path2posix(files.relpath(
self.fullpath,
os.path.dirname(output)
diff --git a/test/test_chordpro/01.sgc b/test/test_chordpro/01.sgc
index ae7eabc4..25d6a677 100644
--- a/test/test_chordpro/01.sgc
+++ b/test/test_chordpro/01.sgc
@@ -1,5 +1,3 @@
{language: english}
-{start_of_verse}
- A verse line
-{end_of_verse}
+A verse line
diff --git a/test/test_chordpro/08.sgc b/test/test_chordpro/08.sgc
index 61224d41..9a9f0566 100644
--- a/test/test_chordpro/08.sgc
+++ b/test/test_chordpro/08.sgc
@@ -1,7 +1,4 @@
{language: english}
-
-{start_of_verse}
- A lot of new lines
-{end_of_verse}
+A lot of new lines
diff --git a/test/test_chordpro/09.sgc b/test/test_chordpro/09.sgc
index 942a91e1..193db1b0 100644
--- a/test/test_chordpro/09.sgc
+++ b/test/test_chordpro/09.sgc
@@ -2,7 +2,4 @@
{title: and a directive}
-
-{start_of_verse}
- A lot of new lines
-{end_of_verse}
+A lot of new lines
diff --git a/test/test_chordpro/10.sgc b/test/test_chordpro/10.sgc
index 23bce4fa..e7537b3b 100644
--- a/test/test_chordpro/10.sgc
+++ b/test/test_chordpro/10.sgc
@@ -1,5 +1,3 @@
{language: english}
-{start_of_verse}
- A line[A] with a chord
-{end_of_verse}
+A line[A] with a chord
diff --git a/test/test_chordpro/11.sgc b/test/test_chordpro/11.sgc
index 79d44702..3610f67c 100644
--- a/test/test_chordpro/11.sgc
+++ b/test/test_chordpro/11.sgc
@@ -1,5 +1,3 @@
{language: english}
-{start_of_verse}
- A line ending with a chord[A]
-{end_of_verse}
+A line ending with a chord[A]
diff --git a/test/test_chordpro/12.sgc b/test/test_chordpro/12.sgc
index 6afb5bf8..029ccad4 100644
--- a/test/test_chordpro/12.sgc
+++ b/test/test_chordpro/12.sgc
@@ -1,5 +1,3 @@
{language: english}
-{start_of_verse}
- [A]A line starting with a chord
-{end_of_verse}
+[A]A line starting with a chord
diff --git a/test/test_chordpro/21.sgc b/test/test_chordpro/21.sgc
index ae7eabc4..25d6a677 100644
--- a/test/test_chordpro/21.sgc
+++ b/test/test_chordpro/21.sgc
@@ -1,5 +1,3 @@
{language: english}
-{start_of_verse}
- A verse line
-{end_of_verse}
+A verse line
diff --git a/test/test_chordpro/28.sgc b/test/test_chordpro/28.sgc
index 61224d41..9a9f0566 100644
--- a/test/test_chordpro/28.sgc
+++ b/test/test_chordpro/28.sgc
@@ -1,7 +1,4 @@
{language: english}
-
-{start_of_verse}
- A lot of new lines
-{end_of_verse}
+A lot of new lines
diff --git a/test/test_chordpro/29.sgc b/test/test_chordpro/29.sgc
index 942a91e1..193db1b0 100644
--- a/test/test_chordpro/29.sgc
+++ b/test/test_chordpro/29.sgc
@@ -2,7 +2,4 @@
{title: and a directive}
-
-{start_of_verse}
- A lot of new lines
-{end_of_verse}
+A lot of new lines
diff --git a/test/test_chordpro/chords.sgc b/test/test_chordpro/chords.sgc
index 942a0223..040dcf07 100644
--- a/test/test_chordpro/chords.sgc
+++ b/test/test_chordpro/chords.sgc
@@ -1,19 +1,17 @@
{language: english}
-{start_of_verse}
- [A]Simple
- [Bb]Bémol
- [C#]Dièse
- [Adim]dim
- [Dmaj]maj
- [Em3]m chiffre
- [G4]Nombre
- [Emaj3]maj et nombre
- [Absus8]bémol, sus et nombre
- [A/A]Deux notes
- [F/Fb]Deux notes, bémol
- [B/C#]Deux notes, dièse
- [Ab B#/A]Plusieurs notes à la suite
- [E5/A*]Avec une étoile
- [B#+8]Avec un plus
-{end_of_verse}
+[A]Simple
+[Bb]Bémol
+[C#]Dièse
+[Adim]dim
+[Dmaj]maj
+[Em3]m chiffre
+[G4]Nombre
+[Emaj3]maj et nombre
+[Absus8]bémol, sus et nombre
+[A/A]Deux notes
+[F/Fb]Deux notes, bémol
+[B/C#]Deux notes, dièse
+[Ab B#/A]Plusieurs notes à la suite
+[E5/A*]Avec une étoile
+[B#+8]Avec un plus
diff --git a/test/test_chordpro/greensleeves.sgc b/test/test_chordpro/greensleeves.sgc
index bd0ddd71..1f0ea7f3 100644
--- a/test/test_chordpro/greensleeves.sgc
+++ b/test/test_chordpro/greensleeves.sgc
@@ -8,16 +8,12 @@
{cov: traditionnel}
-
{partition: greensleeves.ly}
-
-{start_of_verse}
- A[Am]las, my love, ye [G]do me wrong
- To [Am]cast me oft dis[E]curteously
- And [Am]I have loved [G]you so long
- De[Am]lighting [E]in your [Am]companie
-{end_of_verse}
+A[Am]las, my love, ye [G]do me wrong
+To [Am]cast me oft dis[E]curteously
+And [Am]I have loved [G]you so long
+De[Am]lighting [E]in your [Am]companie
{start_of_chorus}
@@ -28,33 +24,25 @@
{end_of_chorus}
-{start_of_verse}
- I [Am]have been ready [G]at your hand
- To [Am]grant what ever [E]you would crave
- I [Am]have both waged [G]life and land
- Your [Am]love and [E]good will [Am]for to have
-{end_of_verse}
+I [Am]have been ready [G]at your hand
+To [Am]grant what ever [E]you would crave
+I [Am]have both waged [G]life and land
+Your [Am]love and [E]good will [Am]for to have
-{start_of_verse}
- I [Am]bought thee kerchers [G]to thy head
- That [Am]were wrought fine and [E]gallantly
- I [Am]kept thee both at [G]boord and bed
- Which [Am]cost my [E]purse well [Am]favouredly
-{end_of_verse}
+I [Am]bought thee kerchers [G]to thy head
+That [Am]were wrought fine and [E]gallantly
+I [Am]kept thee both at [G]boord and bed
+Which [Am]cost my [E]purse well [Am]favouredly
-{start_of_verse}
- I [Am]bought thee peticotes [G]of the best
- The [Am]cloth so fine as [E]fine might be
- I [Am]gave thee jewels [G]for thy chest
- And [Am]all this [E]cost I [Am]spent on thee
-{end_of_verse}
+I [Am]bought thee peticotes [G]of the best
+The [Am]cloth so fine as [E]fine might be
+I [Am]gave thee jewels [G]for thy chest
+And [Am]all this [E]cost I [Am]spent on thee
-{start_of_verse}
- Thy [Am]smock of silke, both [G]faire and white
- With [Am]gold embrodered [E]gorgeously
- Thy [Am]peticote of [G]sendall right
- And [Am]this I [E]bought thee [Am]gladly
-{end_of_verse}
+Thy [Am]smock of silke, both [G]faire and white
+With [Am]gold embrodered [E]gorgeously
+Thy [Am]peticote of [G]sendall right
+And [Am]this I [E]bought thee [Am]gladly
diff --git a/test/test_chordpro/invalid_chord.sgc b/test/test_chordpro/invalid_chord.sgc
index 7ec4fd65..acb5444f 100644
--- a/test/test_chordpro/invalid_chord.sgc
+++ b/test/test_chordpro/invalid_chord.sgc
@@ -1,5 +1,3 @@
{language: english}
-{start_of_verse}
- This is invalid.
-{end_of_verse}
+This is invalid.
diff --git a/test/test_chordpro/test_parser.py b/test/test_chordpro/test_parser.py
index 2bdd15aa..8f857156 100644
--- a/test/test_chordpro/test_parser.py
+++ b/test/test_chordpro/test_parser.py
@@ -16,22 +16,15 @@ LANGUAGES = {
'sgc': 'chordpro',
}
-class TestParsingRendering(unittest.TestCase):
- """Test parsing and rendering"""
+class FileTestMeta(type):
+ """Metaclass that creates on-the-fly test function according to files.
- maxDiff = None
+ See the :class:`FileTest` documentation for more information.
+ """
- def test_all(self):
- """Test of chorpro parser, and several renderers.
+ def __init__(cls, name, bases, nmspc):
+ super().__init__(name, bases, nmspc)
- For any given `foo.source`, it is parsed as a chordpro file, and
- should be rendered as `foo.sgc` with the chordpro renderer, and
- `foo.tex` with the latex renderer.
- """
- config = DEFAULT_CONFIG.copy()
- config.update({
- 'encoding': 'utf8',
- })
for source in sorted(glob.glob(os.path.join(
os.path.dirname(__file__),
'*.source',
@@ -41,15 +34,47 @@ class TestParsingRendering(unittest.TestCase):
destname = "{}.{}".format(base, dest)
if not os.path.exists(destname):
continue
- with open(destname, 'r', encoding='utf8') as expectfile:
- chordproname = "{}.source".format(base)
- config['filename'] = chordproname
- with disable_logging():
- with self.subTest(base=os.path.basename(base), format=dest):
- self.assertMultiLineEqual(
- ChordproSong(None, chordproname, config).render(
- output=chordproname,
- output_format=LANGUAGES[dest],
- ).strip(),
- expectfile.read().strip(),
- )
+ setattr(
+ cls,
+ "test_{}_{}".format(os.path.basename(base), dest),
+ cls._create_test(base, dest),
+ )
+
+ @staticmethod
+ def _create_test(base, dest):
+ """Return a function testing that `base` compilation in `dest` format.
+ """
+
+ def test_parse_render(self):
+ """Test that `base` is correctly parsed and rendered."""
+ if base is None or dest is None:
+ return
+ destname = "{}.{}".format(base, dest)
+ with open(destname, 'r', encoding='utf8') as expectfile:
+ chordproname = "{}.source".format(base)
+ with disable_logging():
+ self.assertMultiLineEqual(
+ ChordproSong(None, chordproname, DEFAULT_CONFIG).render(
+ output=chordproname,
+ output_format=LANGUAGES[dest],
+ ).strip(),
+ expectfile.read().strip(),
+ )
+
+ test_parse_render.__doc__ = (
+ "Test that '{base}' is correctly parsed and rendererd into '{format}' format."
+ ).format(base=os.path.basename(base), format=dest)
+ return test_parse_render
+
+class FileTest(unittest.TestCase, metaclass=FileTestMeta):
+ """Test of chorpro parser, and several renderers.
+
+ For any given `foo.source`, it is parsed as a chordpro file, and should be
+ rendered as `foo.sgc` with the chordpro renderer, and `foo.tex` with the
+ latex renderer.
+
+ This class does nothing by itself, but its metaclass populates it with test
+ methods testing parser and renderers.
+ """
+
+ maxDiff = None
diff --git a/test/test_chordpro/ukulelechords.sgc b/test/test_chordpro/ukulelechords.sgc
new file mode 100644
index 00000000..3b37123d
--- /dev/null
+++ b/test/test_chordpro/ukulelechords.sgc
@@ -0,0 +1,5 @@
+{language: english}
+{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}
+{define: A#+2 base-fret 7 frets 0 1 3 3 x x fingers - 1 2 3 - -}
diff --git a/test/test_chordpro/ukulelechords.source b/test/test_chordpro/ukulelechords.source
new file mode 100644
index 00000000..e21837f6
--- /dev/null
+++ b/test/test_chordpro/ukulelechords.source
@@ -0,0 +1,4 @@
+{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}
+{define: A#+2 base-fret 7 frets 0 1 3 3 x x fingers - 1 2 3 - -}
diff --git a/test/test_chordpro/ukulelechords.tex b/test/test_chordpro/ukulelechords.tex
new file mode 100644
index 00000000..57c247e9
--- /dev/null
+++ b/test/test_chordpro/ukulelechords.tex
@@ -0,0 +1,14 @@
+\selectlanguage{english}
+
+\beginsong{}[
+ by={
+ },
+]
+
+\utab{G}{0232}
+\utab{D7}{2223:1112}
+\gtab{G}{320003}
+\gtab{A#+2}{7:0133XX:012300}
+
+
+\endsong