Browse Source

Merge pull request #70 from patacrep/chordpro

Nouveau format : chordpro
pull/76/head
Luthaf 10 years ago
parent
commit
968891c380
  1. 24
      patacrep/authors.py
  2. 23
      patacrep/build.py
  3. 1
      patacrep/content/__init__.py
  4. 62
      patacrep/content/song.py
  5. 3
      patacrep/content/sorted.py
  6. 1
      patacrep/data/examples/example-all.sb
  7. 51
      patacrep/data/examples/songs/greensleeves.sgc
  8. 80
      patacrep/files.py
  9. 13
      patacrep/index.py
  10. 15
      patacrep/latex/__init__.py
  11. 13
      patacrep/latex/syntax.py
  12. 59
      patacrep/songs/__init__.py
  13. 49
      patacrep/songs/chordpro/__init__.py
  14. 410
      patacrep/songs/chordpro/ast.py
  15. 27
      patacrep/songs/chordpro/data/latex/chordpro.tex
  16. 1
      patacrep/songs/chordpro/data/latex/content_chord.tex
  17. 1
      patacrep/songs/chordpro/data/latex/content_comment.tex
  18. 3
      patacrep/songs/chordpro/data/latex/content_error.tex
  19. 1
      patacrep/songs/chordpro/data/latex/content_guitar_comment.tex
  20. 1
      patacrep/songs/chordpro/data/latex/content_image.tex
  21. 1
      patacrep/songs/chordpro/data/latex/content_line.tex
  22. 2
      patacrep/songs/chordpro/data/latex/content_newline.tex
  23. 1
      patacrep/songs/chordpro/data/latex/content_partition.tex
  24. 1
      patacrep/songs/chordpro/data/latex/content_space.tex
  25. 7
      patacrep/songs/chordpro/data/latex/content_verse.tex
  26. 1
      patacrep/songs/chordpro/data/latex/content_word.tex
  27. 161
      patacrep/songs/chordpro/lexer.py
  28. 217
      patacrep/songs/chordpro/syntax.py
  29. 0
      patacrep/songs/chordpro/test/00.sgc
  30. 1
      patacrep/songs/chordpro/test/00.txt
  31. 1
      patacrep/songs/chordpro/test/01.sgc
  32. 4
      patacrep/songs/chordpro/test/01.txt
  33. 1
      patacrep/songs/chordpro/test/02.sgc
  34. 2
      patacrep/songs/chordpro/test/02.txt
  35. 4
      patacrep/songs/chordpro/test/03.sgc
  36. 1
      patacrep/songs/chordpro/test/03.txt
  37. 3
      patacrep/songs/chordpro/test/04.sgc
  38. 4
      patacrep/songs/chordpro/test/04.txt
  39. 3
      patacrep/songs/chordpro/test/05.sgc
  40. 4
      patacrep/songs/chordpro/test/05.txt
  41. 1
      patacrep/songs/chordpro/test/06.sgc
  42. 1
      patacrep/songs/chordpro/test/06.txt
  43. 3
      patacrep/songs/chordpro/test/07.sgc
  44. 4
      patacrep/songs/chordpro/test/07.txt
  45. 10
      patacrep/songs/chordpro/test/08.sgc
  46. 4
      patacrep/songs/chordpro/test/08.txt
  47. 15
      patacrep/songs/chordpro/test/09.sgc
  48. 5
      patacrep/songs/chordpro/test/09.txt
  49. 1
      patacrep/songs/chordpro/test/10.sgc
  50. 4
      patacrep/songs/chordpro/test/10.txt
  51. 1
      patacrep/songs/chordpro/test/11.sgc
  52. 4
      patacrep/songs/chordpro/test/11.txt
  53. 1
      patacrep/songs/chordpro/test/12.sgc
  54. 4
      patacrep/songs/chordpro/test/12.txt
  55. 5
      patacrep/songs/chordpro/test/13.sgc
  56. 6
      patacrep/songs/chordpro/test/13.txt
  57. 1
      patacrep/songs/chordpro/test/21.sgc
  58. 4
      patacrep/songs/chordpro/test/21.txt
  59. 1
      patacrep/songs/chordpro/test/22.sgc
  60. 2
      patacrep/songs/chordpro/test/22.txt
  61. 4
      patacrep/songs/chordpro/test/23.sgc
  62. 1
      patacrep/songs/chordpro/test/23.txt
  63. 3
      patacrep/songs/chordpro/test/24.sgc
  64. 4
      patacrep/songs/chordpro/test/24.txt
  65. 3
      patacrep/songs/chordpro/test/25.sgc
  66. 4
      patacrep/songs/chordpro/test/25.txt
  67. 1
      patacrep/songs/chordpro/test/26.sgc
  68. 1
      patacrep/songs/chordpro/test/26.txt
  69. 3
      patacrep/songs/chordpro/test/27.sgc
  70. 4
      patacrep/songs/chordpro/test/27.txt
  71. 10
      patacrep/songs/chordpro/test/28.sgc
  72. 4
      patacrep/songs/chordpro/test/28.txt
  73. 15
      patacrep/songs/chordpro/test/29.sgc
  74. 5
      patacrep/songs/chordpro/test/29.txt
  75. 1
      patacrep/songs/chordpro/test/30.sgc
  76. 1
      patacrep/songs/chordpro/test/__init__.py
  77. 44
      patacrep/songs/chordpro/test/greensleeves.sgc
  78. 52
      patacrep/songs/chordpro/test/greensleeves.txt
  79. 20
      patacrep/songs/chordpro/test/metadata.sgc
  80. 21
      patacrep/songs/chordpro/test/metadata.txt
  81. 56
      patacrep/songs/chordpro/test/test_parser.py
  82. 39
      patacrep/songs/latex/__init__.py
  83. 30
      patacrep/songs/tex.py
  84. 40
      patacrep/templates.py
  85. 26
      patacrep/test.py
  86. 3
      setup.py

