Browse Source

Merge pull request #47 from patacrep/contentPlugins

Gestion de `content` par des plugins
pull/54/head
Luthaf 11 years ago
parent
commit
107d199039
  1. 2
      songbook
  2. 31
      songbook_core/authors.py
  3. 139
      songbook_core/build.py
  4. 226
      songbook_core/content/__init__.py
  5. 39
      songbook_core/content/cwd.py
  6. 60
      songbook_core/content/section.py
  7. 114
      songbook_core/content/song.py
  8. 46
      songbook_core/content/songsection.py
  9. 96
      songbook_core/content/sorted.py
  10. 58
      songbook_core/content/tex.py
  11. 10
      songbook_core/data/templates/default.tex
  12. 15
      songbook_core/data/templates/songs.tex
  13. 9
      songbook_core/files.py
  14. 31
      songbook_core/index.py
  15. 1
      songbook_core/plastex.py
  16. 124
      songbook_core/songs.py
  17. 4
      songbook_core/templates.py

2
songbook

@ -32,7 +32,7 @@ class ParseStepsAction(argparse.Action):
(
getattr(namespace, self.dest)
+ [value.strip() for value in values[0].split(',')]
),
),
)
class VerboseAction(argparse.Action):

31
songbook_core/authors.py

@ -3,6 +3,37 @@
"""Authors string management."""
import re
DEFAULT_AUTHWORDS = {
"after": ["by"],
"ignore": ["unknown"],
"sep": ["and"],
}
def compile_authwords(authwords):
"""Convert strings of authwords to compiled regular expressions.
This regexp will later be used to match these words in authors strings.
"""
# Fill missing values
for (key, value) in DEFAULT_AUTHWORDS.items():
if key not in authwords:
authwords[key] = value
# Compilation
authwords['after'] = [
re.compile(r"^.*%s\b(.*)" % word)
for word in authwords['after']
]
authwords['sep'] = [
re.compile(r"^(.*)%s (.*)$" % word)
for word in ([" %s" % word for word in authwords['sep']] + [','])
]
return authwords
def split_author_names(string):
r"""Split author between first and last name.

139
songbook_core/build.py

@ -7,23 +7,17 @@ import codecs
import glob
import logging
import os.path
import re
from subprocess import Popen, PIPE, call
from songbook_core import __DATADIR__
from songbook_core import authors
from songbook_core import content
from songbook_core import errors
from songbook_core.files import recursive_find
from songbook_core.index import process_sxd
from songbook_core.songs import Song, SongbookContent
from songbook_core.templates import TexRenderer
LOGGER = logging.getLogger(__name__)
EOL = "\n"
DEFAULT_AUTHWORDS = {
"after": ["by"],
"ignore": ["unknown"],
"sep": ["and"],
}
DEFAULT_STEPS = ['tex', 'pdf', 'sbx', 'pdf', 'clean']
GENERATED_EXTENSIONS = [
"_auth.sbx",
@ -36,6 +30,12 @@ GENERATED_EXTENSIONS = [
"_title.sbx",
"_title.sxd",
]
DEFAULT_CONFIG = {
'template': "default.tex",
'lang': 'english',
'content': [],
'titleprefixwords': [],
}
@ -50,83 +50,12 @@ class Songbook(object):
def __init__(self, raw_songbook, basename):
super(Songbook, self).__init__()
self.config = raw_songbook
self.basename = basename
# Default values: will be updated while parsing raw_songbook
self.config = {
'template': "default.tex",
'lang': 'english',
'sort': [u"by", u"album", u"@title"],
'content': None,
}
self._parse_raw(raw_songbook)
@staticmethod
def _set_songs_default(config):
"""Set the default values for the Song() class.
Argument:
- config : a dictionary containing the configuration
"""
Song.sort = config['sort']
if 'titleprefixwords' in config:
Song.prefixes = config['titleprefixwords']
else:
Song.prefixes = []
Song.authwords['after'] = [
re.compile(r"^.*%s\b(.*)" % after)
for after
in config['authwords']["after"]
]
Song.authwords['ignore'] = config['authwords']['ignore']
Song.authwords['sep'] = [
re.compile(r"^(.*)%s (.*)$" % sep)
for sep in ([
" %s" % sep for sep in config['authwords']["sep"]
] + [','])
]
def _parse_raw(self, raw_songbook):
"""Parse raw_songbook.
The principle is: some special keys have their value processed; others
are stored verbatim in self.config.
"""
self.config.update(raw_songbook)
self.contentlist = []
# Some special keys have their value processed.
self._set_datadir()
# Compute song list
if self.config['content'] is None:
self.config['content'] = [(
"song",
os.path.relpath(
filename,
os.path.join(self.config['datadir'][0], 'songs'),
))
for filename
in recursive_find(
os.path.join(self.config['datadir'][0], 'songs'),
'*.sg',
)
]
else:
content = self.config["content"]
self.config["content"] = []
for elem in content:
if isinstance(elem, basestring):
self.config["content"].append(("song", elem))
elif isinstance(elem, list):
self.config["content"].append((elem[0], elem[1]))
else:
raise errors.SBFileError(
"Syntax error: could not decode the content "
"of {0}".format(self.basename)
)
# Ensure self.config['authwords'] contains all entries
for (key, value) in DEFAULT_AUTHWORDS.items():
if key not in self.config['authwords']:
self.config['authwords'][key] = value
def _set_datadir(self):
"""Set the default values for datadir"""
try:
@ -140,16 +69,17 @@ class Songbook(object):
if os.path.exists(path) and os.path.isdir(path):
abs_datadir.append(os.path.abspath(path))
else:
LOGGER.warning("Ignoring non-existent datadir '{}'.".format(path))
LOGGER.warning(
"Ignoring non-existent datadir '{}'.".format(path)
)
abs_datadir.append(__DATADIR__)
self.config['datadir'] = abs_datadir
def _parse_songs(self):
"""Parse content included in songbook."""
self.contentlist = SongbookContent(self.config['datadir'])
self.contentlist.append_list(self.config['content'])
self.config['_songdir'] = [
os.path.join(path, 'songs')
for path in self.config['datadir']
]
def write_tex(self, output):
"""Build the '.tex' file corresponding to self.
@ -157,21 +87,32 @@ class Songbook(object):
Arguments:
- output: a file object, in which the file will be written.
"""
self._parse_songs()
# Updating configuration
config = DEFAULT_CONFIG
config.update(self.config)
renderer = TexRenderer(
self.config['template'],
self.config['datadir'],
self.config['lang'],
config['template'],
config['datadir'],
config['lang'],
)
config.update(self.config)
config.update(renderer.get_variables())
config['authwords'] = authors.compile_authwords(config['authwords'])
context = renderer.get_variables()
context.update(self.config)
context['titleprefixkeys'] = ["after", "sep", "ignore"]
context['content'] = self.contentlist
context['filename'] = output.name[:-4]
self.config = config
# Configuration set
self.contentlist = content.process_content(
self.config.get('content', []),
self.config,
)
self.config['render_content'] = content.render_content
self.config['titleprefixkeys'] = ["after", "sep", "ignore"]
self.config['content'] = self.contentlist
self.config['filename'] = output.name[:-4]
self._set_songs_default(context)
renderer.render_tex(output, context)
renderer.render_tex(output, self.config)
class SongbookBuilder(object):

226
songbook_core/content/__init__.py

@ -0,0 +1,226 @@
#!/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

39
songbook_core/content/cwd.py

@ -0,0 +1,39 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Change base directory before importing songs."""
import os
from songbook_core.content import process_content
#pylint: disable=unused-argument
def parse(keyword, config, argument, contentlist):
"""Return a list songs included in contentlist, whith a different base path.
Arguments:
- keyword: unused;
- config: the current songbook configuration dictionary;
- argument: a directory;
- contentlist: songbook content, that is parsed by
songbook_core.content.process_content().
This function adds 'argument' to the directories where songs are searched
for, and then processes the content.
The 'argument' is added:
- first as a relative path to the current directory;
- then as a relative path to every path already present in
config['songdir'].
"""
old_songdir = config['_songdir']
config['_songdir'] = (
[argument] +
[os.path.join(path, argument) for path in config['_songdir']] +
config['_songdir']
)
processed_content = process_content(contentlist, config)
config['_songdir'] = old_songdir
return processed_content
CONTENT_PLUGINS = {'cwd': parse}

