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.

342 lines
11 KiB

#!/usr/bin/env python
# -*- coding: utf-8 -*-
11 years ago
"""Build a songbook, according to parameters found in a .sb file."""
import codecs
import glob
import json
import logging
import os.path
import re
import subprocess
from songbook_core import __SHAREDIR__
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, SongsList
EOL = "\n"
def parse_template(template):
11 years ago
"""Return the list of parameters defined in the template."""
embedded_json_pattern = re.compile(r"^%%:")
with open(template) as template_file:
11 years ago
code = [
line[3:-1]
for line
in template_file
if embedded_json_pattern.match(line)
11 years ago
]
data = json.loads(''.join(code))
parameters = dict()
for param in data:
parameters[param["name"]] = param
return parameters
# pylint: disable=too-many-return-statements
def to_value(parameter, data):
"""Convert 'data' to a LaTeX string.
Conversion is done according to the template parameter it corresponds to.
"""
if "type" not in parameter:
return data
elif parameter["type"] == "stringlist":
if "join" in parameter:
join_text = parameter["join"]
else:
join_text = ''
return join_text.join(data)
elif parameter["type"] == "color":
return data[1:]
elif parameter["type"] == "font":
return data + 'pt'
elif parameter["type"] == "enum":
return data
elif parameter["type"] == "file":
return data
elif parameter["type"] == "flag":
if "join" in parameter:
join_text = parameter["join"]
else:
join_text = ''
return join_text.join(data)
def format_declaration(name, parameter):
"""Return LaTeX code to declare variale 'name'.
'parameter' is the corresponding line of the template (which can provide
default value).
"""
value = ""
if "default" in parameter:
value = parameter["default"]
11 years ago
return (
r'\def\set@{name}#1{{\def\get{name}{{#1}}}}'.format(name=name)
+ EOL
+ format_definition(name, to_value(parameter, value))
)
def format_definition(name, value):
"""Write LaTeX code to set a value to a variable"""
return r'\set@{name}{{{value}}}'.format(name=name, value=value) + EOL
def clean(basename):
11 years ago
"""Clean (some) temporary files used during compilation.
Depending of the LaTeX modules used in the template, there may be others
that are note deleted by this function."""
generated_extensions = [
"_auth.sbx",
"_auth.sxd",
".aux",
".log",
".out",
".sxc",
".tex",
"_title.sbx",
"_title.sxd",
]
for ext in generated_extensions:
try:
os.unlink(basename + ext)
except Exception as exception:
raise errors.CleaningError(basename + ext, exception)
DEFAULT_AUTHWORDS = {
"after": ["by"],
"ignore": ["unknown"],
"sep": ["and"],
}
# pylint: disable=too-few-public-methods
class Songbook(object):
"""Reprensent a songbook (.sb) file.
- Low level: provide a Python representation of the values stored in the
'.sb' file.
- High level: provide some utility functions to manipulate these data.
"""
def __init__(self, raw_songbook, basename):
super(Songbook, self).__init__()
self.basename = basename
# Default values: will be updated while parsing raw_songbook
self.config = {
'template': os.path.join(
__SHAREDIR__,
"templates",
"default.tmpl",
),
'titleprefixwords': [],
'authwords': {},
'lang': 'french',
'sort': [u"by", u"album", u"@title"],
'songs': None,
'datadir': os.path.abspath('.'),
}
self.songslist = None
self._parse(raw_songbook)
self._set_songs_default()
def _set_songs_default(self):
"""Set the default values for the Song() class."""
Song.sort = self.config['sort']
Song.prefixes = self.config['titleprefixwords']
Song.authwords['after'] = [
re.compile(r"^.*%s\b(.*)" % after)
for after
in self.config['authwords']["after"]
]
Song.authwords['ignore'] = self.config['authwords']['ignore']
Song.authwords['sep'] = [
re.compile(r"^(.*)%s (.*)$" % sep)
for sep
in ([
" %s" % sep
for sep
in self.config['authwords']["sep"]
] + [','])
11 years ago
]
def _parse(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)
### Some post-processing
# Compute song list
if self.config['songs'] is None:
self.config['songs'] = [
os.path.relpath(
filename,
os.path.join(self.config['datadir'], 'songs'),
)
for filename
in recursive_find(
os.path.join(self.config['datadir'], 'songs'),
'*.sg',
)
]
self.songslist = SongsList(self.config['datadir'], self.config["lang"])
self.songslist.append_list(self.config['songs'])
# 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 write_tex(self, output):
r"""Build the '.tex' file corresponding to self.
Arguments:
- output: a file object, in which the file will be written.
TODO: Update this.
Quelques notes concernant le rendu
- self.config['titleprefixwords']
> return EOL.join([
> r"\titleprefixwords{%s}" % prefix
> for prefix
> in self.config['titleprefixwords']
> ])
- self.config['authwords']:
> # Processing authwords values
> tex = []
> for key in ["after", "sep", "ignore"]:
> for word in self.config['authwords'][key]:
> if key == "after":
> tex.append(r"\auth%sword{%s}" % ("by", word))
> else:
> tex.append(r"\auth%sword{%s}" % (key, word))
> return EOL.join(tex)
- Liste des langues utilisées (pour chargement par babel):
self.songlist.languages(). Donc la commande pour Babel peut
ressembler à :
> r"\PassOptionsToPackage{%s}{babel}" % ",".join(
> self.songslist.languages()
> )
"""
parameters = parse_template(os.path.join(
self.config['datadir'],
'templates',
self.config['template']
))
output.write((
'%% This file has been automatically '
'generated, do not edit!\n'
))
output.write(r'\makeatletter' + EOL)
# output automatic parameters
output.write(format_declaration("name", {"default": self.basename}))
output.write(format_declaration("songslist", {"type": "stringlist"}))
# output template parameter command
for name, parameter in parameters.iteritems():
output.write(format_declaration(name, parameter))
# output template parameter values
for name, value in self.config.iteritems():
if name in parameters:
output.write(format_definition(
name,
to_value(parameters[name], value),
))
if len(self.config['songs']) > 0:
output.write(format_definition('songslist', self.songslist.latex()))
output.write(r'\makeatother' + EOL)
# output template
comment_pattern = re.compile(r"^\s*%")
with codecs.open(
os.path.join(
self.config['datadir'],
'templates',
self.config['template']
),
'r',
'utf-8',
) as template_file:
content = [
line
for line
in template_file
if not comment_pattern.match(line)
]
output.write(u''.join(content))
def buildsongbook(
raw_songbook,
basename,
interactive=False,
logger=logging.getLogger()
):
"""Build a songbook
Arguments:
- raw_songbook: Python representation of the .sb songbook configuration
file.
- basename: basename of the songbook to be built.
- interactive: in False, do not expect anything from stdin.
"""
songbook = Songbook(raw_songbook, basename)
with codecs.open("{}.tex".format(basename), 'w', 'utf-8') as output:
songbook.write_tex(output)
if not 'TEXINPUTS' in os.environ.keys():
os.environ['TEXINPUTS'] = ''
os.environ['TEXINPUTS'] += os.pathsep + os.path.join(
__SHAREDIR__,
'latex',
)
os.environ['TEXINPUTS'] += os.pathsep + os.path.join(
songbook.config['datadir'],
'latex',
)
# pdflatex options
pdflatex_options = []
pdflatex_options.append("--shell-escape") # Lilypond compilation
if not interactive:
pdflatex_options.append("-halt-on-error")
# First pdflatex pass
if subprocess.call(["pdflatex"] + pdflatex_options + [basename]):
raise errors.LatexCompilationError(basename)
# Make index
sxd_files = glob.glob("%s_*.sxd" % basename)
for sxd_file in sxd_files:
logger.info("processing " + sxd_file)
idx = process_sxd(sxd_file)
index_file = open(sxd_file[:-3] + "sbx", "w")
index_file.write(idx.entries_to_str().encode('utf8'))
index_file.close()
# Second pdflatex pass
if subprocess.call(["pdflatex"] + pdflatex_options + [basename]):
raise errors.LatexCompilationError(basename)
# Cleaning
clean(basename)