24
patacrep/authors.py

@ -164,17 +164,21 @@ def processauthors(authors_string, after=None, ignore=None, sep=None):
For example, we are processing: For example, we are processing:
# processauthors( # processauthors(
# "Lyrics by William Blake (from Milton, 1808), # [
music by Hubert Parry (1916), # "
and sung by The Royal\ Choir~of~Nowhere # Lyrics by William Blake (from Milton, 1808),
(just here to show you how processing is done)", # music by Hubert Parry (1916),
# and sung by The Royal\ Choir~of~Nowhere
# (just here to show you how processing is done)
# ",
# ],
# after = ["by"], # after = ["by"],
# ignore = ["anonymous"], # ignore = ["anonymous"],
# sep = [re.compile('^(.*) and (.*)$')], # 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. 1) First, parenthesis (and its content) are removed.
# "Lyrics by William Blake, music by Hubert Parry, # "Lyrics by William Blake, music by Hubert Parry,
@ -220,3 +224,13 @@ def processauthors(authors_string, after=None, ignore=None, sep=None):
ignore) ignore)
) )
] ]
def process_listauthors(authors_list, after=None, ignore=None, sep=None):
"""Process a list of authors, and return the list of resulting authors."""
authors = []
for sublist in [
processauthors(string, after, ignore, sep)
for string in authors_list
]:
authors.extend(sublist)
return authors

23
patacrep/build.py

@ -10,7 +10,7 @@ from subprocess import Popen, PIPE, call
from patacrep import __DATADIR__, authors, content, errors, files from patacrep import __DATADIR__, authors, content, errors, files
from patacrep.index import process_sxd from patacrep.index import process_sxd
from patacrep.templates import TexRenderer from patacrep.templates import TexBookRenderer
from patacrep.songs import DataSubpath from patacrep.songs import DataSubpath
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -87,7 +87,7 @@ class Songbook(object):
# Updating configuration # Updating configuration
config = DEFAULT_CONFIG.copy() config = DEFAULT_CONFIG.copy()
config.update(self.config) config.update(self.config)
renderer = TexRenderer( renderer = TexBookRenderer(
config['template'], config['template'],
config['datadir'], config['datadir'],
config['lang'], config['lang'],
@ -103,19 +103,13 @@ class Songbook(object):
# Loading custom plugins # Loading custom plugins
config['_content_plugins'] = files.load_plugins( config['_content_plugins'] = files.load_plugins(
datadirs=config.get('datadir', []), datadirs=config.get('datadir', []),
subdir=['content'], root_modules=['content'],
variable='CONTENT_PLUGINS', keyword='CONTENT_PLUGINS',
error=(
"File {filename}: Keyword '{keyword}' is already used. Ignored."
),
) )
config['_file_plugins'] = files.load_plugins( config['_song_plugins'] = files.load_plugins(
datadirs=config.get('datadir', []), datadirs=config.get('datadir', []),
subdir=['songs'], root_modules=['songs'],
variable='FILE_PLUGINS', keyword='SONG_PARSERS',
error=(
"File {filename}: Keyword '{keyword}' is already used. Ignored."
),
) )
# Configuration set # Configuration set
@ -225,8 +219,9 @@ class SongbookBuilder(object):
stdin=PIPE, stdin=PIPE,
stdout=PIPE, stdout=PIPE,
stderr=PIPE, stderr=PIPE,
env=os.environ,
universal_newlines=True, universal_newlines=True,
env=os.environ) )
except Exception as error: except Exception as error:
LOGGER.debug(error) LOGGER.debug(error)
raise errors.LatexCompilationError(self.basename) raise errors.LatexCompilationError(self.basename)

1
patacrep/content/__init__.py

@ -174,7 +174,6 @@ def process_content(content, config=None):
""" """
contentlist = [] contentlist = []
plugins = config.get('_content_plugins', {}) plugins = config.get('_content_plugins', {})
keyword_re = re.compile(r'^ *(?P<keyword>\w*) *(\((?P<argument>.*)\))? *$') keyword_re = re.compile(r'^ *(?P<keyword>\w*) *(\((?P<argument>.*)\))? *$')
if not content: if not content:
content = [["song"]] content = [["song"]]

62
patacrep/content/song.py