60
songbook_core/content/section.py

@ -0,0 +1,60 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Allow LaTeX sections (starred or not) as content of a songbook."""
from songbook_core.content import Content, ContentError
KEYWORDS = [
"part",
"chapter",
"section",
"subsection",
"subsubsection",
"paragraph",
"subparagraph",
]
FULL_KEYWORDS = KEYWORDS + ["{}*".format(word) for word in KEYWORDS]
class Section(Content):
"""A LaTeX section."""
def __init__(self, keyword, name, short=None):
self.keyword = keyword
self.name = name
self.short = short
def render(self, __context):
if self.short is None:
return r'\{}{{{}}}'.format(self.keyword, self.name)
else:
return r'\{}[{}]{{{}}}'.format(self.keyword, self.short, self.name)
#pylint: disable=unused-argument
def parse(keyword, argument, contentlist, config):
"""Parse the contentlist.
Arguments:
- keyword (one of "part", "chapter", "section", ... , "subparagraph", and
their starred versions "part*", "chapter*", ... , "subparagraph*"): the
section to use;
- argument: unused;
- contentlist: a list of one or two strings, which are the names (short
and long) of the section;
- config: configuration dictionary of the current songbook.
"""
if (keyword not in KEYWORDS) and (len(contentlist) != 1):
raise ContentError(
keyword,
"Starred section names must have exactly one argument."
)
if (len(contentlist) not in [1, 2]):
raise ContentError(keyword, "Section can have one or two arguments.")
return [Section(keyword, *contentlist)] #pylint: disable=star-args
CONTENT_PLUGINS = dict([
(word, parse)
for word
in FULL_KEYWORDS
])

114
songbook_core/content/song.py

@ -0,0 +1,114 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Plugin to include songs to the songbook."""
import glob
import jinja2
import logging
import os
from songbook_core.content import Content, process_content, ContentError
from songbook_core import files
from songbook_core.songs import Song
LOGGER = logging.getLogger(__name__)
class SongRenderer(Content, Song):
"""Render a song in the .tex file."""
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}'
def render(self, context):
"""Return the string that will render the song."""
return r'\input{{{}}}'.format(files.relpath(
self.path,
os.path.dirname(context['filename'])
))
#pylint: disable=unused-argument
def parse(keyword, argument, contentlist, config):
"""Parse data associated with keyword 'song'.
Arguments:
- keyword: unused;
- argument: unused;
- contentlist: 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 SongRenderer() instances.
"""
if 'languages' not in config:
config['_languages'] = set()
songlist = []
for songdir in config['_songdir']:
if contentlist:
break
contentlist = [
files.relpath(filename, songdir)
for filename
in files.recursive_find(songdir, "*.sg")
]
for elem in contentlist:
before = len(songlist)
for songdir in config['_songdir']:
for filename in glob.iglob(os.path.join(songdir, elem)):
LOGGER.debug('Parsing file "{}"'.format(filename))
song = SongRenderer(filename, config)
songlist.append(song)
config["_languages"].update(song.languages)
if len(songlist) > before:
break
if len(songlist) == before:
# No songs were added
LOGGER.warning(
"Expression '{}' did not match any file".format(elem)
)
return 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):
super(OnlySongsError, self).__init__()
self.not_songs = not_songs
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 songbook_core.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

