|
@ -16,12 +16,18 @@ from songbook_core import errors |
|
|
from songbook_core.files import recursive_find |
|
|
from songbook_core.files import recursive_find |
|
|
from songbook_core.index import process_sxd |
|
|
from songbook_core.index import process_sxd |
|
|
from songbook_core.songs import Song, SongsList |
|
|
from songbook_core.songs import Song, SongsList |
|
|
|
|
|
from songbook_core.templates import TexRenderer |
|
|
|
|
|
|
|
|
EOL = "\n" |
|
|
EOL = "\n" |
|
|
|
|
|
DEFAULT_AUTHWORDS = { |
|
|
|
|
|
"after": ["by"], |
|
|
|
|
|
"ignore": ["unknown"], |
|
|
|
|
|
"sep": ["and"], |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_template(template): |
|
|
def parse_template(template): |
|
|
"""Return the list of parameters defined in the template.""" |
|
|
"""Return the dict of default parameters defined in the template.""" |
|
|
embedded_json_pattern = re.compile(r"^%%:") |
|
|
embedded_json_pattern = re.compile(r"^%%:") |
|
|
with open(template) as template_file: |
|
|
with open(template) as template_file: |
|
|
code = [ |
|
|
code = [ |
|
@ -31,65 +37,119 @@ def parse_template(template): |
|
|
if embedded_json_pattern.match(line) |
|
|
if embedded_json_pattern.match(line) |
|
|
] |
|
|
] |
|
|
|
|
|
|
|
|
data = json.loads(''.join(code)) |
|
|
|
|
|
parameters = dict() |
|
|
parameters = dict() |
|
|
for param in data: |
|
|
if code: |
|
|
parameters[param["name"]] = param |
|
|
data = json.loads(''.join(code)) |
|
|
|
|
|
for param in data: |
|
|
|
|
|
try: |
|
|
|
|
|
parameters[param["name"]] = param["default"] |
|
|
|
|
|
except KeyError: |
|
|
|
|
|
parameters[param["name"]] = None |
|
|
return parameters |
|
|
return parameters |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# pylint: disable=too-many-return-statements |
|
|
# pylint: disable=too-few-public-methods |
|
|
def to_value(parameter, data): |
|
|
class Songbook(object): |
|
|
"""Convert 'data' to a LaTeX string. |
|
|
"""Represent a songbook (.sb) file. |
|
|
|
|
|
|
|
|
Conversion is done according to the template parameter it corresponds to. |
|
|
- Low level: provide a Python representation of the values stored in the |
|
|
|
|
|
'.sb' file. |
|
|
|
|
|
- High level: provide some utility functions to manipulate these data. |
|
|
""" |
|
|
""" |
|
|
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): |
|
|
|
|
|
"""Write LaTeX code to declare a variable""" |
|
|
|
|
|
value = "" |
|
|
|
|
|
if "default" in parameter: |
|
|
|
|
|
value = parameter["default"] |
|
|
|
|
|
return ( |
|
|
|
|
|
r'\def\set@{name}#1{{\def\get{name}{{#1}}}}'.format(name=name) |
|
|
|
|
|
+ EOL |
|
|
|
|
|
+ format_definition(name, to_value(parameter, value)) |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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': "default.tex", |
|
|
|
|
|
'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"] |
|
|
|
|
|
] + [',']) |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
def format_definition(name, value): |
|
|
def _parse(self, raw_songbook): |
|
|
"""Write LaTeX code to set a value to a variable""" |
|
|
"""Parse raw_songbook. |
|
|
return r'\set@{name}{{{value}}}'.format(name=name, value=value) + EOL |
|
|
|
|
|
|
|
|
The principle is: some special keys have their value processed; others |
|
|
|
|
|
are stored verbatim in self.config. |
|
|
|
|
|
""" |
|
|
|
|
|
self.config.update(raw_songbook) |
|
|
|
|
|
self.config['datadir'] = os.path.abspath(self.config['datadir']) |
|
|
|
|
|
### 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): |
|
|
|
|
|
"""Build the '.tex' file corresponding to self. |
|
|
|
|
|
|
|
|
|
|
|
Arguments: |
|
|
|
|
|
- output: a file object, in which the file will be written. |
|
|
|
|
|
""" |
|
|
|
|
|
renderer = TexRenderer( |
|
|
|
|
|
self.config['template'], |
|
|
|
|
|
self.config['datadir'], |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
context = parse_template(renderer.file_template()) |
|
|
|
|
|
|
|
|
|
|
|
context.update(self.config) |
|
|
|
|
|
context['titleprefixkeys'] = ["after", "sep", "ignore"] |
|
|
|
|
|
context['songlist'] = self.songslist |
|
|
|
|
|
context['filename'] = output.name[:-4] |
|
|
|
|
|
|
|
|
|
|
|
renderer.render_tex(output, context) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clean(basename): |
|
|
def clean(basename): |
|
|
"""Clean (some) temporary files used during compilation. |
|
|
"""Clean (some) temporary files used during compilation. |
|
|
|
|
|
|
|
|
Depending of the LaTeX modules used in the template, there may be others |
|
|
Depending of the LaTeX modules used in the template, there may be others |
|
|
that are note deleted by this function.""" |
|
|
that are not deleted by this function.""" |
|
|
generated_extensions = [ |
|
|
generated_extensions = [ |
|
|
"_auth.sbx", |
|
|
"_auth.sbx", |
|
|
"_auth.sxd", |
|
|
"_auth.sxd", |
|
@ -109,133 +169,8 @@ def clean(basename): |
|
|
raise errors.CleaningError(basename + ext, exception) |
|
|
raise errors.CleaningError(basename + ext, exception) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_tex_file(songbook, output): |
|
|
|
|
|
"""Create the LaTeX file corresponding to the .sb file given in argument.""" |
|
|
|
|
|
datadir = songbook['datadir'] |
|
|
|
|
|
name = output[:-4] |
|
|
|
|
|
template_dir = os.path.join(datadir, 'templates') |
|
|
|
|
|
songs = [] |
|
|
|
|
|
|
|
|
|
|
|
prefixes_tex = "" |
|
|
|
|
|
prefixes = [] |
|
|
|
|
|
|
|
|
|
|
|
authwords_tex = "" |
|
|
|
|
|
authwords = {"after": ["by"], "ignore": ["unknown"], "sep": ["and"]} |
|
|
|
|
|
|
|
|
|
|
|
# parse the songbook data |
|
|
|
|
|
if "template" in songbook: |
|
|
|
|
|
template = songbook["template"] |
|
|
|
|
|
del songbook["template"] |
|
|
|
|
|
else: |
|
|
|
|
|
template = os.path.join(__DATADIR__, "templates", "default.tmpl") |
|
|
|
|
|
if "songs" in songbook: |
|
|
|
|
|
songs = songbook["songs"] |
|
|
|
|
|
del songbook["songs"] |
|
|
|
|
|
if "titleprefixwords" in songbook: |
|
|
|
|
|
prefixes = songbook["titleprefixwords"] |
|
|
|
|
|
for prefix in songbook["titleprefixwords"]: |
|
|
|
|
|
prefixes_tex += r"\titleprefixword{%s}" % prefix + EOL |
|
|
|
|
|
songbook["titleprefixwords"] = prefixes_tex |
|
|
|
|
|
if "authwords" in songbook: |
|
|
|
|
|
# Populating default value |
|
|
|
|
|
for key in ["after", "sep", "ignore"]: |
|
|
|
|
|
if key not in songbook["authwords"]: |
|
|
|
|
|
songbook["authwords"][key] = authwords[key] |
|
|
|
|
|
# Processing authwords values |
|
|
|
|
|
authwords = songbook["authwords"] |
|
|
|
|
|
for key in ["after", "sep", "ignore"]: |
|
|
|
|
|
for word in authwords[key]: |
|
|
|
|
|
if key == "after": |
|
|
|
|
|
authwords_tex += r"\auth%sword{%s}" % ("by", word) + EOL |
|
|
|
|
|
else: |
|
|
|
|
|
authwords_tex += r"\auth%sword{%s}" % (key, word) + EOL |
|
|
|
|
|
songbook["authwords"] = authwords_tex |
|
|
|
|
|
if "after" in authwords: |
|
|
|
|
|
authwords["after"] = [re.compile(r"^.*%s\b(.*)" % after) |
|
|
|
|
|
for after in authwords["after"]] |
|
|
|
|
|
if "sep" in authwords: |
|
|
|
|
|
authwords["sep"] = [" %s" % sep for sep in authwords["sep"]] + [","] |
|
|
|
|
|
authwords["sep"] = [re.compile(r"^(.*)%s (.*)$" % sep) |
|
|
|
|
|
for sep in authwords["sep"]] |
|
|
|
|
|
|
|
|
|
|
|
if "lang" not in songbook: |
|
|
|
|
|
songbook["lang"] = "french" |
|
|
|
|
|
if "sort" in songbook: |
|
|
|
|
|
sort = songbook["sort"] |
|
|
|
|
|
del songbook["sort"] |
|
|
|
|
|
else: |
|
|
|
|
|
sort = [u"by", u"album", u"@title"] |
|
|
|
|
|
Song.sort = sort |
|
|
|
|
|
Song.prefixes = prefixes |
|
|
|
|
|
Song.authwords = authwords |
|
|
|
|
|
|
|
|
|
|
|
parameters = parse_template(os.path.join(template_dir, template)) |
|
|
|
|
|
|
|
|
|
|
|
# compute songslist |
|
|
|
|
|
if songs == "all": |
|
|
|
|
|
songs = [ |
|
|
|
|
|
os.path.relpath(filename, os.path.join(datadir, 'songs')) |
|
|
|
|
|
for filename |
|
|
|
|
|
in recursive_find(os.path.join(datadir, 'songs'), '*.sg') |
|
|
|
|
|
] |
|
|
|
|
|
songslist = SongsList(datadir, songbook["lang"]) |
|
|
|
|
|
songslist.append_list(songs) |
|
|
|
|
|
|
|
|
|
|
|
songbook["languages"] = ",".join(songslist.languages()) |
|
|
|
|
|
|
|
|
|
|
|
# output relevant fields |
|
|
|
|
|
out = codecs.open(output, 'w', 'utf-8') |
|
|
|
|
|
out.write('%% This file has been automatically generated, do not edit!\n') |
|
|
|
|
|
out.write(r'\makeatletter' + EOL) |
|
|
|
|
|
# output automatic parameters |
|
|
|
|
|
out.write(format_declaration("name", {"default": name})) |
|
|
|
|
|
out.write(format_declaration("songslist", {"type": "stringlist"})) |
|
|
|
|
|
# output template parameter command |
|
|
|
|
|
for name, parameter in parameters.iteritems(): |
|
|
|
|
|
out.write(format_declaration(name, parameter)) |
|
|
|
|
|
# output template parameter values |
|
|
|
|
|
for name, value in songbook.iteritems(): |
|
|
|
|
|
if name in parameters: |
|
|
|
|
|
out.write(format_definition( |
|
|
|
|
|
name, |
|
|
|
|
|
to_value(parameters[name], value), |
|
|
|
|
|
)) |
|
|
|
|
|
|
|
|
|
|
|
if len(songs) > 0: |
|
|
|
|
|
out.write(format_definition('songslist', songslist.latex())) |
|
|
|
|
|
out.write(r'\makeatother' + EOL) |
|
|
|
|
|
|
|
|
|
|
|
# output template |
|
|
|
|
|
comment_pattern = re.compile(r"^\s*%") |
|
|
|
|
|
with codecs.open( |
|
|
|
|
|
os.path.join(template_dir, template), 'r', 'utf-8' |
|
|
|
|
|
) as template_file: |
|
|
|
|
|
content = [ |
|
|
|
|
|
line |
|
|
|
|
|
for line |
|
|
|
|
|
in template_file |
|
|
|
|
|
if not comment_pattern.match(line) |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
for index, line in enumerate(content): |
|
|
|
|
|
if re.compile("getDataImgDirectory").search(line): |
|
|
|
|
|
if os.path.abspath(os.path.join(datadir, "img")).startswith( |
|
|
|
|
|
os.path.abspath(os.path.dirname(output)) |
|
|
|
|
|
): |
|
|
|
|
|
imgdir = os.path.relpath( |
|
|
|
|
|
os.path.join(datadir, "img"), |
|
|
|
|
|
os.path.dirname(output) |
|
|
|
|
|
) |
|
|
|
|
|
else: |
|
|
|
|
|
imgdir = os.path.abspath(os.path.join(datadir, "img")) |
|
|
|
|
|
line = line.replace(r"\getDataImgDirectory", ' {%s/} ' % imgdir) |
|
|
|
|
|
content[index] = line |
|
|
|
|
|
|
|
|
|
|
|
out.write(u''.join(content)) |
|
|
|
|
|
out.close() |
|
|
|
|
|
|
|
|
|
|
|
def buildsongbook( |
|
|
def buildsongbook( |
|
|
songbook, |
|
|
raw_songbook, |
|
|
basename, |
|
|
basename, |
|
|
interactive=False, |
|
|
interactive=False, |
|
|
logger=logging.getLogger() |
|
|
logger=logging.getLogger() |
|
@ -243,15 +178,15 @@ def buildsongbook( |
|
|
"""Build a songbook |
|
|
"""Build a songbook |
|
|
|
|
|
|
|
|
Arguments: |
|
|
Arguments: |
|
|
- songbook: Python representation of the .sb songbook configuration file. |
|
|
- raw_songbook: Python representation of the .sb songbook configuration |
|
|
|
|
|
file. |
|
|
- basename: basename of the songbook to be built. |
|
|
- basename: basename of the songbook to be built. |
|
|
- interactive: in False, do not expect anything from stdin. |
|
|
- interactive: in False, do not expect anything from stdin. |
|
|
""" |
|
|
""" |
|
|
|
|
|
|
|
|
tex_file = basename + ".tex" |
|
|
songbook = Songbook(raw_songbook, basename) |
|
|
|
|
|
with codecs.open("{}.tex".format(basename), 'w', 'utf-8') as output: |
|
|
# Make TeX file |
|
|
songbook.write_tex(output) |
|
|
make_tex_file(songbook, tex_file) |
|
|
|
|
|
|
|
|
|
|
|
if not 'TEXINPUTS' in os.environ.keys(): |
|
|
if not 'TEXINPUTS' in os.environ.keys(): |
|
|
os.environ['TEXINPUTS'] = '' |
|
|
os.environ['TEXINPUTS'] = '' |
|
@ -260,18 +195,18 @@ def buildsongbook( |
|
|
'latex', |
|
|
'latex', |
|
|
) |
|
|
) |
|
|
os.environ['TEXINPUTS'] += os.pathsep + os.path.join( |
|
|
os.environ['TEXINPUTS'] += os.pathsep + os.path.join( |
|
|
songbook['datadir'], |
|
|
songbook.config['datadir'], |
|
|
'latex', |
|
|
'latex', |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
# pdflatex options |
|
|
# pdflatex options |
|
|
pdflatex_options = [] |
|
|
pdflatex_options = [] |
|
|
pdflatex_options.append("--shell-escape") # Lilypond compilation |
|
|
pdflatex_options.append("--shell-escape") # Lilypond compilation |
|
|
if not interactive: |
|
|
if not interactive: |
|
|
pdflatex_options.append("-halt-on-error") |
|
|
pdflatex_options.append("-halt-on-error") |
|
|
|
|
|
|
|
|
# First pdflatex pass |
|
|
# First pdflatex pass |
|
|
if subprocess.call(["pdflatex"] + pdflatex_options + [tex_file]): |
|
|
if subprocess.call(["pdflatex"] + pdflatex_options + [basename]): |
|
|
raise errors.LatexCompilationError(basename) |
|
|
raise errors.LatexCompilationError(basename) |
|
|
|
|
|
|
|
|
# Make index |
|
|
# Make index |
|
@ -284,7 +219,7 @@ def buildsongbook( |
|
|
index_file.close() |
|
|
index_file.close() |
|
|
|
|
|
|
|
|
# Second pdflatex pass |
|
|
# Second pdflatex pass |
|
|
if subprocess.call(["pdflatex"] + pdflatex_options + [tex_file]): |
|
|
if subprocess.call(["pdflatex"] + pdflatex_options + [basename]): |
|
|
raise errors.LatexCompilationError(basename) |
|
|
raise errors.LatexCompilationError(basename) |
|
|
|
|
|
|
|
|
# Cleaning |
|
|
# Cleaning |
|
|