@ -1,15 +1,41 @@
"""Plugin to include songs to the songbook.""" """Plugin to include songs to the songbook."""
import glob import glob
import jinja2
import logging import logging
import os import os
from patacrep.content import process_content, ContentError from patacrep.content import process_content, ContentError, Content
from patacrep import files, errors from patacrep import files, errors
from patacrep.songs import Song
LOGGER = logging.getLogger(__name__) 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 #pylint: disable=unused-argument
def parse(keyword, argument, contentlist, config): def parse(keyword, argument, contentlist, config):
"""Parse data associated with keyword 'song'. """Parse data associated with keyword 'song'.
@ -23,15 +49,14 @@ def parse(keyword, argument, contentlist, config):
Return a list of Song() instances. Return a list of Song() instances.
""" """
plugins = config['_song_plugins']
if '_languages' not in config: if '_languages' not in config:
config['_languages'] = set() config['_languages'] = set()
songlist = [] songlist = []
plugins = config.get('_file_plugins', {})
for songdir in config['_songdir']: for songdir in config['_songdir']:
if contentlist: if contentlist:
break break
contentlist = files.recursive_find(songdir.fullpath, plugins.keys()) contentlist = files.recursive_find(songdir.fullpath, plugins.keys())
for elem in contentlist: for elem in contentlist:
before = len(songlist) before = len(songlist)
for songdir in config['_songdir']: for songdir in config['_songdir']:
@ -40,17 +65,22 @@ def parse(keyword, argument, contentlist, config):
with files.chdir(songdir.datadir): with files.chdir(songdir.datadir):
for filename in glob.iglob(os.path.join(songdir.subpath, elem)): for filename in glob.iglob(os.path.join(songdir.subpath, elem)):
LOGGER.debug('Parsing file "{}"'.format(filename)) LOGGER.debug('Parsing file "{}"'.format(filename))
try: extension = filename.split(".")[-1]
renderer = plugins[filename.split('.')[-1]] if extension not in plugins:
except KeyError: LOGGER.warning(
LOGGER.warning(( 'I do not know how to parse "{}": name does not end with one of {}. Ignored.'
'I do not know how to parse file "{}". Ignored.' ).format(
).format(os.path.join(songdir.datadir, filename)) os.path.join(songdir.datadir, filename),
) ", ".join(["'.{}'".format(key) for key in plugins.keys()]),
)
continue continue
song = renderer(songdir.datadir, filename, config) renderer = SongRenderer(plugins[extension](
songlist.append(song) songdir.datadir,
config["_languages"].update(song.languages) filename,
config,
))
songlist.append(renderer)
config["_languages"].update(renderer.song.languages)
if len(songlist) > before: if len(songlist) > before:
break break
if len(songlist) == before: if len(songlist) == before:
@ -69,8 +99,8 @@ CONTENT_PLUGINS = {'song': parse}
class OnlySongsError(ContentError): class OnlySongsError(ContentError):
"A list that should contain only songs also contain other type of content." "A list that should contain only songs also contain other type of content."
def __init__(self, not_songs): def __init__(self, not_songs):
super(OnlySongsError, self).__init__()
self.not_songs = not_songs self.not_songs = not_songs
super().__init__('song', str(self))
def __str__(self): def __str__(self):
return ( return (
@ -89,7 +119,7 @@ def process_songs(content, config=None):
item item
for item for item
in contentlist in contentlist
if not isinstance(item, Song) if not isinstance(item, SongRenderer)
] ]
if not_songs: if not_songs:
raise OnlySongsError(not_songs) raise OnlySongsError(not_songs)

3
patacrep/content/sorted.py

