From a0c19d3b358dc5c681af73115b37eb371a9a1463 Mon Sep 17 00:00:00 2001 From: Luthaf Date: Mon, 6 Oct 2014 22:30:46 +0200 Subject: [PATCH 01/21] First minimalist chordpro lexer --- patacrep/chordpro/__init__.py | 0 patacrep/chordpro/lexer.py | 59 +++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 patacrep/chordpro/__init__.py create mode 100644 patacrep/chordpro/lexer.py diff --git a/patacrep/chordpro/__init__.py b/patacrep/chordpro/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patacrep/chordpro/lexer.py b/patacrep/chordpro/lexer.py new file mode 100644 index 00000000..860e16d5 --- /dev/null +++ b/patacrep/chordpro/lexer.py @@ -0,0 +1,59 @@ +"""ChordPro lexer""" + +import logging +import ply.lex as lex + +LOGGER = logging.getLogger() + +#pylint: disable=invalid-name +tokens = ( + 'LBRACKET', + 'RBRACKET', + 'LBRACE', + 'RBRACE', + 'NEWLINE', + 'COLON', + 'WORD', + 'SPACE', + 'NUMBER' +) + +class ChordProLexer: + """ChordPro Lexer class""" + + tokens = tokens + + t_LBRACKET = r'\[' + t_RBRACKET = r'\]' + t_LBRACE = r'{' + t_RBRACE = r'}' + t_SPACE = r'[ \t]+' + t_COLON = r':' + t_WORD = r'[a-zA-Z_]+' #TODO: handle unicode + + 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\r]' + token.lexer.lineno += 1 + return token + + @staticmethod + def t_comment(token): + r'\#.*' + pass + + @staticmethod + def t_NUMBER(token): + r'[0-9]+' + token.value = int(token.value) + return token + + @staticmethod + def t_error(token): + """Manage errors""" + LOGGER.error("Illegal character '{}'".format(token.value[0])) + token.lexer.skip(1) \ No newline at end of file From ba435f26f9286904a14d7b4f03c389aa105dffcb Mon Sep 17 00:00:00 2001 From: Luthaf Date: Mon, 6 Oct 2014 22:31:05 +0200 Subject: [PATCH 02/21] ChordPro example file --- patacrep/data/examples/songs/greensleeves.sgc | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 patacrep/data/examples/songs/greensleeves.sgc diff --git a/patacrep/data/examples/songs/greensleeves.sgc b/patacrep/data/examples/songs/greensleeves.sgc new file mode 100644 index 00000000..7d6b1000 --- /dev/null +++ b/patacrep/data/examples/songs/greensleeves.sgc @@ -0,0 +1,42 @@ +{language : english} +{columns : 2} +{ title : Greensleeves} +{artist: Traditionnel} +{cover : traditionnel } +{album :Angleterre} + +{partition : greensleeves.ly} + + +A[Am]las, my love, ye [G]do me wrong +To [Am]cast me oft dis[E]curteously +And [Am]{I have} loved [G]you so long +De[Am]lighting [E]in your [Am]companie + +{start_of_chorus} + [C]Greensleeves was [G]all my joy + [Am]Greensleeves was [E]my delight + [C]Greensleeves was my [G]heart of gold + And [Am]who but [E]Ladie [Am]Greensleeves +{end_of_chorus} + +I [Am]have been ready [G]at your hand +To [Am]grant what ever [E]you would crave +I [Am]have both waged [G]life and land +Your [Am]love and [E]good will [Am]for to have + +I [Am]bought thee kerchers [G]to thy head +That [Am]were wrought fine and [E]gallantly +I [Am]kept thee both at [G]boord and bed +Which [Am]cost my [E]purse well [Am]favouredly + +I [Am]bought thee peticotes [G]of the best +The [Am]cloth so fine as [E]fine might be +I [Am]gave thee jewels [G]for thy chest +And [Am]all this [E]cost I [Am]spent on thee + + +Thy [Am]smock of silke, both [G]faire and white +With [Am]gold embrodered [E]gorgeously +Thy [Am]peticote of [G]sendall right +And [Am]this I [E]bought thee [Am]gladly \ No newline at end of file From 2c79570ceb5285602df515c50351e7447e1cda15 Mon Sep 17 00:00:00 2001 From: Luthaf Date: Mon, 19 Jan 2015 21:16:10 +0100 Subject: [PATCH 03/21] [WIP] Chordpro format --- patacrep/chordpro/ast.py | 71 ++++++++++++++++++++++++++++++ patacrep/chordpro/lexer.py | 10 +++-- patacrep/chordpro/parser.py | 88 +++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 patacrep/chordpro/ast.py create mode 100644 patacrep/chordpro/parser.py diff --git a/patacrep/chordpro/ast.py b/patacrep/chordpro/ast.py new file mode 100644 index 00000000..5eec6b1e --- /dev/null +++ b/patacrep/chordpro/ast.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +"""Abstract Syntax Tree for ChordPro code.""" + +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): + """ChordPro 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 SongPart(AST): + """ChordPro start_of/end_of command + + {start_of_chorus}, {end_of_tab}, {eov} ... + """ + + class Type: + CHORUS = ("chorus", + "start_of_chorus", "end_of_chorus", + "soc", "eoc") + VERSE = ("verse", + "start_of_verse", "end_of_verse", + "sov", "eov") + BRIDGE = ("bridge", + "start_of_bridge", "end_of_bridge", + "sob", "eob") + TAB = ("tab", + "start_of_tab", "end_of_tab", + "sot", "eot") + + def __init__(self, name): + if "_" in name: + self.init_long_form(name) + else: + self.init_short_form(name) + + def __str__(self): + return self.name + + def init_short_form(self, name): + self.type = "" + + def init_long_form(self, name): + self.type = "" + + diff --git a/patacrep/chordpro/lexer.py b/patacrep/chordpro/lexer.py index 860e16d5..791d590e 100644 --- a/patacrep/chordpro/lexer.py +++ b/patacrep/chordpro/lexer.py @@ -29,7 +29,6 @@ class ChordProLexer: t_RBRACE = r'}' t_SPACE = r'[ \t]+' t_COLON = r':' - t_WORD = r'[a-zA-Z_]+' #TODO: handle unicode def __init__(self): self.__class__.lexer = lex.lex(module=self) @@ -45,7 +44,12 @@ class ChordProLexer: def t_comment(token): r'\#.*' pass - + + @staticmethod + def t_WORD(token): + r'[a-zA-Z_]+[.,;:!?]?' + return token + @staticmethod def t_NUMBER(token): r'[0-9]+' @@ -56,4 +60,4 @@ class ChordProLexer: def t_error(token): """Manage errors""" LOGGER.error("Illegal character '{}'".format(token.value[0])) - token.lexer.skip(1) \ No newline at end of file + token.lexer.skip(1) diff --git a/patacrep/chordpro/parser.py b/patacrep/chordpro/parser.py new file mode 100644 index 00000000..8c66af28 --- /dev/null +++ b/patacrep/chordpro/parser.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +"""ChordPro parser""" + +import logging +import ply.yacc as yacc + +from patacrep.chordpro.lexer import tokens, ChordProLexer +from patacrep.chordpro import ast +from patacrep.errors import SongbookError + +LOGGER = logging.getLogger() + +class ParsingError(SongbookError): + """Parsing error.""" + + def __init__(self, message): + super().__init__(self) + self.message = message + + def __str__(self): + return self.message + + +class Parser: + """ChordPro parser class""" + + def __init__(self, filename=None): + self.tokens = tokens + 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 {}:{}.".format( + str(self.filename), + token.lineno, + self.__find_column(token), + ) + ) + + @staticmethod + def p_expression(symbols): + """expression : brackets expression + | braces expression + | command expression + | NEWLINE 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 COLON expression RBRACE""" + symbols[0] = symbols[2] + + +def parsesong(string, filename=None): + """Parse song and return its metadata.""" + return yacc.yacc(module=Parser(filename)).parse( + string, + lexer=ChordProLexer().lexer, + ) From e9af147866766c14fdf25e7baf96409bcceeb620 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 23 Jan 2015 14:21:09 +0100 Subject: [PATCH 04/21] Logging: better handling of newlines --- patacrep/build.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/patacrep/build.py b/patacrep/build.py index 212305ca..f804f154 100644 --- a/patacrep/build.py +++ b/patacrep/build.py @@ -200,7 +200,9 @@ class SongbookBuilder(object): stdin=PIPE, stdout=PIPE, stderr=PIPE, - env=os.environ) + env=os.environ, + universal_newlines=True, + ) except Exception as error: LOGGER.debug(error) raise errors.LatexCompilationError(self.basename) From a2e5c476e5257728b768a0a4b46dc91d0c144196 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 23 Jan 2015 19:28:09 +0100 Subject: [PATCH 05/21] =?UTF-8?q?R=C3=A9organisation=20en=20pr=C3=A9vision?= =?UTF-8?q?=20du=20support=20des=20fichiers=20chordpro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref: #64 --- patacrep/chordpro/__init__.py | 0 patacrep/content/__init__.py | 49 +-------------- patacrep/content/song.py | 50 ++++++++-------- patacrep/content/sorted.py | 3 +- patacrep/files.py | 76 ++++++++++++++++++++++-- patacrep/latex/__init__.py | 21 +------ patacrep/latex/syntax.py | 23 ++++--- patacrep/{songs.py => songs/__init__.py} | 28 +++++++-- patacrep/songs/chordpro/__init__.py | 9 +++ patacrep/{ => songs}/chordpro/ast.py | 0 patacrep/{ => songs}/chordpro/lexer.py | 0 patacrep/{ => songs}/chordpro/parser.py | 4 +- patacrep/songs/latex/__init__.py | 32 ++++++++++ 13 files changed, 183 insertions(+), 112 deletions(-) delete mode 100644 patacrep/chordpro/__init__.py rename patacrep/{songs.py => songs/__init__.py} (86%) create mode 100644 patacrep/songs/chordpro/__init__.py rename patacrep/{ => songs}/chordpro/ast.py (100%) rename patacrep/{ => songs}/chordpro/lexer.py (100%) rename patacrep/{ => songs}/chordpro/parser.py (95%) create mode 100644 patacrep/songs/latex/__init__.py diff --git a/patacrep/chordpro/__init__.py b/patacrep/chordpro/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/patacrep/content/__init__.py b/patacrep/content/__init__.py index a53d4d7e..03b2fb0d 100755 --- a/patacrep/content/__init__.py +++ b/patacrep/content/__init__.py @@ -134,53 +134,6 @@ class ContentError(SongbookError): def __str__(self): return "Content: {}: {}".format(self.keyword, self.message) -def load_plugins(config): - """Load all content plugins, and return a dictionary of those plugins. - - Return value: a dictionary where: - - keys are the keywords ; - - values are functions triggered when this keyword is met. - """ - plugins = {} - directory_list = ( - [ - os.path.join(datadir, "python", "content") - for datadir in config.get('datadir', []) - ] - + [os.path.dirname(__file__)] - ) - for directory in directory_list: - if not os.path.exists(directory): - LOGGER.debug( - "Ignoring non-existent directory '%s'.", - directory - ) - continue - sys.path.append(directory) - for name in glob.glob(os.path.join(directory, '*.py')): - if name.endswith(".py") and os.path.basename(name) != "__init__.py": - if directory == os.path.dirname(__file__): - plugin = importlib.import_module( - 'patacrep.content.{}'.format( - os.path.basename(name[:-len('.py')]) - ) - ) - else: - plugin = importlib.import_module( - os.path.basename(name[:-len('.py')]) - ) - for (key, value) in plugin.CONTENT_PLUGINS.items(): - if key in plugins: - LOGGER.warning( - "File %s: Keyword '%s' is already used. Ignored.", - files.relpath(name), - key, - ) - continue - plugins[key] = value - del sys.path[-1] - return plugins - @jinja2.contextfunction def render_content(context, content): """Render the content of the songbook as a LaTeX code. @@ -224,7 +177,7 @@ def process_content(content, config=None): included in the .tex file. """ contentlist = [] - plugins = load_plugins(config) + plugins = files.load_plugins(config, ["content"], "CONTENT_PLUGINS") keyword_re = re.compile(r'^ *(?P\w*) *(\((?P.*)\))? *$') if not content: content = [["song"]] diff --git a/patacrep/content/song.py b/patacrep/content/song.py index 02acf463..1b75ba6b 100755 --- a/patacrep/content/song.py +++ b/patacrep/content/song.py @@ -10,12 +10,15 @@ import os from patacrep.content import Content, process_content, ContentError from patacrep import files, errors -from patacrep.songs import Song LOGGER = logging.getLogger(__name__) -class SongRenderer(Content, Song): - """Render a song in the .tex file.""" +class SongRenderer(Content): + """Render a song in as a tex code.""" + + def __init__(self, song): + super().__init__() + self.song = song def begin_new_block(self, previous, __context): """Return a boolean stating if a new block is to be created.""" @@ -34,11 +37,7 @@ class SongRenderer(Content, Song): 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']) - ))) + return self.song.tex(output=context['filename']) #pylint: disable=unused-argument def parse(keyword, argument, contentlist, config): @@ -53,6 +52,7 @@ def parse(keyword, argument, contentlist, config): Return a list of SongRenderer() instances. """ + plugins = files.load_plugins(config, ["songs"], "SONG_PARSERS") if '_languages' not in config: config['_languages'] = set() songlist = [] @@ -60,13 +60,10 @@ def parse(keyword, argument, contentlist, config): if contentlist: break contentlist = [ - filename - for filename - in ( - files.recursive_find(songdir.fullpath, "*.sg") - + files.recursive_find(songdir.fullpath, "*.is") - ) - ] + filename + for filename + in files.recursive_find(songdir.fullpath, plugins.keys()) + ] for elem in contentlist: before = len(songlist) for songdir in config['_songdir']: @@ -74,23 +71,24 @@ def parse(keyword, argument, contentlist, config): continue with files.chdir(songdir.datadir): for filename in glob.iglob(os.path.join(songdir.subpath, elem)): - if not ( - filename.endswith('.sg') or - filename.endswith('.is') - ): + extension = filename.split(".")[-1] + if extension not in plugins: LOGGER.warning(( - 'File "{}" is not a ".sg" or ".is" file. Ignored.' - ).format(os.path.join(songdir.datadir, filename)) + 'File "{}" does not end with one of {}. Ignored.' + ).format( + os.path.join(songdir.datadir, filename), + ", ".join(["'.{}'".format(key) for key in plugins.keys()]), + ) ) continue LOGGER.debug('Parsing file "{}"…'.format(filename)) - song = SongRenderer( + renderer = SongRenderer(plugins[extension]( songdir.datadir, filename, config, - ) - songlist.append(song) - config["_languages"].update(song.languages) + )) + songlist.append(renderer) + config["_languages"].update(renderer.song.languages) if len(songlist) > before: break if len(songlist) == before: @@ -109,8 +107,8 @@ CONTENT_PLUGINS = {'song': parse} class OnlySongsError(ContentError): "A list that should contain only songs also contain other type of content." def __init__(self, not_songs): - super(OnlySongsError, self).__init__() self.not_songs = not_songs + super().__init__('song', str(self)) def __str__(self): return ( diff --git a/patacrep/content/sorted.py b/patacrep/content/sorted.py index f95065d6..00844335 100755 --- a/patacrep/content/sorted.py +++ b/patacrep/content/sorted.py @@ -43,8 +43,9 @@ def key_generator(sort): - sort: the list of keys used to sort. """ - def ordered_song_keys(song): + def ordered_song_keys(songrenderer): """Return the list of values used to sort the song.""" + song = songrenderer.song songkey = [] for key in sort: if key == "@title": diff --git a/patacrep/files.py b/patacrep/files.py index 6e02d481..a1a12d18 100644 --- a/patacrep/files.py +++ b/patacrep/files.py @@ -3,13 +3,17 @@ from contextlib import contextmanager import fnmatch +import importlib +import logging import os import posixpath -def recursive_find(root_directory, pattern): - """Recursively find files matching a pattern, from a root_directory. +LOGGER = logging.getLogger(__name__) - Return a list of files matching the pattern. +def recursive_find(root_directory, patterns): + """Recursively find files matching one of the patterns, from a root_directory. + + Return a list of files matching one of the patterns. """ if not os.path.isdir(root_directory): return [] @@ -17,8 +21,12 @@ def recursive_find(root_directory, pattern): matches = [] with chdir(root_directory): for root, __ignored, filenames in os.walk(os.curdir): - for filename in fnmatch.filter(filenames, pattern): - matches.append(os.path.join(root, filename)) + for pattern in patterns: + for filename in fnmatch.filter( + filenames, + "*.{}".format(pattern), + ): + matches.append(os.path.join(root, filename)) return matches def relpath(path, start=None): @@ -59,3 +67,61 @@ def chdir(path): os.chdir(olddir) else: yield + +def load_plugins(config, root_modules, keyword): + """Load all plugins, and return a dictionary of those plugins. + + Arguments: + - config: the configuration dictionary of the songbook + - root_modules: the submodule in which plugins are to be searched, as a + list of modules (e.g. ["some", "deep", "module"] for + "some.deep.module"). + - keyword: attribute containing plugin information. + + Return value: a dictionary where: + - keys are the keywords ; + - values are functions triggered when this keyword is met. + """ + # pylint: disable=star-args + plugins = {} + directory_list = ( + [ + os.path.join(datadir, "python", *root_modules) + for datadir in config.get('datadir', []) + ] + + [os.path.join( + os.path.dirname(__file__), + *root_modules + )] + ) + for directory in directory_list: + if not os.path.exists(directory): + LOGGER.debug( + "Ignoring non-existent directory '%s'.", + directory + ) + continue + for (dirpath, __ignored, filenames) in os.walk(directory): + modules = ["patacrep"] + root_modules + if os.path.relpath(dirpath, directory) != ".": + modules.extend(os.path.relpath(dirpath, directory).split("/")) + for name in filenames: + if name == "__init__.py": + modulename = [] + elif name.endswith(".py"): + modulename = [name[:-len('.py')]] + else: + continue + plugin = importlib.import_module(".".join(modules + modulename)) + if hasattr(plugin, keyword): + for (key, value) in getattr(plugin, keyword).items(): + if key in plugins: + LOGGER.warning( + "File %s: Keyword '%s' is already used. Ignored.", + relpath(os.path.join(dirpath, name)), + key, + ) + continue + plugins[key] = value + return plugins + diff --git a/patacrep/latex/__init__.py b/patacrep/latex/__init__.py index fce5470f..90c4e798 100644 --- a/patacrep/latex/__init__.py +++ b/patacrep/latex/__init__.py @@ -1,20 +1,3 @@ -# -*- coding: utf-8 -*- +"""Dumb and very very incomplete LaTeX parser.""" -"""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): - """Return a dictonary of data read from the latex file `path`. - - """ - data = syntax_parsesong(encoding.open_read(path).read(), path) - data['@path'] = path - return data +from patacrep.latex.syntax import tex2plain, parse_song diff --git a/patacrep/latex/syntax.py b/patacrep/latex/syntax.py index fac078dd..094ef124 100644 --- a/patacrep/latex/syntax.py +++ b/patacrep/latex/syntax.py @@ -237,12 +237,21 @@ def tex2plain(string): ) ) -def parsesong(string, filename=None): - """Parse song and return its metadata.""" +def parse_song(content, filename=None): + """Parse some LaTeX code, expected to be a song. + + Arguments: + - content: the code to parse. + - filename: the name of file where content was read from. Used only to + display error messages. + """ return detex( - yacc.yacc(module=Parser(filename)).parse( - string, - lexer=SongLexer().lexer, - ).metadata + yacc.yacc( + module=Parser(filename), + write_tables=0, + debug=0, + ).parse( + content, + lexer=SongLexer().lexer, + ).metadata ) - diff --git a/patacrep/songs.py b/patacrep/songs/__init__.py similarity index 86% rename from patacrep/songs.py rename to patacrep/songs/__init__.py index 43e70fe7..0744d8eb 100644 --- a/patacrep/songs.py +++ b/patacrep/songs/__init__.py @@ -10,7 +10,7 @@ import pickle import re from patacrep.authors import processauthors -from patacrep.latex import parsesong +from patacrep import files, encoding LOGGER = logging.getLogger(__name__) @@ -84,6 +84,12 @@ class Song(object): "_version", ] + # Default data + DEFAULT_DATA = { + '@titles': [], + '@languages': [], + } + def __init__(self, datadir, subpath, config): self.fullpath = os.path.join(datadir, subpath) if datadir: @@ -110,7 +116,11 @@ class Song(object): )) # Data extraction from the latex song - self.data = parsesong(self.fullpath) + self.data = self.DEFAULT_DATA + self.data['@path'] = self.fullpath + self.data.update(self.parse( + encoding.open_read(self.fullpath).read() + )) self.titles = self.data['@titles'] self.languages = self.data['@languages'] self.datadir = datadir @@ -149,6 +159,18 @@ class Song(object): def __repr__(self): return repr((self.titles, self.data, self.fullpath)) + def tex(self, output): # pylint: disable=no-self-use, unused-argument + """Return the LaTeX code rendering this song. + + Arguments: + - output: Name of the output file. + """ + return NotImplementedError() + + def parse(self, content): # pylint: disable=no-self-use, unused-argument + """Parse song, and return a dictionary of its data.""" + return NotImplementedError() + def unprefixed_title(title, prefixes): """Remove the first prefix of the list in the beginning of title (if any). """ @@ -157,5 +179,3 @@ def unprefixed_title(title, prefixes): if match: return match.group(2) return title - - diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py new file mode 100644 index 00000000..bd3b6f3f --- /dev/null +++ b/patacrep/songs/chordpro/__init__.py @@ -0,0 +1,9 @@ + +from patacrep.songs import Song + +class ChordproSong(Song): + pass + +SONG_PARSERS = { + 'sgc': ChordproSong, + } diff --git a/patacrep/chordpro/ast.py b/patacrep/songs/chordpro/ast.py similarity index 100% rename from patacrep/chordpro/ast.py rename to patacrep/songs/chordpro/ast.py diff --git a/patacrep/chordpro/lexer.py b/patacrep/songs/chordpro/lexer.py similarity index 100% rename from patacrep/chordpro/lexer.py rename to patacrep/songs/chordpro/lexer.py diff --git a/patacrep/chordpro/parser.py b/patacrep/songs/chordpro/parser.py similarity index 95% rename from patacrep/chordpro/parser.py rename to patacrep/songs/chordpro/parser.py index 8c66af28..9fa75908 100644 --- a/patacrep/chordpro/parser.py +++ b/patacrep/songs/chordpro/parser.py @@ -4,8 +4,8 @@ import logging import ply.yacc as yacc -from patacrep.chordpro.lexer import tokens, ChordProLexer -from patacrep.chordpro import ast +from patacrep.songs.chordpro.lexer import tokens, ChordProLexer +from patacrep.songs.chordpro import ast from patacrep.errors import SongbookError LOGGER = logging.getLogger() diff --git a/patacrep/songs/latex/__init__.py b/patacrep/songs/latex/__init__.py new file mode 100644 index 00000000..2de16338 --- /dev/null +++ b/patacrep/songs/latex/__init__.py @@ -0,0 +1,32 @@ +"""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. +""" + +import os + +from patacrep import files +from patacrep.latex import parse_song +from patacrep.songs import Song + +class LatexSong(Song): + """LaTeX song parser.""" + + def parse(self, content): + """Parse content, and return the dictinory of song data.""" + return parse_song(content, self.fullpath) + + def tex(self, output): + """Return the LaTeX code rendering the song.""" + return r'\input{{{}}}'.format(files.path2posix( + files.relpath( + self.fullpath, + os.path.dirname(output) + ))) + +SONG_PARSERS = { + 'is': LatexSong, + 'sg': LatexSong, + } From 9e507fcc42ddae81f4994ff14fb4be9d1e53886e Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 23 Jan 2015 23:23:52 +0100 Subject: [PATCH 06/21] =?UTF-8?q?[chordpro][WIP]=20Les=20lignes=20sont=20c?= =?UTF-8?q?orrectement=20analys=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patacrep/songs/chordpro/__init__.py | 17 +++- patacrep/songs/chordpro/lexer.py | 46 +++++++--- patacrep/songs/chordpro/parser.py | 88 ------------------ patacrep/songs/chordpro/syntax.py | 136 ++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 103 deletions(-) delete mode 100644 patacrep/songs/chordpro/parser.py create mode 100644 patacrep/songs/chordpro/syntax.py diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py index bd3b6f3f..baddb158 100644 --- a/patacrep/songs/chordpro/__init__.py +++ b/patacrep/songs/chordpro/__init__.py @@ -1,8 +1,23 @@ +from patacrep import encoding from patacrep.songs import Song +from patacrep.songs.chordpro.syntax import parse_song class ChordproSong(Song): - pass + """Chordpros song parser.""" + + def parse(self): + """Parse content, and return the dictinory of song data.""" + with encoding.open_read(self.fullpath, encoding=self.encoding) as song: + self.data = parse_song(song.read(), self.fullpath) + print(self.data) + import sys; sys.exit(1) + self.titles = self.data['@titles'] + del self.data['@titles'] + self.languages = self.data['@languages'] + del self.data['@languages'] + self.authors = self.data['by'] + del self.data['by'] SONG_PARSERS = { 'sgc': ChordproSong, diff --git a/patacrep/songs/chordpro/lexer.py b/patacrep/songs/chordpro/lexer.py index 791d590e..f33d8f2e 100644 --- a/patacrep/songs/chordpro/lexer.py +++ b/patacrep/songs/chordpro/lexer.py @@ -7,15 +7,16 @@ LOGGER = logging.getLogger() #pylint: disable=invalid-name tokens = ( - 'LBRACKET', - 'RBRACKET', + #'LBRACKET', + #'RBRACKET', + 'CHORD', 'LBRACE', 'RBRACE', 'NEWLINE', - 'COLON', + #'COLON', 'WORD', 'SPACE', - 'NUMBER' + #'NUMBER', ) class ChordProLexer: @@ -23,12 +24,15 @@ class ChordProLexer: tokens = tokens - t_LBRACKET = r'\[' - t_RBRACKET = r'\]' + states = ( + ('chord', 'exclusive'), + ) + t_LBRACE = r'{' t_RBRACE = r'}' t_SPACE = r'[ \t]+' - t_COLON = r':' + #t_COLON = r':' + t_chord_CHORD = r'[A-G7#m]+' # TODO This can be refined def __init__(self): self.__class__.lexer = lex.lex(module=self) @@ -41,23 +45,37 @@ class ChordProLexer: return token @staticmethod - def t_comment(token): + def t_COMMENT(token): r'\#.*' pass @staticmethod def t_WORD(token): - r'[a-zA-Z_]+[.,;:!?]?' + r'[^\n\][\t ]+' return token - @staticmethod - def t_NUMBER(token): - r'[0-9]+' - token.value = int(token.value) - return token + def t_LBRACKET(self, token): + r'\[' + self.lexer.push_state('chord') + + def t_chord_RBRACKET(self, token): + r'\]' + self.lexer.pop_state() + + #@staticmethod + #def t_NUMBER(token): + # r'[0-9]+' + # token.value = int(token.value) + # return token @staticmethod def t_error(token): """Manage errors""" LOGGER.error("Illegal character '{}'".format(token.value[0])) token.lexer.skip(1) + + @staticmethod + def t_chord_error(token): + """Manage errors""" + LOGGER.error("Illegal character '{}' in chord..".format(token.value[0])) + token.lexer.skip(1) diff --git a/patacrep/songs/chordpro/parser.py b/patacrep/songs/chordpro/parser.py deleted file mode 100644 index 9fa75908..00000000 --- a/patacrep/songs/chordpro/parser.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -"""ChordPro parser""" - -import logging -import ply.yacc as yacc - -from patacrep.songs.chordpro.lexer import tokens, ChordProLexer -from patacrep.songs.chordpro import ast -from patacrep.errors import SongbookError - -LOGGER = logging.getLogger() - -class ParsingError(SongbookError): - """Parsing error.""" - - def __init__(self, message): - super().__init__(self) - self.message = message - - def __str__(self): - return self.message - - -class Parser: - """ChordPro parser class""" - - def __init__(self, filename=None): - self.tokens = tokens - 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 {}:{}.".format( - str(self.filename), - token.lineno, - self.__find_column(token), - ) - ) - - @staticmethod - def p_expression(symbols): - """expression : brackets expression - | braces expression - | command expression - | NEWLINE 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 COLON expression RBRACE""" - symbols[0] = symbols[2] - - -def parsesong(string, filename=None): - """Parse song and return its metadata.""" - return yacc.yacc(module=Parser(filename)).parse( - string, - lexer=ChordProLexer().lexer, - ) diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py new file mode 100644 index 00000000..91470ba5 --- /dev/null +++ b/patacrep/songs/chordpro/syntax.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +"""ChordPro parser""" + +import logging +import ply.yacc as yacc + +from patacrep.songs.chordpro.lexer import tokens, ChordProLexer +from patacrep.songs.chordpro import ast +from patacrep.errors import SongbookError + +LOGGER = logging.getLogger() + +class ParsingError(SongbookError): + """Parsing error.""" + + def __init__(self, message): + super().__init__(self) + self.message = message + + def __str__(self): + return self.message + + +class Parser: + """ChordPro parser class""" + + def __init__(self, filename=None): + self.tokens = tokens + 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.""" + if token: # TODO remove this test + LOGGER.error("Error in file {}, line {}:{}.".format( + str(self.filename), + token.lineno, + self.__find_column(token), + ) + ) + + @staticmethod + def p_song(symbols): + """song : block song + | empty + """ + if len(symbols) == 2: + symbols[0] = ('song') + else: + symbols[0] = ('song', symbols[1], symbols[2]) + + @staticmethod + def p_block(symbols): + """block : directive NEWLINE newlines + | stanza NEWLINE newlines + """ + symbols[0] = ('block', symbols[1]) + + @staticmethod + def p_newlines(symbols): + """newlines : NEWLINE newlines + | empty""" + symbols[0] = ('newlines') + + @staticmethod + def p_directive(symbols): + """directive : LBRACE WORD RBRACE""" + symbols[0] = ('directive', symbols[1]) + + @staticmethod + def p_line(symbols): + """line : WORD line_next + | CHORD line_next + | SPACE line_next + """ + symbols[0] = ('line', symbols[1], symbols[2]) + + @staticmethod + def p_line_next(symbols): + """line_next : WORD line_next + | SPACE line_next + | CHORD line_next + | empty + """ + if len(symbols) == 2: + symbols[0] = ('line-next') + else: + symbols[0] = ('line-next', symbols[1], symbols[2]) + + @staticmethod + def p_stanza(symbols): + """stanza : line NEWLINE stanza_next + """ + symbols[0] = ('stanza', symbols[1], symbols[3]) + + @staticmethod + def p_stanza_next(symbols): + """stanza_next : line NEWLINE stanza_next + | empty + """ + if len(symbols) == 2: + symbols[0] = ('stanza-next') + else: + symbols[0] = ('stanza-next', symbols[1], symbols[3]) + + #@staticmethod + #def p_braces(symbols): + # """braces : LBRACE expression COLON expression RBRACE""" + # symbols[0] = symbols[2] + + @staticmethod + def p_empty(symbols): + """empty :""" + symbols[0] = None + + #@staticmethod + #def p_comment(symbols): + # """comment : COMMENT""" + # symbols[0] = ('comment', symbols[1]) + + + +def parse_song(content, filename=None): + """Parse song and return its metadata.""" + return yacc.yacc(module=Parser(filename)).parse( + content, + lexer=ChordProLexer().lexer, + ) From eff4286a7792f192c921c4aaaa85ef12465a8bd0 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 24 Jan 2015 14:36:01 +0100 Subject: [PATCH 07/21] Parsing works on tests --- patacrep/content/song.py | 13 +- patacrep/songs/chordpro/__init__.py | 4 +- patacrep/songs/chordpro/ast.py | 293 +++++++++++++++--- patacrep/songs/chordpro/lexer.py | 111 +++++-- patacrep/songs/chordpro/syntax.py | 211 ++++++++++--- patacrep/songs/chordpro/test/00.sgc | 0 patacrep/songs/chordpro/test/00.txt | 1 + patacrep/songs/chordpro/test/01.sgc | 1 + patacrep/songs/chordpro/test/01.txt | 4 + patacrep/songs/chordpro/test/02.sgc | 1 + patacrep/songs/chordpro/test/02.txt | 2 + patacrep/songs/chordpro/test/03.sgc | 4 + patacrep/songs/chordpro/test/03.txt | 1 + patacrep/songs/chordpro/test/04.sgc | 3 + patacrep/songs/chordpro/test/04.txt | 4 + patacrep/songs/chordpro/test/05.sgc | 3 + patacrep/songs/chordpro/test/05.txt | 4 + patacrep/songs/chordpro/test/06.sgc | 1 + patacrep/songs/chordpro/test/06.txt | 1 + patacrep/songs/chordpro/test/07.sgc | 3 + patacrep/songs/chordpro/test/07.txt | 4 + patacrep/songs/chordpro/test/08.sgc | 10 + patacrep/songs/chordpro/test/08.txt | 4 + patacrep/songs/chordpro/test/09.sgc | 15 + patacrep/songs/chordpro/test/09.txt | 5 + patacrep/songs/chordpro/test/10.sgc | 1 + patacrep/songs/chordpro/test/10.txt | 4 + patacrep/songs/chordpro/test/11.sgc | 1 + patacrep/songs/chordpro/test/11.txt | 4 + patacrep/songs/chordpro/test/12.sgc | 1 + patacrep/songs/chordpro/test/12.txt | 4 + patacrep/songs/chordpro/test/13.sgc | 5 + patacrep/songs/chordpro/test/13.txt | 6 + patacrep/songs/chordpro/test/21.sgc | 1 + patacrep/songs/chordpro/test/21.txt | 4 + patacrep/songs/chordpro/test/22.sgc | 1 + patacrep/songs/chordpro/test/22.txt | 2 + patacrep/songs/chordpro/test/23.sgc | 4 + patacrep/songs/chordpro/test/23.txt | 1 + patacrep/songs/chordpro/test/24.sgc | 3 + patacrep/songs/chordpro/test/24.txt | 4 + patacrep/songs/chordpro/test/25.sgc | 3 + patacrep/songs/chordpro/test/25.txt | 4 + patacrep/songs/chordpro/test/26.sgc | 1 + patacrep/songs/chordpro/test/26.txt | 1 + patacrep/songs/chordpro/test/27.sgc | 3 + patacrep/songs/chordpro/test/27.txt | 4 + patacrep/songs/chordpro/test/28.sgc | 10 + patacrep/songs/chordpro/test/28.txt | 4 + patacrep/songs/chordpro/test/29.sgc | 15 + patacrep/songs/chordpro/test/29.txt | 5 + patacrep/songs/chordpro/test/30.sgc | 1 + patacrep/songs/chordpro/test/__init__.py | 0 patacrep/songs/chordpro/test/greensleeves.sgc | 44 +++ patacrep/songs/chordpro/test/greensleeves.txt | 51 +++ patacrep/songs/chordpro/test/metadata.sgc | 20 ++ patacrep/songs/chordpro/test/metadata.txt | 21 ++ patacrep/songs/chordpro/test/test_parser.py | 36 +++ 58 files changed, 853 insertions(+), 119 deletions(-) create mode 100644 patacrep/songs/chordpro/test/00.sgc create mode 100644 patacrep/songs/chordpro/test/00.txt create mode 100644 patacrep/songs/chordpro/test/01.sgc create mode 100644 patacrep/songs/chordpro/test/01.txt create mode 100644 patacrep/songs/chordpro/test/02.sgc create mode 100644 patacrep/songs/chordpro/test/02.txt create mode 100644 patacrep/songs/chordpro/test/03.sgc create mode 100644 patacrep/songs/chordpro/test/03.txt create mode 100644 patacrep/songs/chordpro/test/04.sgc create mode 100644 patacrep/songs/chordpro/test/04.txt create mode 100644 patacrep/songs/chordpro/test/05.sgc create mode 100644 patacrep/songs/chordpro/test/05.txt create mode 100644 patacrep/songs/chordpro/test/06.sgc create mode 100644 patacrep/songs/chordpro/test/06.txt create mode 100644 patacrep/songs/chordpro/test/07.sgc create mode 100644 patacrep/songs/chordpro/test/07.txt create mode 100644 patacrep/songs/chordpro/test/08.sgc create mode 100644 patacrep/songs/chordpro/test/08.txt create mode 100644 patacrep/songs/chordpro/test/09.sgc create mode 100644 patacrep/songs/chordpro/test/09.txt create mode 100644 patacrep/songs/chordpro/test/10.sgc create mode 100644 patacrep/songs/chordpro/test/10.txt create mode 100644 patacrep/songs/chordpro/test/11.sgc create mode 100644 patacrep/songs/chordpro/test/11.txt create mode 100644 patacrep/songs/chordpro/test/12.sgc create mode 100644 patacrep/songs/chordpro/test/12.txt create mode 100644 patacrep/songs/chordpro/test/13.sgc create mode 100644 patacrep/songs/chordpro/test/13.txt create mode 100644 patacrep/songs/chordpro/test/21.sgc create mode 100644 patacrep/songs/chordpro/test/21.txt create mode 100644 patacrep/songs/chordpro/test/22.sgc create mode 100644 patacrep/songs/chordpro/test/22.txt create mode 100644 patacrep/songs/chordpro/test/23.sgc create mode 100644 patacrep/songs/chordpro/test/23.txt create mode 100644 patacrep/songs/chordpro/test/24.sgc create mode 100644 patacrep/songs/chordpro/test/24.txt create mode 100644 patacrep/songs/chordpro/test/25.sgc create mode 100644 patacrep/songs/chordpro/test/25.txt create mode 100644 patacrep/songs/chordpro/test/26.sgc create mode 100644 patacrep/songs/chordpro/test/26.txt create mode 100644 patacrep/songs/chordpro/test/27.sgc create mode 100644 patacrep/songs/chordpro/test/27.txt create mode 100644 patacrep/songs/chordpro/test/28.sgc create mode 100644 patacrep/songs/chordpro/test/28.txt create mode 100644 patacrep/songs/chordpro/test/29.sgc create mode 100644 patacrep/songs/chordpro/test/29.txt create mode 100644 patacrep/songs/chordpro/test/30.sgc create mode 100644 patacrep/songs/chordpro/test/__init__.py create mode 100644 patacrep/songs/chordpro/test/greensleeves.sgc create mode 100644 patacrep/songs/chordpro/test/greensleeves.txt create mode 100644 patacrep/songs/chordpro/test/metadata.sgc create mode 100644 patacrep/songs/chordpro/test/metadata.txt create mode 100644 patacrep/songs/chordpro/test/test_parser.py diff --git a/patacrep/content/song.py b/patacrep/content/song.py index 8f5d0f45..a0cdb6b0 100755 --- a/patacrep/content/song.py +++ b/patacrep/content/song.py @@ -67,13 +67,7 @@ def parse(keyword, argument, contentlist, config): for filename in glob.iglob(os.path.join(songdir.subpath, elem)): LOGGER.debug('Parsing file "{}"…'.format(filename)) extension = filename.split(".")[-1] - try: - renderer = SongRenderer(plugins[extension]( - songdir.datadir, - filename, - config, - )) - except KeyError: + if extension not in plugins: LOGGER.warning(( 'I do not know how to parse "{}". Ignored.' ).format( @@ -81,6 +75,11 @@ def parse(keyword, argument, contentlist, config): ) ) continue + renderer = SongRenderer(plugins[extension]( + songdir.datadir, + filename, + config, + )) songlist.append(renderer) config["_languages"].update(renderer.song.languages) if len(songlist) > before: diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py index baddb158..e76960ac 100644 --- a/patacrep/songs/chordpro/__init__.py +++ b/patacrep/songs/chordpro/__init__.py @@ -10,10 +10,8 @@ class ChordproSong(Song): """Parse content, and return the dictinory of song data.""" with encoding.open_read(self.fullpath, encoding=self.encoding) as song: self.data = parse_song(song.read(), self.fullpath) - print(self.data) + print(type(self.data), self.data) import sys; sys.exit(1) - self.titles = self.data['@titles'] - del self.data['@titles'] self.languages = self.data['@languages'] del self.data['@languages'] self.authors = self.data['by'] diff --git a/patacrep/songs/chordpro/ast.py b/patacrep/songs/chordpro/ast.py index 5eec6b1e..05affee6 100644 --- a/patacrep/songs/chordpro/ast.py +++ b/patacrep/songs/chordpro/ast.py @@ -1,71 +1,264 @@ # -*- coding: utf-8 -*- """Abstract Syntax Tree for ChordPro code.""" +import functools + +def _indent(string): + return "\n".join([" {}".format(line) for line in string.split('\n')]) + +INLINE_PROPERTIES = { + "lilypond", + "comment", + "guitar_comment", + "image", + } + +DIRECTIVE_SHORTCUTS = { + "t": "title", + "st": "subtitle", + "a": "album", + "by": "artist", + "c": "comment", + "gc": "guitar_comment", + } + +def directive_name(text): + if text in DIRECTIVE_SHORTCUTS: + return DIRECTIVE_SHORTCUTS[text] + return text + + class AST: - """Base class for the tree.""" - # pylint: disable=no-init - metadata = None + inline = False + +class Line(AST): + """A line is a sequence of (possibly truncated) words, spaces and chords.""" + + def __init__(self): + super().__init__() + self.line = [] + + def prepend(self, data): + self.line.insert(0, data) + return self + + def __str__(self): + return "".join([str(item) for item in self.line]) + + def strip(self): + while True: + if not self.line: + return self + if isinstance(self.line[0], Space): + del self.line[0] + continue + if isinstance(self.line[-1], Space): + del self.line[-1] + continue + return self + +class LineElement(AST): + """Something present on a line.""" + pass + +class Word(LineElement): + """A chunk of word.""" + + def __init__(self, value): + super().__init__() + self.value = value - @classmethod - def init_metadata(cls): - """Clear metadata + def __str__(self): + return self.value - As this attribute is a class attribute, it as to be reset at each new - parsing. - """ - cls.metadata = { - '@languages': set(), - } +class Space(LineElement): + """A space between words""" -class Expression(AST): - """ChordPro expression""" + def __init__(self): + super().__init__() + + def __str__(self): + return " " + +class Chord(LineElement): + """A chord.""" def __init__(self, value): super().__init__() - self.content = [value] + self.value = value + + def __str__(self): + return "[{}]".format(self.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) +class Verse(AST): + """A verse (or bridge, or chorus)""" + type = "verse" + inline = True + + def __init__(self, block=None): + super().__init__() + self.lines = [] # TODO check block + + def prepend(self, data): + self.lines.insert(0, data) return self def __str__(self): - return "".join([str(item) for item in self.content]) - -class SongPart(AST): - """ChordPro start_of/end_of command - - {start_of_chorus}, {end_of_tab}, {eov} ... - """ - - class Type: - CHORUS = ("chorus", - "start_of_chorus", "end_of_chorus", - "soc", "eoc") - VERSE = ("verse", - "start_of_verse", "end_of_verse", - "sov", "eov") - BRIDGE = ("bridge", - "start_of_bridge", "end_of_bridge", - "sob", "eob") - TAB = ("tab", - "start_of_tab", "end_of_tab", - "sot", "eot") - - def __init__(self, name): - if "_" in name: - self.init_long_form(name) + return '{{start_of_{type}}}\n{content}\n{{end_of_{type}}}'.format( + type = self.type, + content = _indent("\n".join([str(line) for line in self.lines])), + ) + +class Chorus(Verse): + type = 'chorus' + +class Bridge(Verse): + type = 'bridge' + +class Song(AST): + """A song""" + + METADATA_TYPE = { + "title": "add_title", + "subtitle": "add_subtitle", + "language": "add_language", + "artist": "add_author", + } + + def __init__(self): + super().__init__() + self.content = [] + self.meta = [] + self._authors = [] + self._titles = [] + self._subtitles = [] + self._languages = set() + + def add(self, data): + if data is None: + if not (self.content and isinstance(self.content[0], Newline)): + self.content.insert(0, Newline()) + elif isinstance(data, Line): + if not (self.content and isinstance(self.content[0], Verse)): + self.content.insert(0, Verse()) + self.content[0].prepend(data.strip()) + elif data.inline: + self.content.insert(0, data) + elif isinstance(data, Directive): + name = directive_name(data.keyword) + if name in self.METADATA_TYPE: + getattr(self, self.METADATA_TYPE[name])(*data.as_tuple) + else: + self.meta.append(data) else: - self.init_short_form(name) + raise Exception() + return self + + def str_meta(self): + for title in self.titles: + yield "{{title: {}}}".format(title) + for language in sorted(self.languages): + yield "{{language: {}}}".format(language) + for author in self.authors: + yield "{{by: {}}}".format(author) + for key in sorted(self.meta): + yield str(key) def __str__(self): - return self.name + return ( + "\n".join(self.str_meta()).strip() + + + "\n========\n" + + + "\n".join([str(item) for item in self.content]).strip() + ) + + + def add_title(self, __ignored, title): + self._titles.insert(0, title) + + def add_subtitle(self, __ignored, title): + self._subtitles.insert(0, title) + + @property + def titles(self): + return self._titles + self._subtitles + + def add_author(self, __ignored, title): + self._authors.insert(0, title) - def init_short_form(self, name): - self.type = "" + @property + def authors(self): + return self._authors - def init_long_form(self, name): - self.type = "" + def add_language(self, __ignored, language): + self._languages.add(language) + + @property + def languages(self): + return self._languages + + + +class Newline(AST): + def __str__(self): + return "" + +@functools.total_ordering +class Directive(AST): + """A directive""" + + def __init__(self): + super().__init__() + self.keyword = "" + self.argument = None + @property + def keyword(self): + return self._keyword + + @property + def inline(self): + return self.keyword in INLINE_PROPERTIES + + @keyword.setter + def keyword(self, value): + self._keyword = value.strip() + + def __str__(self): + if self.argument is not None: + return "{{{}: {}}}".format( + self.keyword, + self.argument, + ) + else: + return "{{{}}}".format(self.keyword) + + @property + def as_tuple(self): + return (self.keyword, self.argument) + + def __eq__(self, other): + return self.as_tuple == other.as_tuple + + def __lt__(self, other): + return self.as_tuple < other.as_tuple + +class Tab(AST): + """Tablature""" + + inline = True + + def __init__(self): + super().__init__() + self.content = [] + + def prepend(self, data): + self.content.insert(0, data) + return self + + def __str__(self): + return '{{start_of_tab}}\n{}\n{{end_of_tab}}'.format( + _indent("\n".join(self.content)), + ) diff --git a/patacrep/songs/chordpro/lexer.py b/patacrep/songs/chordpro/lexer.py index f33d8f2e..726c24e4 100644 --- a/patacrep/songs/chordpro/lexer.py +++ b/patacrep/songs/chordpro/lexer.py @@ -7,16 +7,21 @@ LOGGER = logging.getLogger() #pylint: disable=invalid-name tokens = ( - #'LBRACKET', - #'RBRACKET', - 'CHORD', - 'LBRACE', - 'RBRACE', - 'NEWLINE', - #'COLON', - 'WORD', - 'SPACE', - #'NUMBER', + 'LBRACE', + 'RBRACE', + 'CHORD', + 'NEWLINE', + 'COLON', + 'WORD', + 'SPACE', + 'TEXT', + 'KEYWORD', + 'SOC', + 'EOC', + 'SOB', + 'EOB', + 'SOT', + 'EOT', ) class ChordProLexer: @@ -26,14 +31,51 @@ class ChordProLexer: states = ( ('chord', 'exclusive'), + ('directive', 'exclusive'), + ('directiveargument', 'exclusive'), + ('tablature', 'exclusive'), ) - t_LBRACE = r'{' - t_RBRACE = r'}' t_SPACE = r'[ \t]+' - #t_COLON = r':' + t_chord_CHORD = r'[A-G7#m]+' # TODO This can be refined + t_directive_SPACE = r'[ \t]+' + t_directive_KEYWORD = r'[a-zA-Z_]+' + t_directiveargument_TEXT = r'[^}]+' + + def t_SOC(self, token): + r'{(soc|start_of_chorus)}' + return token + def t_EOC(self, token): + r'{(eoc|end_of_chorus)}' + return token + + def t_SOB(self, token): + r'{(sob|start_of_bridge)}' + return token + + def t_EOB(self, token): + r'{(eob|end_of_bridge)}' + return token + + def t_SOT(self, token): + r'{(sot|start_of_tab)}' + self.lexer.push_state('tablature') + return token + + def t_tablature_EOT(self, token): + r'{(eot|end_of_tab)}' + self.lexer.pop_state() + return token + + def t_tablature_SPACE(self, token): + r'[ \t]+' + return token + + t_tablature_TEXT = r'[^\n]+' + t_tablature_NEWLINE = r'\n' + def __init__(self): self.__class__.lexer = lex.lex(module=self) @@ -51,7 +93,7 @@ class ChordProLexer: @staticmethod def t_WORD(token): - r'[^\n\][\t ]+' + r'[^{}\n\][\t ]+' return token def t_LBRACKET(self, token): @@ -62,11 +104,26 @@ class ChordProLexer: r'\]' self.lexer.pop_state() - #@staticmethod - #def t_NUMBER(token): - # r'[0-9]+' - # token.value = int(token.value) - # return token + def t_LBRACE(self, token): + r'{' + self.lexer.push_state('directive') + return token + + def t_directive_RBRACE(self, token): + r'}' + self.lexer.pop_state() + return token + + def t_directiveargument_RBRACE(self, token): + r'}' + self.lexer.pop_state() + self.lexer.pop_state() + return token + + def t_directive_COLON(self, token): + r':' + self.lexer.push_state('directiveargument') + return token @staticmethod def t_error(token): @@ -79,3 +136,19 @@ class ChordProLexer: """Manage errors""" LOGGER.error("Illegal character '{}' in chord..".format(token.value[0])) token.lexer.skip(1) + + @staticmethod + def t_tablature_error(token): + """Manage errors""" + LOGGER.error("Illegal character '{}' in tablature..".format(token.value[0])) + token.lexer.skip(1) + + @staticmethod + def t_directive_error(token): + """Manage errors""" + LOGGER.error("Illegal character '{}' in directive..".format(token.value[0])) + token.lexer.skip(1) + + @staticmethod + def t_directiveargument_error(token): + return t_directive_error(token) diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py index 91470ba5..32fa2c42 100644 --- a/patacrep/songs/chordpro/syntax.py +++ b/patacrep/songs/chordpro/syntax.py @@ -4,9 +4,10 @@ import logging import ply.yacc as yacc -from patacrep.songs.chordpro.lexer import tokens, ChordProLexer -from patacrep.songs.chordpro import ast from patacrep.errors import SongbookError +from patacrep.songs.chordpro import ast +from patacrep.songs.chordpro import ast +from patacrep.songs.chordpro.lexer import tokens, ChordProLexer LOGGER = logging.getLogger() @@ -24,6 +25,8 @@ class ParsingError(SongbookError): class Parser: """ChordPro parser class""" + start = "song" + def __init__(self, filename=None): self.tokens = tokens self.filename = filename @@ -39,7 +42,7 @@ class Parser: def p_error(self, token): """Manage parsing errors.""" - if token: # TODO remove this test + if token: LOGGER.error("Error in file {}, line {}:{}.".format( str(self.filename), token.lineno, @@ -52,85 +55,213 @@ class Parser: """song : block song | empty """ + #if isinstance(symbols[1], str): if len(symbols) == 2: - symbols[0] = ('song') + symbols[0] = ast.Song() else: - symbols[0] = ('song', symbols[1], symbols[2]) + symbols[0] = symbols[2].add(symbols[1]) + + #@staticmethod + #def p_song_next(symbols): + # """song_next : block song_next + # | empty + # """ + # if len(symbols) == 2: + # symbols[0] = ast.Song() + # else: + # symbols[0] = symbols[2].add(symbols[1]) @staticmethod def p_block(symbols): - """block : directive NEWLINE newlines - | stanza NEWLINE newlines + """block : SPACE block + | directive NEWLINE + | line NEWLINE + | chorus NEWLINE + | tab NEWLINE + | bridge NEWLINE + | NEWLINE """ - symbols[0] = ('block', symbols[1]) + if len(symbols) == 3 and isinstance(symbols[1], str): + symbols[0] = symbols[2] + elif (symbols[1] is None) or (len(symbols) == 2): + symbols[0] = None + else: + symbols[0] = symbols[1] @staticmethod - def p_newlines(symbols): - """newlines : NEWLINE newlines - | empty""" - symbols[0] = ('newlines') + def p_maybespace(symbols): + """maybespace : SPACE + | empty + """ + symbols[0] = None + + #@staticmethod + #def p_newlines(symbols): + # """newlines : NEWLINE newlines + # | empty""" + # symbols[0] = ('newlines') @staticmethod def p_directive(symbols): - """directive : LBRACE WORD RBRACE""" - symbols[0] = ('directive', symbols[1]) + """directive : LBRACE KEYWORD directive_next RBRACE + | LBRACE SPACE KEYWORD directive_next RBRACE + """ + if len(symbols) == 5: + symbols[3].keyword = symbols[2] + symbols[0] = symbols[3] + else: + symbols[4].keyword = symbols[3] + symbols[0] = symbols[4] + + @staticmethod + def p_directive_next(symbols): + """directive_next : SPACE COLON TEXT + | COLON TEXT + | empty + """ + symbols[0] = ast.Directive() + if len(symbols) == 3: + symbols[0].argument = symbols[2].strip() + elif len(symbols) == 4: + symbols[0].argument = symbols[3].strip() @staticmethod def p_line(symbols): - """line : WORD line_next - | CHORD line_next - | SPACE line_next + """line : word line_next + | chord line_next """ - symbols[0] = ('line', symbols[1], symbols[2]) + symbols[0] = symbols[2].prepend(symbols[1]) @staticmethod def p_line_next(symbols): - """line_next : WORD line_next - | SPACE line_next - | CHORD line_next + """line_next : word line_next + | space line_next + | chord line_next | empty """ if len(symbols) == 2: - symbols[0] = ('line-next') + symbols[0] = ast.Line() + else: + symbols[0] = symbols[2].prepend(symbols[1]) + + @staticmethod + def p_word(symbols): + """word : WORD""" + symbols[0] = ast.Word(symbols[1]) + + @staticmethod + def p_space(symbols): + """space : SPACE""" + symbols[0] = ast.Space() + + @staticmethod + def p_chord(symbols): + """chord : CHORD""" + symbols[0] = ast.Chord(symbols[1]) + + #@staticmethod + #def p_verse(symbols): + # """verse : line NEWLINE verse_next + # """ + # symbols[0] = symbols[3].prepend(symbols[1]) + + #@staticmethod + #def p_verse_next(symbols): + # """verse_next : line NEWLINE verse_next + # | empty + # """ + # if len(symbols) == 2: + # symbols[0] = ast.Verse() + # else: + # symbols[0] = symbols[3].prepend(symbols[1]) + + @staticmethod + def p_chorus(symbols): + """chorus : SOC maybespace NEWLINE chorus_content EOC maybespace + """ + symbols[0] = symbols[4] + + @staticmethod + def p_chorus_content(symbols): + """chorus_content : line NEWLINE chorus_content + | SPACE chorus_content + | empty + """ + if len(symbols) == 2: + symbols[0] = ast.Chorus() + elif len(symbols) == 3: + symbols[0] = symbols[2] else: - symbols[0] = ('line-next', symbols[1], symbols[2]) + symbols[0] = symbols[3].prepend(symbols[1]) @staticmethod - def p_stanza(symbols): - """stanza : line NEWLINE stanza_next + def p_bridge(symbols): + """bridge : SOB maybespace NEWLINE bridge_content EOB maybespace """ - symbols[0] = ('stanza', symbols[1], symbols[3]) + symbols[0] = symbols[4] @staticmethod - def p_stanza_next(symbols): - """stanza_next : line NEWLINE stanza_next - | empty + def p_bridge_content(symbols): + """bridge_content : line NEWLINE bridge_content + | SPACE bridge_content + | empty """ if len(symbols) == 2: - symbols[0] = ('stanza-next') + symbols[0] = ast.Bridge() + elif len(symbols) == 3: + symbols[0] = symbols[2] else: - symbols[0] = ('stanza-next', symbols[1], symbols[3]) + symbols[0] = symbols[3].prepend(symbols[1]) + #@staticmethod - #def p_braces(symbols): - # """braces : LBRACE expression COLON expression RBRACE""" - # symbols[0] = symbols[2] + #def p_bridge_next(symbols): + # """bridge_next : line NEWLINE bridge_next + # | empty + # """ + # if len(symbols) == 2: + # symbols[0] = ast.Bridge() + # else: + # symbols[0] = symbols[3].prepend(symbols[1]) + + @staticmethod + def p_tab(symbols): + """tab : SOT maybespace NEWLINE tab_content EOT maybespace + """ + symbols[0] = symbols[4] + + @staticmethod + def p_tab_content(symbols): + """tab_content : NEWLINE tab_content + | TEXT tab_content + | SPACE tab_content + | empty + """ + if len(symbols) == 2: + symbols[0] = ast.Tab() + else: + if symbols[1].strip(): + symbols[2].prepend(symbols[1]) + symbols[0] = symbols[2] @staticmethod def p_empty(symbols): """empty :""" symbols[0] = None - #@staticmethod - #def p_comment(symbols): - # """comment : COMMENT""" - # symbols[0] = ('comment', symbols[1]) - - +def lex_song(content): + # TODO delete + lex = ChordProLexer().lexer + lex.input(content) + while 1: + tok = lex.token() + if not tok: break + print(tok) def parse_song(content, filename=None): """Parse song and return its metadata.""" return yacc.yacc(module=Parser(filename)).parse( content, + debug=0, lexer=ChordProLexer().lexer, ) diff --git a/patacrep/songs/chordpro/test/00.sgc b/patacrep/songs/chordpro/test/00.sgc new file mode 100644 index 00000000..e69de29b diff --git a/patacrep/songs/chordpro/test/00.txt b/patacrep/songs/chordpro/test/00.txt new file mode 100644 index 00000000..dbbd9c50 --- /dev/null +++ b/patacrep/songs/chordpro/test/00.txt @@ -0,0 +1 @@ +======== diff --git a/patacrep/songs/chordpro/test/01.sgc b/patacrep/songs/chordpro/test/01.sgc new file mode 100644 index 00000000..8ea8d2a2 --- /dev/null +++ b/patacrep/songs/chordpro/test/01.sgc @@ -0,0 +1 @@ +A verse line diff --git a/patacrep/songs/chordpro/test/01.txt b/patacrep/songs/chordpro/test/01.txt new file mode 100644 index 00000000..84cf4364 --- /dev/null +++ b/patacrep/songs/chordpro/test/01.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + A verse line +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/02.sgc b/patacrep/songs/chordpro/test/02.sgc new file mode 100644 index 00000000..270bc746 --- /dev/null +++ b/patacrep/songs/chordpro/test/02.sgc @@ -0,0 +1 @@ +{title : A directive} diff --git a/patacrep/songs/chordpro/test/02.txt b/patacrep/songs/chordpro/test/02.txt new file mode 100644 index 00000000..f3fbd9e9 --- /dev/null +++ b/patacrep/songs/chordpro/test/02.txt @@ -0,0 +1,2 @@ +{title: A directive} +======== diff --git a/patacrep/songs/chordpro/test/03.sgc b/patacrep/songs/chordpro/test/03.sgc new file mode 100644 index 00000000..fd40910d --- /dev/null +++ b/patacrep/songs/chordpro/test/03.sgc @@ -0,0 +1,4 @@ + + + + diff --git a/patacrep/songs/chordpro/test/03.txt b/patacrep/songs/chordpro/test/03.txt new file mode 100644 index 00000000..dbbd9c50 --- /dev/null +++ b/patacrep/songs/chordpro/test/03.txt @@ -0,0 +1 @@ +======== diff --git a/patacrep/songs/chordpro/test/04.sgc b/patacrep/songs/chordpro/test/04.sgc new file mode 100644 index 00000000..6a266eea --- /dev/null +++ b/patacrep/songs/chordpro/test/04.sgc @@ -0,0 +1,3 @@ +{soc} +A one line chorus +{eoc} diff --git a/patacrep/songs/chordpro/test/04.txt b/patacrep/songs/chordpro/test/04.txt new file mode 100644 index 00000000..3336d565 --- /dev/null +++ b/patacrep/songs/chordpro/test/04.txt @@ -0,0 +1,4 @@ +======== +{start_of_chorus} + A one line chorus +{end_of_chorus} diff --git a/patacrep/songs/chordpro/test/05.sgc b/patacrep/songs/chordpro/test/05.sgc new file mode 100644 index 00000000..9472ebcb --- /dev/null +++ b/patacrep/songs/chordpro/test/05.sgc @@ -0,0 +1,3 @@ +{sob} +A one line bridge +{eob} diff --git a/patacrep/songs/chordpro/test/05.txt b/patacrep/songs/chordpro/test/05.txt new file mode 100644 index 00000000..b2f99be2 --- /dev/null +++ b/patacrep/songs/chordpro/test/05.txt @@ -0,0 +1,4 @@ +======== +{start_of_bridge} + A one line bridge +{end_of_bridge} diff --git a/patacrep/songs/chordpro/test/06.sgc b/patacrep/songs/chordpro/test/06.sgc new file mode 100644 index 00000000..90a2b559 --- /dev/null +++ b/patacrep/songs/chordpro/test/06.sgc @@ -0,0 +1 @@ +# A comment diff --git a/patacrep/songs/chordpro/test/06.txt b/patacrep/songs/chordpro/test/06.txt new file mode 100644 index 00000000..dbbd9c50 --- /dev/null +++ b/patacrep/songs/chordpro/test/06.txt @@ -0,0 +1 @@ +======== diff --git a/patacrep/songs/chordpro/test/07.sgc b/patacrep/songs/chordpro/test/07.sgc new file mode 100644 index 00000000..fd183eff --- /dev/null +++ b/patacrep/songs/chordpro/test/07.sgc @@ -0,0 +1,3 @@ +{sot} +A tab +{eot} diff --git a/patacrep/songs/chordpro/test/07.txt b/patacrep/songs/chordpro/test/07.txt new file mode 100644 index 00000000..7fb8bc64 --- /dev/null +++ b/patacrep/songs/chordpro/test/07.txt @@ -0,0 +1,4 @@ +======== +{start_of_tab} + A tab +{end_of_tab} diff --git a/patacrep/songs/chordpro/test/08.sgc b/patacrep/songs/chordpro/test/08.sgc new file mode 100644 index 00000000..b51646ca --- /dev/null +++ b/patacrep/songs/chordpro/test/08.sgc @@ -0,0 +1,10 @@ + + + +# comment +# comment + + +A lot of new lines + +# comment diff --git a/patacrep/songs/chordpro/test/08.txt b/patacrep/songs/chordpro/test/08.txt new file mode 100644 index 00000000..92c183b1 --- /dev/null +++ b/patacrep/songs/chordpro/test/08.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + A lot of new lines +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/09.sgc b/patacrep/songs/chordpro/test/09.sgc new file mode 100644 index 00000000..b831f230 --- /dev/null +++ b/patacrep/songs/chordpro/test/09.sgc @@ -0,0 +1,15 @@ + + + +# comment +# comment + + +A lot of new lines + +# comment + +{title: and a directive} + +# comment + diff --git a/patacrep/songs/chordpro/test/09.txt b/patacrep/songs/chordpro/test/09.txt new file mode 100644 index 00000000..b7669e62 --- /dev/null +++ b/patacrep/songs/chordpro/test/09.txt @@ -0,0 +1,5 @@ +{title: and a directive} +======== +{start_of_verse} + A lot of new lines +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/10.sgc b/patacrep/songs/chordpro/test/10.sgc new file mode 100644 index 00000000..6519ab80 --- /dev/null +++ b/patacrep/songs/chordpro/test/10.sgc @@ -0,0 +1 @@ +A line[A] with a chord diff --git a/patacrep/songs/chordpro/test/10.txt b/patacrep/songs/chordpro/test/10.txt new file mode 100644 index 00000000..b96d8637 --- /dev/null +++ b/patacrep/songs/chordpro/test/10.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + A line[A] with a chord +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/11.sgc b/patacrep/songs/chordpro/test/11.sgc new file mode 100644 index 00000000..fc3d697d --- /dev/null +++ b/patacrep/songs/chordpro/test/11.sgc @@ -0,0 +1 @@ +A line ending with a chord[A] diff --git a/patacrep/songs/chordpro/test/11.txt b/patacrep/songs/chordpro/test/11.txt new file mode 100644 index 00000000..2a9eaf17 --- /dev/null +++ b/patacrep/songs/chordpro/test/11.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + A line ending with a chord[A] +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/12.sgc b/patacrep/songs/chordpro/test/12.sgc new file mode 100644 index 00000000..a9583451 --- /dev/null +++ b/patacrep/songs/chordpro/test/12.sgc @@ -0,0 +1 @@ +[A]A line starting with a chord diff --git a/patacrep/songs/chordpro/test/12.txt b/patacrep/songs/chordpro/test/12.txt new file mode 100644 index 00000000..83c11625 --- /dev/null +++ b/patacrep/songs/chordpro/test/12.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + [A]A line starting with a chord +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/13.sgc b/patacrep/songs/chordpro/test/13.sgc new file mode 100644 index 00000000..6cc3dfbc --- /dev/null +++ b/patacrep/songs/chordpro/test/13.sgc @@ -0,0 +1,5 @@ +{sot} + A table + wit many # weir [ + [ symbols +{eot} diff --git a/patacrep/songs/chordpro/test/13.txt b/patacrep/songs/chordpro/test/13.txt new file mode 100644 index 00000000..447f67dd --- /dev/null +++ b/patacrep/songs/chordpro/test/13.txt @@ -0,0 +1,6 @@ +======== +{start_of_tab} + A table + wit many # weir [ + [ symbols +{end_of_tab} diff --git a/patacrep/songs/chordpro/test/21.sgc b/patacrep/songs/chordpro/test/21.sgc new file mode 100644 index 00000000..c01b31ad --- /dev/null +++ b/patacrep/songs/chordpro/test/21.sgc @@ -0,0 +1 @@ + A verse line diff --git a/patacrep/songs/chordpro/test/21.txt b/patacrep/songs/chordpro/test/21.txt new file mode 100644 index 00000000..84cf4364 --- /dev/null +++ b/patacrep/songs/chordpro/test/21.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + A verse line +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/22.sgc b/patacrep/songs/chordpro/test/22.sgc new file mode 100644 index 00000000..425fd584 --- /dev/null +++ b/patacrep/songs/chordpro/test/22.sgc @@ -0,0 +1 @@ + {title : A directive} diff --git a/patacrep/songs/chordpro/test/22.txt b/patacrep/songs/chordpro/test/22.txt new file mode 100644 index 00000000..f3fbd9e9 --- /dev/null +++ b/patacrep/songs/chordpro/test/22.txt @@ -0,0 +1,2 @@ +{title: A directive} +======== diff --git a/patacrep/songs/chordpro/test/23.sgc b/patacrep/songs/chordpro/test/23.sgc new file mode 100644 index 00000000..ce5ed9dd --- /dev/null +++ b/patacrep/songs/chordpro/test/23.sgc @@ -0,0 +1,4 @@ + + + + diff --git a/patacrep/songs/chordpro/test/23.txt b/patacrep/songs/chordpro/test/23.txt new file mode 100644 index 00000000..dbbd9c50 --- /dev/null +++ b/patacrep/songs/chordpro/test/23.txt @@ -0,0 +1 @@ +======== diff --git a/patacrep/songs/chordpro/test/24.sgc b/patacrep/songs/chordpro/test/24.sgc new file mode 100644 index 00000000..a929a754 --- /dev/null +++ b/patacrep/songs/chordpro/test/24.sgc @@ -0,0 +1,3 @@ + {soc} + A one line chorus + {eoc} diff --git a/patacrep/songs/chordpro/test/24.txt b/patacrep/songs/chordpro/test/24.txt new file mode 100644 index 00000000..3336d565 --- /dev/null +++ b/patacrep/songs/chordpro/test/24.txt @@ -0,0 +1,4 @@ +======== +{start_of_chorus} + A one line chorus +{end_of_chorus} diff --git a/patacrep/songs/chordpro/test/25.sgc b/patacrep/songs/chordpro/test/25.sgc new file mode 100644 index 00000000..45efba59 --- /dev/null +++ b/patacrep/songs/chordpro/test/25.sgc @@ -0,0 +1,3 @@ + {sob} + A one line bridge + {eob} diff --git a/patacrep/songs/chordpro/test/25.txt b/patacrep/songs/chordpro/test/25.txt new file mode 100644 index 00000000..b2f99be2 --- /dev/null +++ b/patacrep/songs/chordpro/test/25.txt @@ -0,0 +1,4 @@ +======== +{start_of_bridge} + A one line bridge +{end_of_bridge} diff --git a/patacrep/songs/chordpro/test/26.sgc b/patacrep/songs/chordpro/test/26.sgc new file mode 100644 index 00000000..372b96b4 --- /dev/null +++ b/patacrep/songs/chordpro/test/26.sgc @@ -0,0 +1 @@ + # A comment diff --git a/patacrep/songs/chordpro/test/26.txt b/patacrep/songs/chordpro/test/26.txt new file mode 100644 index 00000000..dbbd9c50 --- /dev/null +++ b/patacrep/songs/chordpro/test/26.txt @@ -0,0 +1 @@ +======== diff --git a/patacrep/songs/chordpro/test/27.sgc b/patacrep/songs/chordpro/test/27.sgc new file mode 100644 index 00000000..bfa0cf0e --- /dev/null +++ b/patacrep/songs/chordpro/test/27.sgc @@ -0,0 +1,3 @@ + {sot} + A tab + {eot} diff --git a/patacrep/songs/chordpro/test/27.txt b/patacrep/songs/chordpro/test/27.txt new file mode 100644 index 00000000..7fb8bc64 --- /dev/null +++ b/patacrep/songs/chordpro/test/27.txt @@ -0,0 +1,4 @@ +======== +{start_of_tab} + A tab +{end_of_tab} diff --git a/patacrep/songs/chordpro/test/28.sgc b/patacrep/songs/chordpro/test/28.sgc new file mode 100644 index 00000000..69f980b3 --- /dev/null +++ b/patacrep/songs/chordpro/test/28.sgc @@ -0,0 +1,10 @@ + + + + # comment +# comment + + + A lot of new lines + +# comment diff --git a/patacrep/songs/chordpro/test/28.txt b/patacrep/songs/chordpro/test/28.txt new file mode 100644 index 00000000..92c183b1 --- /dev/null +++ b/patacrep/songs/chordpro/test/28.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + A lot of new lines +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/29.sgc b/patacrep/songs/chordpro/test/29.sgc new file mode 100644 index 00000000..830e0177 --- /dev/null +++ b/patacrep/songs/chordpro/test/29.sgc @@ -0,0 +1,15 @@ + + + +# comment +# comment + + + A lot of new lines + +# comment + + {title: and a directive} + +# comment + diff --git a/patacrep/songs/chordpro/test/29.txt b/patacrep/songs/chordpro/test/29.txt new file mode 100644 index 00000000..b7669e62 --- /dev/null +++ b/patacrep/songs/chordpro/test/29.txt @@ -0,0 +1,5 @@ +{title: and a directive} +======== +{start_of_verse} + A lot of new lines +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/30.sgc b/patacrep/songs/chordpro/test/30.sgc new file mode 100644 index 00000000..ee67061c --- /dev/null +++ b/patacrep/songs/chordpro/test/30.sgc @@ -0,0 +1 @@ + [A]A line starting with a chord diff --git a/patacrep/songs/chordpro/test/__init__.py b/patacrep/songs/chordpro/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patacrep/songs/chordpro/test/greensleeves.sgc b/patacrep/songs/chordpro/test/greensleeves.sgc new file mode 100644 index 00000000..1dee93ff --- /dev/null +++ b/patacrep/songs/chordpro/test/greensleeves.sgc @@ -0,0 +1,44 @@ +{language : english} +{columns : 2} +{subtitle : Un sous titre} +{ title : Greensleeves} +{title : Un autre sous-titre} +{artist: Traditionnel} +{cover : traditionnel } +{album :Angleterre} + +{partition : greensleeves.ly} + + +A[Am]las, my love, ye [G]do me wrong +To [Am]cast me oft dis[E]curteously +And [Am]I have loved [G]you so long +De[Am]lighting [E]in your [Am]companie + +{start_of_chorus} + [C]Green[B]sleeves was [G]all my joy + [Am]Greensleeves was [E]my delight + [C]Greensleeves was my [G]heart of gold + And [Am]who but [E]Ladie [Am]Greensleeves +{end_of_chorus} + +I [Am]have been ready [G]at your hand +To [Am]grant what ever [E]you would crave +I [Am]have both waged [G]life and land +Your [Am]love and [E]good will [Am]for to have + +I [Am]bought thee kerchers [G]to thy head +That [Am]were wrought fine and [E]gallantly +I [Am]kept thee both at [G]boord and bed +Which [Am]cost my [E]purse well [Am]favouredly + +I [Am]bought thee peticotes [G]of the best +The [Am]cloth so fine as [E]fine might be +I [Am]gave thee jewels [G]for thy chest +And [Am]all this [E]cost I [Am]spent on thee + + +Thy [Am]smock of silke, both [G]faire and white +With [Am]gold embrodered [E]gorgeously +Thy [Am]peticote of [G]sendall right +And [Am]this I [E]bought thee [Am]gladly diff --git a/patacrep/songs/chordpro/test/greensleeves.txt b/patacrep/songs/chordpro/test/greensleeves.txt new file mode 100644 index 00000000..38564149 --- /dev/null +++ b/patacrep/songs/chordpro/test/greensleeves.txt @@ -0,0 +1,51 @@ +{title: Greensleeves} +{title: Un autre sous-titre} +{title: Un sous titre} +{language: english} +{by: Traditionnel} +{album: Angleterre} +{columns: 2} +{cover: traditionnel} +{partition: greensleeves.ly} +======== +{start_of_verse} + A[Am]las, my love, ye [G]do me wrong + To [Am]cast me oft dis[E]curteously + And [Am]I have loved [G]you so long + De[Am]lighting [E]in your [Am]companie +{end_of_verse} + +{start_of_chorus} + [C]Green[B]sleeves was [G]all my joy + [Am]Greensleeves was [E]my delight + [C]Greensleeves was my [G]heart of gold + And [Am]who but [E]Ladie [Am]Greensleeves +{end_of_chorus} + +{start_of_verse} + I [Am]have been ready [G]at your hand + To [Am]grant what ever [E]you would crave + I [Am]have both waged [G]life and land + Your [Am]love and [E]good will [Am]for to have +{end_of_verse} + +{start_of_verse} + I [Am]bought thee kerchers [G]to thy head + That [Am]were wrought fine and [E]gallantly + I [Am]kept thee both at [G]boord and bed + Which [Am]cost my [E]purse well [Am]favouredly +{end_of_verse} + +{start_of_verse} + I [Am]bought thee peticotes [G]of the best + The [Am]cloth so fine as [E]fine might be + I [Am]gave thee jewels [G]for thy chest + And [Am]all this [E]cost I [Am]spent on thee +{end_of_verse} + +{start_of_verse} + Thy [Am]smock of silke, both [G]faire and white + With [Am]gold embrodered [E]gorgeously + Thy [Am]peticote of [G]sendall right + And [Am]this I [E]bought thee [Am]gladly +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/metadata.sgc b/patacrep/songs/chordpro/test/metadata.sgc new file mode 100644 index 00000000..c892faf0 --- /dev/null +++ b/patacrep/songs/chordpro/test/metadata.sgc @@ -0,0 +1,20 @@ +{subtitle: Subtitle3} +{title: Title} +{title: Subtitle1} +{subtitle: Subtitle4} +{t: Subtitle2} +{st: Subtitle5} +{language: french} +{language: english} +{by: Author1} +{artist: Author2} +{album: Albom} +{copyright: Copyright} +{cover: Cover} +{vcover: VCover} +{capo: Capo} +{foo: Foo} +{comment: Comment} +{guitar_comment: GuitarComment} +{image: Image} +{lilypond: Lilypond} diff --git a/patacrep/songs/chordpro/test/metadata.txt b/patacrep/songs/chordpro/test/metadata.txt new file mode 100644 index 00000000..8d73f4b8 --- /dev/null +++ b/patacrep/songs/chordpro/test/metadata.txt @@ -0,0 +1,21 @@ +{title: Title} +{title: Subtitle1} +{title: Subtitle2} +{title: Subtitle3} +{title: Subtitle4} +{title: Subtitle5} +{language: english} +{language: french} +{by: Author1} +{by: Author2} +{album: Albom} +{capo: Capo} +{copyright: Copyright} +{cover: Cover} +{foo: Foo} +{vcover: VCover} +======== +{comment: Comment} +{guitar_comment: GuitarComment} +{image: Image} +{lilypond: Lilypond} diff --git a/patacrep/songs/chordpro/test/test_parser.py b/patacrep/songs/chordpro/test/test_parser.py new file mode 100644 index 00000000..85ad4108 --- /dev/null +++ b/patacrep/songs/chordpro/test/test_parser.py @@ -0,0 +1,36 @@ +import glob +import os +import unittest + +from patacrep.songs.chordpro import syntax as chordpro + +class ParserTestCase(unittest.TestCase): + + def test_txt(self): + for txt in sorted(glob.glob(os.path.join( + os.path.dirname(__file__), + '*.txt', + ))): + basename = txt[:-len('.txt')] + with open("{}.sgc".format(basename), 'r', encoding='utf8') as sourcefile: + with open("{}.txt".format(basename), 'r', encoding='utf8') as expectfile: + #print(os.path.basename(sourcefile.name)) + #with open("{}.txt.diff".format(basename), 'w', encoding='utf8') as difffile: + # difffile.write( + # str(chordpro.parse_song( + # sourcefile.read(), + # os.path.basename(sourcefile.name), + # )).strip() + # ) + # sourcefile.seek(0) + self.assertMultiLineEqual( + str(chordpro.parse_song( + sourcefile.read(), + os.path.basename(sourcefile.name), + )).strip(), + expectfile.read().strip(), + ) + + def test_tex(self): + # TODO + pass From cacd957ac5611e0cb1841604554c708fe83c5e27 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 24 Jan 2015 15:11:12 +0100 Subject: [PATCH 08/21] Turned authors in a list of authors --- patacrep/authors.py | 21 ++++++++++++++++----- patacrep/index.py | 13 ++++++++----- patacrep/songs/latex/__init__.py | 2 +- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/patacrep/authors.py b/patacrep/authors.py index 3bdb6b9a..b902bb02 100644 --- a/patacrep/authors.py +++ b/patacrep/authors.py @@ -164,17 +164,21 @@ def processauthors(authors_string, after=None, ignore=None, sep=None): For example, we are processing: # processauthors( - # "Lyrics by William Blake (from Milton, 1808), - music by Hubert Parry (1916), - and sung by The Royal\ Choir~of~Nowhere - (just here to show you how processing is done)", + # [ + # " + # Lyrics by William Blake (from Milton, 1808), + # music by Hubert Parry (1916), + # and sung by The Royal\ Choir~of~Nowhere + # (just here to show you how processing is done) + # ", + # ], # after = ["by"], # ignore = ["anonymous"], # sep = [re.compile('^(.*) and (.*)$')], # ) - The "authors_string" string is processed as: + The "authors_string" is processed as: 1) First, parenthesis (and its content) are removed. # "Lyrics by William Blake, music by Hubert Parry, @@ -220,3 +224,10 @@ def processauthors(authors_string, after=None, ignore=None, sep=None): ignore) ) ] + +def process_listauthors(authors_list): + """Process a list of authors, and return the list of resulting authors.""" + return sum([ + processauthors(string) + for string in authors_list + ]) diff --git a/patacrep/index.py b/patacrep/index.py index 6a4300a2..8540d433 100644 --- a/patacrep/index.py +++ b/patacrep/index.py @@ -19,7 +19,6 @@ EOL = "\n" KEYWORD_PATTERN = re.compile(r"^%(\w+)\s?(.*)$", re.LOCALE) FIRST_LETTER_PATTERN = re.compile(r"^(?:\{?\\\w+\}?)*[^\w]*(\w)", re.LOCALE) - def process_sxd(filename): """Parse sxd file. @@ -162,7 +161,11 @@ class Index(object): def entry_to_str(self, key, entry): """Return the LaTeX code corresponding to the entry.""" - return (r'\idxentry{{{0}}}{{{1}}}' + EOL).format( + return r"""\idxentry{{ + {0} + }}{{ + {1} + }}""".format( self.key_to_str(key), r'\\'.join([self.ref_to_str(ref) for ref in entry]), ) @@ -182,13 +185,13 @@ class Index(object): ] string = r'\begin{idxblock}{' + letter + '}' + EOL for key in sorted(entries, key=sortkey): - string += self.entry_to_str(key, entries[key]['entries']) - string += r'\end{idxblock}' + EOL + string += " " + self.entry_to_str(key, entries[key]['entries']) + string += EOL + r'\end{idxblock}' return string def entries_to_str(self): """Return the LaTeX code corresponding to the index.""" string = "" for letter in sorted(self.data.keys()): - string += self.idxblock_to_str(letter, self.data[letter]) + string += self.idxblock_to_str(letter, self.data[letter]) + EOL return string diff --git a/patacrep/songs/latex/__init__.py b/patacrep/songs/latex/__init__.py index 13270bd4..eaea44bd 100644 --- a/patacrep/songs/latex/__init__.py +++ b/patacrep/songs/latex/__init__.py @@ -22,7 +22,7 @@ class LatexSong(Song): del self.data['@titles'] self.languages = self.data['@languages'] del self.data['@languages'] - self.authors = self.data['by'] + self.authors = [self.data['by']] del self.data['by'] def tex(self, output): From feb642952a92dba1203d907b9b68dc1339caa84f Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 24 Jan 2015 20:11:59 +0100 Subject: [PATCH 09/21] Tex rendere works --- patacrep/authors.py | 13 +- patacrep/build.py | 4 +- patacrep/data/examples/example-all.sb | 1 + patacrep/data/examples/songs/greensleeves.sgc | 13 +- patacrep/songs/__init__.py | 13 +- patacrep/songs/chordpro/__init__.py | 47 ++++- patacrep/songs/chordpro/ast.py | 198 +++++++++++++++--- .../songs/chordpro/data/latex/chordpro.tex | 27 +++ .../chordpro/data/latex/content_chord.tex | 1 + .../chordpro/data/latex/content_comment.tex | 1 + .../chordpro/data/latex/content_error.tex | 3 + .../data/latex/content_guitar_comment.tex | 1 + .../chordpro/data/latex/content_image.tex | 1 + .../chordpro/data/latex/content_line.tex | 1 + .../chordpro/data/latex/content_newline.tex | 2 + .../chordpro/data/latex/content_partition.tex | 1 + .../chordpro/data/latex/content_space.tex | 1 + .../chordpro/data/latex/content_verse.tex | 7 + .../chordpro/data/latex/content_word.tex | 1 + patacrep/songs/chordpro/lexer.py | 27 ++- patacrep/songs/chordpro/syntax.py | 22 +- patacrep/songs/chordpro/test/__init__.py | 1 + patacrep/songs/chordpro/test/greensleeves.txt | 7 +- patacrep/songs/chordpro/test/metadata.sgc | 4 +- patacrep/songs/chordpro/test/metadata.txt | 14 +- patacrep/songs/chordpro/test/test_parser.py | 76 ++++--- patacrep/songs/latex/__init__.py | 2 +- patacrep/templates.py | 40 ++-- patacrep/test.py | 26 +++ setup.py | 3 +- 30 files changed, 422 insertions(+), 136 deletions(-) create mode 100644 patacrep/songs/chordpro/data/latex/chordpro.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_chord.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_comment.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_error.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_guitar_comment.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_image.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_line.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_newline.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_partition.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_space.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_verse.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_word.tex create mode 100644 patacrep/test.py diff --git a/patacrep/authors.py b/patacrep/authors.py index b902bb02..84613a1a 100644 --- a/patacrep/authors.py +++ b/patacrep/authors.py @@ -225,9 +225,12 @@ def processauthors(authors_string, after=None, ignore=None, sep=None): ) ] -def process_listauthors(authors_list): +def process_listauthors(authors_list, after=None, ignore=None, sep=None): """Process a list of authors, and return the list of resulting authors.""" - return sum([ - processauthors(string) - for string in authors_list - ]) + authors = [] + for sublist in [ + processauthors(string, after, ignore, sep) + for string in authors_list + ]: + authors.extend(sublist) + return authors diff --git a/patacrep/build.py b/patacrep/build.py index ac566e07..31d54eeb 100644 --- a/patacrep/build.py +++ b/patacrep/build.py @@ -10,7 +10,7 @@ from subprocess import Popen, PIPE, call from patacrep import __DATADIR__, authors, content, errors, files from patacrep.index import process_sxd -from patacrep.templates import TexRenderer +from patacrep.templates import TexBookRenderer from patacrep.songs import DataSubpath LOGGER = logging.getLogger(__name__) @@ -87,7 +87,7 @@ class Songbook(object): # Updating configuration config = DEFAULT_CONFIG.copy() config.update(self.config) - renderer = TexRenderer( + renderer = TexBookRenderer( config['template'], config['datadir'], config['lang'], diff --git a/patacrep/data/examples/example-all.sb b/patacrep/data/examples/example-all.sb index 0b1252aa..ca35b622 100644 --- a/patacrep/data/examples/example-all.sb +++ b/patacrep/data/examples/example-all.sb @@ -7,6 +7,7 @@ ], "booktype" : "chorded", "lang" : "french", +"encoding": "utf8", "authwords" : { "sep" : ["and", "et"] }, diff --git a/patacrep/data/examples/songs/greensleeves.sgc b/patacrep/data/examples/songs/greensleeves.sgc index 7d6b1000..a039cc74 100644 --- a/patacrep/data/examples/songs/greensleeves.sgc +++ b/patacrep/data/examples/songs/greensleeves.sgc @@ -1,7 +1,9 @@ {language : english} {columns : 2} { title : Greensleeves} +{ title : Un sous titre} {artist: Traditionnel} +{artist: Prénom Nom} {cover : traditionnel } {album :Angleterre} @@ -10,7 +12,7 @@ A[Am]las, my love, ye [G]do me wrong To [Am]cast me oft dis[E]curteously -And [Am]{I have} loved [G]you so long +And [Am]I have loved [G]you so long De[Am]lighting [E]in your [Am]companie {start_of_chorus} @@ -35,8 +37,15 @@ The [Am]cloth so fine as [E]fine might be I [Am]gave thee jewels [G]for thy chest And [Am]all this [E]cost I [Am]spent on thee +{c:test of comment} + +{gc: test of guitar comment} + +{image: traditionnel} Thy [Am]smock of silke, both [G]faire and white With [Am]gold embrodered [E]gorgeously Thy [Am]peticote of [G]sendall right -And [Am]this I [E]bought thee [Am]gladly \ No newline at end of file +And [Am]this I [E]bought thee [Am]gladly + + diff --git a/patacrep/songs/__init__.py b/patacrep/songs/__init__.py index 20fb367e..747b6112 100644 --- a/patacrep/songs/__init__.py +++ b/patacrep/songs/__init__.py @@ -8,7 +8,7 @@ import os import pickle import re -from patacrep.authors import processauthors +from patacrep.authors import process_listauthors from patacrep import files, encoding from patacrep.content import Content @@ -127,7 +127,8 @@ class Song(Content): # Data extraction from the latex song self.titles = [] self.data = {} - self.parse() + self.cached = None + self.parse(config) # Post processing of data self.datadir = datadir @@ -140,16 +141,12 @@ class Song(Content): for title in self.titles ] - self.authors = processauthors( + self.authors = process_listauthors( self.authors, **config["_compiled_authwords"] ) # Cache management - - #: Special attribute to allow plugins to store cached data - self.cached = None - self._version = self.CACHE_VERSION self._write_cache() @@ -176,7 +173,7 @@ class Song(Content): """ raise NotImplementedError() - def parse(self): # pylint: disable=no-self-use + def parse(self, config): # pylint: disable=no-self-use """Parse song. It set the following attributes: diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py index e76960ac..cd3f0d3b 100644 --- a/patacrep/songs/chordpro/__init__.py +++ b/patacrep/songs/chordpro/__init__.py @@ -1,21 +1,50 @@ +"""Chordpro parser""" -from patacrep import encoding +from jinja2 import Environment, FileSystemLoader +import pkg_resources +import os + +from patacrep import encoding, files from patacrep.songs import Song from patacrep.songs.chordpro.syntax import parse_song +from patacrep.templates import TexRenderer class ChordproSong(Song): """Chordpros song parser.""" - def parse(self): + def parse(self, config): """Parse content, and return the dictinory of song data.""" with encoding.open_read(self.fullpath, encoding=self.encoding) as song: - self.data = parse_song(song.read(), self.fullpath) - print(type(self.data), self.data) - import sys; sys.exit(1) - self.languages = self.data['@languages'] - del self.data['@languages'] - self.authors = self.data['by'] - del self.data['by'] + song = parse_song(song.read(), self.fullpath) + self.authors = song.authors + self.titles = song.titles + self.languages = song.get_directives('language') + self.data = dict([meta.as_tuple for meta in song.meta]) + self.cached = { + 'song': song, + } + #: Main language + self.language = song.get_directive('language', config['lang']) + + def tex(self, output): + context = { + 'language': self.language, + 'columns': self.cached['song'].get_directive('columns', 1), + "path": files.relpath(self.fullpath, os.path.dirname(output)), + "titles": r"\\".join(self.titles), + "authors": ", ".join(["{} {}".format(name[1], name[0]) for name in self.authors]), + "metadata": self.data, + "beginsong": self.cached['song'].meta_beginsong(), + "content": self.cached['song'].content, + } + return TexRenderer( + template="chordpro.tex", + encoding='utf8', + texenv=Environment(loader=FileSystemLoader(os.path.join( + os.path.abspath(pkg_resources.resource_filename(__name__, 'data')), + 'latex' + ))), + ).template.render(context) SONG_PARSERS = { 'sgc': ChordproSong, diff --git a/patacrep/songs/chordpro/ast.py b/patacrep/songs/chordpro/ast.py index 05affee6..47ffb8df 100644 --- a/patacrep/songs/chordpro/ast.py +++ b/patacrep/songs/chordpro/ast.py @@ -1,18 +1,37 @@ -# -*- coding: utf-8 -*- """Abstract Syntax Tree for ChordPro code.""" +# pylint: disable=too-few-public-methods + import functools +import logging +import os + +LOGGER = logging.getLogger() def _indent(string): + """Return and indented version of argument.""" return "\n".join([" {}".format(line) for line in string.split('\n')]) +#: List of properties that are to be displayed in the flow of the song (not as +#: metadata at the beginning or end of song. INLINE_PROPERTIES = { - "lilypond", + "partition", "comment", "guitar_comment", "image", } +#: List of properties that are listed in the `\beginsong` LaTeX directive. +BEGINSONG_PROPERTIES = { + "album", + "copyright", + "cov", + "vcov", + "tag", + } + +#: Some directive have alternative names. For instance `{title: Foo}` and `{t: +#: Foo}` are equivalent. DIRECTIVE_SHORTCUTS = { "t": "title", "st": "subtitle", @@ -20,25 +39,40 @@ DIRECTIVE_SHORTCUTS = { "by": "artist", "c": "comment", "gc": "guitar_comment", + "cover": "cov", + "vcover": "vcov", } def directive_name(text): - if text in DIRECTIVE_SHORTCUTS: - return DIRECTIVE_SHORTCUTS[text] - return text + """Return name of the directive, considering eventual shortcuts.""" + return DIRECTIVE_SHORTCUTS.get(text, text) class AST: + """Generic object representing elements of the song.""" + _template = None inline = False + def template(self, extension): + """Return the template to be used to render this object.""" + if self._template is None: + LOGGER.warning("No template defined for {}.".format(self.__class__)) + base = "error" + else: + base = self._template + return "content_{}.{}".format(base, extension) + class Line(AST): """A line is a sequence of (possibly truncated) words, spaces and chords.""" + _template = "line" + def __init__(self): super().__init__() self.line = [] def prepend(self, data): + """Add an object at the beginning of line.""" self.line.insert(0, data) return self @@ -46,6 +80,7 @@ class Line(AST): return "".join([str(item) for item in self.line]) def strip(self): + """Remove spaces at the beginning and end of line.""" while True: if not self.line: return self @@ -63,6 +98,7 @@ class LineElement(AST): class Word(LineElement): """A chunk of word.""" + _template = "word" def __init__(self, value): super().__init__() @@ -73,6 +109,7 @@ class Word(LineElement): class Space(LineElement): """A space between words""" + _template = "space" def __init__(self): super().__init__() @@ -83,6 +120,8 @@ class Space(LineElement): class Chord(LineElement): """A chord.""" + _template = "chord" + def __init__(self, value): super().__init__() self.value = value @@ -92,59 +131,95 @@ class Chord(LineElement): class Verse(AST): """A verse (or bridge, or chorus)""" + _template = "verse" type = "verse" inline = True - def __init__(self, block=None): + def __init__(self): super().__init__() - self.lines = [] # TODO check block + self.lines = [] def prepend(self, data): + """Add data at the beginning of verse.""" self.lines.insert(0, data) return self def __str__(self): return '{{start_of_{type}}}\n{content}\n{{end_of_{type}}}'.format( - type = self.type, - content = _indent("\n".join([str(line) for line in self.lines])), + type=self.type, + content=_indent("\n".join([str(line) for line in self.lines])), ) class Chorus(Verse): + """Chorus""" type = 'chorus' class Bridge(Verse): + """Bridge""" type = 'bridge' class Song(AST): - """A song""" - + r"""A song + + Attributes: + - content: the song content, as a list of objects `foo` such that + `foo.inline` is True. + - titles: The list of titles + - language: The language (if set), None otherwise + - authors: The list of authors + - meta_beginsong: The list of directives that are to be set in the + `\beginsong{}` LaTeX directive. + - meta: Every other metadata. + """ + + #: Some directives are added to the song using special methods. METADATA_TYPE = { "title": "add_title", "subtitle": "add_subtitle", - "language": "add_language", "artist": "add_author", + "key": "add_key", } - def __init__(self): + #: Some directives have to be processed before being considered. + PROCESS_DIRECTIVE = { + "cov": "_process_relative", + "partition": "_process_relative", + "image": "_process_relative", + } + + def __init__(self, filename): super().__init__() self.content = [] self.meta = [] self._authors = [] self._titles = [] self._subtitles = [] - self._languages = set() + self._keys = [] + self.filename = filename def add(self, data): + """Add an element to the song""" + if isinstance(data, Directive): + # Some directives are preprocessed + name = directive_name(data.keyword) + if name in self.PROCESS_DIRECTIVE: + data = getattr(self, self.PROCESS_DIRECTIVE[name])(data) + if data is None: + # New line if not (self.content and isinstance(self.content[0], Newline)): self.content.insert(0, Newline()) elif isinstance(data, Line): + # Add a new line, maybe in the current verse. if not (self.content and isinstance(self.content[0], Verse)): self.content.insert(0, Verse()) self.content[0].prepend(data.strip()) elif data.inline: + # Add an object in the content of the song. self.content.insert(0, data) elif isinstance(data, Directive): + # Add a metadata directive. Some of them are added using special + # methods listed in ``METADATA_TYPE``. name = directive_name(data.keyword) if name in self.METADATA_TYPE: getattr(self, self.METADATA_TYPE[name])(*data.as_tuple) @@ -155,12 +230,13 @@ class Song(AST): return self def str_meta(self): + """Return an iterator over *all* metadata, as strings.""" for title in self.titles: yield "{{title: {}}}".format(title) - for language in sorted(self.languages): - yield "{{language: {}}}".format(language) for author in self.authors: yield "{{by: {}}}".format(author) + for key in sorted(self.keys): + yield "{{key: {}}}".format(str(key)) for key in sorted(self.meta): yield str(key) @@ -175,32 +251,84 @@ class Song(AST): def add_title(self, __ignored, title): + """Add a title""" self._titles.insert(0, title) def add_subtitle(self, __ignored, title): + """Add a subtitle""" self._subtitles.insert(0, title) @property def titles(self): + """Return the list of titles (and subtitles).""" return self._titles + self._subtitles def add_author(self, __ignored, title): + """Add an auhor.""" self._authors.insert(0, title) @property def authors(self): + """Return the list of (raw) authors.""" return self._authors - def add_language(self, __ignored, language): - self._languages.add(language) + def get_directive(self, key, default=None): + """Return the first directive with a given key.""" + for directive in self.meta: + if directive.keyword == directive_name(key): + return directive.argument + return default + + def get_directives(self, key): + """Return the list of directives with a given key.""" + values = [] + for directive in self.meta: + if directive.keyword == directive_name(key): + values.append(directive.argument) + return values + + def add_key(self, __ignored, argument): + """Add a new {key: foo: bar} directive.""" + key, *argument = argument.split(":") + self._keys.append(Directive( + key.strip(), + ":".join(argument).strip(), + )) @property - def languages(self): - return self._languages - - + def keys(self): + """Return the list of keys. + + That is, directive that where given of the form ``{key: foo: bar}``. + """ + return self._keys + + def meta_beginsong(self): + r"""Return the meta information to be put in \beginsong.""" + for directive in BEGINSONG_PROPERTIES: + if self.get_directive(directive) is not None: + yield (directive, self.get_directive(directive)) + for (key, value) in self.keys: + yield (key, value) + + + def _process_relative(self, directive): + """Return the directive, in which the argument is given relative to file + + This argument is expected to be a path (as a string). + """ + return Directive( + directive.keyword, + os.path.join( + os.path.dirname(self.filename), + directive.argument, + ), + ) class Newline(AST): + """New line""" + _template = "newline" + def __str__(self): return "" @@ -208,22 +336,38 @@ class Newline(AST): class Directive(AST): """A directive""" - def __init__(self): + def __init__(self, keyword="", argument=None): super().__init__() - self.keyword = "" - self.argument = None + self._keyword = None + self.keyword = keyword + self.argument = argument + + @property + def _template(self): + """Name of the template to use to render this keyword. + + This only applies if ``self.inline == True`` + """ + return self.keyword @property def keyword(self): + """Keyword of the directive.""" return self._keyword @property def inline(self): + """True iff this directive is to be rendered in the flow on the song. + """ return self.keyword in INLINE_PROPERTIES @keyword.setter def keyword(self, value): - self._keyword = value.strip() + """self.keyword setter + + Replace keyword by its canonical name if it is a shortcut. + """ + self._keyword = directive_name(value.strip()) def __str__(self): if self.argument is not None: @@ -236,6 +380,7 @@ class Directive(AST): @property def as_tuple(self): + """Return the directive as a tuple.""" return (self.keyword, self.argument) def __eq__(self, other): @@ -254,6 +399,7 @@ class Tab(AST): self.content = [] def prepend(self, data): + """Add an element at the beginning of content.""" self.content.insert(0, data) return self diff --git a/patacrep/songs/chordpro/data/latex/chordpro.tex b/patacrep/songs/chordpro/data/latex/chordpro.tex new file mode 100644 index 00000000..77784a67 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/chordpro.tex @@ -0,0 +1,27 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% ((path)) + +(* if language is defined *) +\selectlanguage{((language))} +(* endif *) +\songcolumns{((metadata.columns))} + +\beginsong{((titles))}[ + by={((authors))}, + (* for (key, argument) in beginsong *) + ((key))={((argument))}, + (* endfor *) + ] + + (* if (metadata.cov is defined) or (metadata.vcov is defined) *) + \cover + (* endif *) + + (* for content in content *) + (* include content.template("tex") *) + (* endfor *) + +\endsong +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + diff --git a/patacrep/songs/chordpro/data/latex/content_chord.tex b/patacrep/songs/chordpro/data/latex/content_chord.tex new file mode 100644 index 00000000..bd2bd8b8 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_chord.tex @@ -0,0 +1 @@ +\[(( content.value ))] diff --git a/patacrep/songs/chordpro/data/latex/content_comment.tex b/patacrep/songs/chordpro/data/latex/content_comment.tex new file mode 100644 index 00000000..46fb1f24 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_comment.tex @@ -0,0 +1 @@ +\textnote{(( content.argument ))} diff --git a/patacrep/songs/chordpro/data/latex/content_error.tex b/patacrep/songs/chordpro/data/latex/content_error.tex new file mode 100644 index 00000000..a41faeb5 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_error.tex @@ -0,0 +1,3 @@ + +ERROR : Template not found for \verb+(( content.__class__))+. See the logs for details. + diff --git a/patacrep/songs/chordpro/data/latex/content_guitar_comment.tex b/patacrep/songs/chordpro/data/latex/content_guitar_comment.tex new file mode 100644 index 00000000..29c40ebd --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_guitar_comment.tex @@ -0,0 +1 @@ +\musicnote{(( content.argument ))} diff --git a/patacrep/songs/chordpro/data/latex/content_image.tex b/patacrep/songs/chordpro/data/latex/content_image.tex new file mode 100644 index 00000000..336233d5 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_image.tex @@ -0,0 +1 @@ +\image{(( content.argument ))} diff --git a/patacrep/songs/chordpro/data/latex/content_line.tex b/patacrep/songs/chordpro/data/latex/content_line.tex new file mode 100644 index 00000000..9b0c6af1 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_line.tex @@ -0,0 +1 @@ +(* for content in content.line *)(* include content.template("tex") *)(* endfor *) diff --git a/patacrep/songs/chordpro/data/latex/content_newline.tex b/patacrep/songs/chordpro/data/latex/content_newline.tex new file mode 100644 index 00000000..139597f9 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_newline.tex @@ -0,0 +1,2 @@ + + diff --git a/patacrep/songs/chordpro/data/latex/content_partition.tex b/patacrep/songs/chordpro/data/latex/content_partition.tex new file mode 100644 index 00000000..98bfed57 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_partition.tex @@ -0,0 +1 @@ +\lilypond{((content.argument))} diff --git a/patacrep/songs/chordpro/data/latex/content_space.tex b/patacrep/songs/chordpro/data/latex/content_space.tex new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_space.tex @@ -0,0 +1 @@ + diff --git a/patacrep/songs/chordpro/data/latex/content_verse.tex b/patacrep/songs/chordpro/data/latex/content_verse.tex new file mode 100644 index 00000000..4b4cecbd --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_verse.tex @@ -0,0 +1,7 @@ +\begin{(( content.type ))} + (* for content in content.lines *) + (* include content.template("tex") *) + + (* endfor *) +\end{(( content.type ))} + diff --git a/patacrep/songs/chordpro/data/latex/content_word.tex b/patacrep/songs/chordpro/data/latex/content_word.tex new file mode 100644 index 00000000..d9dd7a30 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_word.tex @@ -0,0 +1 @@ +(( content.value )) diff --git a/patacrep/songs/chordpro/lexer.py b/patacrep/songs/chordpro/lexer.py index 726c24e4..58792fa2 100644 --- a/patacrep/songs/chordpro/lexer.py +++ b/patacrep/songs/chordpro/lexer.py @@ -26,6 +26,7 @@ tokens = ( class ChordProLexer: """ChordPro Lexer class""" + # pylint: disable=too-many-public-methods tokens = tokens @@ -44,18 +45,23 @@ class ChordProLexer: t_directive_KEYWORD = r'[a-zA-Z_]+' t_directiveargument_TEXT = r'[^}]+' - def t_SOC(self, token): + @staticmethod + def t_SOC(token): r'{(soc|start_of_chorus)}' return token - def t_EOC(self, token): + + @staticmethod + def t_EOC(token): r'{(eoc|end_of_chorus)}' return token - def t_SOB(self, token): + @staticmethod + def t_SOB(token): r'{(sob|start_of_bridge)}' return token - def t_EOB(self, token): + @staticmethod + def t_EOB(token): r'{(eob|end_of_bridge)}' return token @@ -69,7 +75,8 @@ class ChordProLexer: self.lexer.pop_state() return token - def t_tablature_SPACE(self, token): + @staticmethod + def t_tablature_SPACE(token): r'[ \t]+' return token @@ -96,11 +103,11 @@ class ChordProLexer: r'[^{}\n\][\t ]+' return token - def t_LBRACKET(self, token): + def t_LBRACKET(self, __token): r'\[' self.lexer.push_state('chord') - def t_chord_RBRACKET(self, token): + def t_chord_RBRACKET(self, __token): r'\]' self.lexer.pop_state() @@ -149,6 +156,6 @@ class ChordProLexer: LOGGER.error("Illegal character '{}' in directive..".format(token.value[0])) token.lexer.skip(1) - @staticmethod - def t_directiveargument_error(token): - return t_directive_error(token) + def t_directiveargument_error(self, token): + """Manage errors""" + return self.t_directive_error(token) diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py index 32fa2c42..8a964564 100644 --- a/patacrep/songs/chordpro/syntax.py +++ b/patacrep/songs/chordpro/syntax.py @@ -6,7 +6,6 @@ import ply.yacc as yacc from patacrep.errors import SongbookError from patacrep.songs.chordpro import ast -from patacrep.songs.chordpro import ast from patacrep.songs.chordpro.lexer import tokens, ChordProLexer LOGGER = logging.getLogger() @@ -50,14 +49,13 @@ class Parser: ) ) - @staticmethod - def p_song(symbols): + def p_song(self, symbols): """song : block song | empty """ #if isinstance(symbols[1], str): if len(symbols) == 2: - symbols[0] = ast.Song() + symbols[0] = ast.Song(self.filename) else: symbols[0] = symbols[2].add(symbols[1]) @@ -249,19 +247,13 @@ class Parser: """empty :""" symbols[0] = None -def lex_song(content): - # TODO delete - lex = ChordProLexer().lexer - lex.input(content) - while 1: - tok = lex.token() - if not tok: break - print(tok) - def parse_song(content, filename=None): """Parse song and return its metadata.""" - return yacc.yacc(module=Parser(filename)).parse( - content, + return yacc.yacc( + module=Parser(filename), debug=0, + write_tables=0, + ).parse( + content, lexer=ChordProLexer().lexer, ) diff --git a/patacrep/songs/chordpro/test/__init__.py b/patacrep/songs/chordpro/test/__init__.py index e69de29b..b73d2af8 100644 --- a/patacrep/songs/chordpro/test/__init__.py +++ b/patacrep/songs/chordpro/test/__init__.py @@ -0,0 +1 @@ +"""Test for chordpro parser""" diff --git a/patacrep/songs/chordpro/test/greensleeves.txt b/patacrep/songs/chordpro/test/greensleeves.txt index 38564149..069fbba8 100644 --- a/patacrep/songs/chordpro/test/greensleeves.txt +++ b/patacrep/songs/chordpro/test/greensleeves.txt @@ -1,13 +1,14 @@ {title: Greensleeves} {title: Un autre sous-titre} {title: Un sous titre} -{language: english} {by: Traditionnel} {album: Angleterre} {columns: 2} -{cover: traditionnel} -{partition: greensleeves.ly} +{cov: DIRNAME/traditionnel} +{language: english} ======== +{partition: DIRNAME/greensleeves.ly} + {start_of_verse} A[Am]las, my love, ye [G]do me wrong To [Am]cast me oft dis[E]curteously diff --git a/patacrep/songs/chordpro/test/metadata.sgc b/patacrep/songs/chordpro/test/metadata.sgc index c892faf0..eee1bf9d 100644 --- a/patacrep/songs/chordpro/test/metadata.sgc +++ b/patacrep/songs/chordpro/test/metadata.sgc @@ -13,8 +13,8 @@ {cover: Cover} {vcover: VCover} {capo: Capo} -{foo: Foo} +{key: foo: Foo} {comment: Comment} {guitar_comment: GuitarComment} {image: Image} -{lilypond: Lilypond} +{partition: Lilypond} diff --git a/patacrep/songs/chordpro/test/metadata.txt b/patacrep/songs/chordpro/test/metadata.txt index 8d73f4b8..1ba5f5c6 100644 --- a/patacrep/songs/chordpro/test/metadata.txt +++ b/patacrep/songs/chordpro/test/metadata.txt @@ -4,18 +4,18 @@ {title: Subtitle3} {title: Subtitle4} {title: Subtitle5} -{language: english} -{language: french} {by: Author1} {by: Author2} +{key: {foo: Foo}} {album: Albom} {capo: Capo} {copyright: Copyright} -{cover: Cover} -{foo: Foo} -{vcover: VCover} +{cov: DIRNAME/Cover} +{language: english} +{language: french} +{vcov: VCover} ======== {comment: Comment} {guitar_comment: GuitarComment} -{image: Image} -{lilypond: Lilypond} +{image: DIRNAME/Image} +{partition: DIRNAME/Lilypond} diff --git a/patacrep/songs/chordpro/test/test_parser.py b/patacrep/songs/chordpro/test/test_parser.py index 85ad4108..2743522e 100644 --- a/patacrep/songs/chordpro/test/test_parser.py +++ b/patacrep/songs/chordpro/test/test_parser.py @@ -1,36 +1,56 @@ +"""Tests for the chordpro parser.""" + +# pylint: disable=too-few-public-methods + import glob import os import unittest from patacrep.songs.chordpro import syntax as chordpro -class ParserTestCase(unittest.TestCase): +class ParserTxtRenderer(unittest.TestCase): + """Test parser, and renderer as a txt file.""" + + maxDiff = None + + def __init__(self, methodname="runTest", basename=None): + super().__init__(methodname) + self.basename = basename + + def shortDescription(self): + return "Parsing file '{}.txt'.".format(self.basename) + + def runTest(self): + """Test txt output (default, debug output).""" + # pylint: disable=invalid-name + + if self.basename is None: + return + with open("{}.sgc".format(self.basename), 'r', encoding='utf8') as sourcefile: + with open("{}.txt".format(self.basename), 'r', encoding='utf8') as expectfile: + #print(os.path.basename(sourcefile.name)) + #with open("{}.txt.diff".format(self.basename), 'w', encoding='utf8') as difffile: + # difffile.write( + # str(chordpro.parse_song( + # sourcefile.read(), + # os.path.basename(sourcefile.name), + # )).strip() + # ) + # sourcefile.seek(0) + self.assertMultiLineEqual( + str(chordpro.parse_song( + sourcefile.read(), + os.path.abspath(sourcefile.name), + )).strip(), + expectfile.read().strip().replace("DIRNAME", os.path.dirname(self.basename)), + ) - def test_txt(self): - for txt in sorted(glob.glob(os.path.join( - os.path.dirname(__file__), - '*.txt', +def load_tests(__loader, tests, __pattern): + """Load several tests given test files present in the directory.""" + # Load all txt files as tests + for txt in sorted(glob.glob(os.path.join( + os.path.dirname(__file__), + '*.txt', ))): - basename = txt[:-len('.txt')] - with open("{}.sgc".format(basename), 'r', encoding='utf8') as sourcefile: - with open("{}.txt".format(basename), 'r', encoding='utf8') as expectfile: - #print(os.path.basename(sourcefile.name)) - #with open("{}.txt.diff".format(basename), 'w', encoding='utf8') as difffile: - # difffile.write( - # str(chordpro.parse_song( - # sourcefile.read(), - # os.path.basename(sourcefile.name), - # )).strip() - # ) - # sourcefile.seek(0) - self.assertMultiLineEqual( - str(chordpro.parse_song( - sourcefile.read(), - os.path.basename(sourcefile.name), - )).strip(), - expectfile.read().strip(), - ) - - def test_tex(self): - # TODO - pass + tests.addTest(ParserTxtRenderer(basename=txt[:-len('.txt')])) + return tests diff --git a/patacrep/songs/latex/__init__.py b/patacrep/songs/latex/__init__.py index eaea44bd..ce08d368 100644 --- a/patacrep/songs/latex/__init__.py +++ b/patacrep/songs/latex/__init__.py @@ -14,7 +14,7 @@ from patacrep.songs import Song class LatexSong(Song): """LaTeX song parser.""" - def parse(self): + def parse(self, __config): """Parse content, and return the dictinory of song data.""" with encoding.open_read(self.fullpath, encoding=self.encoding) as song: self.data = parse_song(song.read(), self.fullpath) diff --git a/patacrep/templates.py b/patacrep/templates.py index ed48529f..6849ad36 100644 --- a/patacrep/templates.py +++ b/patacrep/templates.py @@ -64,9 +64,29 @@ def _escape_tex(value): return newval -class TexRenderer(object): +class TexRenderer: """Render a template to a LaTeX file.""" + def __init__(self, template, texenv, encoding=None): + self.encoding = encoding + self.texenv = texenv + self.texenv.block_start_string = '(*' + self.texenv.block_end_string = '*)' + self.texenv.variable_start_string = '((' + self.texenv.variable_end_string = '))' + self.texenv.comment_start_string = '(% comment %)' + self.texenv.comment_end_string = '(% endcomment %)' + self.texenv.line_comment_prefix = '%!' + self.texenv.filters['escape_tex'] = _escape_tex + self.texenv.trim_blocks = True + self.texenv.lstrip_blocks = True + self.texenv.globals["path2posix"] = files.path2posix + self.template = self.texenv.get_template(template) + + +class TexBookRenderer(TexRenderer): + """Tex renderer for the whole songbook""" + def __init__(self, template, datadirs, lang, encoding=None): '''Start a new jinja2 environment for .tex creation. @@ -78,29 +98,15 @@ class TexRenderer(object): - encoding: if set, encoding of the template. ''' self.lang = lang - self.encoding = encoding # Load templates in filesystem ... loaders = [FileSystemLoader(os.path.join(datadir, 'templates')) for datadir in datadirs] - self.texenv = Environment( + texenv = Environment( loader=ChoiceLoader(loaders), extensions=[VariablesExtension], ) - self.texenv.block_start_string = '(*' - self.texenv.block_end_string = '*)' - self.texenv.variable_start_string = '((' - self.texenv.variable_end_string = '))' - self.texenv.comment_start_string = '(% comment %)' - self.texenv.comment_end_string = '(% endcomment %)' - self.texenv.line_comment_prefix = '%!' - self.texenv.filters['escape_tex'] = _escape_tex - self.texenv.trim_blocks = True - self.texenv.lstrip_blocks = True - - self.texenv.globals["path2posix"] = files.path2posix - try: - self.template = self.texenv.get_template(template) + super().__init__(template, texenv, encoding) except TemplateNotFound as exception: # Only works if all loaders are FileSystemLoader(). paths = [ diff --git a/patacrep/test.py b/patacrep/test.py new file mode 100644 index 00000000..ce135fef --- /dev/null +++ b/patacrep/test.py @@ -0,0 +1,26 @@ +"""Tests""" + +import doctest +import os +import unittest + +import patacrep + +def suite(): + """Return a TestSuite object, to test whole `patacrep` package. + + Both unittest and doctest are tested. + """ + test_loader = unittest.defaultTestLoader + return test_loader.discover(os.path.dirname(__file__)) + +def load_tests(__loader, tests, __pattern): + """Load tests (unittests and doctests).""" + # Loading doctests + tests.addTests(doctest.DocTestSuite(patacrep)) + + # Unittests are loaded by default + return tests + +if __name__ == "__main__": + unittest.TextTestRunner().run(suite()) diff --git a/setup.py b/setup.py index 3313371f..8752fe73 100755 --- a/setup.py +++ b/setup.py @@ -37,5 +37,6 @@ setup( "Programming Language :: Python :: 3.4", "Topic :: Utilities", ], - platforms=["GNU/Linux", "Windows", "MacOsX"] + platforms=["GNU/Linux", "Windows", "MacOsX"], + test_suite="patacrep.test.suite", ) From 37dbc0405bbdff73c559d6c0fa5098aa0fea3206 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 29 Jan 2015 15:34:18 +0100 Subject: [PATCH 10/21] Deleting useless comments --- patacrep/songs/chordpro/syntax.py | 42 ------------------------------- 1 file changed, 42 deletions(-) diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py index 8a964564..68afc738 100644 --- a/patacrep/songs/chordpro/syntax.py +++ b/patacrep/songs/chordpro/syntax.py @@ -59,16 +59,6 @@ class Parser: else: symbols[0] = symbols[2].add(symbols[1]) - #@staticmethod - #def p_song_next(symbols): - # """song_next : block song_next - # | empty - # """ - # if len(symbols) == 2: - # symbols[0] = ast.Song() - # else: - # symbols[0] = symbols[2].add(symbols[1]) - @staticmethod def p_block(symbols): """block : SPACE block @@ -93,12 +83,6 @@ class Parser: """ symbols[0] = None - #@staticmethod - #def p_newlines(symbols): - # """newlines : NEWLINE newlines - # | empty""" - # symbols[0] = ('newlines') - @staticmethod def p_directive(symbols): """directive : LBRACE KEYWORD directive_next RBRACE @@ -157,22 +141,6 @@ class Parser: """chord : CHORD""" symbols[0] = ast.Chord(symbols[1]) - #@staticmethod - #def p_verse(symbols): - # """verse : line NEWLINE verse_next - # """ - # symbols[0] = symbols[3].prepend(symbols[1]) - - #@staticmethod - #def p_verse_next(symbols): - # """verse_next : line NEWLINE verse_next - # | empty - # """ - # if len(symbols) == 2: - # symbols[0] = ast.Verse() - # else: - # symbols[0] = symbols[3].prepend(symbols[1]) - @staticmethod def p_chorus(symbols): """chorus : SOC maybespace NEWLINE chorus_content EOC maybespace @@ -212,16 +180,6 @@ class Parser: symbols[0] = symbols[3].prepend(symbols[1]) - #@staticmethod - #def p_bridge_next(symbols): - # """bridge_next : line NEWLINE bridge_next - # | empty - # """ - # if len(symbols) == 2: - # symbols[0] = ast.Bridge() - # else: - # symbols[0] = symbols[3].prepend(symbols[1]) - @staticmethod def p_tab(symbols): """tab : SOT maybespace NEWLINE tab_content EOT maybespace From e6b4f5db9ab49644d6ef937c554b6fe71f4e147d Mon Sep 17 00:00:00 2001 From: Luthaf Date: Mon, 6 Oct 2014 22:30:46 +0200 Subject: [PATCH 11/21] First minimalist chordpro lexer --- patacrep/chordpro/__init__.py | 0 patacrep/chordpro/lexer.py | 59 +++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 patacrep/chordpro/__init__.py create mode 100644 patacrep/chordpro/lexer.py diff --git a/patacrep/chordpro/__init__.py b/patacrep/chordpro/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patacrep/chordpro/lexer.py b/patacrep/chordpro/lexer.py new file mode 100644 index 00000000..860e16d5 --- /dev/null +++ b/patacrep/chordpro/lexer.py @@ -0,0 +1,59 @@ +"""ChordPro lexer""" + +import logging +import ply.lex as lex + +LOGGER = logging.getLogger() + +#pylint: disable=invalid-name +tokens = ( + 'LBRACKET', + 'RBRACKET', + 'LBRACE', + 'RBRACE', + 'NEWLINE', + 'COLON', + 'WORD', + 'SPACE', + 'NUMBER' +) + +class ChordProLexer: + """ChordPro Lexer class""" + + tokens = tokens + + t_LBRACKET = r'\[' + t_RBRACKET = r'\]' + t_LBRACE = r'{' + t_RBRACE = r'}' + t_SPACE = r'[ \t]+' + t_COLON = r':' + t_WORD = r'[a-zA-Z_]+' #TODO: handle unicode + + 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\r]' + token.lexer.lineno += 1 + return token + + @staticmethod + def t_comment(token): + r'\#.*' + pass + + @staticmethod + def t_NUMBER(token): + r'[0-9]+' + token.value = int(token.value) + return token + + @staticmethod + def t_error(token): + """Manage errors""" + LOGGER.error("Illegal character '{}'".format(token.value[0])) + token.lexer.skip(1) \ No newline at end of file From a2fcfa488d3360d845c1d144f1502ed8cec26c33 Mon Sep 17 00:00:00 2001 From: Luthaf Date: Mon, 6 Oct 2014 22:31:05 +0200 Subject: [PATCH 12/21] ChordPro example file --- patacrep/data/examples/songs/greensleeves.sgc | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 patacrep/data/examples/songs/greensleeves.sgc diff --git a/patacrep/data/examples/songs/greensleeves.sgc b/patacrep/data/examples/songs/greensleeves.sgc new file mode 100644 index 00000000..7d6b1000 --- /dev/null +++ b/patacrep/data/examples/songs/greensleeves.sgc @@ -0,0 +1,42 @@ +{language : english} +{columns : 2} +{ title : Greensleeves} +{artist: Traditionnel} +{cover : traditionnel } +{album :Angleterre} + +{partition : greensleeves.ly} + + +A[Am]las, my love, ye [G]do me wrong +To [Am]cast me oft dis[E]curteously +And [Am]{I have} loved [G]you so long +De[Am]lighting [E]in your [Am]companie + +{start_of_chorus} + [C]Greensleeves was [G]all my joy + [Am]Greensleeves was [E]my delight + [C]Greensleeves was my [G]heart of gold + And [Am]who but [E]Ladie [Am]Greensleeves +{end_of_chorus} + +I [Am]have been ready [G]at your hand +To [Am]grant what ever [E]you would crave +I [Am]have both waged [G]life and land +Your [Am]love and [E]good will [Am]for to have + +I [Am]bought thee kerchers [G]to thy head +That [Am]were wrought fine and [E]gallantly +I [Am]kept thee both at [G]boord and bed +Which [Am]cost my [E]purse well [Am]favouredly + +I [Am]bought thee peticotes [G]of the best +The [Am]cloth so fine as [E]fine might be +I [Am]gave thee jewels [G]for thy chest +And [Am]all this [E]cost I [Am]spent on thee + + +Thy [Am]smock of silke, both [G]faire and white +With [Am]gold embrodered [E]gorgeously +Thy [Am]peticote of [G]sendall right +And [Am]this I [E]bought thee [Am]gladly \ No newline at end of file From e7ea9e07d1fad0ffcfcf1c138f99b4f17593ed34 Mon Sep 17 00:00:00 2001 From: Luthaf Date: Mon, 19 Jan 2015 21:16:10 +0100 Subject: [PATCH 13/21] [WIP] Chordpro format --- patacrep/chordpro/ast.py | 71 ++++++++++++++++++++++++++++++ patacrep/chordpro/lexer.py | 10 +++-- patacrep/chordpro/parser.py | 88 +++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 patacrep/chordpro/ast.py create mode 100644 patacrep/chordpro/parser.py diff --git a/patacrep/chordpro/ast.py b/patacrep/chordpro/ast.py new file mode 100644 index 00000000..5eec6b1e --- /dev/null +++ b/patacrep/chordpro/ast.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +"""Abstract Syntax Tree for ChordPro code.""" + +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): + """ChordPro 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 SongPart(AST): + """ChordPro start_of/end_of command + + {start_of_chorus}, {end_of_tab}, {eov} ... + """ + + class Type: + CHORUS = ("chorus", + "start_of_chorus", "end_of_chorus", + "soc", "eoc") + VERSE = ("verse", + "start_of_verse", "end_of_verse", + "sov", "eov") + BRIDGE = ("bridge", + "start_of_bridge", "end_of_bridge", + "sob", "eob") + TAB = ("tab", + "start_of_tab", "end_of_tab", + "sot", "eot") + + def __init__(self, name): + if "_" in name: + self.init_long_form(name) + else: + self.init_short_form(name) + + def __str__(self): + return self.name + + def init_short_form(self, name): + self.type = "" + + def init_long_form(self, name): + self.type = "" + + diff --git a/patacrep/chordpro/lexer.py b/patacrep/chordpro/lexer.py index 860e16d5..791d590e 100644 --- a/patacrep/chordpro/lexer.py +++ b/patacrep/chordpro/lexer.py @@ -29,7 +29,6 @@ class ChordProLexer: t_RBRACE = r'}' t_SPACE = r'[ \t]+' t_COLON = r':' - t_WORD = r'[a-zA-Z_]+' #TODO: handle unicode def __init__(self): self.__class__.lexer = lex.lex(module=self) @@ -45,7 +44,12 @@ class ChordProLexer: def t_comment(token): r'\#.*' pass - + + @staticmethod + def t_WORD(token): + r'[a-zA-Z_]+[.,;:!?]?' + return token + @staticmethod def t_NUMBER(token): r'[0-9]+' @@ -56,4 +60,4 @@ class ChordProLexer: def t_error(token): """Manage errors""" LOGGER.error("Illegal character '{}'".format(token.value[0])) - token.lexer.skip(1) \ No newline at end of file + token.lexer.skip(1) diff --git a/patacrep/chordpro/parser.py b/patacrep/chordpro/parser.py new file mode 100644 index 00000000..8c66af28 --- /dev/null +++ b/patacrep/chordpro/parser.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +"""ChordPro parser""" + +import logging +import ply.yacc as yacc + +from patacrep.chordpro.lexer import tokens, ChordProLexer +from patacrep.chordpro import ast +from patacrep.errors import SongbookError + +LOGGER = logging.getLogger() + +class ParsingError(SongbookError): + """Parsing error.""" + + def __init__(self, message): + super().__init__(self) + self.message = message + + def __str__(self): + return self.message + + +class Parser: + """ChordPro parser class""" + + def __init__(self, filename=None): + self.tokens = tokens + 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 {}:{}.".format( + str(self.filename), + token.lineno, + self.__find_column(token), + ) + ) + + @staticmethod + def p_expression(symbols): + """expression : brackets expression + | braces expression + | command expression + | NEWLINE 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 COLON expression RBRACE""" + symbols[0] = symbols[2] + + +def parsesong(string, filename=None): + """Parse song and return its metadata.""" + return yacc.yacc(module=Parser(filename)).parse( + string, + lexer=ChordProLexer().lexer, + ) From ca8580d9427ed17fc36f7b6114c361dfd42f5114 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 23 Jan 2015 14:21:09 +0100 Subject: [PATCH 14/21] Logging: better handling of newlines --- patacrep/build.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/patacrep/build.py b/patacrep/build.py index baf5626b..facf51e8 100644 --- a/patacrep/build.py +++ b/patacrep/build.py @@ -225,8 +225,9 @@ class SongbookBuilder(object): stdin=PIPE, stdout=PIPE, stderr=PIPE, + env=os.environ, universal_newlines=True, - env=os.environ) + ) except Exception as error: LOGGER.debug(error) raise errors.LatexCompilationError(self.basename) From eefcbd2bd2626a18773fa5e9d2488b28d238b83b Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 23 Jan 2015 19:28:09 +0100 Subject: [PATCH 15/21] =?UTF-8?q?R=C3=A9organisation=20en=20pr=C3=A9vision?= =?UTF-8?q?=20du=20support=20des=20fichiers=20chordpro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref: #64 --- patacrep/chordpro/__init__.py | 0 patacrep/content/__init__.py | 3 +- patacrep/content/song.py | 61 +++++++++++---- patacrep/content/sorted.py | 3 +- patacrep/files.py | 98 ++++++++++++------------- patacrep/latex/__init__.py | 20 +---- patacrep/latex/syntax.py | 25 +++++-- patacrep/songs/__init__.py | 73 ++++++------------ patacrep/songs/chordpro/__init__.py | 9 +++ patacrep/{ => songs}/chordpro/ast.py | 0 patacrep/{ => songs}/chordpro/lexer.py | 0 patacrep/{ => songs}/chordpro/parser.py | 4 +- patacrep/songs/latex/__init__.py | 32 ++++++++ 13 files changed, 183 insertions(+), 145 deletions(-) delete mode 100644 patacrep/chordpro/__init__.py create mode 100644 patacrep/songs/chordpro/__init__.py rename patacrep/{ => songs}/chordpro/ast.py (100%) rename patacrep/{ => songs}/chordpro/lexer.py (100%) rename patacrep/{ => songs}/chordpro/parser.py (95%) create mode 100644 patacrep/songs/latex/__init__.py diff --git a/patacrep/chordpro/__init__.py b/patacrep/chordpro/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/patacrep/content/__init__.py b/patacrep/content/__init__.py index 07441c85..e00a57a5 100755 --- a/patacrep/content/__init__.py +++ b/patacrep/content/__init__.py @@ -173,8 +173,7 @@ def process_content(content, config=None): included in the .tex file. """ contentlist = [] - plugins = config.get('_content_plugins', {}) - + plugins = files.load_plugins(config, ["content"], "CONTENT_PLUGINS") keyword_re = re.compile(r'^ *(?P\w*) *(\((?P.*)\))? *$') if not content: content = [["song"]] diff --git a/patacrep/content/song.py b/patacrep/content/song.py index 252c5dc8..889c69ce 100755 --- a/patacrep/content/song.py +++ b/patacrep/content/song.py @@ -6,10 +6,35 @@ import os from patacrep.content import process_content, ContentError from patacrep import files, errors -from patacrep.songs import Song LOGGER = logging.getLogger(__name__) +class SongRenderer(Content): + """Render a song in as a tex code.""" + + def __init__(self, song): + super().__init__() + self.song = song + + def begin_new_block(self, previous, __context): + """Return a boolean stating if a new block is to be created.""" + return not isinstance(previous, SongRenderer) + + def begin_block(self, context): + """Return the string to begin a block.""" + indexes = context.resolve("indexes") + if isinstance(indexes, jinja2.runtime.Undefined): + indexes = "" + return r'\begin{songs}{%s}' % indexes + + def end_block(self, __context): + """Return the string to end a block.""" + return r'\end{songs}' + + def render(self, context): + """Return the string that will render the song.""" + return self.song.tex(output=context['filename']) + #pylint: disable=unused-argument def parse(keyword, argument, contentlist, config): """Parse data associated with keyword 'song'. @@ -23,6 +48,7 @@ def parse(keyword, argument, contentlist, config): Return a list of Song() instances. """ + plugins = files.load_plugins(config, ["songs"], "SONG_PARSERS") if '_languages' not in config: config['_languages'] = set() songlist = [] @@ -30,8 +56,11 @@ def parse(keyword, argument, contentlist, config): for songdir in config['_songdir']: if contentlist: break - contentlist = files.recursive_find(songdir.fullpath, plugins.keys()) - + contentlist = [ + filename + for filename + in files.recursive_find(songdir.fullpath, plugins.keys()) + ] for elem in contentlist: before = len(songlist) for songdir in config['_songdir']: @@ -39,18 +68,24 @@ def parse(keyword, argument, contentlist, config): continue with files.chdir(songdir.datadir): for filename in glob.iglob(os.path.join(songdir.subpath, elem)): - LOGGER.debug('Parsing file "{}"…'.format(filename)) - try: - renderer = plugins[filename.split('.')[-1]] - except KeyError: + extension = filename.split(".")[-1] + if extension not in plugins: LOGGER.warning(( - 'I do not know how to parse file "{}". Ignored.' - ).format(os.path.join(songdir.datadir, filename)) + 'File "{}" does not end with one of {}. Ignored.' + ).format( + os.path.join(songdir.datadir, filename), + ", ".join(["'.{}'".format(key) for key in plugins.keys()]), + ) ) continue - song = renderer(songdir.datadir, filename, config) - songlist.append(song) - config["_languages"].update(song.languages) + LOGGER.debug('Parsing file "{}"…'.format(filename)) + renderer = SongRenderer(plugins[extension]( + songdir.datadir, + filename, + config, + )) + songlist.append(renderer) + config["_languages"].update(renderer.song.languages) if len(songlist) > before: break if len(songlist) == before: @@ -69,8 +104,8 @@ CONTENT_PLUGINS = {'song': parse} class OnlySongsError(ContentError): "A list that should contain only songs also contain other type of content." def __init__(self, not_songs): - super(OnlySongsError, self).__init__() self.not_songs = not_songs + super().__init__('song', str(self)) def __str__(self): return ( diff --git a/patacrep/content/sorted.py b/patacrep/content/sorted.py index 57fa4f8d..b3bbe40a 100755 --- a/patacrep/content/sorted.py +++ b/patacrep/content/sorted.py @@ -40,8 +40,9 @@ def key_generator(sort): - sort: the list of keys used to sort. """ - def ordered_song_keys(song): + def ordered_song_keys(songrenderer): """Return the list of values used to sort the song.""" + song = songrenderer.song songkey = [] for key in sort: if key == "@title": diff --git a/patacrep/files.py b/patacrep/files.py index 23ca0920..01f17e39 100644 --- a/patacrep/files.py +++ b/patacrep/files.py @@ -1,7 +1,7 @@ """File system utilities.""" from contextlib import contextmanager -import glob +import fnmatch import importlib import logging import os @@ -11,14 +11,10 @@ import sys LOGGER = logging.getLogger(__name__) -def recursive_find(root_directory, extensions): - """Recursively find files with some extension, from a root_directory. +def recursive_find(root_directory, patterns): + """Recursively find files matching one of the patterns, from a root_directory. - Return a list of files matching those conditions. - - Arguments: - - `extensions`: list of accepted extensions. - - `root_directory`: root directory of the search. + Return a list of files matching one of the patterns. """ if not os.path.isdir(root_directory): return [] @@ -27,8 +23,11 @@ def recursive_find(root_directory, extensions): pattern = re.compile(r'.*\.({})$'.format('|'.join(extensions))) with chdir(root_directory): for root, __ignored, filenames in os.walk(os.curdir): - for filename in filenames: - if pattern.match(filename): + for pattern in patterns: + for filename in fnmatch.filter( + filenames, + "*.{}".format(pattern), + ): matches.append(os.path.join(root, filename)) return matches @@ -71,29 +70,31 @@ def chdir(path): else: yield -def load_plugins(datadirs, subdir, variable, error): - """Load all content plugins, and return a dictionary of those plugins. - - A plugin is a .py file, submodule of `subdir`, located in one of the - directories of `datadirs`. It contains a dictionary `variable`. The return - value is the union of the dictionaries of the loaded plugins. +def load_plugins(config, root_modules, keyword): + """Load all plugins, and return a dictionary of those plugins. Arguments: - - datadirs: list of directories (as strings) in which files has to be - searched. - - subdir: modules (as a list of strings) files has to be submodules of - (e.g. if `subdir` is `['first', 'second']`, search files are of the form - `first/second/*.py`. - - variable: Name of the variable holding the dictionary. - - error: Error message raised if a key appears several times. + - config: the configuration dictionary of the songbook + - root_modules: the submodule in which plugins are to be searched, as a + list of modules (e.g. ["some", "deep", "module"] for + "some.deep.module"). + - keyword: attribute containing plugin information. + + Return value: a dictionary where: + - keys are the keywords ; + - values are functions triggered when this keyword is met. """ + # pylint: disable=star-args plugins = {} directory_list = ( [ - os.path.join(datadir, "python", *subdir) #pylint: disable=star-args - for datadir in datadirs + os.path.join(datadir, "python", *root_modules) + for datadir in config.get('datadir', []) ] - + [os.path.dirname(__file__)] + + [os.path.join( + os.path.dirname(__file__), + *root_modules + )] ) for directory in directory_list: if not os.path.exists(directory): @@ -102,31 +103,26 @@ def load_plugins(datadirs, subdir, variable, error): directory ) continue - sys.path.append(directory) - for name in glob.glob(os.path.join(directory, *(subdir + ['*.py']))): - if name.endswith(".py") and os.path.basename(name) != "__init__.py": - if directory == os.path.dirname(__file__): - plugin = importlib.import_module( - 'patacrep.{}.{}'.format( - ".".join(subdir), - os.path.basename(name[:-len('.py')]) - ) - ) + for (dirpath, __ignored, filenames) in os.walk(directory): + modules = ["patacrep"] + root_modules + if os.path.relpath(dirpath, directory) != ".": + modules.extend(os.path.relpath(dirpath, directory).split("/")) + for name in filenames: + if name == "__init__.py": + modulename = [] + elif name.endswith(".py"): + modulename = [name[:-len('.py')]] else: - plugin = importlib.import_module( - os.path.basename(name[:-len('.py')]) + continue + plugin = importlib.import_module(".".join(modules + modulename)) + if hasattr(plugin, keyword): + for (key, value) in getattr(plugin, keyword).items(): + if key in plugins: + LOGGER.warning( + "File %s: Keyword '%s' is already used. Ignored.", + relpath(os.path.join(dirpath, name)), + key, ) - for (key, value) in getattr(plugin, variable, {}).items(): - if key in plugins: - LOGGER.warning( - error.format( - filename=relpath(name), - key=key, - ) - ) - continue - plugins[key] = value - del sys.path[-1] + continue + plugins[key] = value return plugins - - diff --git a/patacrep/latex/__init__.py b/patacrep/latex/__init__.py index 140fa51e..90c4e798 100644 --- a/patacrep/latex/__init__.py +++ b/patacrep/latex/__init__.py @@ -1,19 +1,3 @@ -"""Very simple LaTeX parser +"""Dumb and very very incomplete 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 +from patacrep.latex.syntax import tex2plain, parse_song diff --git a/patacrep/latex/syntax.py b/patacrep/latex/syntax.py index 896d8442..edd759cc 100644 --- a/patacrep/latex/syntax.py +++ b/patacrep/latex/syntax.py @@ -245,12 +245,21 @@ def tex2plain(string): ) ) -def parsesong(string, filename=None): - """Parse song and return its metadata.""" - return detex( - silent_yacc(module=Parser(filename)).parse( - string, - lexer=SongLexer().lexer, - ).metadata - ) +def parse_song(content, filename=None): + """Parse some LaTeX code, expected to be a song. + Arguments: + - content: the code to parse. + - filename: the name of file where content was read from. Used only to + display error messages. + """ + return detex( + yacc.yacc( + module=Parser(filename), + write_tables=0, + debug=0, + ).parse( + content, + lexer=SongLexer().lexer, + ).metadata + ) diff --git a/patacrep/songs/__init__.py b/patacrep/songs/__init__.py index df5514bb..e8931d34 100644 --- a/patacrep/songs/__init__.py +++ b/patacrep/songs/__init__.py @@ -9,7 +9,7 @@ import pickle import re from patacrep.authors import processauthors -from patacrep.content import Content +from patacrep import files, encoding LOGGER = logging.getLogger(__name__) @@ -95,6 +95,12 @@ class Song(Content): "_version", ] + # Default data + DEFAULT_DATA = { + '@titles': [], + '@languages': [], + } + def __init__(self, datadir, subpath, config): self.fullpath = os.path.join(datadir, subpath) self.datadir = datadir @@ -123,14 +129,14 @@ class Song(Content): self.fullpath )) - # Default values - self.data = {} - self.titles = [] - self.languages = [] - self.authors = [] - - # Parsing and data processing - self.parse() + # Data extraction from the latex song + self.data = self.DEFAULT_DATA + self.data['@path'] = self.fullpath + self.data.update(self.parse( + encoding.open_read(self.fullpath).read() + )) + self.titles = self.data['@titles'] + self.languages = self.data['@languages'] self.datadir = datadir self.unprefixed_titles = [ unprefixed_title( @@ -169,49 +175,17 @@ class Song(Content): def __repr__(self): return repr((self.titles, self.data, self.fullpath)) - def begin_new_block(self, previous, __context): - """Return a boolean stating if a new block is to be created.""" - return not isinstance(previous, Song) - - def begin_block(self, context): - """Return the string to begin a block.""" - indexes = context.resolve("indexes") - if isinstance(indexes, jinja2.runtime.Undefined): - indexes = "" - return r'\begin{songs}{%s}' % indexes - - def end_block(self, __context): - """Return the string to end a block.""" - return r'\end{songs}' - - def render(self, __context): - """Returns the TeX code rendering the song. + def tex(self, output): # pylint: disable=no-self-use, unused-argument + """Return the LaTeX code rendering this song. - This function is to be defined by subclasses. + Arguments: + - output: Name of the output file. """ - return '' + return NotImplementedError() - def parse(self): - """Parse file `self.fullpath`. - - This function is to be defined by subclasses. - - It set the following attributes: - - - titles: the list of (raw) titles. This list will be processed to - remove prefixes. - - languages: the list of languages used in the song, as languages - recognized by the LaTeX babel package. - - authors: the list of (raw) authors. This list will be processed to - 'clean' it (see function :func:`patacrep.authors.processauthors`). - - data: song metadata. Used (among others) to sort the songs. - - cached: additional data that will be cached. Thus, data stored in - this attribute must be picklable. - """ - self.data = {} - self.titles = [] - self.languages = [] - self.authors = [] + def parse(self, content): # pylint: disable=no-self-use, unused-argument + """Parse song, and return a dictionary of its data.""" + return NotImplementedError() def unprefixed_title(title, prefixes): """Remove the first prefix of the list in the beginning of title (if any). @@ -221,4 +195,3 @@ def unprefixed_title(title, prefixes): if match: return match.group(2) return title - diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py new file mode 100644 index 00000000..bd3b6f3f --- /dev/null +++ b/patacrep/songs/chordpro/__init__.py @@ -0,0 +1,9 @@ + +from patacrep.songs import Song + +class ChordproSong(Song): + pass + +SONG_PARSERS = { + 'sgc': ChordproSong, + } diff --git a/patacrep/chordpro/ast.py b/patacrep/songs/chordpro/ast.py similarity index 100% rename from patacrep/chordpro/ast.py rename to patacrep/songs/chordpro/ast.py diff --git a/patacrep/chordpro/lexer.py b/patacrep/songs/chordpro/lexer.py similarity index 100% rename from patacrep/chordpro/lexer.py rename to patacrep/songs/chordpro/lexer.py diff --git a/patacrep/chordpro/parser.py b/patacrep/songs/chordpro/parser.py similarity index 95% rename from patacrep/chordpro/parser.py rename to patacrep/songs/chordpro/parser.py index 8c66af28..9fa75908 100644 --- a/patacrep/chordpro/parser.py +++ b/patacrep/songs/chordpro/parser.py @@ -4,8 +4,8 @@ import logging import ply.yacc as yacc -from patacrep.chordpro.lexer import tokens, ChordProLexer -from patacrep.chordpro import ast +from patacrep.songs.chordpro.lexer import tokens, ChordProLexer +from patacrep.songs.chordpro import ast from patacrep.errors import SongbookError LOGGER = logging.getLogger() diff --git a/patacrep/songs/latex/__init__.py b/patacrep/songs/latex/__init__.py new file mode 100644 index 00000000..2de16338 --- /dev/null +++ b/patacrep/songs/latex/__init__.py @@ -0,0 +1,32 @@ +"""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. +""" + +import os + +from patacrep import files +from patacrep.latex import parse_song +from patacrep.songs import Song + +class LatexSong(Song): + """LaTeX song parser.""" + + def parse(self, content): + """Parse content, and return the dictinory of song data.""" + return parse_song(content, self.fullpath) + + def tex(self, output): + """Return the LaTeX code rendering the song.""" + return r'\input{{{}}}'.format(files.path2posix( + files.relpath( + self.fullpath, + os.path.dirname(output) + ))) + +SONG_PARSERS = { + 'is': LatexSong, + 'sg': LatexSong, + } From 5d0d0d70002dbecf93f12399716bd686fc57e3cc Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 23 Jan 2015 23:23:52 +0100 Subject: [PATCH 16/21] =?UTF-8?q?[chordpro][WIP]=20Les=20lignes=20sont=20c?= =?UTF-8?q?orrectement=20analys=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patacrep/songs/chordpro/__init__.py | 17 +++- patacrep/songs/chordpro/lexer.py | 46 +++++++--- patacrep/songs/chordpro/parser.py | 88 ------------------ patacrep/songs/chordpro/syntax.py | 136 ++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 103 deletions(-) delete mode 100644 patacrep/songs/chordpro/parser.py create mode 100644 patacrep/songs/chordpro/syntax.py diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py index bd3b6f3f..baddb158 100644 --- a/patacrep/songs/chordpro/__init__.py +++ b/patacrep/songs/chordpro/__init__.py @@ -1,8 +1,23 @@ +from patacrep import encoding from patacrep.songs import Song +from patacrep.songs.chordpro.syntax import parse_song class ChordproSong(Song): - pass + """Chordpros song parser.""" + + def parse(self): + """Parse content, and return the dictinory of song data.""" + with encoding.open_read(self.fullpath, encoding=self.encoding) as song: + self.data = parse_song(song.read(), self.fullpath) + print(self.data) + import sys; sys.exit(1) + self.titles = self.data['@titles'] + del self.data['@titles'] + self.languages = self.data['@languages'] + del self.data['@languages'] + self.authors = self.data['by'] + del self.data['by'] SONG_PARSERS = { 'sgc': ChordproSong, diff --git a/patacrep/songs/chordpro/lexer.py b/patacrep/songs/chordpro/lexer.py index 791d590e..f33d8f2e 100644 --- a/patacrep/songs/chordpro/lexer.py +++ b/patacrep/songs/chordpro/lexer.py @@ -7,15 +7,16 @@ LOGGER = logging.getLogger() #pylint: disable=invalid-name tokens = ( - 'LBRACKET', - 'RBRACKET', + #'LBRACKET', + #'RBRACKET', + 'CHORD', 'LBRACE', 'RBRACE', 'NEWLINE', - 'COLON', + #'COLON', 'WORD', 'SPACE', - 'NUMBER' + #'NUMBER', ) class ChordProLexer: @@ -23,12 +24,15 @@ class ChordProLexer: tokens = tokens - t_LBRACKET = r'\[' - t_RBRACKET = r'\]' + states = ( + ('chord', 'exclusive'), + ) + t_LBRACE = r'{' t_RBRACE = r'}' t_SPACE = r'[ \t]+' - t_COLON = r':' + #t_COLON = r':' + t_chord_CHORD = r'[A-G7#m]+' # TODO This can be refined def __init__(self): self.__class__.lexer = lex.lex(module=self) @@ -41,23 +45,37 @@ class ChordProLexer: return token @staticmethod - def t_comment(token): + def t_COMMENT(token): r'\#.*' pass @staticmethod def t_WORD(token): - r'[a-zA-Z_]+[.,;:!?]?' + r'[^\n\][\t ]+' return token - @staticmethod - def t_NUMBER(token): - r'[0-9]+' - token.value = int(token.value) - return token + def t_LBRACKET(self, token): + r'\[' + self.lexer.push_state('chord') + + def t_chord_RBRACKET(self, token): + r'\]' + self.lexer.pop_state() + + #@staticmethod + #def t_NUMBER(token): + # r'[0-9]+' + # token.value = int(token.value) + # return token @staticmethod def t_error(token): """Manage errors""" LOGGER.error("Illegal character '{}'".format(token.value[0])) token.lexer.skip(1) + + @staticmethod + def t_chord_error(token): + """Manage errors""" + LOGGER.error("Illegal character '{}' in chord..".format(token.value[0])) + token.lexer.skip(1) diff --git a/patacrep/songs/chordpro/parser.py b/patacrep/songs/chordpro/parser.py deleted file mode 100644 index 9fa75908..00000000 --- a/patacrep/songs/chordpro/parser.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -"""ChordPro parser""" - -import logging -import ply.yacc as yacc - -from patacrep.songs.chordpro.lexer import tokens, ChordProLexer -from patacrep.songs.chordpro import ast -from patacrep.errors import SongbookError - -LOGGER = logging.getLogger() - -class ParsingError(SongbookError): - """Parsing error.""" - - def __init__(self, message): - super().__init__(self) - self.message = message - - def __str__(self): - return self.message - - -class Parser: - """ChordPro parser class""" - - def __init__(self, filename=None): - self.tokens = tokens - 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 {}:{}.".format( - str(self.filename), - token.lineno, - self.__find_column(token), - ) - ) - - @staticmethod - def p_expression(symbols): - """expression : brackets expression - | braces expression - | command expression - | NEWLINE 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 COLON expression RBRACE""" - symbols[0] = symbols[2] - - -def parsesong(string, filename=None): - """Parse song and return its metadata.""" - return yacc.yacc(module=Parser(filename)).parse( - string, - lexer=ChordProLexer().lexer, - ) diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py new file mode 100644 index 00000000..91470ba5 --- /dev/null +++ b/patacrep/songs/chordpro/syntax.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +"""ChordPro parser""" + +import logging +import ply.yacc as yacc + +from patacrep.songs.chordpro.lexer import tokens, ChordProLexer +from patacrep.songs.chordpro import ast +from patacrep.errors import SongbookError + +LOGGER = logging.getLogger() + +class ParsingError(SongbookError): + """Parsing error.""" + + def __init__(self, message): + super().__init__(self) + self.message = message + + def __str__(self): + return self.message + + +class Parser: + """ChordPro parser class""" + + def __init__(self, filename=None): + self.tokens = tokens + 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.""" + if token: # TODO remove this test + LOGGER.error("Error in file {}, line {}:{}.".format( + str(self.filename), + token.lineno, + self.__find_column(token), + ) + ) + + @staticmethod + def p_song(symbols): + """song : block song + | empty + """ + if len(symbols) == 2: + symbols[0] = ('song') + else: + symbols[0] = ('song', symbols[1], symbols[2]) + + @staticmethod + def p_block(symbols): + """block : directive NEWLINE newlines + | stanza NEWLINE newlines + """ + symbols[0] = ('block', symbols[1]) + + @staticmethod + def p_newlines(symbols): + """newlines : NEWLINE newlines + | empty""" + symbols[0] = ('newlines') + + @staticmethod + def p_directive(symbols): + """directive : LBRACE WORD RBRACE""" + symbols[0] = ('directive', symbols[1]) + + @staticmethod + def p_line(symbols): + """line : WORD line_next + | CHORD line_next + | SPACE line_next + """ + symbols[0] = ('line', symbols[1], symbols[2]) + + @staticmethod + def p_line_next(symbols): + """line_next : WORD line_next + | SPACE line_next + | CHORD line_next + | empty + """ + if len(symbols) == 2: + symbols[0] = ('line-next') + else: + symbols[0] = ('line-next', symbols[1], symbols[2]) + + @staticmethod + def p_stanza(symbols): + """stanza : line NEWLINE stanza_next + """ + symbols[0] = ('stanza', symbols[1], symbols[3]) + + @staticmethod + def p_stanza_next(symbols): + """stanza_next : line NEWLINE stanza_next + | empty + """ + if len(symbols) == 2: + symbols[0] = ('stanza-next') + else: + symbols[0] = ('stanza-next', symbols[1], symbols[3]) + + #@staticmethod + #def p_braces(symbols): + # """braces : LBRACE expression COLON expression RBRACE""" + # symbols[0] = symbols[2] + + @staticmethod + def p_empty(symbols): + """empty :""" + symbols[0] = None + + #@staticmethod + #def p_comment(symbols): + # """comment : COMMENT""" + # symbols[0] = ('comment', symbols[1]) + + + +def parse_song(content, filename=None): + """Parse song and return its metadata.""" + return yacc.yacc(module=Parser(filename)).parse( + content, + lexer=ChordProLexer().lexer, + ) From 2039001dd1b5e30c52e5482d74f20a7f9e1992b5 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 24 Jan 2015 14:36:01 +0100 Subject: [PATCH 17/21] Parsing works on tests --- patacrep/songs/chordpro/__init__.py | 4 +- patacrep/songs/chordpro/ast.py | 293 +++++++++++++++--- patacrep/songs/chordpro/lexer.py | 111 +++++-- patacrep/songs/chordpro/syntax.py | 211 ++++++++++--- patacrep/songs/chordpro/test/00.sgc | 0 patacrep/songs/chordpro/test/00.txt | 1 + patacrep/songs/chordpro/test/01.sgc | 1 + patacrep/songs/chordpro/test/01.txt | 4 + patacrep/songs/chordpro/test/02.sgc | 1 + patacrep/songs/chordpro/test/02.txt | 2 + patacrep/songs/chordpro/test/03.sgc | 4 + patacrep/songs/chordpro/test/03.txt | 1 + patacrep/songs/chordpro/test/04.sgc | 3 + patacrep/songs/chordpro/test/04.txt | 4 + patacrep/songs/chordpro/test/05.sgc | 3 + patacrep/songs/chordpro/test/05.txt | 4 + patacrep/songs/chordpro/test/06.sgc | 1 + patacrep/songs/chordpro/test/06.txt | 1 + patacrep/songs/chordpro/test/07.sgc | 3 + patacrep/songs/chordpro/test/07.txt | 4 + patacrep/songs/chordpro/test/08.sgc | 10 + patacrep/songs/chordpro/test/08.txt | 4 + patacrep/songs/chordpro/test/09.sgc | 15 + patacrep/songs/chordpro/test/09.txt | 5 + patacrep/songs/chordpro/test/10.sgc | 1 + patacrep/songs/chordpro/test/10.txt | 4 + patacrep/songs/chordpro/test/11.sgc | 1 + patacrep/songs/chordpro/test/11.txt | 4 + patacrep/songs/chordpro/test/12.sgc | 1 + patacrep/songs/chordpro/test/12.txt | 4 + patacrep/songs/chordpro/test/13.sgc | 5 + patacrep/songs/chordpro/test/13.txt | 6 + patacrep/songs/chordpro/test/21.sgc | 1 + patacrep/songs/chordpro/test/21.txt | 4 + patacrep/songs/chordpro/test/22.sgc | 1 + patacrep/songs/chordpro/test/22.txt | 2 + patacrep/songs/chordpro/test/23.sgc | 4 + patacrep/songs/chordpro/test/23.txt | 1 + patacrep/songs/chordpro/test/24.sgc | 3 + patacrep/songs/chordpro/test/24.txt | 4 + patacrep/songs/chordpro/test/25.sgc | 3 + patacrep/songs/chordpro/test/25.txt | 4 + patacrep/songs/chordpro/test/26.sgc | 1 + patacrep/songs/chordpro/test/26.txt | 1 + patacrep/songs/chordpro/test/27.sgc | 3 + patacrep/songs/chordpro/test/27.txt | 4 + patacrep/songs/chordpro/test/28.sgc | 10 + patacrep/songs/chordpro/test/28.txt | 4 + patacrep/songs/chordpro/test/29.sgc | 15 + patacrep/songs/chordpro/test/29.txt | 5 + patacrep/songs/chordpro/test/30.sgc | 1 + patacrep/songs/chordpro/test/__init__.py | 0 patacrep/songs/chordpro/test/greensleeves.sgc | 44 +++ patacrep/songs/chordpro/test/greensleeves.txt | 51 +++ patacrep/songs/chordpro/test/metadata.sgc | 20 ++ patacrep/songs/chordpro/test/metadata.txt | 21 ++ patacrep/songs/chordpro/test/test_parser.py | 36 +++ 57 files changed, 847 insertions(+), 112 deletions(-) create mode 100644 patacrep/songs/chordpro/test/00.sgc create mode 100644 patacrep/songs/chordpro/test/00.txt create mode 100644 patacrep/songs/chordpro/test/01.sgc create mode 100644 patacrep/songs/chordpro/test/01.txt create mode 100644 patacrep/songs/chordpro/test/02.sgc create mode 100644 patacrep/songs/chordpro/test/02.txt create mode 100644 patacrep/songs/chordpro/test/03.sgc create mode 100644 patacrep/songs/chordpro/test/03.txt create mode 100644 patacrep/songs/chordpro/test/04.sgc create mode 100644 patacrep/songs/chordpro/test/04.txt create mode 100644 patacrep/songs/chordpro/test/05.sgc create mode 100644 patacrep/songs/chordpro/test/05.txt create mode 100644 patacrep/songs/chordpro/test/06.sgc create mode 100644 patacrep/songs/chordpro/test/06.txt create mode 100644 patacrep/songs/chordpro/test/07.sgc create mode 100644 patacrep/songs/chordpro/test/07.txt create mode 100644 patacrep/songs/chordpro/test/08.sgc create mode 100644 patacrep/songs/chordpro/test/08.txt create mode 100644 patacrep/songs/chordpro/test/09.sgc create mode 100644 patacrep/songs/chordpro/test/09.txt create mode 100644 patacrep/songs/chordpro/test/10.sgc create mode 100644 patacrep/songs/chordpro/test/10.txt create mode 100644 patacrep/songs/chordpro/test/11.sgc create mode 100644 patacrep/songs/chordpro/test/11.txt create mode 100644 patacrep/songs/chordpro/test/12.sgc create mode 100644 patacrep/songs/chordpro/test/12.txt create mode 100644 patacrep/songs/chordpro/test/13.sgc create mode 100644 patacrep/songs/chordpro/test/13.txt create mode 100644 patacrep/songs/chordpro/test/21.sgc create mode 100644 patacrep/songs/chordpro/test/21.txt create mode 100644 patacrep/songs/chordpro/test/22.sgc create mode 100644 patacrep/songs/chordpro/test/22.txt create mode 100644 patacrep/songs/chordpro/test/23.sgc create mode 100644 patacrep/songs/chordpro/test/23.txt create mode 100644 patacrep/songs/chordpro/test/24.sgc create mode 100644 patacrep/songs/chordpro/test/24.txt create mode 100644 patacrep/songs/chordpro/test/25.sgc create mode 100644 patacrep/songs/chordpro/test/25.txt create mode 100644 patacrep/songs/chordpro/test/26.sgc create mode 100644 patacrep/songs/chordpro/test/26.txt create mode 100644 patacrep/songs/chordpro/test/27.sgc create mode 100644 patacrep/songs/chordpro/test/27.txt create mode 100644 patacrep/songs/chordpro/test/28.sgc create mode 100644 patacrep/songs/chordpro/test/28.txt create mode 100644 patacrep/songs/chordpro/test/29.sgc create mode 100644 patacrep/songs/chordpro/test/29.txt create mode 100644 patacrep/songs/chordpro/test/30.sgc create mode 100644 patacrep/songs/chordpro/test/__init__.py create mode 100644 patacrep/songs/chordpro/test/greensleeves.sgc create mode 100644 patacrep/songs/chordpro/test/greensleeves.txt create mode 100644 patacrep/songs/chordpro/test/metadata.sgc create mode 100644 patacrep/songs/chordpro/test/metadata.txt create mode 100644 patacrep/songs/chordpro/test/test_parser.py diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py index baddb158..e76960ac 100644 --- a/patacrep/songs/chordpro/__init__.py +++ b/patacrep/songs/chordpro/__init__.py @@ -10,10 +10,8 @@ class ChordproSong(Song): """Parse content, and return the dictinory of song data.""" with encoding.open_read(self.fullpath, encoding=self.encoding) as song: self.data = parse_song(song.read(), self.fullpath) - print(self.data) + print(type(self.data), self.data) import sys; sys.exit(1) - self.titles = self.data['@titles'] - del self.data['@titles'] self.languages = self.data['@languages'] del self.data['@languages'] self.authors = self.data['by'] diff --git a/patacrep/songs/chordpro/ast.py b/patacrep/songs/chordpro/ast.py index 5eec6b1e..05affee6 100644 --- a/patacrep/songs/chordpro/ast.py +++ b/patacrep/songs/chordpro/ast.py @@ -1,71 +1,264 @@ # -*- coding: utf-8 -*- """Abstract Syntax Tree for ChordPro code.""" +import functools + +def _indent(string): + return "\n".join([" {}".format(line) for line in string.split('\n')]) + +INLINE_PROPERTIES = { + "lilypond", + "comment", + "guitar_comment", + "image", + } + +DIRECTIVE_SHORTCUTS = { + "t": "title", + "st": "subtitle", + "a": "album", + "by": "artist", + "c": "comment", + "gc": "guitar_comment", + } + +def directive_name(text): + if text in DIRECTIVE_SHORTCUTS: + return DIRECTIVE_SHORTCUTS[text] + return text + + class AST: - """Base class for the tree.""" - # pylint: disable=no-init - metadata = None + inline = False + +class Line(AST): + """A line is a sequence of (possibly truncated) words, spaces and chords.""" + + def __init__(self): + super().__init__() + self.line = [] + + def prepend(self, data): + self.line.insert(0, data) + return self + + def __str__(self): + return "".join([str(item) for item in self.line]) + + def strip(self): + while True: + if not self.line: + return self + if isinstance(self.line[0], Space): + del self.line[0] + continue + if isinstance(self.line[-1], Space): + del self.line[-1] + continue + return self + +class LineElement(AST): + """Something present on a line.""" + pass + +class Word(LineElement): + """A chunk of word.""" + + def __init__(self, value): + super().__init__() + self.value = value - @classmethod - def init_metadata(cls): - """Clear metadata + def __str__(self): + return self.value - As this attribute is a class attribute, it as to be reset at each new - parsing. - """ - cls.metadata = { - '@languages': set(), - } +class Space(LineElement): + """A space between words""" -class Expression(AST): - """ChordPro expression""" + def __init__(self): + super().__init__() + + def __str__(self): + return " " + +class Chord(LineElement): + """A chord.""" def __init__(self, value): super().__init__() - self.content = [value] + self.value = value + + def __str__(self): + return "[{}]".format(self.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) +class Verse(AST): + """A verse (or bridge, or chorus)""" + type = "verse" + inline = True + + def __init__(self, block=None): + super().__init__() + self.lines = [] # TODO check block + + def prepend(self, data): + self.lines.insert(0, data) return self def __str__(self): - return "".join([str(item) for item in self.content]) - -class SongPart(AST): - """ChordPro start_of/end_of command - - {start_of_chorus}, {end_of_tab}, {eov} ... - """ - - class Type: - CHORUS = ("chorus", - "start_of_chorus", "end_of_chorus", - "soc", "eoc") - VERSE = ("verse", - "start_of_verse", "end_of_verse", - "sov", "eov") - BRIDGE = ("bridge", - "start_of_bridge", "end_of_bridge", - "sob", "eob") - TAB = ("tab", - "start_of_tab", "end_of_tab", - "sot", "eot") - - def __init__(self, name): - if "_" in name: - self.init_long_form(name) + return '{{start_of_{type}}}\n{content}\n{{end_of_{type}}}'.format( + type = self.type, + content = _indent("\n".join([str(line) for line in self.lines])), + ) + +class Chorus(Verse): + type = 'chorus' + +class Bridge(Verse): + type = 'bridge' + +class Song(AST): + """A song""" + + METADATA_TYPE = { + "title": "add_title", + "subtitle": "add_subtitle", + "language": "add_language", + "artist": "add_author", + } + + def __init__(self): + super().__init__() + self.content = [] + self.meta = [] + self._authors = [] + self._titles = [] + self._subtitles = [] + self._languages = set() + + def add(self, data): + if data is None: + if not (self.content and isinstance(self.content[0], Newline)): + self.content.insert(0, Newline()) + elif isinstance(data, Line): + if not (self.content and isinstance(self.content[0], Verse)): + self.content.insert(0, Verse()) + self.content[0].prepend(data.strip()) + elif data.inline: + self.content.insert(0, data) + elif isinstance(data, Directive): + name = directive_name(data.keyword) + if name in self.METADATA_TYPE: + getattr(self, self.METADATA_TYPE[name])(*data.as_tuple) + else: + self.meta.append(data) else: - self.init_short_form(name) + raise Exception() + return self + + def str_meta(self): + for title in self.titles: + yield "{{title: {}}}".format(title) + for language in sorted(self.languages): + yield "{{language: {}}}".format(language) + for author in self.authors: + yield "{{by: {}}}".format(author) + for key in sorted(self.meta): + yield str(key) def __str__(self): - return self.name + return ( + "\n".join(self.str_meta()).strip() + + + "\n========\n" + + + "\n".join([str(item) for item in self.content]).strip() + ) + + + def add_title(self, __ignored, title): + self._titles.insert(0, title) + + def add_subtitle(self, __ignored, title): + self._subtitles.insert(0, title) + + @property + def titles(self): + return self._titles + self._subtitles + + def add_author(self, __ignored, title): + self._authors.insert(0, title) - def init_short_form(self, name): - self.type = "" + @property + def authors(self): + return self._authors - def init_long_form(self, name): - self.type = "" + def add_language(self, __ignored, language): + self._languages.add(language) + + @property + def languages(self): + return self._languages + + + +class Newline(AST): + def __str__(self): + return "" + +@functools.total_ordering +class Directive(AST): + """A directive""" + + def __init__(self): + super().__init__() + self.keyword = "" + self.argument = None + @property + def keyword(self): + return self._keyword + + @property + def inline(self): + return self.keyword in INLINE_PROPERTIES + + @keyword.setter + def keyword(self, value): + self._keyword = value.strip() + + def __str__(self): + if self.argument is not None: + return "{{{}: {}}}".format( + self.keyword, + self.argument, + ) + else: + return "{{{}}}".format(self.keyword) + + @property + def as_tuple(self): + return (self.keyword, self.argument) + + def __eq__(self, other): + return self.as_tuple == other.as_tuple + + def __lt__(self, other): + return self.as_tuple < other.as_tuple + +class Tab(AST): + """Tablature""" + + inline = True + + def __init__(self): + super().__init__() + self.content = [] + + def prepend(self, data): + self.content.insert(0, data) + return self + + def __str__(self): + return '{{start_of_tab}}\n{}\n{{end_of_tab}}'.format( + _indent("\n".join(self.content)), + ) diff --git a/patacrep/songs/chordpro/lexer.py b/patacrep/songs/chordpro/lexer.py index f33d8f2e..726c24e4 100644 --- a/patacrep/songs/chordpro/lexer.py +++ b/patacrep/songs/chordpro/lexer.py @@ -7,16 +7,21 @@ LOGGER = logging.getLogger() #pylint: disable=invalid-name tokens = ( - #'LBRACKET', - #'RBRACKET', - 'CHORD', - 'LBRACE', - 'RBRACE', - 'NEWLINE', - #'COLON', - 'WORD', - 'SPACE', - #'NUMBER', + 'LBRACE', + 'RBRACE', + 'CHORD', + 'NEWLINE', + 'COLON', + 'WORD', + 'SPACE', + 'TEXT', + 'KEYWORD', + 'SOC', + 'EOC', + 'SOB', + 'EOB', + 'SOT', + 'EOT', ) class ChordProLexer: @@ -26,14 +31,51 @@ class ChordProLexer: states = ( ('chord', 'exclusive'), + ('directive', 'exclusive'), + ('directiveargument', 'exclusive'), + ('tablature', 'exclusive'), ) - t_LBRACE = r'{' - t_RBRACE = r'}' t_SPACE = r'[ \t]+' - #t_COLON = r':' + t_chord_CHORD = r'[A-G7#m]+' # TODO This can be refined + t_directive_SPACE = r'[ \t]+' + t_directive_KEYWORD = r'[a-zA-Z_]+' + t_directiveargument_TEXT = r'[^}]+' + + def t_SOC(self, token): + r'{(soc|start_of_chorus)}' + return token + def t_EOC(self, token): + r'{(eoc|end_of_chorus)}' + return token + + def t_SOB(self, token): + r'{(sob|start_of_bridge)}' + return token + + def t_EOB(self, token): + r'{(eob|end_of_bridge)}' + return token + + def t_SOT(self, token): + r'{(sot|start_of_tab)}' + self.lexer.push_state('tablature') + return token + + def t_tablature_EOT(self, token): + r'{(eot|end_of_tab)}' + self.lexer.pop_state() + return token + + def t_tablature_SPACE(self, token): + r'[ \t]+' + return token + + t_tablature_TEXT = r'[^\n]+' + t_tablature_NEWLINE = r'\n' + def __init__(self): self.__class__.lexer = lex.lex(module=self) @@ -51,7 +93,7 @@ class ChordProLexer: @staticmethod def t_WORD(token): - r'[^\n\][\t ]+' + r'[^{}\n\][\t ]+' return token def t_LBRACKET(self, token): @@ -62,11 +104,26 @@ class ChordProLexer: r'\]' self.lexer.pop_state() - #@staticmethod - #def t_NUMBER(token): - # r'[0-9]+' - # token.value = int(token.value) - # return token + def t_LBRACE(self, token): + r'{' + self.lexer.push_state('directive') + return token + + def t_directive_RBRACE(self, token): + r'}' + self.lexer.pop_state() + return token + + def t_directiveargument_RBRACE(self, token): + r'}' + self.lexer.pop_state() + self.lexer.pop_state() + return token + + def t_directive_COLON(self, token): + r':' + self.lexer.push_state('directiveargument') + return token @staticmethod def t_error(token): @@ -79,3 +136,19 @@ class ChordProLexer: """Manage errors""" LOGGER.error("Illegal character '{}' in chord..".format(token.value[0])) token.lexer.skip(1) + + @staticmethod + def t_tablature_error(token): + """Manage errors""" + LOGGER.error("Illegal character '{}' in tablature..".format(token.value[0])) + token.lexer.skip(1) + + @staticmethod + def t_directive_error(token): + """Manage errors""" + LOGGER.error("Illegal character '{}' in directive..".format(token.value[0])) + token.lexer.skip(1) + + @staticmethod + def t_directiveargument_error(token): + return t_directive_error(token) diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py index 91470ba5..32fa2c42 100644 --- a/patacrep/songs/chordpro/syntax.py +++ b/patacrep/songs/chordpro/syntax.py @@ -4,9 +4,10 @@ import logging import ply.yacc as yacc -from patacrep.songs.chordpro.lexer import tokens, ChordProLexer -from patacrep.songs.chordpro import ast from patacrep.errors import SongbookError +from patacrep.songs.chordpro import ast +from patacrep.songs.chordpro import ast +from patacrep.songs.chordpro.lexer import tokens, ChordProLexer LOGGER = logging.getLogger() @@ -24,6 +25,8 @@ class ParsingError(SongbookError): class Parser: """ChordPro parser class""" + start = "song" + def __init__(self, filename=None): self.tokens = tokens self.filename = filename @@ -39,7 +42,7 @@ class Parser: def p_error(self, token): """Manage parsing errors.""" - if token: # TODO remove this test + if token: LOGGER.error("Error in file {}, line {}:{}.".format( str(self.filename), token.lineno, @@ -52,85 +55,213 @@ class Parser: """song : block song | empty """ + #if isinstance(symbols[1], str): if len(symbols) == 2: - symbols[0] = ('song') + symbols[0] = ast.Song() else: - symbols[0] = ('song', symbols[1], symbols[2]) + symbols[0] = symbols[2].add(symbols[1]) + + #@staticmethod + #def p_song_next(symbols): + # """song_next : block song_next + # | empty + # """ + # if len(symbols) == 2: + # symbols[0] = ast.Song() + # else: + # symbols[0] = symbols[2].add(symbols[1]) @staticmethod def p_block(symbols): - """block : directive NEWLINE newlines - | stanza NEWLINE newlines + """block : SPACE block + | directive NEWLINE + | line NEWLINE + | chorus NEWLINE + | tab NEWLINE + | bridge NEWLINE + | NEWLINE """ - symbols[0] = ('block', symbols[1]) + if len(symbols) == 3 and isinstance(symbols[1], str): + symbols[0] = symbols[2] + elif (symbols[1] is None) or (len(symbols) == 2): + symbols[0] = None + else: + symbols[0] = symbols[1] @staticmethod - def p_newlines(symbols): - """newlines : NEWLINE newlines - | empty""" - symbols[0] = ('newlines') + def p_maybespace(symbols): + """maybespace : SPACE + | empty + """ + symbols[0] = None + + #@staticmethod + #def p_newlines(symbols): + # """newlines : NEWLINE newlines + # | empty""" + # symbols[0] = ('newlines') @staticmethod def p_directive(symbols): - """directive : LBRACE WORD RBRACE""" - symbols[0] = ('directive', symbols[1]) + """directive : LBRACE KEYWORD directive_next RBRACE + | LBRACE SPACE KEYWORD directive_next RBRACE + """ + if len(symbols) == 5: + symbols[3].keyword = symbols[2] + symbols[0] = symbols[3] + else: + symbols[4].keyword = symbols[3] + symbols[0] = symbols[4] + + @staticmethod + def p_directive_next(symbols): + """directive_next : SPACE COLON TEXT + | COLON TEXT + | empty + """ + symbols[0] = ast.Directive() + if len(symbols) == 3: + symbols[0].argument = symbols[2].strip() + elif len(symbols) == 4: + symbols[0].argument = symbols[3].strip() @staticmethod def p_line(symbols): - """line : WORD line_next - | CHORD line_next - | SPACE line_next + """line : word line_next + | chord line_next """ - symbols[0] = ('line', symbols[1], symbols[2]) + symbols[0] = symbols[2].prepend(symbols[1]) @staticmethod def p_line_next(symbols): - """line_next : WORD line_next - | SPACE line_next - | CHORD line_next + """line_next : word line_next + | space line_next + | chord line_next | empty """ if len(symbols) == 2: - symbols[0] = ('line-next') + symbols[0] = ast.Line() + else: + symbols[0] = symbols[2].prepend(symbols[1]) + + @staticmethod + def p_word(symbols): + """word : WORD""" + symbols[0] = ast.Word(symbols[1]) + + @staticmethod + def p_space(symbols): + """space : SPACE""" + symbols[0] = ast.Space() + + @staticmethod + def p_chord(symbols): + """chord : CHORD""" + symbols[0] = ast.Chord(symbols[1]) + + #@staticmethod + #def p_verse(symbols): + # """verse : line NEWLINE verse_next + # """ + # symbols[0] = symbols[3].prepend(symbols[1]) + + #@staticmethod + #def p_verse_next(symbols): + # """verse_next : line NEWLINE verse_next + # | empty + # """ + # if len(symbols) == 2: + # symbols[0] = ast.Verse() + # else: + # symbols[0] = symbols[3].prepend(symbols[1]) + + @staticmethod + def p_chorus(symbols): + """chorus : SOC maybespace NEWLINE chorus_content EOC maybespace + """ + symbols[0] = symbols[4] + + @staticmethod + def p_chorus_content(symbols): + """chorus_content : line NEWLINE chorus_content + | SPACE chorus_content + | empty + """ + if len(symbols) == 2: + symbols[0] = ast.Chorus() + elif len(symbols) == 3: + symbols[0] = symbols[2] else: - symbols[0] = ('line-next', symbols[1], symbols[2]) + symbols[0] = symbols[3].prepend(symbols[1]) @staticmethod - def p_stanza(symbols): - """stanza : line NEWLINE stanza_next + def p_bridge(symbols): + """bridge : SOB maybespace NEWLINE bridge_content EOB maybespace """ - symbols[0] = ('stanza', symbols[1], symbols[3]) + symbols[0] = symbols[4] @staticmethod - def p_stanza_next(symbols): - """stanza_next : line NEWLINE stanza_next - | empty + def p_bridge_content(symbols): + """bridge_content : line NEWLINE bridge_content + | SPACE bridge_content + | empty """ if len(symbols) == 2: - symbols[0] = ('stanza-next') + symbols[0] = ast.Bridge() + elif len(symbols) == 3: + symbols[0] = symbols[2] else: - symbols[0] = ('stanza-next', symbols[1], symbols[3]) + symbols[0] = symbols[3].prepend(symbols[1]) + #@staticmethod - #def p_braces(symbols): - # """braces : LBRACE expression COLON expression RBRACE""" - # symbols[0] = symbols[2] + #def p_bridge_next(symbols): + # """bridge_next : line NEWLINE bridge_next + # | empty + # """ + # if len(symbols) == 2: + # symbols[0] = ast.Bridge() + # else: + # symbols[0] = symbols[3].prepend(symbols[1]) + + @staticmethod + def p_tab(symbols): + """tab : SOT maybespace NEWLINE tab_content EOT maybespace + """ + symbols[0] = symbols[4] + + @staticmethod + def p_tab_content(symbols): + """tab_content : NEWLINE tab_content + | TEXT tab_content + | SPACE tab_content + | empty + """ + if len(symbols) == 2: + symbols[0] = ast.Tab() + else: + if symbols[1].strip(): + symbols[2].prepend(symbols[1]) + symbols[0] = symbols[2] @staticmethod def p_empty(symbols): """empty :""" symbols[0] = None - #@staticmethod - #def p_comment(symbols): - # """comment : COMMENT""" - # symbols[0] = ('comment', symbols[1]) - - +def lex_song(content): + # TODO delete + lex = ChordProLexer().lexer + lex.input(content) + while 1: + tok = lex.token() + if not tok: break + print(tok) def parse_song(content, filename=None): """Parse song and return its metadata.""" return yacc.yacc(module=Parser(filename)).parse( content, + debug=0, lexer=ChordProLexer().lexer, ) diff --git a/patacrep/songs/chordpro/test/00.sgc b/patacrep/songs/chordpro/test/00.sgc new file mode 100644 index 00000000..e69de29b diff --git a/patacrep/songs/chordpro/test/00.txt b/patacrep/songs/chordpro/test/00.txt new file mode 100644 index 00000000..dbbd9c50 --- /dev/null +++ b/patacrep/songs/chordpro/test/00.txt @@ -0,0 +1 @@ +======== diff --git a/patacrep/songs/chordpro/test/01.sgc b/patacrep/songs/chordpro/test/01.sgc new file mode 100644 index 00000000..8ea8d2a2 --- /dev/null +++ b/patacrep/songs/chordpro/test/01.sgc @@ -0,0 +1 @@ +A verse line diff --git a/patacrep/songs/chordpro/test/01.txt b/patacrep/songs/chordpro/test/01.txt new file mode 100644 index 00000000..84cf4364 --- /dev/null +++ b/patacrep/songs/chordpro/test/01.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + A verse line +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/02.sgc b/patacrep/songs/chordpro/test/02.sgc new file mode 100644 index 00000000..270bc746 --- /dev/null +++ b/patacrep/songs/chordpro/test/02.sgc @@ -0,0 +1 @@ +{title : A directive} diff --git a/patacrep/songs/chordpro/test/02.txt b/patacrep/songs/chordpro/test/02.txt new file mode 100644 index 00000000..f3fbd9e9 --- /dev/null +++ b/patacrep/songs/chordpro/test/02.txt @@ -0,0 +1,2 @@ +{title: A directive} +======== diff --git a/patacrep/songs/chordpro/test/03.sgc b/patacrep/songs/chordpro/test/03.sgc new file mode 100644 index 00000000..fd40910d --- /dev/null +++ b/patacrep/songs/chordpro/test/03.sgc @@ -0,0 +1,4 @@ + + + + diff --git a/patacrep/songs/chordpro/test/03.txt b/patacrep/songs/chordpro/test/03.txt new file mode 100644 index 00000000..dbbd9c50 --- /dev/null +++ b/patacrep/songs/chordpro/test/03.txt @@ -0,0 +1 @@ +======== diff --git a/patacrep/songs/chordpro/test/04.sgc b/patacrep/songs/chordpro/test/04.sgc new file mode 100644 index 00000000..6a266eea --- /dev/null +++ b/patacrep/songs/chordpro/test/04.sgc @@ -0,0 +1,3 @@ +{soc} +A one line chorus +{eoc} diff --git a/patacrep/songs/chordpro/test/04.txt b/patacrep/songs/chordpro/test/04.txt new file mode 100644 index 00000000..3336d565 --- /dev/null +++ b/patacrep/songs/chordpro/test/04.txt @@ -0,0 +1,4 @@ +======== +{start_of_chorus} + A one line chorus +{end_of_chorus} diff --git a/patacrep/songs/chordpro/test/05.sgc b/patacrep/songs/chordpro/test/05.sgc new file mode 100644 index 00000000..9472ebcb --- /dev/null +++ b/patacrep/songs/chordpro/test/05.sgc @@ -0,0 +1,3 @@ +{sob} +A one line bridge +{eob} diff --git a/patacrep/songs/chordpro/test/05.txt b/patacrep/songs/chordpro/test/05.txt new file mode 100644 index 00000000..b2f99be2 --- /dev/null +++ b/patacrep/songs/chordpro/test/05.txt @@ -0,0 +1,4 @@ +======== +{start_of_bridge} + A one line bridge +{end_of_bridge} diff --git a/patacrep/songs/chordpro/test/06.sgc b/patacrep/songs/chordpro/test/06.sgc new file mode 100644 index 00000000..90a2b559 --- /dev/null +++ b/patacrep/songs/chordpro/test/06.sgc @@ -0,0 +1 @@ +# A comment diff --git a/patacrep/songs/chordpro/test/06.txt b/patacrep/songs/chordpro/test/06.txt new file mode 100644 index 00000000..dbbd9c50 --- /dev/null +++ b/patacrep/songs/chordpro/test/06.txt @@ -0,0 +1 @@ +======== diff --git a/patacrep/songs/chordpro/test/07.sgc b/patacrep/songs/chordpro/test/07.sgc new file mode 100644 index 00000000..fd183eff --- /dev/null +++ b/patacrep/songs/chordpro/test/07.sgc @@ -0,0 +1,3 @@ +{sot} +A tab +{eot} diff --git a/patacrep/songs/chordpro/test/07.txt b/patacrep/songs/chordpro/test/07.txt new file mode 100644 index 00000000..7fb8bc64 --- /dev/null +++ b/patacrep/songs/chordpro/test/07.txt @@ -0,0 +1,4 @@ +======== +{start_of_tab} + A tab +{end_of_tab} diff --git a/patacrep/songs/chordpro/test/08.sgc b/patacrep/songs/chordpro/test/08.sgc new file mode 100644 index 00000000..b51646ca --- /dev/null +++ b/patacrep/songs/chordpro/test/08.sgc @@ -0,0 +1,10 @@ + + + +# comment +# comment + + +A lot of new lines + +# comment diff --git a/patacrep/songs/chordpro/test/08.txt b/patacrep/songs/chordpro/test/08.txt new file mode 100644 index 00000000..92c183b1 --- /dev/null +++ b/patacrep/songs/chordpro/test/08.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + A lot of new lines +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/09.sgc b/patacrep/songs/chordpro/test/09.sgc new file mode 100644 index 00000000..b831f230 --- /dev/null +++ b/patacrep/songs/chordpro/test/09.sgc @@ -0,0 +1,15 @@ + + + +# comment +# comment + + +A lot of new lines + +# comment + +{title: and a directive} + +# comment + diff --git a/patacrep/songs/chordpro/test/09.txt b/patacrep/songs/chordpro/test/09.txt new file mode 100644 index 00000000..b7669e62 --- /dev/null +++ b/patacrep/songs/chordpro/test/09.txt @@ -0,0 +1,5 @@ +{title: and a directive} +======== +{start_of_verse} + A lot of new lines +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/10.sgc b/patacrep/songs/chordpro/test/10.sgc new file mode 100644 index 00000000..6519ab80 --- /dev/null +++ b/patacrep/songs/chordpro/test/10.sgc @@ -0,0 +1 @@ +A line[A] with a chord diff --git a/patacrep/songs/chordpro/test/10.txt b/patacrep/songs/chordpro/test/10.txt new file mode 100644 index 00000000..b96d8637 --- /dev/null +++ b/patacrep/songs/chordpro/test/10.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + A line[A] with a chord +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/11.sgc b/patacrep/songs/chordpro/test/11.sgc new file mode 100644 index 00000000..fc3d697d --- /dev/null +++ b/patacrep/songs/chordpro/test/11.sgc @@ -0,0 +1 @@ +A line ending with a chord[A] diff --git a/patacrep/songs/chordpro/test/11.txt b/patacrep/songs/chordpro/test/11.txt new file mode 100644 index 00000000..2a9eaf17 --- /dev/null +++ b/patacrep/songs/chordpro/test/11.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + A line ending with a chord[A] +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/12.sgc b/patacrep/songs/chordpro/test/12.sgc new file mode 100644 index 00000000..a9583451 --- /dev/null +++ b/patacrep/songs/chordpro/test/12.sgc @@ -0,0 +1 @@ +[A]A line starting with a chord diff --git a/patacrep/songs/chordpro/test/12.txt b/patacrep/songs/chordpro/test/12.txt new file mode 100644 index 00000000..83c11625 --- /dev/null +++ b/patacrep/songs/chordpro/test/12.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + [A]A line starting with a chord +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/13.sgc b/patacrep/songs/chordpro/test/13.sgc new file mode 100644 index 00000000..6cc3dfbc --- /dev/null +++ b/patacrep/songs/chordpro/test/13.sgc @@ -0,0 +1,5 @@ +{sot} + A table + wit many # weir [ + [ symbols +{eot} diff --git a/patacrep/songs/chordpro/test/13.txt b/patacrep/songs/chordpro/test/13.txt new file mode 100644 index 00000000..447f67dd --- /dev/null +++ b/patacrep/songs/chordpro/test/13.txt @@ -0,0 +1,6 @@ +======== +{start_of_tab} + A table + wit many # weir [ + [ symbols +{end_of_tab} diff --git a/patacrep/songs/chordpro/test/21.sgc b/patacrep/songs/chordpro/test/21.sgc new file mode 100644 index 00000000..c01b31ad --- /dev/null +++ b/patacrep/songs/chordpro/test/21.sgc @@ -0,0 +1 @@ + A verse line diff --git a/patacrep/songs/chordpro/test/21.txt b/patacrep/songs/chordpro/test/21.txt new file mode 100644 index 00000000..84cf4364 --- /dev/null +++ b/patacrep/songs/chordpro/test/21.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + A verse line +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/22.sgc b/patacrep/songs/chordpro/test/22.sgc new file mode 100644 index 00000000..425fd584 --- /dev/null +++ b/patacrep/songs/chordpro/test/22.sgc @@ -0,0 +1 @@ + {title : A directive} diff --git a/patacrep/songs/chordpro/test/22.txt b/patacrep/songs/chordpro/test/22.txt new file mode 100644 index 00000000..f3fbd9e9 --- /dev/null +++ b/patacrep/songs/chordpro/test/22.txt @@ -0,0 +1,2 @@ +{title: A directive} +======== diff --git a/patacrep/songs/chordpro/test/23.sgc b/patacrep/songs/chordpro/test/23.sgc new file mode 100644 index 00000000..ce5ed9dd --- /dev/null +++ b/patacrep/songs/chordpro/test/23.sgc @@ -0,0 +1,4 @@ + + + + diff --git a/patacrep/songs/chordpro/test/23.txt b/patacrep/songs/chordpro/test/23.txt new file mode 100644 index 00000000..dbbd9c50 --- /dev/null +++ b/patacrep/songs/chordpro/test/23.txt @@ -0,0 +1 @@ +======== diff --git a/patacrep/songs/chordpro/test/24.sgc b/patacrep/songs/chordpro/test/24.sgc new file mode 100644 index 00000000..a929a754 --- /dev/null +++ b/patacrep/songs/chordpro/test/24.sgc @@ -0,0 +1,3 @@ + {soc} + A one line chorus + {eoc} diff --git a/patacrep/songs/chordpro/test/24.txt b/patacrep/songs/chordpro/test/24.txt new file mode 100644 index 00000000..3336d565 --- /dev/null +++ b/patacrep/songs/chordpro/test/24.txt @@ -0,0 +1,4 @@ +======== +{start_of_chorus} + A one line chorus +{end_of_chorus} diff --git a/patacrep/songs/chordpro/test/25.sgc b/patacrep/songs/chordpro/test/25.sgc new file mode 100644 index 00000000..45efba59 --- /dev/null +++ b/patacrep/songs/chordpro/test/25.sgc @@ -0,0 +1,3 @@ + {sob} + A one line bridge + {eob} diff --git a/patacrep/songs/chordpro/test/25.txt b/patacrep/songs/chordpro/test/25.txt new file mode 100644 index 00000000..b2f99be2 --- /dev/null +++ b/patacrep/songs/chordpro/test/25.txt @@ -0,0 +1,4 @@ +======== +{start_of_bridge} + A one line bridge +{end_of_bridge} diff --git a/patacrep/songs/chordpro/test/26.sgc b/patacrep/songs/chordpro/test/26.sgc new file mode 100644 index 00000000..372b96b4 --- /dev/null +++ b/patacrep/songs/chordpro/test/26.sgc @@ -0,0 +1 @@ + # A comment diff --git a/patacrep/songs/chordpro/test/26.txt b/patacrep/songs/chordpro/test/26.txt new file mode 100644 index 00000000..dbbd9c50 --- /dev/null +++ b/patacrep/songs/chordpro/test/26.txt @@ -0,0 +1 @@ +======== diff --git a/patacrep/songs/chordpro/test/27.sgc b/patacrep/songs/chordpro/test/27.sgc new file mode 100644 index 00000000..bfa0cf0e --- /dev/null +++ b/patacrep/songs/chordpro/test/27.sgc @@ -0,0 +1,3 @@ + {sot} + A tab + {eot} diff --git a/patacrep/songs/chordpro/test/27.txt b/patacrep/songs/chordpro/test/27.txt new file mode 100644 index 00000000..7fb8bc64 --- /dev/null +++ b/patacrep/songs/chordpro/test/27.txt @@ -0,0 +1,4 @@ +======== +{start_of_tab} + A tab +{end_of_tab} diff --git a/patacrep/songs/chordpro/test/28.sgc b/patacrep/songs/chordpro/test/28.sgc new file mode 100644 index 00000000..69f980b3 --- /dev/null +++ b/patacrep/songs/chordpro/test/28.sgc @@ -0,0 +1,10 @@ + + + + # comment +# comment + + + A lot of new lines + +# comment diff --git a/patacrep/songs/chordpro/test/28.txt b/patacrep/songs/chordpro/test/28.txt new file mode 100644 index 00000000..92c183b1 --- /dev/null +++ b/patacrep/songs/chordpro/test/28.txt @@ -0,0 +1,4 @@ +======== +{start_of_verse} + A lot of new lines +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/29.sgc b/patacrep/songs/chordpro/test/29.sgc new file mode 100644 index 00000000..830e0177 --- /dev/null +++ b/patacrep/songs/chordpro/test/29.sgc @@ -0,0 +1,15 @@ + + + +# comment +# comment + + + A lot of new lines + +# comment + + {title: and a directive} + +# comment + diff --git a/patacrep/songs/chordpro/test/29.txt b/patacrep/songs/chordpro/test/29.txt new file mode 100644 index 00000000..b7669e62 --- /dev/null +++ b/patacrep/songs/chordpro/test/29.txt @@ -0,0 +1,5 @@ +{title: and a directive} +======== +{start_of_verse} + A lot of new lines +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/30.sgc b/patacrep/songs/chordpro/test/30.sgc new file mode 100644 index 00000000..ee67061c --- /dev/null +++ b/patacrep/songs/chordpro/test/30.sgc @@ -0,0 +1 @@ + [A]A line starting with a chord diff --git a/patacrep/songs/chordpro/test/__init__.py b/patacrep/songs/chordpro/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patacrep/songs/chordpro/test/greensleeves.sgc b/patacrep/songs/chordpro/test/greensleeves.sgc new file mode 100644 index 00000000..1dee93ff --- /dev/null +++ b/patacrep/songs/chordpro/test/greensleeves.sgc @@ -0,0 +1,44 @@ +{language : english} +{columns : 2} +{subtitle : Un sous titre} +{ title : Greensleeves} +{title : Un autre sous-titre} +{artist: Traditionnel} +{cover : traditionnel } +{album :Angleterre} + +{partition : greensleeves.ly} + + +A[Am]las, my love, ye [G]do me wrong +To [Am]cast me oft dis[E]curteously +And [Am]I have loved [G]you so long +De[Am]lighting [E]in your [Am]companie + +{start_of_chorus} + [C]Green[B]sleeves was [G]all my joy + [Am]Greensleeves was [E]my delight + [C]Greensleeves was my [G]heart of gold + And [Am]who but [E]Ladie [Am]Greensleeves +{end_of_chorus} + +I [Am]have been ready [G]at your hand +To [Am]grant what ever [E]you would crave +I [Am]have both waged [G]life and land +Your [Am]love and [E]good will [Am]for to have + +I [Am]bought thee kerchers [G]to thy head +That [Am]were wrought fine and [E]gallantly +I [Am]kept thee both at [G]boord and bed +Which [Am]cost my [E]purse well [Am]favouredly + +I [Am]bought thee peticotes [G]of the best +The [Am]cloth so fine as [E]fine might be +I [Am]gave thee jewels [G]for thy chest +And [Am]all this [E]cost I [Am]spent on thee + + +Thy [Am]smock of silke, both [G]faire and white +With [Am]gold embrodered [E]gorgeously +Thy [Am]peticote of [G]sendall right +And [Am]this I [E]bought thee [Am]gladly diff --git a/patacrep/songs/chordpro/test/greensleeves.txt b/patacrep/songs/chordpro/test/greensleeves.txt new file mode 100644 index 00000000..38564149 --- /dev/null +++ b/patacrep/songs/chordpro/test/greensleeves.txt @@ -0,0 +1,51 @@ +{title: Greensleeves} +{title: Un autre sous-titre} +{title: Un sous titre} +{language: english} +{by: Traditionnel} +{album: Angleterre} +{columns: 2} +{cover: traditionnel} +{partition: greensleeves.ly} +======== +{start_of_verse} + A[Am]las, my love, ye [G]do me wrong + To [Am]cast me oft dis[E]curteously + And [Am]I have loved [G]you so long + De[Am]lighting [E]in your [Am]companie +{end_of_verse} + +{start_of_chorus} + [C]Green[B]sleeves was [G]all my joy + [Am]Greensleeves was [E]my delight + [C]Greensleeves was my [G]heart of gold + And [Am]who but [E]Ladie [Am]Greensleeves +{end_of_chorus} + +{start_of_verse} + I [Am]have been ready [G]at your hand + To [Am]grant what ever [E]you would crave + I [Am]have both waged [G]life and land + Your [Am]love and [E]good will [Am]for to have +{end_of_verse} + +{start_of_verse} + I [Am]bought thee kerchers [G]to thy head + That [Am]were wrought fine and [E]gallantly + I [Am]kept thee both at [G]boord and bed + Which [Am]cost my [E]purse well [Am]favouredly +{end_of_verse} + +{start_of_verse} + I [Am]bought thee peticotes [G]of the best + The [Am]cloth so fine as [E]fine might be + I [Am]gave thee jewels [G]for thy chest + And [Am]all this [E]cost I [Am]spent on thee +{end_of_verse} + +{start_of_verse} + Thy [Am]smock of silke, both [G]faire and white + With [Am]gold embrodered [E]gorgeously + Thy [Am]peticote of [G]sendall right + And [Am]this I [E]bought thee [Am]gladly +{end_of_verse} diff --git a/patacrep/songs/chordpro/test/metadata.sgc b/patacrep/songs/chordpro/test/metadata.sgc new file mode 100644 index 00000000..c892faf0 --- /dev/null +++ b/patacrep/songs/chordpro/test/metadata.sgc @@ -0,0 +1,20 @@ +{subtitle: Subtitle3} +{title: Title} +{title: Subtitle1} +{subtitle: Subtitle4} +{t: Subtitle2} +{st: Subtitle5} +{language: french} +{language: english} +{by: Author1} +{artist: Author2} +{album: Albom} +{copyright: Copyright} +{cover: Cover} +{vcover: VCover} +{capo: Capo} +{foo: Foo} +{comment: Comment} +{guitar_comment: GuitarComment} +{image: Image} +{lilypond: Lilypond} diff --git a/patacrep/songs/chordpro/test/metadata.txt b/patacrep/songs/chordpro/test/metadata.txt new file mode 100644 index 00000000..8d73f4b8 --- /dev/null +++ b/patacrep/songs/chordpro/test/metadata.txt @@ -0,0 +1,21 @@ +{title: Title} +{title: Subtitle1} +{title: Subtitle2} +{title: Subtitle3} +{title: Subtitle4} +{title: Subtitle5} +{language: english} +{language: french} +{by: Author1} +{by: Author2} +{album: Albom} +{capo: Capo} +{copyright: Copyright} +{cover: Cover} +{foo: Foo} +{vcover: VCover} +======== +{comment: Comment} +{guitar_comment: GuitarComment} +{image: Image} +{lilypond: Lilypond} diff --git a/patacrep/songs/chordpro/test/test_parser.py b/patacrep/songs/chordpro/test/test_parser.py new file mode 100644 index 00000000..85ad4108 --- /dev/null +++ b/patacrep/songs/chordpro/test/test_parser.py @@ -0,0 +1,36 @@ +import glob +import os +import unittest + +from patacrep.songs.chordpro import syntax as chordpro + +class ParserTestCase(unittest.TestCase): + + def test_txt(self): + for txt in sorted(glob.glob(os.path.join( + os.path.dirname(__file__), + '*.txt', + ))): + basename = txt[:-len('.txt')] + with open("{}.sgc".format(basename), 'r', encoding='utf8') as sourcefile: + with open("{}.txt".format(basename), 'r', encoding='utf8') as expectfile: + #print(os.path.basename(sourcefile.name)) + #with open("{}.txt.diff".format(basename), 'w', encoding='utf8') as difffile: + # difffile.write( + # str(chordpro.parse_song( + # sourcefile.read(), + # os.path.basename(sourcefile.name), + # )).strip() + # ) + # sourcefile.seek(0) + self.assertMultiLineEqual( + str(chordpro.parse_song( + sourcefile.read(), + os.path.basename(sourcefile.name), + )).strip(), + expectfile.read().strip(), + ) + + def test_tex(self): + # TODO + pass From 9c49243db1944a34676f0df568c44c017df90a69 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 24 Jan 2015 15:11:12 +0100 Subject: [PATCH 18/21] Turned authors in a list of authors --- patacrep/authors.py | 21 ++++++++++++++++----- patacrep/index.py | 13 ++++++++----- patacrep/songs/latex/__init__.py | 9 ++++++++- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/patacrep/authors.py b/patacrep/authors.py index 3bdb6b9a..b902bb02 100644 --- a/patacrep/authors.py +++ b/patacrep/authors.py @@ -164,17 +164,21 @@ def processauthors(authors_string, after=None, ignore=None, sep=None): For example, we are processing: # processauthors( - # "Lyrics by William Blake (from Milton, 1808), - music by Hubert Parry (1916), - and sung by The Royal\ Choir~of~Nowhere - (just here to show you how processing is done)", + # [ + # " + # Lyrics by William Blake (from Milton, 1808), + # music by Hubert Parry (1916), + # and sung by The Royal\ Choir~of~Nowhere + # (just here to show you how processing is done) + # ", + # ], # after = ["by"], # ignore = ["anonymous"], # sep = [re.compile('^(.*) and (.*)$')], # ) - The "authors_string" string is processed as: + The "authors_string" is processed as: 1) First, parenthesis (and its content) are removed. # "Lyrics by William Blake, music by Hubert Parry, @@ -220,3 +224,10 @@ def processauthors(authors_string, after=None, ignore=None, sep=None): ignore) ) ] + +def process_listauthors(authors_list): + """Process a list of authors, and return the list of resulting authors.""" + return sum([ + processauthors(string) + for string in authors_list + ]) diff --git a/patacrep/index.py b/patacrep/index.py index 6a4300a2..8540d433 100644 --- a/patacrep/index.py +++ b/patacrep/index.py @@ -19,7 +19,6 @@ EOL = "\n" KEYWORD_PATTERN = re.compile(r"^%(\w+)\s?(.*)$", re.LOCALE) FIRST_LETTER_PATTERN = re.compile(r"^(?:\{?\\\w+\}?)*[^\w]*(\w)", re.LOCALE) - def process_sxd(filename): """Parse sxd file. @@ -162,7 +161,11 @@ class Index(object): def entry_to_str(self, key, entry): """Return the LaTeX code corresponding to the entry.""" - return (r'\idxentry{{{0}}}{{{1}}}' + EOL).format( + return r"""\idxentry{{ + {0} + }}{{ + {1} + }}""".format( self.key_to_str(key), r'\\'.join([self.ref_to_str(ref) for ref in entry]), ) @@ -182,13 +185,13 @@ class Index(object): ] string = r'\begin{idxblock}{' + letter + '}' + EOL for key in sorted(entries, key=sortkey): - string += self.entry_to_str(key, entries[key]['entries']) - string += r'\end{idxblock}' + EOL + string += " " + self.entry_to_str(key, entries[key]['entries']) + string += EOL + r'\end{idxblock}' return string def entries_to_str(self): """Return the LaTeX code corresponding to the index.""" string = "" for letter in sorted(self.data.keys()): - string += self.idxblock_to_str(letter, self.data[letter]) + string += self.idxblock_to_str(letter, self.data[letter]) + EOL return string diff --git a/patacrep/songs/latex/__init__.py b/patacrep/songs/latex/__init__.py index 2de16338..93fbccc7 100644 --- a/patacrep/songs/latex/__init__.py +++ b/patacrep/songs/latex/__init__.py @@ -16,7 +16,14 @@ class LatexSong(Song): def parse(self, content): """Parse content, and return the dictinory of song data.""" - return parse_song(content, self.fullpath) + with encoding.open_read(self.fullpath, encoding=self.encoding) as song: + self.data = parse_song(song.read(), self.fullpath) + self.titles = self.data['@titles'] + del self.data['@titles'] + self.languages = self.data['@languages'] + del self.data['@languages'] + self.authors = [self.data['by']] + del self.data['by'] def tex(self, output): """Return the LaTeX code rendering the song.""" From b9d459ec2497b35666f0f828be5971eefcdd648b Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 24 Jan 2015 20:11:59 +0100 Subject: [PATCH 19/21] Tex rendere works --- patacrep/authors.py | 13 +- patacrep/build.py | 4 +- patacrep/data/examples/example-all.sb | 1 + patacrep/data/examples/songs/greensleeves.sgc | 13 +- patacrep/songs/__init__.py | 45 ++-- patacrep/songs/chordpro/__init__.py | 47 ++++- patacrep/songs/chordpro/ast.py | 198 +++++++++++++++--- .../songs/chordpro/data/latex/chordpro.tex | 27 +++ .../chordpro/data/latex/content_chord.tex | 1 + .../chordpro/data/latex/content_comment.tex | 1 + .../chordpro/data/latex/content_error.tex | 3 + .../data/latex/content_guitar_comment.tex | 1 + .../chordpro/data/latex/content_image.tex | 1 + .../chordpro/data/latex/content_line.tex | 1 + .../chordpro/data/latex/content_newline.tex | 2 + .../chordpro/data/latex/content_partition.tex | 1 + .../chordpro/data/latex/content_space.tex | 1 + .../chordpro/data/latex/content_verse.tex | 7 + .../chordpro/data/latex/content_word.tex | 1 + patacrep/songs/chordpro/lexer.py | 27 ++- patacrep/songs/chordpro/syntax.py | 22 +- patacrep/songs/chordpro/test/__init__.py | 1 + patacrep/songs/chordpro/test/greensleeves.txt | 7 +- patacrep/songs/chordpro/test/metadata.sgc | 4 +- patacrep/songs/chordpro/test/metadata.txt | 14 +- patacrep/songs/chordpro/test/test_parser.py | 76 ++++--- patacrep/songs/latex/__init__.py | 2 +- patacrep/templates.py | 40 ++-- patacrep/test.py | 26 +++ setup.py | 3 +- 30 files changed, 443 insertions(+), 147 deletions(-) create mode 100644 patacrep/songs/chordpro/data/latex/chordpro.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_chord.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_comment.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_error.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_guitar_comment.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_image.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_line.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_newline.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_partition.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_space.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_verse.tex create mode 100644 patacrep/songs/chordpro/data/latex/content_word.tex create mode 100644 patacrep/test.py diff --git a/patacrep/authors.py b/patacrep/authors.py index b902bb02..84613a1a 100644 --- a/patacrep/authors.py +++ b/patacrep/authors.py @@ -225,9 +225,12 @@ def processauthors(authors_string, after=None, ignore=None, sep=None): ) ] -def process_listauthors(authors_list): +def process_listauthors(authors_list, after=None, ignore=None, sep=None): """Process a list of authors, and return the list of resulting authors.""" - return sum([ - processauthors(string) - for string in authors_list - ]) + authors = [] + for sublist in [ + processauthors(string, after, ignore, sep) + for string in authors_list + ]: + authors.extend(sublist) + return authors diff --git a/patacrep/build.py b/patacrep/build.py index facf51e8..a16daca2 100644 --- a/patacrep/build.py +++ b/patacrep/build.py @@ -10,7 +10,7 @@ from subprocess import Popen, PIPE, call from patacrep import __DATADIR__, authors, content, errors, files from patacrep.index import process_sxd -from patacrep.templates import TexRenderer +from patacrep.templates import TexBookRenderer from patacrep.songs import DataSubpath LOGGER = logging.getLogger(__name__) @@ -87,7 +87,7 @@ class Songbook(object): # Updating configuration config = DEFAULT_CONFIG.copy() config.update(self.config) - renderer = TexRenderer( + renderer = TexBookRenderer( config['template'], config['datadir'], config['lang'], diff --git a/patacrep/data/examples/example-all.sb b/patacrep/data/examples/example-all.sb index 0b1252aa..ca35b622 100644 --- a/patacrep/data/examples/example-all.sb +++ b/patacrep/data/examples/example-all.sb @@ -7,6 +7,7 @@ ], "booktype" : "chorded", "lang" : "french", +"encoding": "utf8", "authwords" : { "sep" : ["and", "et"] }, diff --git a/patacrep/data/examples/songs/greensleeves.sgc b/patacrep/data/examples/songs/greensleeves.sgc index 7d6b1000..a039cc74 100644 --- a/patacrep/data/examples/songs/greensleeves.sgc +++ b/patacrep/data/examples/songs/greensleeves.sgc @@ -1,7 +1,9 @@ {language : english} {columns : 2} { title : Greensleeves} +{ title : Un sous titre} {artist: Traditionnel} +{artist: Prénom Nom} {cover : traditionnel } {album :Angleterre} @@ -10,7 +12,7 @@ A[Am]las, my love, ye [G]do me wrong To [Am]cast me oft dis[E]curteously -And [Am]{I have} loved [G]you so long +And [Am]I have loved [G]you so long De[Am]lighting [E]in your [Am]companie {start_of_chorus} @@ -35,8 +37,15 @@ The [Am]cloth so fine as [E]fine might be I [Am]gave thee jewels [G]for thy chest And [Am]all this [E]cost I [Am]spent on thee +{c:test of comment} + +{gc: test of guitar comment} + +{image: traditionnel} Thy [Am]smock of silke, both [G]faire and white With [Am]gold embrodered [E]gorgeously Thy [Am]peticote of [G]sendall right -And [Am]this I [E]bought thee [Am]gladly \ No newline at end of file +And [Am]this I [E]bought thee [Am]gladly + + diff --git a/patacrep/songs/__init__.py b/patacrep/songs/__init__.py index e8931d34..c85e935a 100644 --- a/patacrep/songs/__init__.py +++ b/patacrep/songs/__init__.py @@ -8,7 +8,7 @@ import os import pickle import re -from patacrep.authors import processauthors +from patacrep.authors import process_listauthors from patacrep import files, encoding LOGGER = logging.getLogger(__name__) @@ -130,13 +130,12 @@ class Song(Content): )) # Data extraction from the latex song - self.data = self.DEFAULT_DATA - self.data['@path'] = self.fullpath - self.data.update(self.parse( - encoding.open_read(self.fullpath).read() - )) - self.titles = self.data['@titles'] - self.languages = self.data['@languages'] + self.titles = [] + self.data = {} + self.cached = None + self.parse(config) + + # Post processing of data self.datadir = datadir self.unprefixed_titles = [ unprefixed_title( @@ -146,17 +145,12 @@ class Song(Content): for title in self.titles ] - self.subpath = subpath - self.authors = processauthors( + self.authors = process_listauthors( self.authors, **config["_compiled_authwords"] ) # Cache management - - #: Special attribute to allow plugins to store cached data - self.cached = None - self._version = self.CACHE_VERSION self._write_cache() @@ -181,11 +175,24 @@ class Song(Content): Arguments: - output: Name of the output file. """ - return NotImplementedError() - - def parse(self, content): # pylint: disable=no-self-use, unused-argument - """Parse song, and return a dictionary of its data.""" - return NotImplementedError() + raise NotImplementedError() + + def parse(self, config): # pylint: disable=no-self-use + """Parse song. + + It set the following attributes: + + - titles: the list of (raw) titles. This list will be processed to + remove prefixes. + - languages: the list of languages used in the song, as languages + recognized by the LaTeX babel package. + - authors: the list of (raw) authors. This list will be processed to + 'clean' it (see function :func:`patacrep.authors.processauthors`). + - data: song metadata. Used (among others) to sort the songs. + - cached: additional data that will be cached. Thus, data stored in + this attribute must be picklable. + """ + raise NotImplementedError() def unprefixed_title(title, prefixes): """Remove the first prefix of the list in the beginning of title (if any). diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py index e76960ac..cd3f0d3b 100644 --- a/patacrep/songs/chordpro/__init__.py +++ b/patacrep/songs/chordpro/__init__.py @@ -1,21 +1,50 @@ +"""Chordpro parser""" -from patacrep import encoding +from jinja2 import Environment, FileSystemLoader +import pkg_resources +import os + +from patacrep import encoding, files from patacrep.songs import Song from patacrep.songs.chordpro.syntax import parse_song +from patacrep.templates import TexRenderer class ChordproSong(Song): """Chordpros song parser.""" - def parse(self): + def parse(self, config): """Parse content, and return the dictinory of song data.""" with encoding.open_read(self.fullpath, encoding=self.encoding) as song: - self.data = parse_song(song.read(), self.fullpath) - print(type(self.data), self.data) - import sys; sys.exit(1) - self.languages = self.data['@languages'] - del self.data['@languages'] - self.authors = self.data['by'] - del self.data['by'] + song = parse_song(song.read(), self.fullpath) + self.authors = song.authors + self.titles = song.titles + self.languages = song.get_directives('language') + self.data = dict([meta.as_tuple for meta in song.meta]) + self.cached = { + 'song': song, + } + #: Main language + self.language = song.get_directive('language', config['lang']) + + def tex(self, output): + context = { + 'language': self.language, + 'columns': self.cached['song'].get_directive('columns', 1), + "path": files.relpath(self.fullpath, os.path.dirname(output)), + "titles": r"\\".join(self.titles), + "authors": ", ".join(["{} {}".format(name[1], name[0]) for name in self.authors]), + "metadata": self.data, + "beginsong": self.cached['song'].meta_beginsong(), + "content": self.cached['song'].content, + } + return TexRenderer( + template="chordpro.tex", + encoding='utf8', + texenv=Environment(loader=FileSystemLoader(os.path.join( + os.path.abspath(pkg_resources.resource_filename(__name__, 'data')), + 'latex' + ))), + ).template.render(context) SONG_PARSERS = { 'sgc': ChordproSong, diff --git a/patacrep/songs/chordpro/ast.py b/patacrep/songs/chordpro/ast.py index 05affee6..47ffb8df 100644 --- a/patacrep/songs/chordpro/ast.py +++ b/patacrep/songs/chordpro/ast.py @@ -1,18 +1,37 @@ -# -*- coding: utf-8 -*- """Abstract Syntax Tree for ChordPro code.""" +# pylint: disable=too-few-public-methods + import functools +import logging +import os + +LOGGER = logging.getLogger() def _indent(string): + """Return and indented version of argument.""" return "\n".join([" {}".format(line) for line in string.split('\n')]) +#: List of properties that are to be displayed in the flow of the song (not as +#: metadata at the beginning or end of song. INLINE_PROPERTIES = { - "lilypond", + "partition", "comment", "guitar_comment", "image", } +#: List of properties that are listed in the `\beginsong` LaTeX directive. +BEGINSONG_PROPERTIES = { + "album", + "copyright", + "cov", + "vcov", + "tag", + } + +#: Some directive have alternative names. For instance `{title: Foo}` and `{t: +#: Foo}` are equivalent. DIRECTIVE_SHORTCUTS = { "t": "title", "st": "subtitle", @@ -20,25 +39,40 @@ DIRECTIVE_SHORTCUTS = { "by": "artist", "c": "comment", "gc": "guitar_comment", + "cover": "cov", + "vcover": "vcov", } def directive_name(text): - if text in DIRECTIVE_SHORTCUTS: - return DIRECTIVE_SHORTCUTS[text] - return text + """Return name of the directive, considering eventual shortcuts.""" + return DIRECTIVE_SHORTCUTS.get(text, text) class AST: + """Generic object representing elements of the song.""" + _template = None inline = False + def template(self, extension): + """Return the template to be used to render this object.""" + if self._template is None: + LOGGER.warning("No template defined for {}.".format(self.__class__)) + base = "error" + else: + base = self._template + return "content_{}.{}".format(base, extension) + class Line(AST): """A line is a sequence of (possibly truncated) words, spaces and chords.""" + _template = "line" + def __init__(self): super().__init__() self.line = [] def prepend(self, data): + """Add an object at the beginning of line.""" self.line.insert(0, data) return self @@ -46,6 +80,7 @@ class Line(AST): return "".join([str(item) for item in self.line]) def strip(self): + """Remove spaces at the beginning and end of line.""" while True: if not self.line: return self @@ -63,6 +98,7 @@ class LineElement(AST): class Word(LineElement): """A chunk of word.""" + _template = "word" def __init__(self, value): super().__init__() @@ -73,6 +109,7 @@ class Word(LineElement): class Space(LineElement): """A space between words""" + _template = "space" def __init__(self): super().__init__() @@ -83,6 +120,8 @@ class Space(LineElement): class Chord(LineElement): """A chord.""" + _template = "chord" + def __init__(self, value): super().__init__() self.value = value @@ -92,59 +131,95 @@ class Chord(LineElement): class Verse(AST): """A verse (or bridge, or chorus)""" + _template = "verse" type = "verse" inline = True - def __init__(self, block=None): + def __init__(self): super().__init__() - self.lines = [] # TODO check block + self.lines = [] def prepend(self, data): + """Add data at the beginning of verse.""" self.lines.insert(0, data) return self def __str__(self): return '{{start_of_{type}}}\n{content}\n{{end_of_{type}}}'.format( - type = self.type, - content = _indent("\n".join([str(line) for line in self.lines])), + type=self.type, + content=_indent("\n".join([str(line) for line in self.lines])), ) class Chorus(Verse): + """Chorus""" type = 'chorus' class Bridge(Verse): + """Bridge""" type = 'bridge' class Song(AST): - """A song""" - + r"""A song + + Attributes: + - content: the song content, as a list of objects `foo` such that + `foo.inline` is True. + - titles: The list of titles + - language: The language (if set), None otherwise + - authors: The list of authors + - meta_beginsong: The list of directives that are to be set in the + `\beginsong{}` LaTeX directive. + - meta: Every other metadata. + """ + + #: Some directives are added to the song using special methods. METADATA_TYPE = { "title": "add_title", "subtitle": "add_subtitle", - "language": "add_language", "artist": "add_author", + "key": "add_key", } - def __init__(self): + #: Some directives have to be processed before being considered. + PROCESS_DIRECTIVE = { + "cov": "_process_relative", + "partition": "_process_relative", + "image": "_process_relative", + } + + def __init__(self, filename): super().__init__() self.content = [] self.meta = [] self._authors = [] self._titles = [] self._subtitles = [] - self._languages = set() + self._keys = [] + self.filename = filename def add(self, data): + """Add an element to the song""" + if isinstance(data, Directive): + # Some directives are preprocessed + name = directive_name(data.keyword) + if name in self.PROCESS_DIRECTIVE: + data = getattr(self, self.PROCESS_DIRECTIVE[name])(data) + if data is None: + # New line if not (self.content and isinstance(self.content[0], Newline)): self.content.insert(0, Newline()) elif isinstance(data, Line): + # Add a new line, maybe in the current verse. if not (self.content and isinstance(self.content[0], Verse)): self.content.insert(0, Verse()) self.content[0].prepend(data.strip()) elif data.inline: + # Add an object in the content of the song. self.content.insert(0, data) elif isinstance(data, Directive): + # Add a metadata directive. Some of them are added using special + # methods listed in ``METADATA_TYPE``. name = directive_name(data.keyword) if name in self.METADATA_TYPE: getattr(self, self.METADATA_TYPE[name])(*data.as_tuple) @@ -155,12 +230,13 @@ class Song(AST): return self def str_meta(self): + """Return an iterator over *all* metadata, as strings.""" for title in self.titles: yield "{{title: {}}}".format(title) - for language in sorted(self.languages): - yield "{{language: {}}}".format(language) for author in self.authors: yield "{{by: {}}}".format(author) + for key in sorted(self.keys): + yield "{{key: {}}}".format(str(key)) for key in sorted(self.meta): yield str(key) @@ -175,32 +251,84 @@ class Song(AST): def add_title(self, __ignored, title): + """Add a title""" self._titles.insert(0, title) def add_subtitle(self, __ignored, title): + """Add a subtitle""" self._subtitles.insert(0, title) @property def titles(self): + """Return the list of titles (and subtitles).""" return self._titles + self._subtitles def add_author(self, __ignored, title): + """Add an auhor.""" self._authors.insert(0, title) @property def authors(self): + """Return the list of (raw) authors.""" return self._authors - def add_language(self, __ignored, language): - self._languages.add(language) + def get_directive(self, key, default=None): + """Return the first directive with a given key.""" + for directive in self.meta: + if directive.keyword == directive_name(key): + return directive.argument + return default + + def get_directives(self, key): + """Return the list of directives with a given key.""" + values = [] + for directive in self.meta: + if directive.keyword == directive_name(key): + values.append(directive.argument) + return values + + def add_key(self, __ignored, argument): + """Add a new {key: foo: bar} directive.""" + key, *argument = argument.split(":") + self._keys.append(Directive( + key.strip(), + ":".join(argument).strip(), + )) @property - def languages(self): - return self._languages - - + def keys(self): + """Return the list of keys. + + That is, directive that where given of the form ``{key: foo: bar}``. + """ + return self._keys + + def meta_beginsong(self): + r"""Return the meta information to be put in \beginsong.""" + for directive in BEGINSONG_PROPERTIES: + if self.get_directive(directive) is not None: + yield (directive, self.get_directive(directive)) + for (key, value) in self.keys: + yield (key, value) + + + def _process_relative(self, directive): + """Return the directive, in which the argument is given relative to file + + This argument is expected to be a path (as a string). + """ + return Directive( + directive.keyword, + os.path.join( + os.path.dirname(self.filename), + directive.argument, + ), + ) class Newline(AST): + """New line""" + _template = "newline" + def __str__(self): return "" @@ -208,22 +336,38 @@ class Newline(AST): class Directive(AST): """A directive""" - def __init__(self): + def __init__(self, keyword="", argument=None): super().__init__() - self.keyword = "" - self.argument = None + self._keyword = None + self.keyword = keyword + self.argument = argument + + @property + def _template(self): + """Name of the template to use to render this keyword. + + This only applies if ``self.inline == True`` + """ + return self.keyword @property def keyword(self): + """Keyword of the directive.""" return self._keyword @property def inline(self): + """True iff this directive is to be rendered in the flow on the song. + """ return self.keyword in INLINE_PROPERTIES @keyword.setter def keyword(self, value): - self._keyword = value.strip() + """self.keyword setter + + Replace keyword by its canonical name if it is a shortcut. + """ + self._keyword = directive_name(value.strip()) def __str__(self): if self.argument is not None: @@ -236,6 +380,7 @@ class Directive(AST): @property def as_tuple(self): + """Return the directive as a tuple.""" return (self.keyword, self.argument) def __eq__(self, other): @@ -254,6 +399,7 @@ class Tab(AST): self.content = [] def prepend(self, data): + """Add an element at the beginning of content.""" self.content.insert(0, data) return self diff --git a/patacrep/songs/chordpro/data/latex/chordpro.tex b/patacrep/songs/chordpro/data/latex/chordpro.tex new file mode 100644 index 00000000..77784a67 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/chordpro.tex @@ -0,0 +1,27 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% ((path)) + +(* if language is defined *) +\selectlanguage{((language))} +(* endif *) +\songcolumns{((metadata.columns))} + +\beginsong{((titles))}[ + by={((authors))}, + (* for (key, argument) in beginsong *) + ((key))={((argument))}, + (* endfor *) + ] + + (* if (metadata.cov is defined) or (metadata.vcov is defined) *) + \cover + (* endif *) + + (* for content in content *) + (* include content.template("tex") *) + (* endfor *) + +\endsong +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + diff --git a/patacrep/songs/chordpro/data/latex/content_chord.tex b/patacrep/songs/chordpro/data/latex/content_chord.tex new file mode 100644 index 00000000..bd2bd8b8 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_chord.tex @@ -0,0 +1 @@ +\[(( content.value ))] diff --git a/patacrep/songs/chordpro/data/latex/content_comment.tex b/patacrep/songs/chordpro/data/latex/content_comment.tex new file mode 100644 index 00000000..46fb1f24 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_comment.tex @@ -0,0 +1 @@ +\textnote{(( content.argument ))} diff --git a/patacrep/songs/chordpro/data/latex/content_error.tex b/patacrep/songs/chordpro/data/latex/content_error.tex new file mode 100644 index 00000000..a41faeb5 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_error.tex @@ -0,0 +1,3 @@ + +ERROR : Template not found for \verb+(( content.__class__))+. See the logs for details. + diff --git a/patacrep/songs/chordpro/data/latex/content_guitar_comment.tex b/patacrep/songs/chordpro/data/latex/content_guitar_comment.tex new file mode 100644 index 00000000..29c40ebd --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_guitar_comment.tex @@ -0,0 +1 @@ +\musicnote{(( content.argument ))} diff --git a/patacrep/songs/chordpro/data/latex/content_image.tex b/patacrep/songs/chordpro/data/latex/content_image.tex new file mode 100644 index 00000000..336233d5 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_image.tex @@ -0,0 +1 @@ +\image{(( content.argument ))} diff --git a/patacrep/songs/chordpro/data/latex/content_line.tex b/patacrep/songs/chordpro/data/latex/content_line.tex new file mode 100644 index 00000000..9b0c6af1 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_line.tex @@ -0,0 +1 @@ +(* for content in content.line *)(* include content.template("tex") *)(* endfor *) diff --git a/patacrep/songs/chordpro/data/latex/content_newline.tex b/patacrep/songs/chordpro/data/latex/content_newline.tex new file mode 100644 index 00000000..139597f9 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_newline.tex @@ -0,0 +1,2 @@ + + diff --git a/patacrep/songs/chordpro/data/latex/content_partition.tex b/patacrep/songs/chordpro/data/latex/content_partition.tex new file mode 100644 index 00000000..98bfed57 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_partition.tex @@ -0,0 +1 @@ +\lilypond{((content.argument))} diff --git a/patacrep/songs/chordpro/data/latex/content_space.tex b/patacrep/songs/chordpro/data/latex/content_space.tex new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_space.tex @@ -0,0 +1 @@ + diff --git a/patacrep/songs/chordpro/data/latex/content_verse.tex b/patacrep/songs/chordpro/data/latex/content_verse.tex new file mode 100644 index 00000000..4b4cecbd --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_verse.tex @@ -0,0 +1,7 @@ +\begin{(( content.type ))} + (* for content in content.lines *) + (* include content.template("tex") *) + + (* endfor *) +\end{(( content.type ))} + diff --git a/patacrep/songs/chordpro/data/latex/content_word.tex b/patacrep/songs/chordpro/data/latex/content_word.tex new file mode 100644 index 00000000..d9dd7a30 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_word.tex @@ -0,0 +1 @@ +(( content.value )) diff --git a/patacrep/songs/chordpro/lexer.py b/patacrep/songs/chordpro/lexer.py index 726c24e4..58792fa2 100644 --- a/patacrep/songs/chordpro/lexer.py +++ b/patacrep/songs/chordpro/lexer.py @@ -26,6 +26,7 @@ tokens = ( class ChordProLexer: """ChordPro Lexer class""" + # pylint: disable=too-many-public-methods tokens = tokens @@ -44,18 +45,23 @@ class ChordProLexer: t_directive_KEYWORD = r'[a-zA-Z_]+' t_directiveargument_TEXT = r'[^}]+' - def t_SOC(self, token): + @staticmethod + def t_SOC(token): r'{(soc|start_of_chorus)}' return token - def t_EOC(self, token): + + @staticmethod + def t_EOC(token): r'{(eoc|end_of_chorus)}' return token - def t_SOB(self, token): + @staticmethod + def t_SOB(token): r'{(sob|start_of_bridge)}' return token - def t_EOB(self, token): + @staticmethod + def t_EOB(token): r'{(eob|end_of_bridge)}' return token @@ -69,7 +75,8 @@ class ChordProLexer: self.lexer.pop_state() return token - def t_tablature_SPACE(self, token): + @staticmethod + def t_tablature_SPACE(token): r'[ \t]+' return token @@ -96,11 +103,11 @@ class ChordProLexer: r'[^{}\n\][\t ]+' return token - def t_LBRACKET(self, token): + def t_LBRACKET(self, __token): r'\[' self.lexer.push_state('chord') - def t_chord_RBRACKET(self, token): + def t_chord_RBRACKET(self, __token): r'\]' self.lexer.pop_state() @@ -149,6 +156,6 @@ class ChordProLexer: LOGGER.error("Illegal character '{}' in directive..".format(token.value[0])) token.lexer.skip(1) - @staticmethod - def t_directiveargument_error(token): - return t_directive_error(token) + def t_directiveargument_error(self, token): + """Manage errors""" + return self.t_directive_error(token) diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py index 32fa2c42..8a964564 100644 --- a/patacrep/songs/chordpro/syntax.py +++ b/patacrep/songs/chordpro/syntax.py @@ -6,7 +6,6 @@ import ply.yacc as yacc from patacrep.errors import SongbookError from patacrep.songs.chordpro import ast -from patacrep.songs.chordpro import ast from patacrep.songs.chordpro.lexer import tokens, ChordProLexer LOGGER = logging.getLogger() @@ -50,14 +49,13 @@ class Parser: ) ) - @staticmethod - def p_song(symbols): + def p_song(self, symbols): """song : block song | empty """ #if isinstance(symbols[1], str): if len(symbols) == 2: - symbols[0] = ast.Song() + symbols[0] = ast.Song(self.filename) else: symbols[0] = symbols[2].add(symbols[1]) @@ -249,19 +247,13 @@ class Parser: """empty :""" symbols[0] = None -def lex_song(content): - # TODO delete - lex = ChordProLexer().lexer - lex.input(content) - while 1: - tok = lex.token() - if not tok: break - print(tok) - def parse_song(content, filename=None): """Parse song and return its metadata.""" - return yacc.yacc(module=Parser(filename)).parse( - content, + return yacc.yacc( + module=Parser(filename), debug=0, + write_tables=0, + ).parse( + content, lexer=ChordProLexer().lexer, ) diff --git a/patacrep/songs/chordpro/test/__init__.py b/patacrep/songs/chordpro/test/__init__.py index e69de29b..b73d2af8 100644 --- a/patacrep/songs/chordpro/test/__init__.py +++ b/patacrep/songs/chordpro/test/__init__.py @@ -0,0 +1 @@ +"""Test for chordpro parser""" diff --git a/patacrep/songs/chordpro/test/greensleeves.txt b/patacrep/songs/chordpro/test/greensleeves.txt index 38564149..069fbba8 100644 --- a/patacrep/songs/chordpro/test/greensleeves.txt +++ b/patacrep/songs/chordpro/test/greensleeves.txt @@ -1,13 +1,14 @@ {title: Greensleeves} {title: Un autre sous-titre} {title: Un sous titre} -{language: english} {by: Traditionnel} {album: Angleterre} {columns: 2} -{cover: traditionnel} -{partition: greensleeves.ly} +{cov: DIRNAME/traditionnel} +{language: english} ======== +{partition: DIRNAME/greensleeves.ly} + {start_of_verse} A[Am]las, my love, ye [G]do me wrong To [Am]cast me oft dis[E]curteously diff --git a/patacrep/songs/chordpro/test/metadata.sgc b/patacrep/songs/chordpro/test/metadata.sgc index c892faf0..eee1bf9d 100644 --- a/patacrep/songs/chordpro/test/metadata.sgc +++ b/patacrep/songs/chordpro/test/metadata.sgc @@ -13,8 +13,8 @@ {cover: Cover} {vcover: VCover} {capo: Capo} -{foo: Foo} +{key: foo: Foo} {comment: Comment} {guitar_comment: GuitarComment} {image: Image} -{lilypond: Lilypond} +{partition: Lilypond} diff --git a/patacrep/songs/chordpro/test/metadata.txt b/patacrep/songs/chordpro/test/metadata.txt index 8d73f4b8..1ba5f5c6 100644 --- a/patacrep/songs/chordpro/test/metadata.txt +++ b/patacrep/songs/chordpro/test/metadata.txt @@ -4,18 +4,18 @@ {title: Subtitle3} {title: Subtitle4} {title: Subtitle5} -{language: english} -{language: french} {by: Author1} {by: Author2} +{key: {foo: Foo}} {album: Albom} {capo: Capo} {copyright: Copyright} -{cover: Cover} -{foo: Foo} -{vcover: VCover} +{cov: DIRNAME/Cover} +{language: english} +{language: french} +{vcov: VCover} ======== {comment: Comment} {guitar_comment: GuitarComment} -{image: Image} -{lilypond: Lilypond} +{image: DIRNAME/Image} +{partition: DIRNAME/Lilypond} diff --git a/patacrep/songs/chordpro/test/test_parser.py b/patacrep/songs/chordpro/test/test_parser.py index 85ad4108..2743522e 100644 --- a/patacrep/songs/chordpro/test/test_parser.py +++ b/patacrep/songs/chordpro/test/test_parser.py @@ -1,36 +1,56 @@ +"""Tests for the chordpro parser.""" + +# pylint: disable=too-few-public-methods + import glob import os import unittest from patacrep.songs.chordpro import syntax as chordpro -class ParserTestCase(unittest.TestCase): +class ParserTxtRenderer(unittest.TestCase): + """Test parser, and renderer as a txt file.""" + + maxDiff = None + + def __init__(self, methodname="runTest", basename=None): + super().__init__(methodname) + self.basename = basename + + def shortDescription(self): + return "Parsing file '{}.txt'.".format(self.basename) + + def runTest(self): + """Test txt output (default, debug output).""" + # pylint: disable=invalid-name + + if self.basename is None: + return + with open("{}.sgc".format(self.basename), 'r', encoding='utf8') as sourcefile: + with open("{}.txt".format(self.basename), 'r', encoding='utf8') as expectfile: + #print(os.path.basename(sourcefile.name)) + #with open("{}.txt.diff".format(self.basename), 'w', encoding='utf8') as difffile: + # difffile.write( + # str(chordpro.parse_song( + # sourcefile.read(), + # os.path.basename(sourcefile.name), + # )).strip() + # ) + # sourcefile.seek(0) + self.assertMultiLineEqual( + str(chordpro.parse_song( + sourcefile.read(), + os.path.abspath(sourcefile.name), + )).strip(), + expectfile.read().strip().replace("DIRNAME", os.path.dirname(self.basename)), + ) - def test_txt(self): - for txt in sorted(glob.glob(os.path.join( - os.path.dirname(__file__), - '*.txt', +def load_tests(__loader, tests, __pattern): + """Load several tests given test files present in the directory.""" + # Load all txt files as tests + for txt in sorted(glob.glob(os.path.join( + os.path.dirname(__file__), + '*.txt', ))): - basename = txt[:-len('.txt')] - with open("{}.sgc".format(basename), 'r', encoding='utf8') as sourcefile: - with open("{}.txt".format(basename), 'r', encoding='utf8') as expectfile: - #print(os.path.basename(sourcefile.name)) - #with open("{}.txt.diff".format(basename), 'w', encoding='utf8') as difffile: - # difffile.write( - # str(chordpro.parse_song( - # sourcefile.read(), - # os.path.basename(sourcefile.name), - # )).strip() - # ) - # sourcefile.seek(0) - self.assertMultiLineEqual( - str(chordpro.parse_song( - sourcefile.read(), - os.path.basename(sourcefile.name), - )).strip(), - expectfile.read().strip(), - ) - - def test_tex(self): - # TODO - pass + tests.addTest(ParserTxtRenderer(basename=txt[:-len('.txt')])) + return tests diff --git a/patacrep/songs/latex/__init__.py b/patacrep/songs/latex/__init__.py index 93fbccc7..bbab88cc 100644 --- a/patacrep/songs/latex/__init__.py +++ b/patacrep/songs/latex/__init__.py @@ -14,7 +14,7 @@ from patacrep.songs import Song class LatexSong(Song): """LaTeX song parser.""" - def parse(self, content): + def parse(self, __config): """Parse content, and return the dictinory of song data.""" with encoding.open_read(self.fullpath, encoding=self.encoding) as song: self.data = parse_song(song.read(), self.fullpath) diff --git a/patacrep/templates.py b/patacrep/templates.py index ed48529f..6849ad36 100644 --- a/patacrep/templates.py +++ b/patacrep/templates.py @@ -64,9 +64,29 @@ def _escape_tex(value): return newval -class TexRenderer(object): +class TexRenderer: """Render a template to a LaTeX file.""" + def __init__(self, template, texenv, encoding=None): + self.encoding = encoding + self.texenv = texenv + self.texenv.block_start_string = '(*' + self.texenv.block_end_string = '*)' + self.texenv.variable_start_string = '((' + self.texenv.variable_end_string = '))' + self.texenv.comment_start_string = '(% comment %)' + self.texenv.comment_end_string = '(% endcomment %)' + self.texenv.line_comment_prefix = '%!' + self.texenv.filters['escape_tex'] = _escape_tex + self.texenv.trim_blocks = True + self.texenv.lstrip_blocks = True + self.texenv.globals["path2posix"] = files.path2posix + self.template = self.texenv.get_template(template) + + +class TexBookRenderer(TexRenderer): + """Tex renderer for the whole songbook""" + def __init__(self, template, datadirs, lang, encoding=None): '''Start a new jinja2 environment for .tex creation. @@ -78,29 +98,15 @@ class TexRenderer(object): - encoding: if set, encoding of the template. ''' self.lang = lang - self.encoding = encoding # Load templates in filesystem ... loaders = [FileSystemLoader(os.path.join(datadir, 'templates')) for datadir in datadirs] - self.texenv = Environment( + texenv = Environment( loader=ChoiceLoader(loaders), extensions=[VariablesExtension], ) - self.texenv.block_start_string = '(*' - self.texenv.block_end_string = '*)' - self.texenv.variable_start_string = '((' - self.texenv.variable_end_string = '))' - self.texenv.comment_start_string = '(% comment %)' - self.texenv.comment_end_string = '(% endcomment %)' - self.texenv.line_comment_prefix = '%!' - self.texenv.filters['escape_tex'] = _escape_tex - self.texenv.trim_blocks = True - self.texenv.lstrip_blocks = True - - self.texenv.globals["path2posix"] = files.path2posix - try: - self.template = self.texenv.get_template(template) + super().__init__(template, texenv, encoding) except TemplateNotFound as exception: # Only works if all loaders are FileSystemLoader(). paths = [ diff --git a/patacrep/test.py b/patacrep/test.py new file mode 100644 index 00000000..ce135fef --- /dev/null +++ b/patacrep/test.py @@ -0,0 +1,26 @@ +"""Tests""" + +import doctest +import os +import unittest + +import patacrep + +def suite(): + """Return a TestSuite object, to test whole `patacrep` package. + + Both unittest and doctest are tested. + """ + test_loader = unittest.defaultTestLoader + return test_loader.discover(os.path.dirname(__file__)) + +def load_tests(__loader, tests, __pattern): + """Load tests (unittests and doctests).""" + # Loading doctests + tests.addTests(doctest.DocTestSuite(patacrep)) + + # Unittests are loaded by default + return tests + +if __name__ == "__main__": + unittest.TextTestRunner().run(suite()) diff --git a/setup.py b/setup.py index d153d49e..5a315fbd 100755 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ setup( "Programming Language :: Python :: 3.4", "Topic :: Utilities", ], - platforms=["GNU/Linux", "Windows", "MacOsX"] + platforms=["GNU/Linux", "Windows", "MacOsX"], + test_suite="patacrep.test.suite", long_description = open("README.rst", "r").read(), ) From 68892d83aab31d8dbd788fbc1160292ba6a4b023 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 29 Jan 2015 15:34:18 +0100 Subject: [PATCH 20/21] Deleting useless comments --- patacrep/songs/chordpro/syntax.py | 42 ------------------------------- 1 file changed, 42 deletions(-) diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py index 8a964564..68afc738 100644 --- a/patacrep/songs/chordpro/syntax.py +++ b/patacrep/songs/chordpro/syntax.py @@ -59,16 +59,6 @@ class Parser: else: symbols[0] = symbols[2].add(symbols[1]) - #@staticmethod - #def p_song_next(symbols): - # """song_next : block song_next - # | empty - # """ - # if len(symbols) == 2: - # symbols[0] = ast.Song() - # else: - # symbols[0] = symbols[2].add(symbols[1]) - @staticmethod def p_block(symbols): """block : SPACE block @@ -93,12 +83,6 @@ class Parser: """ symbols[0] = None - #@staticmethod - #def p_newlines(symbols): - # """newlines : NEWLINE newlines - # | empty""" - # symbols[0] = ('newlines') - @staticmethod def p_directive(symbols): """directive : LBRACE KEYWORD directive_next RBRACE @@ -157,22 +141,6 @@ class Parser: """chord : CHORD""" symbols[0] = ast.Chord(symbols[1]) - #@staticmethod - #def p_verse(symbols): - # """verse : line NEWLINE verse_next - # """ - # symbols[0] = symbols[3].prepend(symbols[1]) - - #@staticmethod - #def p_verse_next(symbols): - # """verse_next : line NEWLINE verse_next - # | empty - # """ - # if len(symbols) == 2: - # symbols[0] = ast.Verse() - # else: - # symbols[0] = symbols[3].prepend(symbols[1]) - @staticmethod def p_chorus(symbols): """chorus : SOC maybespace NEWLINE chorus_content EOC maybespace @@ -212,16 +180,6 @@ class Parser: symbols[0] = symbols[3].prepend(symbols[1]) - #@staticmethod - #def p_bridge_next(symbols): - # """bridge_next : line NEWLINE bridge_next - # | empty - # """ - # if len(symbols) == 2: - # symbols[0] = ast.Bridge() - # else: - # symbols[0] = symbols[3].prepend(symbols[1]) - @staticmethod def p_tab(symbols): """tab : SOT maybespace NEWLINE tab_content EOT maybespace From edf844f98d745c17687a3b246927ff09da896a27 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 29 Jan 2015 18:07:51 +0100 Subject: [PATCH 21/21] Corrections (due to problems with git merge) --- patacrep/content/song.py | 4 ++-- patacrep/files.py | 6 +++++- patacrep/songs/__init__.py | 6 ------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/patacrep/content/song.py b/patacrep/content/song.py index def0e643..e54f58c2 100755 --- a/patacrep/content/song.py +++ b/patacrep/content/song.py @@ -64,16 +64,16 @@ def parse(keyword, argument, contentlist, config): continue with files.chdir(songdir.datadir): for filename in glob.iglob(os.path.join(songdir.subpath, elem)): + LOGGER.debug('Parsing file "{}"…'.format(filename)) extension = filename.split(".")[-1] if extension not in plugins: LOGGER.warning( - 'File "{}" does not end with one of {}. Ignored.' + 'I do not know how to parse "{}": name does not end with one of {}. Ignored.' ).format( os.path.join(songdir.datadir, filename), ", ".join(["'.{}'".format(key) for key in plugins.keys()]), ) continue - LOGGER.debug('Parsing file "{}"…'.format(filename)) renderer = SongRenderer(plugins[extension]( songdir.datadir, filename, diff --git a/patacrep/files.py b/patacrep/files.py index a68146bf..a82c50b5 100644 --- a/patacrep/files.py +++ b/patacrep/files.py @@ -13,7 +13,11 @@ LOGGER = logging.getLogger(__name__) def recursive_find(root_directory, extensions): """Recursively find files with the given extensions, from a root_directory. - Return a list of files ending with one of the given extensions. + Return a list of files matching those conditions. + + Arguments: + - `extensions`: list of accepted extensions. + - `root_directory`: root directory of the search. """ if not os.path.isdir(root_directory): return [] diff --git a/patacrep/songs/__init__.py b/patacrep/songs/__init__.py index 3bddb86e..53da8d48 100644 --- a/patacrep/songs/__init__.py +++ b/patacrep/songs/__init__.py @@ -96,12 +96,6 @@ class Song(Content): "_version", ] - # Default data - DEFAULT_DATA = { - '@titles': [], - '@languages': [], - } - def __init__(self, datadir, subpath, config): self.fullpath = os.path.join(datadir, subpath) self.datadir = datadir