diff --git a/index.py b/index.py index 965bdad9..4bd529b4 100644 --- a/index.py +++ b/index.py @@ -104,13 +104,13 @@ class index: self.prefix_patterns = [] if 'prefix' in self.keywords: for prefix in self.keywords['prefix']: - self.prefix_patterns.append(re.compile(r"^(%s)\b\s*(.*)$" % prefix)) + self.prefix_patterns.append(re.compile(r"^(%s)(\b|\\)(\s*.*)$" % prefix)) def add(self, key, number, link): for pattern in self.prefix_patterns: match = pattern.match(key) if match: - key = "%s (%s)" % (match.group(2), match.group(1)) + key = "%s (%s)" % (match.group(2) + match.group(3), match.group(1)) break # Only one match per key (first, key) = self.filter(key) if not self.data.has_key(first): diff --git a/songbook.py b/songbook.py index 082e31e2..6b3d711b 100755 --- a/songbook.py +++ b/songbook.py @@ -10,10 +10,46 @@ import shutil import json import re from subprocess import call - from tools import recursiveFind -from song import * from index import * +from unidecode import unidecode +from utils.plastex import parsetex + +class Song: + #: Ordre de tri + sort = [] + #: Préfixes à ignorer pour le tri + prefixes = [] + + def __init__(self, path, languages, titles, args): + self.titles = titles + self.normalized_titles = [locale.strxfrm(unprefixed(unidecode(unicode(title, "utf-8")), self.prefixes)) for title in titles] + self.args = args + self.path = path + self.languages = languages + + 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) + 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 matchRegexp(reg, iterable): return [ m.group(1) for m in (reg.match(l) for l in iterable) if m ] @@ -27,28 +63,56 @@ def unprefixed(title, prefixes): return match.group(2) return title -def songslist(library, songs, prefixes): - song_objects = [] - for s in songs: - path = library + 'songs/' + s - with open(path, 'r+') as f: - data = f.read() - title = reTitle.search(data).group(0) - artist = reArtist.search(data.replace("{","")).group(0) - match = reAlbum.search(data.replace("{","")) - lilypond = False - if match: - album = match.group(0) +class SongsList: + """Manipulation et traitement de liste de chansons""" + + def __init__(self, library, language): + self._library = library + self._language = language + + # Liste triée des chansons + self.songs = [] + + + def append(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.). + """ + path = os.path.join(self._library, 'songs', filename) + # Exécution de PlasTeX + data = parsetex(path) + + song = Song(path, data['languages'], data['titles'], data['args']) + low, high = 0, len(self.songs) + while low != high: + middle = (low + high) / 2 + if song < self.songs[middle]: + high = middle else: - album = '' - song_objects.append(Song(title, artist, album, path, lilypond)) + low = middle + 1 + self.songs.insert(low, song) + + def append_list(self, filelist): + """Ajoute une liste de chansons à la liste + + L'argument est une liste de chaînes, représentant des noms de fichiers. + """ + for filename in filelist: + self.append(filename) - song_objects = sorted(song_objects, key=lambda x: locale.strxfrm(unprefixed(x.title, prefixes))) - song_objects = sorted(song_objects, key=lambda x: locale.strxfrm(x.album)) - song_objects = sorted(song_objects, key=lambda x: locale.strxfrm(x.artist)) + def latex(self): + """Renvoie le code LaTeX nécessaire pour intégrer la liste de chansons. + """ + result = [ '\\input{{{0}}}'.format(song.path.replace("\\","/").strip()) for song in self.songs] + result.append('\\selectlanguage{%s}' % self._language) + return '\n'.join(result) - result = [ '\\input{{{0}}}'.format(song.path.replace("\\","/").strip()) for song in song_objects ] - return '\n'.join(result) + def languages(self): + """Renvoie la liste des langues utilisées par les chansons""" + return set().union(*[set(song.languages) for song in self.songs]) def parseTemplate(template): embeddedJsonPattern = re.compile(r"^%%:") @@ -115,9 +179,26 @@ def makeTexFile(sb, library, output): for prefix in sb["titleprefixwords"]: titleprefixwords += "\\titleprefixword{%s}\n" % prefix sb["titleprefixwords"] = titleprefixwords + if "lang" not in sb: + sb["lang"] = "french" + if "sort" in sb: + sort = sb["sort"] + del sb["sort"] + else: + sort = [u"by", u"album", u"@title"] + Song.sort = sort + Song.prefixes = prefixes parameters = parseTemplate("templates/"+template) + # compute songslist + if songs == "all": + songs = map(lambda x: x[len(library) + 6:], recursiveFind(os.path.join(library, 'songs'), '*.sg')) + songslist = SongsList(library, sb["lang"]) + songslist.append_list(songs) + + sb["languages"] = ",".join(songslist.languages()) + # output relevant fields out = open(output, 'w') out.write('%% This file has been automatically generated, do not edit!\n') @@ -132,12 +213,9 @@ def makeTexFile(sb, library, output): for name, value in sb.iteritems(): if name in parameters: out.write(formatDefinition(name, toValue(parameters[name],value))) - # output songslist - if songs == "all": - songs = map(lambda x: x[len(library) + 6:], recursiveFind(os.path.join(library, 'songs'), '*.sg')) if len(songs) > 0: - out.write(formatDefinition('songslist', songslist(library, songs, prefixes))) + out.write(formatDefinition('songslist', songslist.latex())) out.write('\\makeatother\n') # output template diff --git a/templates/ancient.tmpl b/templates/ancient.tmpl index b4075a75..f3631c7d 100644 --- a/templates/ancient.tmpl +++ b/templates/ancient.tmpl @@ -42,14 +42,17 @@ %%: {"name":"mainfontsize", "description":"Font Size", "type":"font", "default":"10"}, %%: {"name":"songnumberbgcolor", "description":"Number Shade", "type":"color", "default":"#D1E4AE"}, %%: {"name":"notebgcolor", "description":"Note Shade", "type":"color", "default":"#FFFDB3"}, -%%: {"name":"indexbgcolor", "description":"Index Shade", "type":"color", "default":"#D1E4AE"} +%%: {"name":"indexbgcolor", "description":"Index Shade", "type":"color", "default":"#D1E4AE"}, +%%: {"name":"languages", "description":"List of languages used by songs", "default":""} %%:] %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % begin document \makeatletter\def\input@path{{tex/}} \documentclass[\getbooktype,\getinstruments,\getbookoptions,\getmainfontsize]{crepbook} \usepackage[utf8]{inputenc} -\usepackage[italian,portuguese,english,spanish,french]{babel} +\PassOptionsToPackage{\getlanguages}{babel} +\PassOptionsToPackage{\getlang}{babel} +\usepackage{babel} \usepackage[T1]{fontenc} \usepackage{venturisold} diff --git a/templates/minimal.tmpl b/templates/minimal.tmpl index a159a5b4..c6157c5a 100644 --- a/templates/minimal.tmpl +++ b/templates/minimal.tmpl @@ -30,7 +30,8 @@ %%: {"name":"instruments", "description":"Instruments", "type":"flag", "values":["guitar","ukulele"], "join":",", "mandatory":true, "default":["guitar"]}, %%: {"name":"bookoptions", "description":"Options", "type":"flag", "values":["diagram","importantdiagramonly","lilypond","pictures","tabs","repeatchords","onesongperpage"], "join":",", "mandatory":true, "default":["pictures"]}, %%: {"name":"mainfontsize", "description":"Font Size", "type":"font", "default":"10"}, -%%: {"name":"titleprefixwords", "description":"Ignore some words in the beginning of song titles"} +%%: {"name":"titleprefixwords", "description":"Ignore some words in the beginning of song titles"}, +%%: {"name":"languages", "description":"List of languages used by songs", "default":""} %%:] %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % begin document @@ -39,7 +40,9 @@ \usepackage[utf8]{inputenc} \usepackage[T1]{fontenc} \usepackage{lmodern} -\usepackage[italian,portuguese,spanish,english,french]{babel} +\PassOptionsToPackage{\getlanguages}{babel} +\PassOptionsToPackage{\getlang}{babel} +\usepackage{babel} \lang{\getlang} \graphicspath{ {img/}, {\getLibraryImgDirectory}, {\getLibraryLilypondDirectory} } diff --git a/templates/patacrep.tmpl b/templates/patacrep.tmpl index 9a1fa2e6..cc493b3f 100644 --- a/templates/patacrep.tmpl +++ b/templates/patacrep.tmpl @@ -43,7 +43,8 @@ %%: {"name":"songnumberbgcolor", "description":"Number Shade", "type":"color", "default":"#D1E4AE"}, %%: {"name":"notebgcolor", "description":"Note Shade", "type":"color", "default":"#D1E4AE"}, %%: {"name":"indexbgcolor", "description":"Index Shade", "type":"color", "default":"#D1E4AE"}, -%%: {"name":"titleprefixwords", "description":"Ignore some words in the beginning of song titles"} +%%: {"name":"titleprefixwords", "description":"Ignore some words in the beginning of song titles"}, +%%: {"name":"languages", "description":"List of languages used by songs", "default":""} %%:] %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % begin document @@ -52,7 +53,10 @@ \usepackage[utf8]{inputenc} \usepackage[T1]{fontenc} \usepackage{lmodern} -\usepackage[italian,portuguese,spanish,english,french]{babel} + +\PassOptionsToPackage{\getlanguages}{babel} +\PassOptionsToPackage{\getlang}{babel} +\usepackage{babel} \title{\gettitle} \author{\getauthor} diff --git a/utils/patchedbabel.py b/utils/patchedbabel.py new file mode 100644 index 00000000..abfe920f --- /dev/null +++ b/utils/patchedbabel.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Patch pour le paquet Babel de PlasTeX + +Un bug dans PlasTeX intervient lorsqu'on essaye d'analyser une commande LaTeX +\selectlanguage{}, que nouv 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 1.3 du fichier babel.py : +http://plastex.cvs.sourceforge.net/viewvc/plastex/plastex/plasTeX/Packages/babel.py?view=log + +# 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 + +""" + +from plasTeX import Command + +class selectlanguage(Command): + args = 'lang:str' + + def invoke(self, tex): + res = Command.invoke(self, tex) + self.ownerDocument.context.loadLanguage(self.attributes['lang'], self.ownerDocument) + return res diff --git a/utils/plastex.py b/utils/plastex.py new file mode 100755 index 00000000..b316111d --- /dev/null +++ b/utils/plastex.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from plasTeX.TeX import TeX +import codecs +import copy +import os +import sys + +class SongParser: + """Analyseur syntaxique de fichiers .sg""" + + @staticmethod + def _create_TeX(): + tex = TeX() + tex.disableLogging() + tex.ownerDocument.context.loadBaseMacros() + sys.path.append(os.path.dirname(__file__)) + tex.ownerDocument.context.loadPackage(tex, "patchedbabel") + tex.ownerDocument.context.loadPackage(tex, "songs") + sys.path.pop() + return tex + + @classmethod + def parse(cls, filename): + tex = cls._create_TeX() + tex.input(codecs.open(filename, 'r+', 'utf-8', 'replace')) + return tex.parse() + +def parsetex(filename): + """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. + """ + # Analyse syntaxique + doc = SongParser.parse(filename) + + # Extraction des données + data = { + "languages": set(), + } + for node in doc.allChildNodes: + if node.nodeName == "selectlanguage": + data["languages"].add(node.attributes['lang']) + if node.nodeName == "beginsong": + data["titles"] = node.attributes["titles"] + data["args"] = node.attributes["args"] + + return data diff --git a/utils/songs.py b/utils/songs.py new file mode 100644 index 00000000..f8694e03 --- /dev/null +++ b/utils/songs.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import plasTeX + +def split_linebreak(texlist): + return_list = [] + current = [] + for token in texlist: + if token.nodeName == '\\': + return_list.append(current) + current = [] + else: + current.append(token.textContent.encode('utf-8')) + if current: + return_list.append(current) + return return_list + +class beginsong(plasTeX.Command): + args = '{titles}[ args:dict ]' + def invoke(self, tex): + plasTeX.Command.invoke(self, tex) + + # Parsing title + titles = [] + for tokens in split_linebreak(self.attributes['titles'].allChildNodes): + titles.append("".join(tokens)) + self.attributes['titles'] = titles + + # Parsing keyval arguments + args = {} + for (key, val) in self.attributes['args'].iteritems(): + args[key] = val.textContent.encode('utf-8') + self.attributes['args'] = args