@ -40,8 +40,9 @@ def key_generator(sort):
- sort: the list of keys used to 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.""" """Return the list of values used to sort the song."""
song = songrenderer.song
songkey = [] songkey = []
for key in sort: for key in sort:
if key == "@title": if key == "@title":

1
patacrep/data/examples/example-all.sb

@ -7,6 +7,7 @@
], ],
"booktype" : "chorded", "booktype" : "chorded",
"lang" : "french", "lang" : "french",
"encoding": "utf8",
"authwords" : { "authwords" : {
"sep" : ["and", "et"] "sep" : ["and", "et"]
}, },

51
patacrep/data/examples/songs/greensleeves.sgc

@ -0,0 +1,51 @@
{language : english}
{columns : 2}
{ title : Greensleeves}
{ title : Un sous titre}
{artist: Traditionnel}
{artist: Prénom Nom}
{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
{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

80
patacrep/files.py

@ -1,18 +1,17 @@
"""File system utilities.""" """File system utilities."""
from contextlib import contextmanager from contextlib import contextmanager
import glob import fnmatch
import importlib import importlib
import logging import logging
import os import os
import posixpath import posixpath
import re import re
import sys
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
def recursive_find(root_directory, extensions): def recursive_find(root_directory, extensions):
"""Recursively find files with some extension, from a root_directory. """Recursively find files with the given extensions, from a root_directory.
Return a list of files matching those conditions. Return a list of files matching those conditions.
@ -71,29 +70,35 @@ def chdir(path):
else: else:
yield yield
def load_plugins(datadirs, subdir, variable, error): def load_plugins(datadirs, root_modules, keyword):
"""Load all content plugins, and return a dictionary of those plugins. """Load all plugins, and return a dictionary of those plugins.
A plugin is a .py file, submodule of `subdir`, located in one of the A plugin is a .py file, submodule of `subdir`, located in one of the
directories of `datadirs`. It contains a dictionary `variable`. The return directories of `datadirs`. It contains a dictionary `keyword`. The return
value is the union of the dictionaries of the loaded plugins. value is the union of the dictionaries of the loaded plugins.
Arguments: Arguments:
- datadirs: list of directories (as strings) in which files has to be - datadirs: List of directories in which plugins are to be searched.
searched. - root_modules: the submodule in which plugins are to be searched, as a
- subdir: modules (as a list of strings) files has to be submodules of list of modules (e.g. ["some", "deep", "module"] for
(e.g. if `subdir` is `['first', 'second']`, search files are of the form "some.deep.module").
`first/second/*.py`. - keyword: attribute containing plugin information.
- variable: Name of the variable holding the dictionary.
- error: Error message raised if a key appears several times. Return value: a dictionary where:
- keys are the keywords ;
- values are functions triggered when this keyword is met.
""" """
# pylint: disable=star-args
plugins = {} plugins = {}
directory_list = ( directory_list = (
[ [
os.path.join(datadir, "python", *subdir) #pylint: disable=star-args os.path.join(datadir, "python", *root_modules)
for datadir in datadirs for datadir in datadirs
] ]
+ [os.path.dirname(__file__)] + [os.path.join(
os.path.dirname(__file__),
*root_modules
)]
) )
for directory in directory_list: for directory in directory_list:
if not os.path.exists(directory): if not os.path.exists(directory):
@ -102,31 +107,26 @@ def load_plugins(datadirs, subdir, variable, error):
directory directory
) )
continue continue
sys.path.append(directory) for (dirpath, __ignored, filenames) in os.walk(directory):
for name in glob.glob(os.path.join(directory, *(subdir + ['*.py']))): modules = ["patacrep"] + root_modules
if name.endswith(".py") and os.path.basename(name) != "__init__.py": if os.path.relpath(dirpath, directory) != ".":
if directory == os.path.dirname(__file__): modules.extend(os.path.relpath(dirpath, directory).split("/"))
plugin = importlib.import_module( for name in filenames:
'patacrep.{}.{}'.format( if name == "__init__.py":
".".join(subdir), modulename = []
os.path.basename(name[:-len('.py')]) elif name.endswith(".py"):
) modulename = [name[:-len('.py')]]
)
else: else:
plugin = importlib.import_module( continue
os.path.basename(name[:-len('.py')]) plugin = importlib.import_module(".".join(modules + modulename))
) if hasattr(plugin, keyword):
for (key, value) in getattr(plugin, variable, {}).items(): for (key, value) in getattr(plugin, keyword).items():
if key in plugins: if key in plugins:
LOGGER.warning( LOGGER.warning(
error.format( "File %s: Keyword '%s' is already used. Ignored.",
filename=relpath(name), relpath(os.path.join(dirpath, name)),
key=key, key,
)
) )
continue continue
plugins[key] = value plugins[key] = value
del sys.path[-1]
return plugins return plugins

13
patacrep/index.py

@ -19,7 +19,6 @@ EOL = "\n"
KEYWORD_PATTERN = re.compile(r"^%(\w+)\s?(.*)$", re.LOCALE) KEYWORD_PATTERN = re.compile(r"^%(\w+)\s?(.*)$", re.LOCALE)
FIRST_LETTER_PATTERN = re.compile(r"^(?:\{?\\\w+\}?)*[^\w]*(\w)", re.LOCALE) FIRST_LETTER_PATTERN = re.compile(r"^(?:\{?\\\w+\}?)*[^\w]*(\w)", re.LOCALE)
def process_sxd(filename): def process_sxd(filename):
"""Parse sxd file. """Parse sxd file.
@ -162,7 +161,11 @@ class Index(object):
def entry_to_str(self, key, entry): def entry_to_str(self, key, entry):
"""Return the LaTeX code corresponding to the 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), self.key_to_str(key),
r'\\'.join([self.ref_to_str(ref) for ref in entry]), r'\\'.join([self.ref_to_str(ref) for ref in entry]),
) )
@ -182,13 +185,13 @@ class Index(object):
] ]
string = r'\begin{idxblock}{' + letter + '}' + EOL string = r'\begin{idxblock}{' + letter + '}' + EOL
for key in sorted(entries, key=sortkey): for key in sorted(entries, key=sortkey):
string += self.entry_to_str(key, entries[key]['entries']) string += " " + self.entry_to_str(key, entries[key]['entries'])
string += r'\end{idxblock}' + EOL string += EOL + r'\end{idxblock}'
return string return string
def entries_to_str(self): def entries_to_str(self):
"""Return the LaTeX code corresponding to the index.""" """Return the LaTeX code corresponding to the index."""
string = "" string = ""
for letter in sorted(self.data.keys()): 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 return string

15
patacrep/latex/__init__.py

@ -1,19 +1,8 @@
"""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 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 *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. will work on simple cases, but not on complex ones.
""" """
from patacrep.latex.syntax import tex2plain from patacrep.latex.syntax import tex2plain, parse_song
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

13
patacrep/latex/syntax.py

@ -245,12 +245,17 @@ def tex2plain(string):
) )
) )
def parsesong(string, filename=None): def parse_song(content, filename=None):
"""Parse song and return its metadata.""" """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( return detex(
silent_yacc(module=Parser(filename)).parse( silent_yacc(module=Parser(filename)).parse(
string, content,
lexer=SongLexer().lexer, lexer=SongLexer().lexer,
).metadata ).metadata
) )