46
songbook_core/content/songsection.py

@ -0,0 +1,46 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Allow 'songchapter' and 'songsection' as content of a songbook."""
from songbook_core.content import Content, ContentError
KEYWORDS = [
"songchapter",
"songsection",
]
class SongSection(Content):
"""A songsection or songchapter."""
def __init__(self, keyword, name):
self.keyword = keyword
self.name = name
def render(self, __context):
"""Render this section or chapter."""
return r'\{}{{{}}}'.format(self.keyword, self.name)
#pylint: disable=unused-argument
def parse(keyword, argument, contentlist, config):
"""Parse the contentlist.
Arguments:
- keyword ("songsection" or "songchapter"): the section to use;
- argument: unused;
- contentlist: a list of one string, which is the name of the section;
- config: configuration dictionary of the current songbook.
"""
if (keyword not in KEYWORDS) and (len(contentlist) != 1):
raise ContentError(
keyword,
"Starred section names must have exactly one argument.",
)
return [SongSection(keyword, contentlist[0])]
CONTENT_PLUGINS = dict([
(word, parse)
for word
in KEYWORDS
])

96
songbook_core/content/sorted.py

@ -0,0 +1,96 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Sorted list of songs.
This plugin provides keyword 'sorted', used to include a sorted list of songs
to a songbook.
"""
import locale
import logging
from songbook_core import files
from songbook_core.content import ContentError
from songbook_core.content.song import OnlySongsError, process_songs
LOGGER = logging.getLogger(__name__)
DEFAULT_SORT = ['by', 'album', '@title']
def normalize_string(string):
"""Return a normalized string.
Normalized means:
- no surrounding spaces;
- lower case;
- passed through locale.strxfrm().
"""
return locale.strxfrm(string.lower().strip())
def normalize_field(field):
"""Return a normalized field, it being a string or a list of strings."""
if isinstance(field, basestring):
return normalize_string(field)
elif isinstance(field, list):
return [normalize_string(string) for string in field]
def key_generator(sort):
"""Return a function that returns the list of values used to sort the song.
Arguments:
- sort: the list of keys used to sort.
"""
def ordered_song_keys(song):
"""Return the list of values used to sort the song."""
songkey = []
for key in sort:
if key == "@title":
field = song.unprefixed_titles
elif key == "@path":
field = song.path
elif key == "by":
field = song.authors
else:
try:
field = song.args[key]
except KeyError:
LOGGER.debug(
"Ignoring unknown key '{}' for song {}.".format(
key,
files.relpath(song.path),
)
)
field = ""
songkey.append(normalize_field(field))
return songkey
return ordered_song_keys
#pylint: disable=unused-argument
def parse(keyword, config, argument, contentlist):
"""Return a sorted list of songs contained in 'contentlist'.
Arguments:
- keyword: the string 'sorted';
- config: the current songbook configuration dictionary;
- argument: the list of the fields used to sort songs, as strings
separated by commas (e.g. "by, album, @title");
- contentlist: the list of content to be sorted. If this content
contain something else than a song, an exception is raised.
"""
if argument:
sort = [key.strip() for key in argument.split(",")]
else:
sort = DEFAULT_SORT
try:
songlist = process_songs(contentlist, config)
except OnlySongsError as error:
raise ContentError(keyword, (
"Content list of this keyword can be only songs (or content "
"that result into songs), and the following are not:" +
str(error.not_songs)
))
return sorted(songlist, key=key_generator(sort))
CONTENT_PLUGINS = {'sorted': parse}

