|
|
@ -3,8 +3,8 @@ |
|
|
|
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. |
|
|
|
module), which parses the content, and return a ContentList object, which is |
|
|
|
little more than a list of instances of the ContentItem class. |
|
|
|
|
|
|
|
# Plugin definition |
|
|
|
|
|
|
@ -27,8 +27,8 @@ A parser is a function which takes as arguments: |
|
|
|
- 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). |
|
|
|
A parser returns a ContentList object (a list of instances of the ContentItem |
|
|
|
class), defined in this module (or of subclasses of this class). |
|
|
|
|
|
|
|
Example: When the following piece of content is met |
|
|
|
|
|
|
@ -55,13 +55,13 @@ 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 |
|
|
|
# ContentItem 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 |
|
|
|
The content classes are subclasses of class ContentItem defined in this module. |
|
|
|
ContentItem is a perfectly valid class, but instances of it will not generate |
|
|
|
anything in the resulting .tex. |
|
|
|
|
|
|
|
More documentation in the docstring of Content. |
|
|
|
More documentation in the docstring of ContentItem. |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
@ -74,13 +74,13 @@ import sys |
|
|
|
import jinja2 |
|
|
|
|
|
|
|
from patacrep import files |
|
|
|
from patacrep.errors import SongbookError |
|
|
|
from patacrep.errors import SharedError |
|
|
|
|
|
|
|
LOGGER = logging.getLogger(__name__) |
|
|
|
EOL = '\n' |
|
|
|
|
|
|
|
#pylint: disable=no-self-use |
|
|
|
class Content(object): |
|
|
|
class ContentItem: |
|
|
|
"""Content item. Will render to something in the .tex file. |
|
|
|
|
|
|
|
The current jinja2.runtime.Context is passed to all function defined |
|
|
@ -101,8 +101,8 @@ class Content(object): |
|
|
|
|
|
|
|
# Arguments |
|
|
|
|
|
|
|
- __previous: the songbook.content.Content object of the previous item. |
|
|
|
- __context: see Content() documentation. |
|
|
|
- __previous: the songbook.content.ContentItem object of the previous item. |
|
|
|
- __context: see ContentItem() documentation. |
|
|
|
|
|
|
|
# Return |
|
|
|
|
|
|
@ -121,15 +121,81 @@ class Content(object): |
|
|
|
"""Return the string to end a block.""" |
|
|
|
return "" |
|
|
|
|
|
|
|
class ContentError(SongbookError): |
|
|
|
class ContentError(SharedError): |
|
|
|
"""Error in a content plugin.""" |
|
|
|
def __init__(self, keyword, message): |
|
|
|
def __init__(self, keyword=None, message=None): |
|
|
|
super().__init__() |
|
|
|
self.keyword = keyword |
|
|
|
self.message = message |
|
|
|
|
|
|
|
def __str__(self): |
|
|
|
return "Content: {}: {}".format(self.keyword, self.message) |
|
|
|
text = "Content" |
|
|
|
if self.keyword is not None: |
|
|
|
text += ": " + self.keyword |
|
|
|
if self.message is not None: |
|
|
|
text += ": " + self.message |
|
|
|
return text |
|
|
|
|
|
|
|
@property |
|
|
|
def __dict__(self): |
|
|
|
parent = vars(super()) |
|
|
|
parent.update({ |
|
|
|
'keyword': self.keyword, |
|
|
|
'message': self.message, |
|
|
|
}) |
|
|
|
return parent |
|
|
|
|
|
|
|
class ContentList: |
|
|
|
"""List of content items""" |
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs): |
|
|
|
self._content = list(*args, **kwargs) |
|
|
|
self._errors = [] |
|
|
|
|
|
|
|
def __iter__(self): |
|
|
|
yield from self._content |
|
|
|
|
|
|
|
def extend(self, iterator): |
|
|
|
"""Extend content list with an iterator. |
|
|
|
|
|
|
|
If the argument is of the same type, the list of errors is |
|
|
|
also extended. |
|
|
|
""" |
|
|
|
self._content.extend(iterator) |
|
|
|
if isinstance(iterator, ContentList): |
|
|
|
self._errors.extend(iterator.iter_errors()) |
|
|
|
|
|
|
|
def append(self, item): |
|
|
|
"""Append an item to the content list.""" |
|
|
|
return self._content.append(item) |
|
|
|
|
|
|
|
def __len__(self): |
|
|
|
return len(self._content) |
|
|
|
|
|
|
|
def append_error(self, error): |
|
|
|
"""Log and append an error to the error list.""" |
|
|
|
LOGGER.warning(error) |
|
|
|
self._errors.append(error) |
|
|
|
|
|
|
|
def extend_error(self, errors): |
|
|
|
"""Extend the error list with the argument, which is logged.""" |
|
|
|
for error in errors: |
|
|
|
self.append_error(error) |
|
|
|
|
|
|
|
def iter_errors(self): |
|
|
|
"""Iterate over errors.""" |
|
|
|
yield from self._errors |
|
|
|
for item in self: |
|
|
|
if not hasattr(item, "iter_errors"): |
|
|
|
continue |
|
|
|
yield from item.iter_errors() |
|
|
|
|
|
|
|
class EmptyContentList(ContentList): |
|
|
|
"""Empty content list: contain only errors.""" |
|
|
|
def __init__(self, *, errors): |
|
|
|
super().__init__() |
|
|
|
for error in errors: |
|
|
|
self.append_error(error) |
|
|
|
|
|
|
|
@jinja2.contextfunction |
|
|
|
def render(context, content): |
|
|
@ -138,15 +204,15 @@ def render(context, content): |
|
|
|
Arguments: |
|
|
|
- context: the jinja2.runtime.context of the current template |
|
|
|
compilation. |
|
|
|
- content: a list of Content() instances, as the one that was returned by |
|
|
|
- content: a list of ContentItem() 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)) |
|
|
|
if not isinstance(elem, ContentItem): |
|
|
|
LOGGER.warning("Ignoring bad content item '{}'.".format(elem)) |
|
|
|
continue |
|
|
|
|
|
|
|
last = elem |
|
|
@ -157,23 +223,23 @@ def render(context, content): |
|
|
|
rendered += elem.render(context) + EOL |
|
|
|
previous = elem |
|
|
|
|
|
|
|
if isinstance(last, Content): |
|
|
|
if last is not None: |
|
|
|
rendered += last.end_block(context) + EOL |
|
|
|
|
|
|
|
return rendered |
|
|
|
|
|
|
|
def process_content(content, config=None): |
|
|
|
"""Process content, and return a list of Content() objects. |
|
|
|
"""Process content, and return a list of ContentItem() 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 |
|
|
|
Return: a list of ContentItem objects, corresponding to the content to be |
|
|
|
included in the .tex file. |
|
|
|
""" |
|
|
|
contentlist = [] |
|
|
|
contentlist = ContentList() |
|
|
|
plugins = config.get('_content_plugins', {}) |
|
|
|
keyword_re = re.compile(r'^ *(?P<keyword>[\w\*]*) *(\((?P<argument>.*)\))? *$') |
|
|
|
if not content: |
|
|
@ -184,10 +250,12 @@ def process_content(content, config=None): |
|
|
|
try: |
|
|
|
match = keyword_re.match(elem[0]).groupdict() |
|
|
|
except AttributeError: |
|
|
|
raise ContentError(elem[0], "Cannot parse content type.") |
|
|
|
contentlist.append_error(ContentError(elem[0], "Cannot parse content type.")) |
|
|
|
continue |
|
|
|
(keyword, argument) = (match['keyword'], match['argument']) |
|
|
|
if keyword not in plugins: |
|
|
|
raise ContentError(keyword, "Unknown content type.") |
|
|
|
contentlist.append_error(ContentError(keyword, "Unknown content type.")) |
|
|
|
continue |
|
|
|
contentlist.extend(plugins[keyword]( |
|
|
|
keyword, |
|
|
|
argument=argument, |
|
|
|