59
patacrep/songs/__init__.py

@ -8,7 +8,8 @@ import os
import pickle import pickle
import re import re
from patacrep.authors import processauthors from patacrep.authors import process_listauthors
from patacrep import files, encoding
from patacrep.content import Content from patacrep.content import Content
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -99,6 +100,7 @@ class Song(Content):
self.fullpath = os.path.join(datadir, subpath) self.fullpath = os.path.join(datadir, subpath)
self.datadir = datadir self.datadir = datadir
self.encoding = config["encoding"] self.encoding = config["encoding"]
self.config = config
if datadir: if datadir:
# Only songs in datadirs are cached # Only songs in datadirs are cached
@ -123,15 +125,15 @@ class Song(Content):
self.fullpath self.fullpath
)) ))
# Default values # Data extraction from the latex song
self.data = {}
self.titles = [] self.titles = []
self.languages = [] self.data = {}
self.authors = [] self.cached = None
self.parse(config)
# Parsing and data processing # Post processing of data
self.parse()
self.datadir = datadir self.datadir = datadir
self.subpath = subpath
self.unprefixed_titles = [ self.unprefixed_titles = [
unprefixed_title( unprefixed_title(
title, title,
@ -140,17 +142,12 @@ class Song(Content):
for title for title
in self.titles in self.titles
] ]
self.subpath = subpath self.authors = process_listauthors(
self.authors = processauthors(
self.authors, self.authors,
**config["_compiled_authwords"] **config["_compiled_authwords"]
) )
# Cache management # Cache management
#: Special attribute to allow plugins to store cached data
self.cached = None
self._version = self.CACHE_VERSION self._version = self.CACHE_VERSION
self._write_cache() self._write_cache()
@ -169,32 +166,16 @@ class Song(Content):
def __repr__(self): def __repr__(self):
return repr((self.titles, self.data, self.fullpath)) return repr((self.titles, self.data, self.fullpath))
def begin_new_block(self, previous, __context): def tex(self, output): # pylint: disable=no-self-use, unused-argument
"""Return a boolean stating if a new block is to be created.""" """Return the LaTeX code rendering this song.
return not isinstance(previous, Song)
def begin_block(self, context): Arguments:
"""Return the string to begin a block.""" - output: Name of the output file.
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.
This function is to be defined by subclasses.
""" """
return '' raise NotImplementedError()
def parse(self):
"""Parse file `self.fullpath`.
This function is to be defined by subclasses. def parse(self, config): # pylint: disable=no-self-use
"""Parse song.
It set the following attributes: It set the following attributes:
@ -208,10 +189,7 @@ class Song(Content):
- cached: additional data that will be cached. Thus, data stored in - cached: additional data that will be cached. Thus, data stored in
this attribute must be picklable. this attribute must be picklable.
""" """
self.data = {} raise NotImplementedError()
self.titles = []
self.languages = []
self.authors = []
def unprefixed_title(title, prefixes): def unprefixed_title(title, prefixes):
"""Remove the first prefix of the list in the beginning of title (if any). """Remove the first prefix of the list in the beginning of title (if any).
@ -221,4 +199,3 @@ def unprefixed_title(title, prefixes):
if match: if match:
return match.group(2) return match.group(2)
return title return title

49
patacrep/songs/chordpro/__init__.py

@ -0,0 +1,49 @@
"""Chordpro parser"""
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, config):
"""Parse content, and return the dictinory of song data."""
with encoding.open_read(self.fullpath, encoding=self.encoding) as song:
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,
}
def tex(self, output):
context = {
'language': self.cached['song'].get_directive('language', self.config['lang']),
'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,
}

410
patacrep/songs/chordpro/ast.py

@ -0,0 +1,410 @@
"""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 = {
"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",
"a": "album",
"by": "artist",
"c": "comment",
"gc": "guitar_comment",
"cover": "cov",
"vcover": "vcov",
}
def directive_name(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
def __str__(self):
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
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."""
_template = "word"
def __init__(self, value):
super().__init__()
self.value = value
def __str__(self):
return self.value
class Space(LineElement):
"""A space between words"""
_template = "space"
def __init__(self):
super().__init__()
def __str__(self):
return " "
class Chord(LineElement):
"""A chord."""
_template = "chord"
def __init__(self, value):
super().__init__()
self.value = value
def __str__(self):
return "[{}]".format(self.value)
class Verse(AST):
"""A verse (or bridge, or chorus)"""
_template = "verse"
type = "verse"
inline = True
def __init__(self):
super().__init__()
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])),
)
class Chorus(Verse):
"""Chorus"""
type = 'chorus'
class Bridge(Verse):
"""Bridge"""
type = 'bridge'
class Song(AST):
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",
"artist": "add_author",
"key": "add_key",
}
#: 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._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)
else:
self.meta.append(data)
else:
raise Exception()
return self
def str_meta(self):
"""Return an iterator over *all* metadata, as strings."""
for title in self.titles:
yield "{{title: {}}}".format(title)
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)
def __str__(self):
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):
"""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 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 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 ""
@functools.total_ordering
class Directive(AST):
"""A directive"""
def __init__(self, keyword="", argument=None):
super().__init__()
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 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:
return "{{{}: {}}}".format(
self.keyword,
self.argument,
)
else:
return "{{{}}}".format(self.keyword)
@property
def as_tuple(self):
"""Return the directive as a tuple."""
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):
"""Add an element at the beginning of content."""
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)),
)

