|
|
@ -5,10 +5,9 @@ |
|
|
|
|
|
|
|
import codecs |
|
|
|
import glob |
|
|
|
import logging |
|
|
|
import os.path |
|
|
|
import re |
|
|
|
import subprocess |
|
|
|
from subprocess import Popen, PIPE, call |
|
|
|
|
|
|
|
from songbook_core import __DATADIR__ |
|
|
|
from songbook_core import errors |
|
|
@ -24,6 +23,18 @@ DEFAULT_AUTHWORDS = { |
|
|
|
"sep": ["and"], |
|
|
|
} |
|
|
|
DEFAULT_STEPS = ['tex', 'pdf', 'sbx', 'pdf', 'clean'] |
|
|
|
GENERATED_EXTENSIONS = [ |
|
|
|
"_auth.sbx", |
|
|
|
"_auth.sxd", |
|
|
|
".aux", |
|
|
|
".log", |
|
|
|
".out", |
|
|
|
".sxc", |
|
|
|
".tex", |
|
|
|
"_title.sbx", |
|
|
|
"_title.sxd", |
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# pylint: disable=too-few-public-methods |
|
|
@ -47,9 +58,10 @@ class Songbook(object): |
|
|
|
'datadir': os.path.abspath('.'), |
|
|
|
} |
|
|
|
self.songslist = None |
|
|
|
self._parse(raw_songbook) |
|
|
|
self._parse_raw(raw_songbook) |
|
|
|
|
|
|
|
def _set_songs_default(self, config): |
|
|
|
@staticmethod |
|
|
|
def _set_songs_default(config): |
|
|
|
"""Set the default values for the Song() class. |
|
|
|
|
|
|
|
Argument: |
|
|
@ -70,7 +82,7 @@ class Songbook(object): |
|
|
|
] + [',']) |
|
|
|
] |
|
|
|
|
|
|
|
def _parse(self, raw_songbook): |
|
|
|
def _parse_raw(self, raw_songbook): |
|
|
|
"""Parse raw_songbook. |
|
|
|
|
|
|
|
The principle is: some special keys have their value processed; others |
|
|
@ -78,7 +90,7 @@ class Songbook(object): |
|
|
|
""" |
|
|
|
self.config.update(raw_songbook) |
|
|
|
self.config['datadir'] = os.path.abspath(self.config['datadir']) |
|
|
|
### Some post-processing |
|
|
|
|
|
|
|
# Compute song list |
|
|
|
if self.config['content'] is None: |
|
|
|
self.config['content'] = [ |
|
|
@ -92,20 +104,24 @@ class Songbook(object): |
|
|
|
'*.sg', |
|
|
|
) |
|
|
|
] |
|
|
|
self.songslist = SongsList(self.config['datadir']) |
|
|
|
self.songslist.append_list(self.config['content']) |
|
|
|
|
|
|
|
# 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 _parse_songs(self): |
|
|
|
"""Parse songs included in songbook.""" |
|
|
|
self.songslist = SongsList(self.config['datadir']) |
|
|
|
self.songslist.append_list(self.config['content']) |
|
|
|
|
|
|
|
def write_tex(self, output): |
|
|
|
"""Build the '.tex' file corresponding to self. |
|
|
|
|
|
|
|
Arguments: |
|
|
|
- output: a file object, in which the file will be written. |
|
|
|
""" |
|
|
|
self._parse_songs() |
|
|
|
renderer = TexRenderer( |
|
|
|
self.config['template'], |
|
|
|
self.config['datadir'], |
|
|
@ -123,58 +139,38 @@ class Songbook(object): |
|
|
|
renderer.render_tex(output, context) |
|
|
|
|
|
|
|
|
|
|
|
def clean(basename): |
|
|
|
"""Clean (some) temporary files used during compilation. |
|
|
|
|
|
|
|
Depending of the LaTeX modules used in the template, there may be others |
|
|
|
that are not deleted by this function.""" |
|
|
|
generated_extensions = [ |
|
|
|
"_auth.sbx", |
|
|
|
"_auth.sxd", |
|
|
|
".aux", |
|
|
|
".log", |
|
|
|
".out", |
|
|
|
".sxc", |
|
|
|
".tex", |
|
|
|
"_title.sbx", |
|
|
|
"_title.sxd", |
|
|
|
] |
|
|
|
class SongbookBuilder(object): |
|
|
|
"""Provide methods to compile a songbook.""" |
|
|
|
|
|
|
|
for ext in generated_extensions: |
|
|
|
if os.path.isfile(basename + ext): |
|
|
|
try: |
|
|
|
os.unlink(basename + ext) |
|
|
|
except Exception as exception: |
|
|
|
raise errors.CleaningError(basename + ext, exception) |
|
|
|
# if False, do not expect anything from stdin. |
|
|
|
interactive = False |
|
|
|
# if True, allow unsafe option, like adding the --shell-escape to pdflatex |
|
|
|
unsafe = False |
|
|
|
# Options to add to pdflatex |
|
|
|
_pdflatex_options = [] |
|
|
|
# Dictionary of functions that have been called by self._run_once(). Keys |
|
|
|
# are function; values are return values of functions. |
|
|
|
_called_functions = {} |
|
|
|
|
|
|
|
def __init__(self, raw_songbook, basename, logger=None): |
|
|
|
# Representation of the .sb songbook configuration file. |
|
|
|
self.songbook = Songbook(raw_songbook, basename) |
|
|
|
# Basename of the songbook to be built. |
|
|
|
self.basename = basename |
|
|
|
# logging object to use |
|
|
|
self.logger = logger |
|
|
|
|
|
|
|
def buildsongbook( |
|
|
|
raw_songbook, |
|
|
|
basename, |
|
|
|
steps=None, |
|
|
|
interactive=False, |
|
|
|
logger=logging.getLogger(), |
|
|
|
): |
|
|
|
"""Build a songbook |
|
|
|
def _run_once(self, function, *args, **kwargs): |
|
|
|
"""Run function if it has not been run yet. |
|
|
|
|
|
|
|
Arguments: |
|
|
|
- raw_songbook: Python representation of the .sb songbook configuration |
|
|
|
file. |
|
|
|
- steps: list of steps to perform to compile songbook. Available steps are: |
|
|
|
- tex: build .tex file from templates; |
|
|
|
- pdf: compile .tex using pdflatex; |
|
|
|
- sbx: compile song and author indexes; |
|
|
|
- clean: remove temporary files, |
|
|
|
- any string beginning with a sharp sign (#): it is interpreted as a |
|
|
|
command to run in a shell. |
|
|
|
- basename: basename of the songbook to be built. |
|
|
|
- interactive: in False, do not expect anything from stdin. |
|
|
|
If it as, return the previous return value. |
|
|
|
""" |
|
|
|
if function not in self._called_functions: |
|
|
|
self._called_functions[function] = function(*args, **kwargs) |
|
|
|
return self._called_functions[function] |
|
|
|
|
|
|
|
if steps is None: |
|
|
|
steps = DEFAULT_STEPS |
|
|
|
|
|
|
|
songbook = Songbook(raw_songbook, basename) |
|
|
|
def _set_latex(self): |
|
|
|
"""Set TEXINPUTS and LaTeX options.""" |
|
|
|
if not 'TEXINPUTS' in os.environ.keys(): |
|
|
|
os.environ['TEXINPUTS'] = '' |
|
|
|
os.environ['TEXINPUTS'] += os.pathsep + os.path.join( |
|
|
@ -182,43 +178,95 @@ def buildsongbook( |
|
|
|
'latex', |
|
|
|
) |
|
|
|
os.environ['TEXINPUTS'] += os.pathsep + os.path.join( |
|
|
|
songbook.config['datadir'], |
|
|
|
self.songbook.config['datadir'], |
|
|
|
'latex', |
|
|
|
) |
|
|
|
|
|
|
|
# pdflatex options |
|
|
|
pdflatex_options = [] |
|
|
|
pdflatex_options.append("--shell-escape") # Lilypond compilation |
|
|
|
if not interactive: |
|
|
|
pdflatex_options.append("-halt-on-error") |
|
|
|
if self.unsafe: |
|
|
|
self._pdflatex_options.append("--shell-escape") |
|
|
|
if not self.interactive: |
|
|
|
self._pdflatex_options.append("-halt-on-error") |
|
|
|
|
|
|
|
def build_steps(self, steps=None): |
|
|
|
"""Perform steps on the songbook by calling relevant self.build_*() |
|
|
|
|
|
|
|
Arguments: |
|
|
|
- steps: list of steps to perform to compile songbook. Available steps |
|
|
|
are: |
|
|
|
- tex: build .tex file from templates; |
|
|
|
- pdf: compile .tex using pdflatex; |
|
|
|
- sbx: compile song and author indexes; |
|
|
|
- clean: remove temporary files, |
|
|
|
- any string beginning with a sharp sign (#): it is interpreted as a |
|
|
|
command to run in a shell. |
|
|
|
""" |
|
|
|
if not steps: |
|
|
|
steps = DEFAULT_STEPS |
|
|
|
|
|
|
|
# Compilation |
|
|
|
for step in steps: |
|
|
|
if step == 'tex': |
|
|
|
# Building .tex file from templates |
|
|
|
with codecs.open("{}.tex".format(basename), 'w', 'utf-8') as output: |
|
|
|
songbook.write_tex(output) |
|
|
|
self.build_tex() |
|
|
|
elif step == 'pdf': |
|
|
|
if subprocess.call(["pdflatex"] + pdflatex_options + [basename]): |
|
|
|
raise errors.LatexCompilationError(basename) |
|
|
|
self.build_pdf() |
|
|
|
elif step == 'sbx': |
|
|
|
# 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() |
|
|
|
self.build_sbx() |
|
|
|
elif step == 'clean': |
|
|
|
# Cleaning |
|
|
|
clean(basename) |
|
|
|
self.clean() |
|
|
|
elif step.startswith("%"): |
|
|
|
# Shell command |
|
|
|
command = step[1:] |
|
|
|
exit_code = subprocess.call(command, shell=True) |
|
|
|
if exit_code: |
|
|
|
raise errors.StepCommandError(command, exit_code) |
|
|
|
self.build_custom(step[1:]) |
|
|
|
else: |
|
|
|
# Unknown step name |
|
|
|
raise errors.UnknownStep(step) |
|
|
|
|
|
|
|
def build_tex(self): |
|
|
|
"""Build .tex file from templates""" |
|
|
|
with codecs.open( |
|
|
|
"{}.tex".format(self.basename), 'w', 'utf-8', |
|
|
|
) as output: |
|
|
|
self.songbook.write_tex(output) |
|
|
|
|
|
|
|
def build_pdf(self): |
|
|
|
"""Build .pdf file from .tex file""" |
|
|
|
self._run_once(self._set_latex) |
|
|
|
p = Popen( |
|
|
|
["pdflatex"] + self._pdflatex_options + [self.basename], |
|
|
|
stdout=PIPE, |
|
|
|
stderr=PIPE) |
|
|
|
log = '' |
|
|
|
line = p.stdout.readline() |
|
|
|
while line: |
|
|
|
log += line |
|
|
|
line = p.stdout.readline() |
|
|
|
self.logger.info(log) |
|
|
|
|
|
|
|
if p.returncode: |
|
|
|
raise errors.LatexCompilationError(self.basename) |
|
|
|
|
|
|
|
def build_sbx(self): |
|
|
|
"""Make index""" |
|
|
|
sxd_files = glob.glob("%s_*.sxd" % self.basename) |
|
|
|
for sxd_file in sxd_files: |
|
|
|
if self.logger: |
|
|
|
self.logger.info("processing " + sxd_file) |
|
|
|
idx = process_sxd(sxd_file) |
|
|
|
with open(sxd_file[:-3] + "sbx", "w") as index_file: |
|
|
|
index_file.write(idx.entries_to_str().encode('utf8')) |
|
|
|
|
|
|
|
@staticmethod |
|
|
|
def build_custom(command): |
|
|
|
"""Run a shell command""" |
|
|
|
exit_code = call(command, shell=True) |
|
|
|
if exit_code: |
|
|
|
raise errors.StepCommandError(command, exit_code) |
|
|
|
|
|
|
|
def clean(self): |
|
|
|
"""Clean (some) temporary files used during compilation. |
|
|
|
|
|
|
|
Depending of the LaTeX modules used in the template, there may be others |
|
|
|
that are not deleted by this function.""" |
|
|
|
for ext in GENERATED_EXTENSIONS: |
|
|
|
if os.path.isfile(self.basename + ext): |
|
|
|
try: |
|
|
|
os.unlink(self.basename + ext) |
|
|
|
except Exception as exception: |
|
|
|
raise errors.CleaningError(self.basename + ext, exception) |
|
|
|