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.
226 lines
7.2 KiB
226 lines
7.2 KiB
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""Content plugin management.
|
|
|
|
Content that can be included in a songbook is controlled by plugins. From the
|
|
user (or .sb file) point of view, each piece of content is introduced by a
|
|
keyword. This keywold is associated with a plugin (a submodule of this very
|
|
module), which parses the content, and return a list of instances of the
|
|
Content class.
|
|
|
|
# Plugin definition
|
|
|
|
A plugin is a submodule of this module, which have a variable
|
|
CONTENT_PLUGINS, which is a dictionary where:
|
|
- keys are keywords,
|
|
- values are parsers (see below).
|
|
|
|
When analysing the content field of the .sb file, when those keywords are
|
|
met, the corresponding parser is called.
|
|
|
|
# Parsers
|
|
|
|
A parser is a function which takes as arguments:
|
|
- keyword: the keyword triggering this function;
|
|
- argument: the argument of the keyword (see below);
|
|
- contentlist: the list of content, that is, the part of the list
|
|
following the keyword (see example below);
|
|
- config: the configuration object of the current songbook. Plugins can
|
|
change it.
|
|
|
|
A parser returns a list of instances of the Content class, defined in
|
|
this module (or of subclasses of this class).
|
|
|
|
Example: When the following piece of content is met
|
|
|
|
["sorted(author, @title)", "a_song.sg", "another_song.sg"]
|
|
|
|
the parser associated to keyword 'sorted' get the arguments:
|
|
- keyword = "sorted"
|
|
- argument = "author, @title"
|
|
- contentlist = ["a_song.sg", "another_song.sg"]
|
|
- config = <the config file of the current songbook>.
|
|
|
|
# Keyword
|
|
|
|
A keyword is either an identifier (alphanumeric characters, and underscore),
|
|
or such an identifier, with some text surrounded by parenthesis (like a
|
|
function definition); this text is called the argument to the keyword.
|
|
Examples:
|
|
- sorted
|
|
- sorted(author, @title)
|
|
- cwd(some/path)
|
|
|
|
If the keyword has an argument, it can be anything, given that it is
|
|
surrounded by parenthesis. It is up to the plugin to parse this argument. For
|
|
intance, keyword "foo()(( bar()" is a perfectly valid keyword, and the parser
|
|
associated to "foo" will get as argument the string ")(( bar(".
|
|
|
|
# Content class
|
|
|
|
The content classes are subclasses of class Content defined in this module.
|
|
Content is a perfectly valid class, but instances of it will not generate
|
|
anything in the resulting .tex.
|
|
|
|
More documentation in the docstring of Content.
|
|
|
|
"""
|
|
|
|
import glob
|
|
import importlib
|
|
import jinja2
|
|
import logging
|
|
import os
|
|
import re
|
|
|
|
from songbook_core import files
|
|
from songbook_core.errors import SongbookError
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
EOL = '\n'
|
|
|
|
#pylint: disable=no-self-use
|
|
class Content(object):
|
|
"""Content item. Will render to something in the .tex file.
|
|
|
|
The current jinja2.runtime.Context is passed to all function defined
|
|
here.
|
|
"""
|
|
|
|
def render(self, __context):
|
|
"""Render this content item.
|
|
|
|
Returns a string, to be placed verbatim in the generated .tex file.
|
|
"""
|
|
return ""
|
|
|
|
# Block management
|
|
|
|
def begin_new_block(self, __previous, __context):
|
|
"""Return a boolean stating if a new block is to be created.
|
|
|
|
# Arguments
|
|
|
|
- __previous: the songbook.content.Content object of the previous item.
|
|
- __context: see Content() documentation.
|
|
|
|
# Return
|
|
|
|
- True if the renderer has to close previous block, and begin a new
|
|
one,
|
|
- False otherwise (the generated code for this item is part of the
|
|
current block).
|
|
"""
|
|
return True
|
|
|
|
def begin_block(self, __context):
|
|
"""Return the string to begin a block."""
|
|
return ""
|
|
|
|
def end_block(self, __context):
|
|
"""Return the string to end a block."""
|
|
return ""
|
|
|
|
class ContentError(SongbookError):
|
|
"""Error in a content plugin."""
|
|
def __init__(self, keyword, message):
|
|
super(ContentError, self).__init__()
|
|
self.keyword = keyword
|
|
self.message = message
|
|
|
|
def __str__(self):
|
|
return "Content: {}: {}".format(self.keyword, self.message)
|
|
|
|
def load_plugins():
|
|
"""Load all content plugins, and return a dictionary of those plugins.
|
|
|
|
Return value: a dictionary where:
|
|
- keys are the keywords ;
|
|
- values are functions triggered when this keyword is met.
|
|
"""
|
|
plugins = {}
|
|
for name in glob.glob(os.path.join(os.path.dirname(__file__), "*.py")):
|
|
if name.endswith(".py") and os.path.basename(name) != "__init__.py":
|
|
plugin = importlib.import_module(
|
|
'songbook_core.content.{}'.format(
|
|
os.path.basename(name[:-len('.py')])
|
|
)
|
|
)
|
|
for (key, value) in plugin.CONTENT_PLUGINS.items():
|
|
if key in plugins:
|
|
LOGGER.warning(
|
|
"File %s: Keyword '%s' is already used. Ignored.",
|
|
files.relpath(name),
|
|
key,
|
|
)
|
|
continue
|
|
plugins[key] = value
|
|
return plugins
|
|
|
|
@jinja2.contextfunction
|
|
def render_content(context, content):
|
|
"""Render the content of the songbook as a LaTeX code.
|
|
|
|
Arguments:
|
|
- context: the jinja2.runtime.context of the current template
|
|
compilation.
|
|
- content: a list of Content() instances, as the one that was returned by
|
|
process_content().
|
|
"""
|
|
rendered = ""
|
|
previous = None
|
|
last = None
|
|
for elem in content:
|
|
if not isinstance(elem, Content):
|
|
LOGGER.error("Ignoring bad content item '{}'.".format(elem))
|
|
continue
|
|
|
|
last = elem
|
|
if elem.begin_new_block(previous, context):
|
|
if previous:
|
|
rendered += previous.end_block(context) + EOL
|
|
rendered += elem.begin_block(context) + EOL
|
|
rendered += elem.render(context) + EOL
|
|
previous = elem
|
|
|
|
if isinstance(last, Content):
|
|
rendered += last.end_block(context) + EOL
|
|
|
|
return rendered
|
|
|
|
def process_content(content, config=None):
|
|
"""Process content, and return a list of Content() objects.
|
|
|
|
Arguments are:
|
|
- content: the content field of the .sb file, which should be a list, and
|
|
describe what is to be included in the songbook;
|
|
- config: the configuration dictionary of the current songbook.
|
|
|
|
Return: a list of Content objects, corresponding to the content to be
|
|
included in the .tex file.
|
|
"""
|
|
contentlist = []
|
|
plugins = load_plugins()
|
|
keyword_re = re.compile(r'^ *(?P<keyword>\w*) *(\((?P<argument>.*)\))? *$')
|
|
if not content:
|
|
content = [["song"]]
|
|
for elem in content:
|
|
if isinstance(elem, basestring):
|
|
elem = ["song", elem]
|
|
if len(content) == 0:
|
|
content = ["song"]
|
|
try:
|
|
match = keyword_re.match(elem[0]).groupdict()
|
|
except AttributeError:
|
|
raise ContentError(elem[0], "Cannot parse content type.")
|
|
(keyword, argument) = (match['keyword'], match['argument'])
|
|
if keyword not in plugins:
|
|
raise ContentError(keyword, "Unknown content type.")
|
|
contentlist.extend(plugins[keyword](
|
|
keyword,
|
|
argument=argument,
|
|
contentlist=elem[1:],
|
|
config=config,
|
|
))
|
|
return contentlist
|
|
|