27
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
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

1
patacrep/songs/chordpro/data/latex/content_chord.tex

@ -0,0 +1 @@
\[(( content.value ))]

1
patacrep/songs/chordpro/data/latex/content_comment.tex

@ -0,0 +1 @@
\textnote{(( content.argument ))}

3
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.

1
patacrep/songs/chordpro/data/latex/content_guitar_comment.tex

@ -0,0 +1 @@
\musicnote{(( content.argument ))}

1
patacrep/songs/chordpro/data/latex/content_image.tex

@ -0,0 +1 @@
\image{(( content.argument ))}

1
patacrep/songs/chordpro/data/latex/content_line.tex

@ -0,0 +1 @@
(* for content in content.line *)(* include content.template("tex") *)(* endfor *)

2
patacrep/songs/chordpro/data/latex/content_newline.tex

@ -0,0 +1,2 @@

1
patacrep/songs/chordpro/data/latex/content_partition.tex

@ -0,0 +1 @@
\lilypond{((content.argument))}

1
patacrep/songs/chordpro/data/latex/content_space.tex

@ -0,0 +1 @@

7
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 ))}

1
patacrep/songs/chordpro/data/latex/content_word.tex

@ -0,0 +1 @@
(( content.value ))

161
patacrep/songs/chordpro/lexer.py

@ -0,0 +1,161 @@
"""ChordPro lexer"""
import logging
import ply.lex as lex
LOGGER = logging.getLogger()
#pylint: disable=invalid-name
tokens = (
'LBRACE',
'RBRACE',
'CHORD',
'NEWLINE',
'COLON',
'WORD',
'SPACE',
'TEXT',
'KEYWORD',
'SOC',
'EOC',
'SOB',
'EOB',
'SOT',
'EOT',
)
class ChordProLexer:
"""ChordPro Lexer class"""
# pylint: disable=too-many-public-methods
tokens = tokens
states = (
('chord', 'exclusive'),
('directive', 'exclusive'),
('directiveargument', 'exclusive'),
('tablature', 'exclusive'),
)
t_SPACE = r'[ \t]+'
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'[^}]+'
@staticmethod
def t_SOC(token):
r'{(soc|start_of_chorus)}'
return token
@staticmethod
def t_EOC(token):
r'{(eoc|end_of_chorus)}'
return token
@staticmethod
def t_SOB(token):
r'{(sob|start_of_bridge)}'
return token
@staticmethod
def t_EOB(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
@staticmethod
def t_tablature_SPACE(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)
# 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_WORD(token):
r'[^{}\n\][\t ]+'
return token
def t_LBRACKET(self, __token):
r'\['
self.lexer.push_state('chord')
def t_chord_RBRACKET(self, __token):
r'\]'
self.lexer.pop_state()
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):
"""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)
@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)
def t_directiveargument_error(self, token):
"""Manage errors"""
return self.t_directive_error(token)

217
patacrep/songs/chordpro/syntax.py

@ -0,0 +1,217 @@
# -*- coding: utf-8 -*-
"""ChordPro parser"""
import logging
import ply.yacc as yacc
from patacrep.errors import SongbookError
from patacrep.songs.chordpro import ast
from patacrep.songs.chordpro.lexer import tokens, ChordProLexer
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"""
start = "song"
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:
LOGGER.error("Error in file {}, line {}:{}.".format(
str(self.filename),
token.lineno,
self.__find_column(token),
)
)
def p_song(self, symbols):
"""song : block song
| empty
"""
#if isinstance(symbols[1], str):
if len(symbols) == 2:
symbols[0] = ast.Song(self.filename)
else:
symbols[0] = symbols[2].add(symbols[1])
@staticmethod
def p_block(symbols):
"""block : SPACE block
| directive NEWLINE
| line NEWLINE
| chorus NEWLINE
| tab NEWLINE
| bridge NEWLINE
| NEWLINE
"""
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_maybespace(symbols):
"""maybespace : SPACE
| empty
"""
symbols[0] = None
@staticmethod
def p_directive(symbols):
"""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
"""
symbols[0] = symbols[2].prepend(symbols[1])
@staticmethod
def p_line_next(symbols):
"""line_next : word line_next
| space line_next
| chord line_next
| empty
"""
if len(symbols) == 2:
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_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] = symbols[3].prepend(symbols[1])
@staticmethod
def p_bridge(symbols):
"""bridge : SOB maybespace NEWLINE bridge_content EOB maybespace
"""
symbols[0] = symbols[4]
@staticmethod
def p_bridge_content(symbols):
"""bridge_content : line NEWLINE bridge_content
| SPACE bridge_content
| empty
"""
if len(symbols) == 2:
symbols[0] = ast.Bridge()
elif len(symbols) == 3:
symbols[0] = symbols[2]
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
def parse_song(content, filename=None):
"""Parse song and return its metadata."""
return yacc.yacc(
module=Parser(filename),
debug=0,
write_tables=0,
).parse(
content,
lexer=ChordProLexer().lexer,
)

