mirror of https://github.com/patacrep/patacrep.git
Engine for LaTeX songbooks
http://www.patacrep.com
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
186 lines
6.4 KiB
186 lines
6.4 KiB
"""Plugin to include songs to the songbook."""
|
|
|
|
import glob
|
|
import logging
|
|
import os
|
|
import textwrap
|
|
|
|
import jinja2
|
|
|
|
from patacrep.content import process_content, validate_parser_argument
|
|
from patacrep.content import ContentError, ContentItem, ContentList
|
|
from patacrep import files, errors
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
class SongRenderer(ContentItem):
|
|
"""Render a song in as a tex code."""
|
|
|
|
def __init__(self, song):
|
|
super().__init__()
|
|
self.song = song
|
|
|
|
def iter_errors(self):
|
|
"""Iterate over song errors."""
|
|
yield from self.song.errors
|
|
|
|
def has_errors(self):
|
|
"""Return `True` iff errors has been found."""
|
|
for _ in self.iter_errors():
|
|
return True
|
|
return False
|
|
|
|
def begin_new_block(self, previous, context):
|
|
"""Return a boolean stating if a new block is to be created."""
|
|
return not isinstance(previous, SongRenderer)
|
|
|
|
def begin_block(self, context):
|
|
"""Return the string to begin a block."""
|
|
indexes = context.resolve("indexes")
|
|
if isinstance(indexes, jinja2.runtime.Undefined):
|
|
indexes = ""
|
|
return r'\begin{songs}{%s}' % indexes
|
|
|
|
def end_block(self, context):
|
|
"""Return the string to end a block."""
|
|
return r'\end{songs}'
|
|
|
|
#pylint: disable=unused-argument
|
|
def render(self, context):
|
|
"""Return the string that will render the song."""
|
|
return textwrap.dedent("""\
|
|
{separator}
|
|
%% {path}
|
|
|
|
{song}
|
|
""").format(
|
|
separator="%"*80,
|
|
path=files.path2posix(self.song.subpath),
|
|
song=self.song.render(),
|
|
)
|
|
|
|
def __lt__(self, other):
|
|
"""Order by song path"""
|
|
return self.song.fullpath < other.song.fullpath
|
|
|
|
def to_dict(self):
|
|
return {'song': self.song.fullpath}
|
|
|
|
#pylint: disable=unused-argument
|
|
#pylint: disable=too-many-branches
|
|
@validate_parser_argument("""
|
|
type: //any
|
|
of:
|
|
- type: //nil
|
|
- type: //str
|
|
- type: //arr
|
|
contents: //str
|
|
""")
|
|
def parse(keyword, argument, config):
|
|
"""Parse data associated with keyword 'song'.
|
|
|
|
Arguments:
|
|
- keyword: unused;
|
|
- argument: a list of strings, which are interpreted as regular
|
|
expressions (interpreted using the glob module), referring to songs.
|
|
- config: the current songbook configuration dictionary.
|
|
|
|
Return a list of Song() instances.
|
|
"""
|
|
contentlist = argument
|
|
if isinstance(contentlist, str):
|
|
contentlist = [contentlist]
|
|
plugins = files.load_renderer_plugins(config['_datadir'])['tsg']
|
|
if '_langs' not in config:
|
|
config['_langs'] = set()
|
|
songlist = ContentList()
|
|
for songdir in config['_songdir']:
|
|
if contentlist:
|
|
break
|
|
contentlist = files.recursive_find(songdir.fullpath, plugins.keys())
|
|
if contentlist is None:
|
|
contentlist = [] # No content was set or found
|
|
for elem in contentlist:
|
|
before = len(songlist)
|
|
for songdir in config['_songdir']:
|
|
if not os.path.isdir(songdir.datadir):
|
|
continue
|
|
with files.chdir(songdir.datadir):
|
|
# Starting with Python 3.5 glob can be recursive: **/*.csg for instance
|
|
# for filename in glob.iglob(os.path.join(songdir.subpath, elem), recursive=True):
|
|
for filename in glob.iglob(os.path.join(songdir.subpath, elem)):
|
|
LOGGER.debug('Parsing file "{}"…'.format(filename))
|
|
extension = filename.split(".")[-1]
|
|
if extension not in plugins:
|
|
LOGGER.info(
|
|
(
|
|
'Cannot parse "%s": name does not end with one '
|
|
'of %s. Ignored.'
|
|
),
|
|
os.path.join(songdir.datadir, filename),
|
|
", ".join(["'.{}'".format(key) for key in plugins.keys()])
|
|
)
|
|
continue
|
|
try:
|
|
renderer = SongRenderer(plugins[extension](
|
|
filename,
|
|
config,
|
|
datadir=songdir.datadir,
|
|
))
|
|
except ContentError as error:
|
|
songlist.append_error(error)
|
|
if config['_error'] == "failonsong":
|
|
raise errors.SongbookError(
|
|
"Error in song '{}'. Stopping as requested."
|
|
.format(os.path.join(songdir.fullpath, filename))
|
|
)
|
|
continue
|
|
if renderer.has_errors() and config['_error'] == "failonsong":
|
|
raise errors.SongbookError(
|
|
"Error in song '{}'. Stopping as requested."
|
|
.format(os.path.join(songdir.fullpath, filename))
|
|
)
|
|
songlist.append(renderer)
|
|
config["_langs"].add(renderer.song.lang)
|
|
if len(songlist) > before:
|
|
break
|
|
if len(songlist) == before:
|
|
# No songs were added
|
|
LOGGER.warning(errors.notfound(
|
|
elem,
|
|
[item.fullpath for item in config['_songdir']],
|
|
message='Ignoring "{name}": did not match any file in {paths}.',
|
|
))
|
|
return sorted(songlist)
|
|
|
|
CONTENT_PLUGINS = {'song': parse}
|
|
|
|
|
|
class OnlySongsError(ContentError):
|
|
"A list that should contain only songs also contain other type of content."
|
|
def __init__(self, not_songs):
|
|
self.not_songs = not_songs
|
|
super().__init__('song', str(self))
|
|
|
|
def __str__(self):
|
|
return (
|
|
"Only songs are allowed, and the following items are not:" +
|
|
str(self.not_songs)
|
|
)
|
|
|
|
def process_songs(content, config=None):
|
|
"""Process content that containt only songs.
|
|
|
|
Call patacrep.content.process_content(), checks if the returned list
|
|
contains only songs, and raise an exception if not.
|
|
"""
|
|
contentlist = process_content(content, config)
|
|
not_songs = [
|
|
item
|
|
for item
|
|
in contentlist
|
|
if not isinstance(item, SongRenderer)
|
|
]
|
|
if not_songs:
|
|
raise OnlySongsError(not_songs)
|
|
return contentlist
|
|
|