mirror of https://github.com/patacrep/patacrep.git
Luthaf
10 years ago
35 changed files with 1165 additions and 928 deletions
@ -0,0 +1,2 @@ |
|||||
|
include LICENSE NEWS readme.md Requirements.txt |
||||
|
recursive-include patacrep/data * |
@ -0,0 +1,6 @@ |
|||||
|
\selectlanguage{french} |
||||
|
\sortassong{}[by={QQ}] |
||||
|
\begin{intersong} |
||||
|
|
||||
|
Lorem ipsum |
||||
|
\end{intersong} |
@ -0,0 +1,21 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
"""Very simple LaTeX parser |
||||
|
|
||||
|
This module uses an LALR parser to try to parse LaTeX code. LaTeX language |
||||
|
*cannot* be parsed by an LALR parser, so this is a very simple attemps, which |
||||
|
will work on simple cases, but not on complex ones. |
||||
|
""" |
||||
|
|
||||
|
from patacrep.latex.syntax import tex2plain |
||||
|
from patacrep.latex.syntax import parsesong as syntax_parsesong |
||||
|
from patacrep import encoding |
||||
|
|
||||
|
def parsesong(path, fileencoding=None): |
||||
|
"""Return a dictonary of data read from the latex file `path`. |
||||
|
|
||||
|
""" |
||||
|
with encoding.open_read(path, encoding=fileencoding) as songfile: |
||||
|
data = syntax_parsesong(songfile.read(), path) |
||||
|
data['@path'] = path |
||||
|
return data |
@ -0,0 +1,65 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
"""Abstract Syntax Tree for LaTeX code.""" |
||||
|
|
||||
|
# pylint: disable=too-few-public-methods |
||||
|
|
||||
|
class AST: |
||||
|
"""Base class for the tree.""" |
||||
|
# pylint: disable=no-init |
||||
|
|
||||
|
metadata = None |
||||
|
|
||||
|
@classmethod |
||||
|
def init_metadata(cls): |
||||
|
"""Clear metadata |
||||
|
|
||||
|
As this attribute is a class attribute, it as to be reset at each new |
||||
|
parsing. |
||||
|
""" |
||||
|
cls.metadata = { |
||||
|
'@languages': set(), |
||||
|
} |
||||
|
|
||||
|
class Expression(AST): |
||||
|
"""LaTeX expression""" |
||||
|
|
||||
|
def __init__(self, value): |
||||
|
super().__init__() |
||||
|
self.content = [value] |
||||
|
|
||||
|
def prepend(self, value): |
||||
|
"""Add a value at the beginning of the content list.""" |
||||
|
if value is not None: |
||||
|
self.content.insert(0, value) |
||||
|
return self |
||||
|
|
||||
|
def __str__(self): |
||||
|
return "".join([str(item) for item in self.content]) |
||||
|
|
||||
|
class Command(AST): |
||||
|
"""LaTeX command""" |
||||
|
|
||||
|
def __init__(self, name, optional, mandatory): |
||||
|
self.name = name |
||||
|
self.mandatory = mandatory |
||||
|
self.optional = optional |
||||
|
|
||||
|
if name == r'\selectlanguage': |
||||
|
self.metadata['@languages'] |= set(self.mandatory) |
||||
|
|
||||
|
def __str__(self): |
||||
|
if self.name in [r'\emph']: |
||||
|
return str(self.mandatory[0]) |
||||
|
return "{}{}{}".format( |
||||
|
self.name, |
||||
|
"".join(["[{}]".format(item) for item in self.optional]), |
||||
|
"".join(["{{{}}}".format(item) for item in self.mandatory]), |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class BeginSong(AST): |
||||
|
"""Beginsong command""" |
||||
|
|
||||
|
def __init__(self, titles, arguments): |
||||
|
self.titles = titles |
||||
|
self.arguments = arguments |
@ -0,0 +1,123 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
"""Render `very simple` TeX commands in a simple TeX code.""" |
||||
|
|
||||
|
import logging |
||||
|
|
||||
|
LOGGER = logging.getLogger() |
||||
|
|
||||
|
MATCH = [ |
||||
|
# Diacritics: a |
||||
|
(r"\'a", "á"), |
||||
|
(r"\'A", "Á"), |
||||
|
(r"\`a", "à"), |
||||
|
(r"\`A", "À"), |
||||
|
(r"\^a", "â"), |
||||
|
(r"\^A", "Â"), |
||||
|
(r"\"a", "ä"), |
||||
|
(r"\"A", "Ä"), |
||||
|
|
||||
|
# Diacritics: e |
||||
|
(r"\'e", "é"), |
||||
|
(r"\'E", "É"), |
||||
|
(r"\`e", "è"), |
||||
|
(r"\`E", "È"), |
||||
|
(r"\^e", "ê"), |
||||
|
(r"\^E", "Ê"), |
||||
|
(r"\"e", "ë"), |
||||
|
(r"\"E", "Ë"), |
||||
|
|
||||
|
# Diacritics: i |
||||
|
(r"\'i", "í"), |
||||
|
(r"\'I", "Í"), |
||||
|
(r"\`i", "ì"), |
||||
|
(r"\`I", "Ì"), |
||||
|
(r"\^i", "î"), |
||||
|
(r"\^I", "Î"), |
||||
|
(r"\"i", "ï"), |
||||
|
(r"\"I", "Ï"), |
||||
|
(r"\'\i", "í"), |
||||
|
(r"\'\I", "Í"), |
||||
|
(r"\`\i", "ì"), |
||||
|
(r"\`\I", "Ì"), |
||||
|
(r"\^\i", "î"), |
||||
|
(r"\^\I", "Î"), |
||||
|
(r"\"\i", "ï"), |
||||
|
(r"\"\I", "Ï"), |
||||
|
|
||||
|
# Diacritics: o |
||||
|
(r"\'o", "ó"), |
||||
|
(r"\'O", "Ó"), |
||||
|
(r"\`o", "ò"), |
||||
|
(r"\`O", "Ò"), |
||||
|
(r"\^o", "ô"), |
||||
|
(r"\^O", "Ô"), |
||||
|
(r"\"o", "ö"), |
||||
|
(r"\"O", "Ö"), |
||||
|
|
||||
|
# Diacritics: u |
||||
|
(r"\'u", "ú"), |
||||
|
(r"\'U", "Ú"), |
||||
|
(r"\`u", "ù"), |
||||
|
(r"\`U", "Ù"), |
||||
|
(r"\^u", "û"), |
||||
|
(r"\^U", "Û"), |
||||
|
(r"\"u", "ü"), |
||||
|
(r"\"U", "Ü"), |
||||
|
|
||||
|
# Cedille |
||||
|
(r"\c c", "ç"), |
||||
|
(r"\c C", "Ç"), |
||||
|
|
||||
|
# œ, æ |
||||
|
(r"\oe", "œ"), |
||||
|
(r"\OE", "Œ"), |
||||
|
(r"\ae", "æ"), |
||||
|
(r"\AE", "Æ"), |
||||
|
|
||||
|
# Spaces |
||||
|
(r"\ ", " "), |
||||
|
(r"\,", " "), |
||||
|
(r"\~", " "), |
||||
|
|
||||
|
# IeC |
||||
|
(r"\IeC ", ""), |
||||
|
|
||||
|
# Miscallenous |
||||
|
(r"\dots", "…"), |
||||
|
(r"\%", "%"), |
||||
|
(r"\&", "&"), |
||||
|
(r"\_", "_"), |
||||
|
|
||||
|
] |
||||
|
|
||||
|
|
||||
|
def detex(arg): |
||||
|
"""Render very simple TeX commands from argument. |
||||
|
|
||||
|
Argument can be: |
||||
|
- a string: it is processed; |
||||
|
- a list, dict or set: its values are processed. |
||||
|
""" |
||||
|
if isinstance(arg, dict): |
||||
|
return dict([ |
||||
|
(key, detex(value)) |
||||
|
for (key, value) |
||||
|
in arg.items() |
||||
|
]) |
||||
|
elif isinstance(arg, list): |
||||
|
return [ |
||||
|
detex(item) |
||||
|
for item |
||||
|
in arg |
||||
|
] |
||||
|
elif isinstance(arg, set): |
||||
|
return set(detex(list(arg))) |
||||
|
elif isinstance(arg, str): |
||||
|
string = arg |
||||
|
for (latex, plain) in MATCH: |
||||
|
string = string.replace(latex, plain) |
||||
|
if '\\' in string: |
||||
|
LOGGER.warning("Remaining command in string '{}'.".format(string)) |
||||
|
return string.strip() |
||||
|
else: |
||||
|
return detex(str(arg)) |
@ -0,0 +1,151 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
"""Very simple LaTeX lexer.""" |
||||
|
|
||||
|
import logging |
||||
|
import ply.lex as lex |
||||
|
|
||||
|
LOGGER = logging.getLogger() |
||||
|
|
||||
|
#pylint: disable=invalid-name |
||||
|
tokens = ( |
||||
|
'LBRACKET', |
||||
|
'RBRACKET', |
||||
|
'LBRACE', |
||||
|
'RBRACE', |
||||
|
'COMMAND', |
||||
|
'NEWLINE', |
||||
|
'COMMA', |
||||
|
'EQUAL', |
||||
|
'CHARACTER', |
||||
|
'SPACE', |
||||
|
'BEGINSONG', |
||||
|
'SONG_LTITLE', |
||||
|
'SONG_RTITLE', |
||||
|
'SONG_LOPTIONS', |
||||
|
'SONG_ROPTIONS', |
||||
|
) |
||||
|
|
||||
|
class SimpleLexer: |
||||
|
"""Very simple LaTeX lexer.""" |
||||
|
|
||||
|
tokens = tokens |
||||
|
|
||||
|
t_LBRACKET = r'\[' |
||||
|
t_RBRACKET = r'\]' |
||||
|
t_LBRACE = r'{' |
||||
|
t_RBRACE = r'}' |
||||
|
t_COMMAND = r'\\([@a-zA-Z]+|[^\\])' |
||||
|
t_NEWLINE = r'\\\\' |
||||
|
SPECIAL_CHARACTERS = ( |
||||
|
t_LBRACKET + |
||||
|
t_RBRACKET + |
||||
|
t_RBRACE + |
||||
|
t_LBRACE + |
||||
|
r"\\" + |
||||
|
r" " + |
||||
|
r"\n" + |
||||
|
r"\r" + |
||||
|
r"%" + |
||||
|
r"=" + |
||||
|
r"," |
||||
|
) |
||||
|
t_CHARACTER = r'[^{}]'.format(SPECIAL_CHARACTERS) |
||||
|
t_EQUAL = r'=' |
||||
|
t_COMMA = r',' |
||||
|
|
||||
|
t_SPACE = r'[ \t\n\r]+' |
||||
|
|
||||
|
def __init__(self): |
||||
|
self.__class__.lexer = lex.lex(module=self) |
||||
|
|
||||
|
# Define a rule so we can track line numbers |
||||
|
@staticmethod |
||||
|
def t_newline(token): |
||||
|
r'\n+' |
||||
|
token.lexer.lineno += len(token.value) |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_comment(token): |
||||
|
r'%.*' |
||||
|
pass |
||||
|
|
||||
|
# Error handling rule |
||||
|
@staticmethod |
||||
|
def t_error(token): |
||||
|
"""Manage errors""" |
||||
|
LOGGER.error("Illegal character '{}'".format(token.value[0])) |
||||
|
token.lexer.skip(1) |
||||
|
|
||||
|
class SongLexer(SimpleLexer): |
||||
|
r"""Very simple song lexer. |
||||
|
|
||||
|
In the context of this class, a "song" is some LaTeX code containing the |
||||
|
``\beginsong`` (or ``\sortassong``) command. |
||||
|
""" |
||||
|
|
||||
|
states = ( |
||||
|
('beginsong', 'inclusive'), |
||||
|
) |
||||
|
|
||||
|
# State beginsong |
||||
|
@staticmethod |
||||
|
def t_INITIAL_BEGINSONG(token): |
||||
|
r'(\\beginsong|\\sortassong)' |
||||
|
token.lexer.push_state('beginsong') |
||||
|
token.lexer.open_brackets = 0 |
||||
|
token.lexer.open_braces = 0 |
||||
|
return token |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_beginsong_LBRACKET(token): |
||||
|
r'\[' |
||||
|
if token.lexer.open_brackets == 0: |
||||
|
token.type = 'SONG_LOPTIONS' |
||||
|
|
||||
|
# Count opening and closing braces to know when to leave the |
||||
|
# `beginsong` state. |
||||
|
token.lexer.open_braces += 1 |
||||
|
token.lexer.open_brackets += 1 |
||||
|
return token |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_beginsong_RBRACKET(token): |
||||
|
r'\]' |
||||
|
token.lexer.open_brackets -= 1 |
||||
|
if token.lexer.open_brackets == 0: |
||||
|
token.type = 'SONG_ROPTIONS' |
||||
|
token.lexer.open_braces -= 1 |
||||
|
token.lexer.pop_state() |
||||
|
for __ignored in token.lexer: |
||||
|
# In this parser, we only want to read metadata. So, after the |
||||
|
# first ``\beginsong`` command, we can stop parsing. |
||||
|
pass |
||||
|
return token |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_beginsong_LBRACE(token): |
||||
|
r'{' |
||||
|
if token.lexer.open_braces == 0: |
||||
|
token.type = 'SONG_LTITLE' |
||||
|
token.lexer.open_braces += 1 |
||||
|
return token |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_beginsong_RBRACE1(token): |
||||
|
r'}(?![ \t\r\n]*\[)' |
||||
|
token.lexer.open_braces -= 1 |
||||
|
token.type = 'RBRACE' |
||||
|
if token.lexer.open_braces == 0: |
||||
|
token.lexer.pop_state() |
||||
|
token.type = 'SONG_RTITLE' |
||||
|
return token |
||||
|
|
||||
|
@staticmethod |
||||
|
def t_beginsong_RBRACE2(token): |
||||
|
r'}(?=[ \t\r\n]*\[)' |
||||
|
token.lexer.open_braces -= 1 |
||||
|
token.type = 'RBRACE' |
||||
|
if token.lexer.open_braces == 0: |
||||
|
token.type = 'SONG_RTITLE' |
||||
|
return token |
||||
|
|
@ -0,0 +1,256 @@ |
|||||
|
"""Very simple LaTeX parser""" |
||||
|
|
||||
|
import logging |
||||
|
import ply.yacc as yacc |
||||
|
|
||||
|
from patacrep.latex.lexer import tokens, SimpleLexer, SongLexer |
||||
|
from patacrep.latex import ast |
||||
|
from patacrep.errors import SongbookError |
||||
|
from patacrep.latex.detex import detex |
||||
|
|
||||
|
LOGGER = logging.getLogger() |
||||
|
|
||||
|
class ParsingError(SongbookError): |
||||
|
"""Parsing error.""" |
||||
|
|
||||
|
def __init__(self, message): |
||||
|
super().__init__(self) |
||||
|
self.message = message |
||||
|
|
||||
|
def __str__(self): |
||||
|
return self.message |
||||
|
|
||||
|
# pylint: disable=line-too-long |
||||
|
class Parser: |
||||
|
"""LaTeX parser.""" |
||||
|
|
||||
|
def __init__(self, filename=None): |
||||
|
self.tokens = tokens |
||||
|
self.ast = ast.AST |
||||
|
self.ast.init_metadata() |
||||
|
self.filename = filename |
||||
|
|
||||
|
@staticmethod |
||||
|
def __find_column(token): |
||||
|
"""Return the column of ``token``.""" |
||||
|
last_cr = token.lexer.lexdata.rfind('\n', 0, token.lexpos) |
||||
|
if last_cr < 0: |
||||
|
last_cr = 0 |
||||
|
column = (token.lexpos - last_cr) + 1 |
||||
|
return column |
||||
|
|
||||
|
def p_error(self, token): |
||||
|
"""Manage parsing errors.""" |
||||
|
LOGGER.error( |
||||
|
"Error in file {}, line {} at position {}.".format( |
||||
|
str(self.filename), |
||||
|
token.lineno, |
||||
|
self.__find_column(token), |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_expression(symbols): |
||||
|
"""expression : brackets expression |
||||
|
| braces expression |
||||
|
| command expression |
||||
|
| NEWLINE expression |
||||
|
| beginsong expression |
||||
|
| word expression |
||||
|
| SPACE expression |
||||
|
| empty |
||||
|
""" |
||||
|
if len(symbols) == 3: |
||||
|
if symbols[2] is None: |
||||
|
symbols[0] = ast.Expression(symbols[1]) |
||||
|
else: |
||||
|
symbols[0] = symbols[2].prepend(symbols[1]) |
||||
|
else: |
||||
|
symbols[0] = None |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_empty(__symbols): |
||||
|
"""empty :""" |
||||
|
return None |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_brackets(symbols): |
||||
|
"""brackets : LBRACKET expression RBRACKET""" |
||||
|
symbols[0] = symbols[2] |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_braces(symbols): |
||||
|
"""braces : LBRACE expression RBRACE""" |
||||
|
symbols[0] = symbols[2] |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_command(symbols): |
||||
|
"""command : COMMAND brackets_list braces_list""" |
||||
|
symbols[0] = ast.Command(symbols[1], symbols[2], symbols[3]) |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_brackets_list(symbols): |
||||
|
"""brackets_list : brackets brackets_list |
||||
|
| empty |
||||
|
""" |
||||
|
if len(symbols) == 3: |
||||
|
symbols[0] = symbols[2] |
||||
|
symbols[0].insert(0, symbols[1]) |
||||
|
else: |
||||
|
symbols[0] = [] |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_braces_list(symbols): |
||||
|
"""braces_list : braces braces_list |
||||
|
| empty |
||||
|
""" |
||||
|
if len(symbols) == 3: |
||||
|
symbols[0] = symbols[2] |
||||
|
symbols[0].insert(0, symbols[1]) |
||||
|
else: |
||||
|
symbols[0] = [] |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_word(symbols): |
||||
|
"""word : CHARACTER word_next |
||||
|
| COMMA word_next |
||||
|
| EQUAL word_next |
||||
|
""" |
||||
|
symbols[0] = symbols[1] + symbols[2] |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_word_next(symbols): |
||||
|
"""word_next : CHARACTER word_next |
||||
|
| empty |
||||
|
""" |
||||
|
if len(symbols) == 2: |
||||
|
symbols[0] = "" |
||||
|
else: |
||||
|
symbols[0] = symbols[1] + symbols[2] |
||||
|
|
||||
|
def p_beginsong(self, symbols): |
||||
|
"""beginsong : BEGINSONG separator songbraces separator songbrackets""" |
||||
|
self.ast.metadata["@titles"] = symbols[3] |
||||
|
self.ast.metadata.update(symbols[5]) |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_songbrackets(symbols): |
||||
|
"""songbrackets : SONG_LOPTIONS separator dictionary separator SONG_ROPTIONS |
||||
|
| empty |
||||
|
""" |
||||
|
if len(symbols) == 6: |
||||
|
symbols[0] = symbols[3] |
||||
|
else: |
||||
|
symbols[0] = {} |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_songbraces(symbols): |
||||
|
"""songbraces : SONG_LTITLE separator titles separator SONG_RTITLE |
||||
|
| empty |
||||
|
""" |
||||
|
if len(symbols) == 6: |
||||
|
symbols[0] = symbols[3] |
||||
|
else: |
||||
|
symbols[0] = [] |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_dictionary(symbols): |
||||
|
"""dictionary : identifier EQUAL braces dictionary_next |
||||
|
| identifier EQUAL error dictionary_next |
||||
|
""" |
||||
|
if isinstance(symbols[3], ast.Expression): |
||||
|
symbols[0] = {} |
||||
|
symbols[0][symbols[1]] = symbols[3] |
||||
|
symbols[0].update(symbols[4]) |
||||
|
else: |
||||
|
raise ParsingError("Do enclose arguments between braces.") |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_identifier(symbols): |
||||
|
"""identifier : CHARACTER identifier |
||||
|
| empty |
||||
|
""" |
||||
|
if len(symbols) == 2: |
||||
|
symbols[0] = "" |
||||
|
else: |
||||
|
symbols[0] = symbols[1] + symbols[2] |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_separator(symbols): |
||||
|
"""separator : SPACE |
||||
|
| empty |
||||
|
""" |
||||
|
symbols[0] = None |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_dictonary_next(symbols): |
||||
|
"""dictionary_next : separator COMMA separator dictionary |
||||
|
| empty |
||||
|
""" |
||||
|
if len(symbols) == 5: |
||||
|
symbols[0] = symbols[4] |
||||
|
else: |
||||
|
symbols[0] = {} |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_titles(symbols): |
||||
|
"""titles : title titles_next""" |
||||
|
symbols[0] = [symbols[1]] + symbols[2] |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_titles_next(symbols): |
||||
|
"""titles_next : NEWLINE title titles_next |
||||
|
| empty |
||||
|
""" |
||||
|
if len(symbols) == 2: |
||||
|
symbols[0] = [] |
||||
|
else: |
||||
|
symbols[0] = [symbols[2]] + symbols[3] |
||||
|
|
||||
|
@staticmethod |
||||
|
def p_title(symbols): |
||||
|
"""title : brackets title |
||||
|
| braces title |
||||
|
| command title |
||||
|
| word title |
||||
|
| SPACE title |
||||
|
| empty |
||||
|
""" |
||||
|
if len(symbols) == 2: |
||||
|
symbols[0] = None |
||||
|
else: |
||||
|
if symbols[2] is None: |
||||
|
symbols[0] = ast.Expression(symbols[1]) |
||||
|
else: |
||||
|
symbols[0] = symbols[2].prepend(symbols[1]) |
||||
|
|
||||
|
def silent_yacc(*args, **kwargs): |
||||
|
"""Call yacc, suppressing (as far as possible) output and generated files. |
||||
|
""" |
||||
|
return yacc.yacc( |
||||
|
write_tables=0, |
||||
|
debug=0, |
||||
|
*args, |
||||
|
**kwargs |
||||
|
) |
||||
|
|
||||
|
def tex2plain(string): |
||||
|
"""Parse string and return its plain text version.""" |
||||
|
return detex( |
||||
|
silent_yacc( |
||||
|
module=Parser(), |
||||
|
).parse( |
||||
|
string, |
||||
|
lexer=SimpleLexer().lexer, |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
def parsesong(string, filename=None): |
||||
|
"""Parse song and return its metadata.""" |
||||
|
return detex( |
||||
|
silent_yacc(module=Parser(filename)).parse( |
||||
|
string, |
||||
|
lexer=SongLexer().lexer, |
||||
|
).metadata |
||||
|
) |
||||
|
|
@ -1,117 +0,0 @@ |
|||||
# -*- coding: utf-8 -*- |
|
||||
|
|
||||
"""PlasTeX module to process song files.""" |
|
||||
|
|
||||
from plasTeX.TeX import TeX |
|
||||
from plasTeX.Base.LaTeX import Sentences |
|
||||
|
|
||||
import locale |
|
||||
import os |
|
||||
import sys |
|
||||
|
|
||||
from patacrep import encoding |
|
||||
|
|
||||
def process_unbr_spaces(node): |
|
||||
#pylint: disable=line-too-long |
|
||||
r"""Replace '~' and '\ ' in node by nodes that |
|
||||
will be rendered as unbreakable space. |
|
||||
|
|
||||
Return node object for convenience. |
|
||||
|
|
||||
This function is a workaround to a bug that has been solved since: |
|
||||
- https://github.com/tiarno/plastex/commit/76bb78d5fbaac48e68025a3545286cc63cb4e7ad |
|
||||
- https://github.com/tiarno/plastex/commit/682a0d223b99d6b949bacf1c974d24dc9bb1d18e |
|
||||
|
|
||||
It can be deleted once this bug has been merged in production version of |
|
||||
PlasTeX. |
|
||||
""" |
|
||||
if (type(node) == Sentences.InterWordSpace or |
|
||||
(type(node) == Sentences.NoLineBreak and node.source == '~ ')): |
|
||||
node.unicode = unichr(160) |
|
||||
for child in node.childNodes: |
|
||||
process_unbr_spaces(child) |
|
||||
|
|
||||
return node |
|
||||
|
|
||||
|
|
||||
def simpleparse(text): |
|
||||
"""Parse a simple LaTeX string. |
|
||||
""" |
|
||||
tex = TeX() |
|
||||
tex.disableLogging() |
|
||||
tex.input(text) |
|
||||
doc = tex.parse() |
|
||||
return process_unbr_spaces(doc.textContent) |
|
||||
|
|
||||
|
|
||||
class SongParser(object): |
|
||||
"""Analyseur syntaxique de fichiers .sg""" |
|
||||
|
|
||||
@staticmethod |
|
||||
def create_tex(): |
|
||||
"""Create a TeX object, ready to parse a tex file.""" |
|
||||
tex = TeX() |
|
||||
tex.disableLogging() |
|
||||
tex.ownerDocument.context.loadBaseMacros() |
|
||||
sys.path.append(os.path.dirname(__file__)) |
|
||||
tex.ownerDocument.context.loadPackage(tex, "plastex_patchedbabel") |
|
||||
tex.ownerDocument.context.loadPackage(tex, "plastex_chord") |
|
||||
tex.ownerDocument.context.loadPackage(tex, "plastex_songs") |
|
||||
tex.ownerDocument.context.loadPackage(tex, "plastex_misc_commands") |
|
||||
sys.path.pop() |
|
||||
return tex |
|
||||
|
|
||||
@classmethod |
|
||||
def parse(cls, filename): |
|
||||
"""Parse a TeX file, and return its plasTeX representation.""" |
|
||||
tex = cls.create_tex() |
|
||||
tex.input(encoding.open_read(filename, 'r')) |
|
||||
return tex.parse() |
|
||||
|
|
||||
|
|
||||
def parsetex(filename): |
|
||||
r"""Analyse syntaxique d'un fichier .sg |
|
||||
|
|
||||
Renvoie un dictionnaire contenant les métadonnées lues dans le fichier. Les |
|
||||
clefs sont : |
|
||||
- languages: l'ensemble des langages utilisés (recherche des |
|
||||
\selectlanguages{}) ; |
|
||||
- titles: la liste des titres ; |
|
||||
- args: le dictionnaire des paramètres passés à \beginsong. |
|
||||
""" |
|
||||
# /* BEGIN plasTeX patch |
|
||||
# The following lines, and another line a few lines later, are used to |
|
||||
# circumvent a plasTeX bug. It has been reported and corrected : |
|
||||
# https://github.com/tiarno/plastex/commit/8f4e5a385f3cb6a04d5863f731ce24a7e856f2a4 |
|
||||
# To see if you can delete those lines, set your LC_TIME locale to French, |
|
||||
# during a month containing diacritics (e.g. Février), and run songbook. If |
|
||||
# no plasTeX bug appears, it is safe to remove those lines. |
|
||||
oldlocale = locale.getlocale(locale.LC_TIME) |
|
||||
locale.setlocale(locale.LC_TIME, 'C') |
|
||||
# plasTeX patch END */ |
|
||||
|
|
||||
# Analyse syntaxique |
|
||||
doc = SongParser.parse(filename) |
|
||||
|
|
||||
# /* BEGIN plasTeX patch |
|
||||
if oldlocale[0] and oldlocale[1]: |
|
||||
try: |
|
||||
locale.setlocale(locale.LC_TIME, "%s.%s" % oldlocale) |
|
||||
except locale.Error: |
|
||||
pass # Workaround a bug on windows |
|
||||
# plasTeX patch END */ |
|
||||
|
|
||||
# Extraction des données |
|
||||
data = { |
|
||||
"languages": set(), |
|
||||
"_doc": doc, |
|
||||
"_filename": filename, |
|
||||
} |
|
||||
for node in doc.allChildNodes: |
|
||||
if node.nodeName == "selectlanguage": |
|
||||
data["languages"].add(node.attributes['lang']) |
|
||||
if node.nodeName in ["beginsong", "sortassong"]: |
|
||||
data["titles"] = node.attributes["titles"] |
|
||||
data["args"] = node.attributes["args"] |
|
||||
|
|
||||
return data |
|
@ -1,181 +0,0 @@ |
|||||
# -*- coding: utf-8 -*- |
|
||||
|
|
||||
r"""PlasTeX module to deal with chords commands of the songs LaTeX package |
|
||||
|
|
||||
Chords are set using commands like \[C]. This package parses those commands. |
|
||||
""" |
|
||||
|
|
||||
import logging |
|
||||
|
|
||||
import plasTeX |
|
||||
from plasTeX import Command, Environment, Macro |
|
||||
from plasTeX.Base.LaTeX.Math import BeginDisplayMath |
|
||||
|
|
||||
LOGGER = logging.getLogger(__name__) |
|
||||
|
|
||||
# Count the number of levels of 'verse' environment: IN_VERSE==1 means that we |
|
||||
# are in a 'verse' environment; IN_VERSE==2 means that we are in two included |
|
||||
# 'verse' environment, and so on. |
|
||||
IN_VERSE = 0 |
|
||||
|
|
||||
def wrap_displaymath(cls): |
|
||||
"""Decorator to store the depth of 'verse' environment |
|
||||
|
|
||||
In the invoke() method classes, global variable IN_VERSE indicates the |
|
||||
number of 'verse' (or 'chorus' or 'verse*') environment we are in. |
|
||||
""" |
|
||||
|
|
||||
# pylint: disable=no-init,too-few-public-methods |
|
||||
class WrappedClass(cls): |
|
||||
"""Wrapper to LaTeX environment updating IN_VERSE""" |
|
||||
blockType = True |
|
||||
# pylint: disable=super-on-old-class,global-statement,no-member |
|
||||
def invoke(self, tex): |
|
||||
"""Wrapper to invoke() to update global variable IN_VERSE.""" |
|
||||
global IN_VERSE |
|
||||
if self.macroMode == Macro.MODE_BEGIN: |
|
||||
self.ownerDocument.context.push() |
|
||||
self.ownerDocument.context.catcode("\n", 13) |
|
||||
IN_VERSE += 1 |
|
||||
|
|
||||
# Removing spaces and line breaks at the beginning of verse |
|
||||
token = None |
|
||||
for token in tex: |
|
||||
if not match_space(token): |
|
||||
break |
|
||||
if token is not None: |
|
||||
tex.pushToken(token) |
|
||||
|
|
||||
else: |
|
||||
self.ownerDocument.context.pop() |
|
||||
IN_VERSE -= 1 |
|
||||
return super(WrappedClass, self).invoke(tex) |
|
||||
return WrappedClass |
|
||||
|
|
||||
# pylint: disable=too-many-public-methods |
|
||||
@wrap_displaymath |
|
||||
class Verse(Environment): |
|
||||
"""LaTeX 'verse' environment""" |
|
||||
macroName = 'verse' |
|
||||
|
|
||||
# pylint: disable=too-many-public-methods |
|
||||
@wrap_displaymath |
|
||||
class VerseStar(Environment): |
|
||||
"""LaTeX 'verse*' environment""" |
|
||||
macroName = 'verse*' |
|
||||
|
|
||||
# pylint: disable=too-many-public-methods |
|
||||
@wrap_displaymath |
|
||||
class Chorus(Environment): |
|
||||
"""LaTeX 'chorus' environment""" |
|
||||
macroName = 'chorus' |
|
||||
|
|
||||
def match_space(token): |
|
||||
"""Return True if token is a space or newline character.""" |
|
||||
return ( |
|
||||
isinstance(token, plasTeX.Tokenizer.Space) |
|
||||
or token.nodeName == 'active::\n' |
|
||||
) |
|
||||
|
|
||||
def match_closing_square_bracket(token): |
|
||||
"""Return True if token is character ']'.""" |
|
||||
return token.nodeType == token.TEXT_NODE and token.nodeValue == ']' |
|
||||
|
|
||||
def match_egroup(token): |
|
||||
"""Return True if token is of type `egroup` (end of group).""" |
|
||||
return isinstance(token, plasTeX.Base.Text.egroup) #pylint: disable=no-member |
|
||||
|
|
||||
def match_space_or_chord(token): |
|
||||
"""Return True if token is a space or a chord.""" |
|
||||
return match_space(token) or isinstance(token, Chord) |
|
||||
|
|
||||
def parse_until(tex, end=lambda x: False): |
|
||||
"""Parse `tex` until condition `end`, or `egroup` is met. |
|
||||
|
|
||||
Arguments: |
|
||||
- tex: object to parse |
|
||||
- end: function taking a token in argument, and returning a boolean. |
|
||||
Parsing stops when this function returns True, or an `egroup` is met. |
|
||||
|
|
||||
Return: a tuple of two items (the list of parsed tokens, last token). This |
|
||||
is done so that caller can decide whether they want to discard it or not. |
|
||||
Last token can be None if everything has been parsed without the end |
|
||||
condition being met. |
|
||||
""" |
|
||||
parsed = [] |
|
||||
last = None |
|
||||
for token in tex: |
|
||||
if end(token) or match_egroup(token): |
|
||||
last = token |
|
||||
break |
|
||||
elif isinstance(token, plasTeX.Base.Text.bgroup): #pylint: disable=no-member |
|
||||
# pylint: disable=expression-not-assigned |
|
||||
[token.appendChild(item) for item in parse_until(tex, match_egroup)[0]] |
|
||||
parsed.append(token) |
|
||||
return (parsed, last) |
|
||||
|
|
||||
|
|
||||
class Chord(Command): |
|
||||
"""Beginning of a chord notation""" |
|
||||
macroName = 'chord' |
|
||||
macroMode = Command.MODE_NONE |
|
||||
|
|
||||
class BeginChordOrDisplayMath(BeginDisplayMath): |
|
||||
r"""Wrapper to BeginDisplayMath |
|
||||
|
|
||||
In a 'verse' (or 'verse*' or 'chorus') environment, the '\[' macro |
|
||||
displays a chord. Otherwise, it corresponds to the usual LaTeX math mode. |
|
||||
This class calls the right method, depending on the inclusion of this |
|
||||
macro in a verse environment. |
|
||||
""" |
|
||||
macroName = '[' |
|
||||
|
|
||||
def invoke(self, tex): |
|
||||
"""Process this macro""" |
|
||||
if IN_VERSE: |
|
||||
chord = Chord() |
|
||||
|
|
||||
self.ownerDocument.context.push() #pylint: disable=no-member |
|
||||
self.ownerDocument.context.catcode("&", 13) #pylint: disable=no-member |
|
||||
chord.setAttribute( |
|
||||
'name', |
|
||||
parse_until(tex, match_closing_square_bracket)[0], |
|
||||
) |
|
||||
self.ownerDocument.context.pop() #pylint: disable=no-member |
|
||||
|
|
||||
token = next(iter(tex), None) |
|
||||
if token is None: |
|
||||
return [chord] |
|
||||
elif match_space(token): |
|
||||
return [chord, token] |
|
||||
elif ( |
|
||||
isinstance(token, Verse) |
|
||||
or isinstance(token, VerseStar) |
|
||||
or isinstance(token, Chorus) |
|
||||
): |
|
||||
LOGGER.warning(( |
|
||||
"{} L{}: '\\end{{verse}}' (or 'verse*' or 'chorus') not " |
|
||||
"allowed directly after '\\['." |
|
||||
).format(tex.filename, tex.lineNumber) |
|
||||
) |
|
||||
return [chord] |
|
||||
elif isinstance(token, Chord): |
|
||||
token.attributes['name'] = ( |
|
||||
chord.attributes['name'] |
|
||||
+ token.attributes['name'] |
|
||||
) |
|
||||
chord = token |
|
||||
return [chord] |
|
||||
elif isinstance(token, plasTeX.Base.Text.bgroup): #pylint: disable=no-member |
|
||||
# pylint: disable=expression-not-assigned |
|
||||
[chord.appendChild(item) for item in parse_until(tex)[0]] |
|
||||
return [chord] |
|
||||
else: |
|
||||
chord.appendChild(token) |
|
||||
(parsed, last) = parse_until(tex, match_space_or_chord) |
|
||||
# pylint: disable=expression-not-assigned |
|
||||
[chord.appendChild(item) for item in parsed] |
|
||||
return [chord, last] |
|
||||
else: |
|
||||
return super(BeginChordOrDisplayMath, self).invoke(tex) |
|
||||
|
|
@ -1,15 +0,0 @@ |
|||||
# -*- coding: utf-8 -*- |
|
||||
|
|
||||
"""Quick management of random LaTeX commands.""" |
|
||||
|
|
||||
from plasTeX import Command |
|
||||
|
|
||||
# pylint: disable=invalid-name,too-many-public-methods |
|
||||
class songcolumns(Command): |
|
||||
r"""Manage `\songcolumns` command""" |
|
||||
args = '{num:int}' |
|
||||
|
|
||||
# pylint: disable=invalid-name,too-many-public-methods |
|
||||
class gtab(Command): |
|
||||
r"""Manage `\gta` command""" |
|
||||
args = '{chord:str}{diagram:str}' |
|
@ -1,58 +0,0 @@ |
|||||
# -*- coding: utf-8 -*- |
|
||||
|
|
||||
r"""Patch pour le paquet Babel de PlasTeX |
|
||||
|
|
||||
Un bug dans PlasTeX intervient lorsqu'on essaye d'analyser une commande LaTeX |
|
||||
\selectlanguage{}, que nous voulons utiliser ici. Un patch a été proposé aux |
|
||||
développeurs de plasTeX, et accepté. Mais il faut que cette correction arrive |
|
||||
en production. En attendant, nous utilisons cette version modifiée. |
|
||||
|
|
||||
Dés que la correction sera entrée en production, il faudra supprimer ce |
|
||||
fichier, et remplater l'occurence à "patchedbabel" par "babel" dans le fichier |
|
||||
"plastex.py". |
|
||||
La correction à suveiller est la révision |
|
||||
41a48c0c229dd46b69fb0e3720595000a71b17d8 du fichier babel.py : |
|
||||
https://github.com/tiarno/plastex/commit/41a48c0c229dd46b69fb0e3720595000a71b17d8 |
|
||||
|
|
||||
# Comment vérifier si on peut supprimer ce fichier ? |
|
||||
|
|
||||
1) Remplacer l'occurence à patchedbabel par babel dans le fichier plastex.py. |
|
||||
|
|
||||
2) Générer un fichier .tex à partir d'un fichier .sb, ce dernier faisant |
|
||||
intervenir des chansons dans lesquelles \selectlanguage est utilisé (par |
|
||||
exemple, "make -B matteo.tex" ou "make -B naheulbeuk.tex" pour des fichiers pas |
|
||||
trop gros. |
|
||||
|
|
||||
3) Si l'erreur suivante apparaît, c'est qu'il faut encore attendre. |
|
||||
|
|
||||
> Traceback (most recent call last): |
|
||||
> [...] |
|
||||
> File "/usr/lib/pymodules/python2.7/plasTeX/Packages/babel.py", line 18, in |
|
||||
> invoke context.loadLanguage(self.attributes['lang'], self.ownerDocument) |
|
||||
> NameError: global name 'context' is not defined |
|
||||
|
|
||||
3 bis) Si elle n'apparait pas : youpi ! Supprimez ce fichier ! |
|
||||
|
|
||||
# Contact et commentaires |
|
||||
|
|
||||
Mercredi 27 mars 2013 |
|
||||
Louis <spalax(at)gresille.org> |
|
||||
|
|
||||
""" |
|
||||
|
|
||||
from plasTeX import Command |
|
||||
|
|
||||
# pylint: disable=invalid-name,too-many-public-methods |
|
||||
class selectlanguage(Command): |
|
||||
"""Patch of vanilla selectlanguage class. |
|
||||
|
|
||||
See module docstring for more information.""" |
|
||||
args = 'lang:str' |
|
||||
|
|
||||
def invoke(self, tex): |
|
||||
res = Command.invoke(self, tex) |
|
||||
self.ownerDocument.context.loadLanguage( # pylint: disable=no-member |
|
||||
self.attributes['lang'], |
|
||||
self.ownerDocument |
|
||||
) |
|
||||
return res |
|
@ -1,70 +0,0 @@ |
|||||
# -*- coding: utf-8 -*- |
|
||||
|
|
||||
"""Module to process song LaTeX environment. |
|
||||
""" |
|
||||
|
|
||||
import plasTeX |
|
||||
|
|
||||
from patacrep import encoding |
|
||||
from patacrep.plastex import process_unbr_spaces |
|
||||
|
|
||||
|
|
||||
def split_linebreak(texlist): |
|
||||
"""Return a list of alternative title. |
|
||||
|
|
||||
A title can be defined with alternative names : |
|
||||
|
|
||||
A real name\\ |
|
||||
Alternative name\\ |
|
||||
Another alternative name |
|
||||
|
|
||||
This function takes the object representation of a list of titles, and |
|
||||
return a list of titles. |
|
||||
""" |
|
||||
return_list = [] |
|
||||
current = [] |
|
||||
for token in texlist: |
|
||||
if token.nodeName == '\\': |
|
||||
return_list.append(current) |
|
||||
current = [] |
|
||||
else: |
|
||||
current.append(encoding.basestring2unicode( |
|
||||
process_unbr_spaces(token).textContent |
|
||||
)) |
|
||||
if current: |
|
||||
return_list.append(current) |
|
||||
return return_list |
|
||||
|
|
||||
|
|
||||
class beginsong(plasTeX.Command): # pylint: disable=invalid-name,too-many-public-methods |
|
||||
"""Class parsing the LaTeX song environment.""" |
|
||||
|
|
||||
args = '{titles}[args:dict]' |
|
||||
|
|
||||
def invoke(self, tex): |
|
||||
"""Parse an occurence of song environment.""" |
|
||||
|
|
||||
plasTeX.Command.invoke(self, tex) |
|
||||
|
|
||||
# Parsing title |
|
||||
titles = [] |
|
||||
for tokens in split_linebreak(self.attributes['titles'].allChildNodes): |
|
||||
titles.append("".join(tokens)) |
|
||||
self.attributes['titles'] = encoding.list2unicode(titles) |
|
||||
|
|
||||
# Parsing keyval arguments |
|
||||
args = {} |
|
||||
for (key, val) in self.attributes['args'].iteritems(): |
|
||||
if isinstance(val, plasTeX.DOM.Element): |
|
||||
args[key] = encoding.basestring2unicode( |
|
||||
process_unbr_spaces(val).textContent |
|
||||
) |
|
||||
elif isinstance(val, basestring): |
|
||||
args[key] = encoding.basestring2unicode(val) |
|
||||
else: |
|
||||
args[key] = unicode(val) |
|
||||
self.attributes['args'] = args |
|
||||
|
|
||||
class sortassong(beginsong): # pylint: disable=invalid-name,too-many-public-methods |
|
||||
r"""Treat '\sortassong' exactly as if it were a '\beginsong'.""" |
|
||||
pass |
|
@ -0,0 +1,157 @@ |
|||||
|
#! /usr/bin/env python3 |
||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
"""Command line tool to compile songbooks using the songbook library.""" |
||||
|
|
||||
|
import argparse |
||||
|
import json |
||||
|
import locale |
||||
|
import logging |
||||
|
import os.path |
||||
|
import textwrap |
||||
|
import sys |
||||
|
|
||||
|
from patacrep.build import SongbookBuilder, DEFAULT_STEPS |
||||
|
from patacrep import __version__ |
||||
|
from patacrep import errors |
||||
|
import patacrep.encoding |
||||
|
|
||||
|
# Logging configuration |
||||
|
logging.basicConfig(level=logging.INFO) |
||||
|
LOGGER = logging.getLogger() |
||||
|
|
||||
|
# pylint: disable=too-few-public-methods |
||||
|
class ParseStepsAction(argparse.Action): |
||||
|
"""Argparse action to split a string into a list.""" |
||||
|
def __call__(self, __parser, namespace, values, __option_string=None): |
||||
|
if not getattr(namespace, self.dest): |
||||
|
setattr(namespace, self.dest, []) |
||||
|
setattr( |
||||
|
namespace, |
||||
|
self.dest, |
||||
|
( |
||||
|
getattr(namespace, self.dest) |
||||
|
+ [value.strip() for value in values[0].split(',')] |
||||
|
), |
||||
|
) |
||||
|
|
||||
|
class VerboseAction(argparse.Action): |
||||
|
"""Set verbosity level with option --verbose.""" |
||||
|
def __call__(self, *_args, **_kwargs): |
||||
|
LOGGER.setLevel(logging.DEBUG) |
||||
|
|
||||
|
def argument_parser(args): |
||||
|
"""Parse arguments""" |
||||
|
parser = argparse.ArgumentParser(description="A song book compiler") |
||||
|
|
||||
|
parser.add_argument('--version', help='Show version', action='version', |
||||
|
version='%(prog)s ' + __version__) |
||||
|
|
||||
|
parser.add_argument('book', nargs=1, help=textwrap.dedent("""\ |
||||
|
Book to compile. |
||||
|
""")) |
||||
|
|
||||
|
parser.add_argument('--datadir', '-d', nargs='+', type=str, action='append', |
||||
|
help=textwrap.dedent("""\ |
||||
|
Data location. Expected (not necessarily required) |
||||
|
subdirectories are 'songs', 'img', 'latex', 'templates'. |
||||
|
""")) |
||||
|
|
||||
|
parser.add_argument('--verbose', '-v', nargs=0, action=VerboseAction, |
||||
|
help=textwrap.dedent("""\ |
||||
|
Show details about the compilation process. |
||||
|
""")) |
||||
|
|
||||
|
parser.add_argument('--steps', '-s', nargs=1, type=str, |
||||
|
action=ParseStepsAction, |
||||
|
help=textwrap.dedent("""\ |
||||
|
Steps to run. Default is "{steps}". |
||||
|
Available steps are: |
||||
|
"tex" produce .tex file from templates; |
||||
|
"pdf" compile .tex file; |
||||
|
"sbx" compile index files; |
||||
|
"clean" remove temporary files; |
||||
|
any string beginning with '%%' (in this case, it will be run |
||||
|
in a shell). Several steps (excepted the custom shell |
||||
|
command) can be combinend in one --steps argument, as a |
||||
|
comma separated string. |
||||
|
""".format(steps=','.join(DEFAULT_STEPS))), |
||||
|
default=None, |
||||
|
) |
||||
|
|
||||
|
options = parser.parse_args(args) |
||||
|
|
||||
|
return options |
||||
|
|
||||
|
|
||||
|
def main(): |
||||
|
"""Main function:""" |
||||
|
|
||||
|
# set script locale to match user's |
||||
|
try: |
||||
|
locale.setlocale(locale.LC_ALL, '') |
||||
|
except locale.Error as error: |
||||
|
# Locale is not installed on user's system, or wrongly configured. |
||||
|
LOGGER.error("Locale error: {}\n".format(str(error))) |
||||
|
|
||||
|
options = argument_parser(sys.argv[1:]) |
||||
|
|
||||
|
songbook_path = options.book[0] |
||||
|
|
||||
|
basename = os.path.basename(songbook_path)[:-3] |
||||
|
|
||||
|
try: |
||||
|
with patacrep.encoding.open_read(songbook_path) as songbook_file: |
||||
|
songbook = json.load(songbook_file) |
||||
|
if 'encoding' in songbook: |
||||
|
with patacrep.encoding.open_read( |
||||
|
songbook_path, |
||||
|
encoding=songbook['encoding'] |
||||
|
) as songbook_file: |
||||
|
songbook = json.load(songbook_file) |
||||
|
except Exception as error: # pylint: disable=broad-except |
||||
|
LOGGER.error(error) |
||||
|
LOGGER.error("Error while loading file '{}'.".format(songbook_path)) |
||||
|
sys.exit(1) |
||||
|
|
||||
|
# Gathering datadirs |
||||
|
datadirs = [] |
||||
|
if options.datadir: |
||||
|
# Command line options |
||||
|
datadirs += [item[0] for item in options.datadir] |
||||
|
if 'datadir' in songbook: |
||||
|
# .sg file |
||||
|
if isinstance(songbook['datadir'], str): |
||||
|
songbook['datadir'] = [songbook['datadir']] |
||||
|
datadirs += [ |
||||
|
os.path.join( |
||||
|
os.path.dirname(os.path.abspath(songbook_path)), |
||||
|
path |
||||
|
) |
||||
|
for path in songbook['datadir'] |
||||
|
] |
||||
|
# Default value |
||||
|
datadirs.append(os.path.dirname(os.path.abspath(songbook_path))) |
||||
|
|
||||
|
songbook['datadir'] = datadirs |
||||
|
|
||||
|
try: |
||||
|
sb_builder = SongbookBuilder(songbook, basename) |
||||
|
sb_builder.unsafe = True |
||||
|
|
||||
|
sb_builder.build_steps(options.steps) |
||||
|
except errors.SongbookError as error: |
||||
|
LOGGER.error(error) |
||||
|
if LOGGER.level >= logging.INFO: |
||||
|
LOGGER.error( |
||||
|
"Running again with option '-v' may give more information." |
||||
|
) |
||||
|
sys.exit(1) |
||||
|
except KeyboardInterrupt: |
||||
|
LOGGER.warning("Aborted by user.") |
||||
|
sys.exit(1) |
||||
|
|
||||
|
sys.exit(0) |
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
main() |
@ -0,0 +1,32 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
"""Very simple LaTeX parsing.""" |
||||
|
|
||||
|
import os |
||||
|
|
||||
|
from patacrep import files |
||||
|
from patacrep.latex import parsesong |
||||
|
from patacrep.songs import Song |
||||
|
|
||||
|
class TexRenderer(Song): |
||||
|
"""Renderer for song and intersong files.""" |
||||
|
|
||||
|
def parse(self): |
||||
|
"""Parse song and set metadata.""" |
||||
|
self.data = parsesong(self.fullpath, self.encoding) |
||||
|
self.titles = self.data['@titles'] |
||||
|
self.languages = self.data['@languages'] |
||||
|
self.authors = self.data['by'] |
||||
|
|
||||
|
def render(self, context): |
||||
|
"""Return the string that will render the song.""" |
||||
|
return r'\input{{{}}}'.format(files.path2posix( |
||||
|
files.relpath( |
||||
|
self.fullpath, |
||||
|
os.path.dirname(context['filename']) |
||||
|
))) |
||||
|
|
||||
|
FILE_PLUGINS = { |
||||
|
'sg': TexRenderer, |
||||
|
'is': TexRenderer, |
||||
|
} |
@ -1,152 +1,9 @@ |
|||||
#! /usr/bin/env python2 |
#! /usr/bin/env python3 |
||||
# -*- coding: utf-8 -*- |
|
||||
|
|
||||
"""Command line tool to compile songbooks using the songbook library.""" |
# Do not edit this file. This file is just a helper file for development test. |
||||
|
# It is not part of the distributed software. |
||||
import argparse |
|
||||
import json |
|
||||
import locale |
|
||||
import logging |
|
||||
import os.path |
|
||||
import textwrap |
|
||||
import sys |
|
||||
|
|
||||
from patacrep.build import SongbookBuilder, DEFAULT_STEPS |
|
||||
from patacrep import __version__ |
|
||||
from patacrep import errors |
|
||||
from patacrep import encoding |
|
||||
|
|
||||
# Logging configuration |
|
||||
logging.basicConfig(level=logging.INFO) |
|
||||
LOGGER = logging.getLogger() |
|
||||
|
|
||||
# pylint: disable=too-few-public-methods |
|
||||
class ParseStepsAction(argparse.Action): |
|
||||
"""Argparse action to split a string into a list.""" |
|
||||
def __call__(self, __parser, namespace, values, __option_string=None): |
|
||||
if not getattr(namespace, self.dest): |
|
||||
setattr(namespace, self.dest, []) |
|
||||
setattr( |
|
||||
namespace, |
|
||||
self.dest, |
|
||||
( |
|
||||
getattr(namespace, self.dest) |
|
||||
+ [value.strip() for value in values[0].split(',')] |
|
||||
), |
|
||||
) |
|
||||
|
|
||||
class VerboseAction(argparse.Action): |
|
||||
"""Set verbosity level with option --verbose.""" |
|
||||
def __call__(self, *_args, **_kwargs): |
|
||||
LOGGER.setLevel(logging.DEBUG) |
|
||||
|
|
||||
def argument_parser(args): |
|
||||
"""Parse arguments""" |
|
||||
parser = argparse.ArgumentParser(description="A song book compiler") |
|
||||
|
|
||||
parser.add_argument('--version', help='Show version', action='version', |
|
||||
version='%(prog)s ' + __version__) |
|
||||
|
|
||||
parser.add_argument('book', nargs=1, help=textwrap.dedent("""\ |
|
||||
Book to compile. |
|
||||
""")) |
|
||||
|
|
||||
parser.add_argument('--datadir', '-d', nargs='+', type=str, action='append', |
|
||||
help=textwrap.dedent("""\ |
|
||||
Data location. Expected (not necessarily required) |
|
||||
subdirectories are 'songs', 'img', 'latex', 'templates'. |
|
||||
""")) |
|
||||
|
|
||||
parser.add_argument('--verbose', '-v', nargs=0, action=VerboseAction, |
|
||||
help=textwrap.dedent("""\ |
|
||||
Show details about the compilation process. |
|
||||
""")) |
|
||||
|
|
||||
parser.add_argument('--steps', '-s', nargs=1, type=str, |
|
||||
action=ParseStepsAction, |
|
||||
help=textwrap.dedent("""\ |
|
||||
Steps to run. Default is "{steps}". |
|
||||
Available steps are: |
|
||||
"tex" produce .tex file from templates; |
|
||||
"pdf" compile .tex file; |
|
||||
"sbx" compile index files; |
|
||||
"clean" remove temporary files; |
|
||||
any string beginning with '%%' (in this case, it will be run |
|
||||
in a shell). Several steps (excepted the custom shell |
|
||||
command) can be combinend in one --steps argument, as a |
|
||||
comma separated string. |
|
||||
""".format(steps=','.join(DEFAULT_STEPS))), |
|
||||
default=None, |
|
||||
) |
|
||||
|
|
||||
options = parser.parse_args(args) |
|
||||
|
|
||||
return options |
"""Command line tool to compile songbooks using the songbook library.""" |
||||
|
|
||||
|
|
||||
def main(): |
|
||||
"""Main function:""" |
|
||||
|
|
||||
# set script locale to match user's |
|
||||
try: |
|
||||
locale.setlocale(locale.LC_ALL, '') |
|
||||
except locale.Error as error: |
|
||||
# Locale is not installed on user's system, or wrongly configured. |
|
||||
sys.stderr.write("Locale error: {}\n".format(error.message)) |
|
||||
|
|
||||
options = argument_parser(sys.argv[1:]) |
|
||||
|
|
||||
songbook_path = options.book[0] |
|
||||
|
|
||||
basename = os.path.basename(songbook_path)[:-3] |
|
||||
|
|
||||
songbook_file = None |
|
||||
try: |
|
||||
songbook_file = encoding.open_read(songbook_path) |
|
||||
songbook = json.load(songbook_file) |
|
||||
except Exception as error: # pylint: disable=broad-except |
|
||||
LOGGER.error(error) |
|
||||
LOGGER.error("Error while loading file '{}'.".format(songbook_path)) |
|
||||
sys.exit(1) |
|
||||
finally: |
|
||||
if songbook_file: |
|
||||
songbook_file.close() |
|
||||
|
|
||||
# Gathering datadirs |
|
||||
datadirs = [] |
|
||||
if options.datadir: |
|
||||
# Command line options |
|
||||
datadirs += [item[0] for item in options.datadir] |
|
||||
if 'datadir' in songbook: |
|
||||
# .sg file |
|
||||
if isinstance(songbook['datadir'], basestring): |
|
||||
songbook['datadir'] = [songbook['datadir']] |
|
||||
datadirs += [ |
|
||||
os.path.join( |
|
||||
os.path.dirname(os.path.abspath(songbook_path)), |
|
||||
path |
|
||||
) |
|
||||
for path in songbook['datadir'] |
|
||||
] |
|
||||
# Default value |
|
||||
datadirs.append(os.path.dirname(os.path.abspath(songbook_path))) |
|
||||
|
|
||||
songbook['datadir'] = datadirs |
|
||||
|
|
||||
try: |
|
||||
sb_builder = SongbookBuilder(songbook, basename) |
|
||||
sb_builder.unsafe = True |
|
||||
|
|
||||
sb_builder.build_steps(options.steps) |
|
||||
except errors.SongbookError as error: |
|
||||
LOGGER.error(error) |
|
||||
if LOGGER.level >= logging.INFO: |
|
||||
LOGGER.error( |
|
||||
"Running again with option '-v' may give more information." |
|
||||
) |
|
||||
sys.exit(1) |
|
||||
|
|
||||
sys.exit(0) |
|
||||
|
|
||||
if __name__ == '__main__': |
from patacrep.songbook import main |
||||
main() |
main() |
||||
|
@ -1,6 +0,0 @@ |
|||||
[DEFAULT] |
|
||||
Depends: python-jinja2, python-pkg-resources, python-plastex, python-chardet, python-unidecode, texlive-latex-base, texlive-latex-recommended, texlive-latex-extra, lilypond, texlive-fonts-recommended |
|
||||
Recommends: texlive-lang-english, texlive-lang-french, texlive-lang-portuguese, texlive-lang-spanish, texlive-fonts-extra |
|
||||
XS-Python-Version: >=2.7 |
|
||||
Section: tex |
|
||||
|
|
Loading…
Reference in new issue