0
patacrep/songs/chordpro/test/00.sgc

1
patacrep/songs/chordpro/test/00.txt

@ -0,0 +1 @@
========

1
patacrep/songs/chordpro/test/01.sgc

@ -0,0 +1 @@
A verse line

4
patacrep/songs/chordpro/test/01.txt

@ -0,0 +1,4 @@
========
{start_of_verse}
A verse line
{end_of_verse}

1
patacrep/songs/chordpro/test/02.sgc

@ -0,0 +1 @@
{title : A directive}

2
patacrep/songs/chordpro/test/02.txt

@ -0,0 +1,2 @@
{title: A directive}
========

4
patacrep/songs/chordpro/test/03.sgc

@ -0,0 +1,4 @@

1
patacrep/songs/chordpro/test/03.txt

@ -0,0 +1 @@
========

3
patacrep/songs/chordpro/test/04.sgc

@ -0,0 +1,3 @@
{soc}
A one line chorus
{eoc}

4
patacrep/songs/chordpro/test/04.txt

@ -0,0 +1,4 @@
========
{start_of_chorus}
A one line chorus
{end_of_chorus}

3
patacrep/songs/chordpro/test/05.sgc

@ -0,0 +1,3 @@
{sob}
A one line bridge
{eob}

4
patacrep/songs/chordpro/test/05.txt

@ -0,0 +1,4 @@
========
{start_of_bridge}
A one line bridge
{end_of_bridge}

1
patacrep/songs/chordpro/test/06.sgc

@ -0,0 +1 @@
# A comment

1
patacrep/songs/chordpro/test/06.txt

@ -0,0 +1 @@
========

3
patacrep/songs/chordpro/test/07.sgc

@ -0,0 +1,3 @@
{sot}
A tab
{eot}

4
patacrep/songs/chordpro/test/07.txt

@ -0,0 +1,4 @@
========
{start_of_tab}
A tab
{end_of_tab}

10
patacrep/songs/chordpro/test/08.sgc

@ -0,0 +1,10 @@
# comment
# comment
A lot of new lines
# comment

4
patacrep/songs/chordpro/test/08.txt

@ -0,0 +1,4 @@
========
{start_of_verse}
A lot of new lines
{end_of_verse}

15
patacrep/songs/chordpro/test/09.sgc

@ -0,0 +1,15 @@
# comment
# comment
A lot of new lines
# comment
{title: and a directive}
# comment

5
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}

1
patacrep/songs/chordpro/test/10.sgc

@ -0,0 +1 @@
A line[A] with a chord

4
patacrep/songs/chordpro/test/10.txt

@ -0,0 +1,4 @@
========
{start_of_verse}
A line[A] with a chord
{end_of_verse}

1
patacrep/songs/chordpro/test/11.sgc

@ -0,0 +1 @@
A line ending with a chord[A]

4
patacrep/songs/chordpro/test/11.txt

@ -0,0 +1,4 @@
========
{start_of_verse}
A line ending with a chord[A]
{end_of_verse}

1
patacrep/songs/chordpro/test/12.sgc

@ -0,0 +1 @@
[A]A line starting with a chord

4
patacrep/songs/chordpro/test/12.txt

@ -0,0 +1,4 @@
========
{start_of_verse}
[A]A line starting with a chord
{end_of_verse}

5
patacrep/songs/chordpro/test/13.sgc

@ -0,0 +1,5 @@
{sot}
A table
wit many # weir [
[ symbols
{eot}

6
patacrep/songs/chordpro/test/13.txt

@ -0,0 +1,6 @@
========
{start_of_tab}
A table
wit many # weir [
[ symbols
{end_of_tab}

1
patacrep/songs/chordpro/test/21.sgc

@ -0,0 +1 @@
A verse line

4
patacrep/songs/chordpro/test/21.txt

@ -0,0 +1,4 @@
========
{start_of_verse}
A verse line
{end_of_verse}

1
patacrep/songs/chordpro/test/22.sgc

@ -0,0 +1 @@
{title : A directive}

2
patacrep/songs/chordpro/test/22.txt

@ -0,0 +1,2 @@
{title: A directive}
========

4
patacrep/songs/chordpro/test/23.sgc

@ -0,0 +1,4 @@

1
patacrep/songs/chordpro/test/23.txt

@ -0,0 +1 @@
========

3
patacrep/songs/chordpro/test/24.sgc

@ -0,0 +1,3 @@
{soc}
A one line chorus
{eoc}

4
patacrep/songs/chordpro/test/24.txt

@ -0,0 +1,4 @@
========
{start_of_chorus}
A one line chorus
{end_of_chorus}

3
patacrep/songs/chordpro/test/25.sgc

@ -0,0 +1,3 @@
{sob}
A one line bridge
{eob}

4
patacrep/songs/chordpro/test/25.txt

@ -0,0 +1,4 @@
========
{start_of_bridge}
A one line bridge
{end_of_bridge}

1
patacrep/songs/chordpro/test/26.sgc

@ -0,0 +1 @@
# A comment

1
patacrep/songs/chordpro/test/26.txt

@ -0,0 +1 @@
========

3
patacrep/songs/chordpro/test/27.sgc

@ -0,0 +1,3 @@
{sot}
A tab
{eot}

4
patacrep/songs/chordpro/test/27.txt

@ -0,0 +1,4 @@
========
{start_of_tab}
A tab
{end_of_tab}

10
patacrep/songs/chordpro/test/28.sgc

@ -0,0 +1,10 @@
# comment
# comment
A lot of new lines
# comment

4
patacrep/songs/chordpro/test/28.txt

@ -0,0 +1,4 @@
========
{start_of_verse}
A lot of new lines
{end_of_verse}

15
patacrep/songs/chordpro/test/29.sgc

@ -0,0 +1,15 @@
# comment
# comment
A lot of new lines
# comment
{title: and a directive}
# comment

5
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}

1
patacrep/songs/chordpro/test/30.sgc

@ -0,0 +1 @@
[A]A line starting with a chord

1
patacrep/songs/chordpro/test/__init__.py

@ -0,0 +1 @@
"""Test for chordpro parser"""

44
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

52
patacrep/songs/chordpro/test/greensleeves.txt

@ -0,0 +1,52 @@
{title: Greensleeves}
{title: Un autre sous-titre}
{title: Un sous titre}
{by: Traditionnel}
{album: Angleterre}
{columns: 2}
{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
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}

20
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}
{key: foo: Foo}
{comment: Comment}
{guitar_comment: GuitarComment}
{image: Image}
{partition: Lilypond}