58
songbook_core/content/tex.py

@ -0,0 +1,58 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Include LaTeX raw code in the songbook."""
import logging
import os
from songbook_core import files
from songbook_core.content import Content
LOGGER = logging.getLogger(__name__)
class LaTeX(Content):
"""Inclusion of LaTeX code"""
def __init__(self, filename):
self.filename = filename
def render(self, context):
return r'\input{{{}}}'.format(files.relpath(
self.filename,
os.path.dirname(context['filename']),
))
#pylint: disable=unused-argument
def parse(keyword, argument, contentlist, config):
"""Parse the contentlist.
Arguments:
- keyword: unused;
- argument: unused;
- contentlist: a list of name of tex files;
- config: configuration dictionary of the current songbook.
"""
if not contentlist:
LOGGER.warning(
"Useless 'tex' content: list of files to include is empty."
)
filelist = []
for filename in contentlist:
checked_file = None
for path in config['_songdir']:
if os.path.exists(os.path.join(path, filename)):
checked_file = os.path.relpath(os.path.join(path, filename))
break
if not checked_file:
LOGGER.warning(
("Cannot find file '{}' in '{}'. Compilation may fail "
"later.").format(filename, str(config['_songdir']))
)
continue
filelist.append(LaTeX(checked_file))
return filelist
CONTENT_PLUGINS = {'tex': parse}

10
songbook_core/data/templates/default.tex

@ -98,11 +98,11 @@
(* endfor*)
(* for key in titleprefixkeys *)
(* for word in authwords.key *)
(* if key=="after" *)
\authbyword{((word))}
(* else *)
\auth((key))word{((word))}
(* endif *)
(* if key=="after" *)
\authbyword{((word))}
(* else *)
\auth((key))word{((word))}
(* endif *)
(* endfor *)
(* endfor*)

15
songbook_core/data/templates/songs.tex

@ -70,7 +70,7 @@
(* block songbookpreambule *)
(( super() ))
(* for lang in content.languages() *)
(* for lang in _languages *)
\PassOptionsToPackage{((lang))}{babel}
(* endfor *)
\usepackage[((lang))]{babel}
@ -93,15 +93,6 @@
\phantomsection
\addcontentsline{toc}{section}{\songlistname}
\begin{songs}{((indexes|default('')))}
(* for type, elem in content.content *)
(* if type=="song" *)
\input{((elem.path))}
(* elif type=="section" *)
\end{songs}
\songsection{((elem))}
\begin{songs}{((indexes|default('')))}
(* endif *)
(* endfor *)
\end{songs}
((render_content(content) ))
(* endblock *)

9
songbook_core/files.py

@ -17,3 +17,12 @@ def recursive_find(root_directory, pattern):
for filename in fnmatch.filter(filenames, pattern):
matches.append(os.path.join(root, filename))
return matches
def relpath(path, start=None):
"""Return relative filepath to path if a subpath of start."""
if start is None:
start = os.curdir
if os.path.abspath(path).startswith(os.path.abspath(start)):
return os.path.relpath(path, start)
else:
return os.path.abspath(path)

31
songbook_core/index.py

@ -13,7 +13,7 @@ import locale
import re
import codecs
from songbook_core.authors import processauthors
from songbook_core import authors
from songbook_core.plastex import simpleparse
EOL = u"\n"
@ -30,7 +30,9 @@ def sortkey(value):
don't forget to call locale.setlocale(locale.LC_ALL, '')). It also handles
the sort with latex escape sequences.
"""
return locale.strxfrm(unidecode(simpleparse(value).replace(' ', 'A')))
return locale.strxfrm(
unidecode(simpleparse(value).replace(' ', 'A')).lower()
)
def process_sxd(filename):
@ -65,8 +67,8 @@ class Index(object):
def __init__(self, indextype):
self.data = dict()
self.keywords = dict()
self.authwords = dict()
self.prefix_patterns = []
self.authwords = {"after": [], "ignore": [], "sep": []}
if indextype == "TITLE INDEX DATA FILE":
self.indextype = "TITLE"
elif indextype == "SCRIPTURE INDEX DATA FILE":
@ -100,26 +102,7 @@ class Index(object):
))
if self.indextype == "AUTHOR":
for key in self.keywords:
if key in self.authwords:
self.authwords[key] = self.keywords[key]
for word in self.authwords.keys():
if word in self.keywords:
if word == "after":
self.authwords[word] = [
re.compile(r"^.*{after}\b(.*)".format(after=after))
for after in self.keywords[word]
]
elif word == "sep":
self.authwords[word] = [" {sep}".format(sep=sep)
for sep in self.authwords[word]
] + [","]
self.authwords[word] = [
re.compile(r"^(.*){sep} (.*)$".format(sep=sep))
for sep in self.authwords[word]
]
else:
self.authwords[word] = self.keywords[word]
self.authwords = authors.compile_authwords(self.keywords)
def _raw_add(self, key, number, link):
"""Add a song to the list.
@ -157,7 +140,7 @@ class Index(object):
if self.indextype == "AUTHOR":
# Processing authors
for author in processauthors(
for author in authors.processauthors(
key,
**self.authwords):
self._raw_add(author, number, link)

1
songbook_core/plastex.py

@ -13,6 +13,7 @@ import sys
def process_unbr_spaces(node):
#pylint: disable=line-too-long
r"""Replace '~' and '\ ' in node by nodes that
will be rendered as unbreakable space.