21
patacrep/songs/chordpro/test/metadata.txt

@ -0,0 +1,21 @@
{title: Title}
{title: Subtitle1}
{title: Subtitle2}
{title: Subtitle3}
{title: Subtitle4}
{title: Subtitle5}
{by: Author1}
{by: Author2}
{key: {foo: Foo}}
{album: Albom}
{capo: Capo}
{copyright: Copyright}
{cov: DIRNAME/Cover}
{language: english}
{language: french}
{vcov: VCover}
========
{comment: Comment}
{guitar_comment: GuitarComment}
{image: DIRNAME/Image}
{partition: DIRNAME/Lilypond}

56
patacrep/songs/chordpro/test/test_parser.py

@ -0,0 +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 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 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',
))):
tests.addTest(ParserTxtRenderer(basename=txt[:-len('.txt')]))
return tests

39
patacrep/songs/latex/__init__.py

@ -0,0 +1,39 @@
"""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, encoding
from patacrep.latex import parse_song
from patacrep.songs import Song
class LatexSong(Song):
"""LaTeX song parser."""
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)
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."""
return r'\input{{{}}}'.format(files.path2posix(
files.relpath(
self.fullpath,
os.path.dirname(output)
)))
SONG_PARSERS = {
'is': LatexSong,
'sg': LatexSong,
}

30
patacrep/songs/tex.py

@ -1,30 +0,0 @@
"""Very simple LaTeX parsing."""
import os
from patacrep import files
from patacrep.latex import parsesong
from patacrep.songs import Song
class TexRenderer(Song):
"""Renderer for song and intersong files."""
def parse(self):
"""Parse song and set metadata."""
self.data = parsesong(self.fullpath, self.encoding)
self.titles = self.data['@titles']
self.languages = self.data['@languages']
self.authors = self.data['by']
def render(self, context):
"""Return the string that will render the song."""
return r'\input{{{}}}'.format(files.path2posix(
files.relpath(
self.fullpath,
os.path.dirname(context['filename'])
)))
FILE_PLUGINS = {
'sg': TexRenderer,
'is': TexRenderer,
}

40
patacrep/templates.py

@ -64,9 +64,29 @@ def _escape_tex(value):
return newval return newval
class TexRenderer(object): class TexRenderer:
"""Render a template to a LaTeX file.""" """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): def __init__(self, template, datadirs, lang, encoding=None):
'''Start a new jinja2 environment for .tex creation. '''Start a new jinja2 environment for .tex creation.
@ -78,29 +98,15 @@ class TexRenderer(object):
- encoding: if set, encoding of the template. - encoding: if set, encoding of the template.
''' '''
self.lang = lang self.lang = lang
self.encoding = encoding
# Load templates in filesystem ... # Load templates in filesystem ...
loaders = [FileSystemLoader(os.path.join(datadir, 'templates')) loaders = [FileSystemLoader(os.path.join(datadir, 'templates'))
for datadir in datadirs] for datadir in datadirs]
self.texenv = Environment( texenv = Environment(
loader=ChoiceLoader(loaders), loader=ChoiceLoader(loaders),
extensions=[VariablesExtension], 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: try:
self.template = self.texenv.get_template(template) super().__init__(template, texenv, encoding)
except TemplateNotFound as exception: except TemplateNotFound as exception:
# Only works if all loaders are FileSystemLoader(). # Only works if all loaders are FileSystemLoader().
paths = [ paths = [

26
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())

3
setup.py

@ -38,6 +38,7 @@ setup(
"Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.4",
"Topic :: Utilities", "Topic :: Utilities",
], ],
platforms=["GNU/Linux", "Windows", "MacOsX"] platforms=["GNU/Linux", "Windows", "MacOsX"],
test_suite="patacrep.test.suite",
long_description = open("README.rst", "r").read(), long_description = open("README.rst", "r").read(),
) )

Loading…
Cancel
Save