124
songbook_core/songs.py

@ -4,79 +4,41 @@
"""Song management."""
from unidecode import unidecode
import glob
import locale
import os.path
import re
import logging
from songbook_core.authors import processauthors
from songbook_core.plastex import parsetex
LOGGER = logging.getLogger(__name__)
# pylint: disable=too-few-public-methods
class Song(object):
"""Song management"""
#: Ordre de tri
sort = []
#: Préfixes à ignorer pour le tri par titres
prefixes = []
#: Dictionnaire des options pour le traitement des auteurs
authwords = {"after": [], "ignore": [], "sep": []}
def __init__(self, path, languages, titles, args):
self.titles = titles
self.normalized_titles = [
locale.strxfrm(
unprefixed_title(
unidecode(unicode(title, "utf-8")),
self.prefixes
)
def __init__(self, filename, config):
# Data extraction from the song with plastex
data = parsetex(filename)
self.titles = data['titles']
self.unprefixed_titles = [
unprefixed_title(
unidecode(unicode(title, "utf-8")),
config['titleprefixwords']
)
for title
in titles
in self.titles
]
self.args = args
self.path = path
self.languages = languages
self.args = data['args']
self.path = filename
self.languages = data['languages']
if "by" in self.args.keys():
self.normalized_authors = [
locale.strxfrm(author)
for author
in processauthors(self.args["by"], **self.authwords)
]
self.authors = processauthors(
self.args["by"],
**config["authwords"]
)
else:
self.normalized_authors = []
self.authors = []
def __repr__(self):
return repr((self.titles, self.args, self.path))
def __cmp__(self, other):
if not isinstance(other, Song):
return NotImplemented
for key in self.sort:
if key == "@title":
self_key = self.normalized_titles
other_key = other.normalized_titles
elif key == "@path":
self_key = locale.strxfrm(self.path)
other_key = locale.strxfrm(other.path)
elif key == "by":
self_key = self.normalized_authors
other_key = other.normalized_authors
else:
self_key = locale.strxfrm(self.args.get(key, ""))
other_key = locale.strxfrm(other.args.get(key, ""))
if self_key < other_key:
return -1
elif self_key > other_key:
return 1
return 0
def unprefixed_title(title, prefixes):
"""Remove the first prefix of the list in the beginning of title (if any).
"""
@ -87,55 +49,3 @@ def unprefixed_title(title, prefixes):
return title
class SongbookContent(object):
"""Manipulation et traitement de liste de chansons"""
def __init__(self, datadirs):
self.songdirs = [os.path.join(d, 'songs')
for d in datadirs]
self.content = [] # Sorted list of the content
def append_song(self, filename):
"""Ajout d'une chanson à la liste
Effets de bord : analyse syntaxique plus ou moins sommaire du fichier
pour en extraire et traiter certaines information (titre, langue,
album, etc.).
"""
LOGGER.debug('Parsing file "{}"'.format(filename))
# Data extraction from the song with plastex
data = parsetex(filename)
song = Song(filename, data['languages'], data['titles'], data['args'])
self.content.append(("song", song))
def append(self, type, value):
""" Append a generic element to the content list"""
self.content.append((type, value))
def append_list(self, contentlist):
"""Ajoute une liste de chansons à la liste
L'argument est une liste de chaînes, représentant des noms de fichiers
sous la forme d'expressions régulières destinées à être analysées avec
le module glob.
"""
for type, elem in contentlist:
if type == "song":
# Add all the songs matching the regex
before = len(self.content)
for songdir in self.songdirs:
for filename in glob.iglob(os.path.join(songdir, elem)):
self.append_song(filename)
if len(self.content) > before:
break
if len(self.content) == before:
# No songs were added
LOGGER.warning(
"Expression '{}' did not match any file".format(elem)
)
else:
self.append(type, elem)
def languages(self):
"""Renvoie la liste des langues utilisées par les chansons"""
return set().union(*[set(song.languages) for type, song in self.content if type=="song"])

4
songbook_core/templates.py

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
"""Template for .tex generation settings and utilities"""
from jinja2 import Environment, FileSystemLoader, ChoiceLoader, PackageLoader, \
from jinja2 import Environment, FileSystemLoader, ChoiceLoader, \
TemplateNotFound, nodes
from jinja2.ext import Extension
from jinja2.meta import find_referenced_templates as find_templates
@ -54,7 +54,7 @@ class VariablesExtension(Extension):
end_tokens=['name:endvariables'],
drop_needle=True,
)
return nodes.Const("")
return nodes.Const("") # pylint: disable=no-value-for-parameter
def _escape_tex(value):

Loading…
Cancel
Save