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:
# 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,13 @@ def processauthors(authors_string, after=None, ignore=None, sep=None):
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.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'],
@ -103,19 +103,13 @@ class Songbook(object):
# Loading custom plugins
config['_content_plugins'] = files.load_plugins(
datadirs=config.get('datadir', []),
subdir=['content'],
variable='CONTENT_PLUGINS',
error=(
"File {filename}: Keyword '{keyword}' is already used. Ignored."
),
root_modules=['content'],
keyword='CONTENT_PLUGINS',
)
config['_file_plugins'] = files.load_plugins(
config['_song_plugins'] = files.load_plugins(
datadirs=config.get('datadir', []),
subdir=['songs'],
variable='FILE_PLUGINS',
error=(
"File {filename}: Keyword '{keyword}' is already used. Ignored."
),
root_modules=['songs'],
keyword='SONG_PARSERS',
)
# Configuration set
@ -225,8 +219,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)

1
patacrep/content/__init__.py

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

62
patacrep/content/song.py

@ -1,15 +1,41 @@
"""Plugin to include songs to the songbook."""
import glob
import jinja2
import logging
import os
from patacrep.content import process_content, ContentError
from patacrep.content import process_content, ContentError, Content
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,15 +49,14 @@ def parse(keyword, argument, contentlist, config):
Return a list of Song() instances.
"""
plugins = config['_song_plugins']
if '_languages' not in config:
config['_languages'] = set()
songlist = []
plugins = config.get('_file_plugins', {})
for songdir in config['_songdir']:
if contentlist:
break
contentlist = files.recursive_find(songdir.fullpath, plugins.keys())
for elem in contentlist:
before = len(songlist)
for songdir in config['_songdir']:
@ -40,17 +65,22 @@ def parse(keyword, argument, contentlist, config):
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:
LOGGER.warning((
'I do not know how to parse file "{}". Ignored.'
).format(os.path.join(songdir.datadir, filename))
)
extension = filename.split(".")[-1]
if extension not in plugins:
LOGGER.warning(
'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
song = renderer(songdir.datadir, filename, config)
songlist.append(song)
config["_languages"].update(song.languages)
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 +99,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 (
@ -89,7 +119,7 @@ def process_songs(content, config=None):
item
for item
in contentlist
if not isinstance(item, Song)
if not isinstance(item, SongRenderer)
]
if 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.
"""
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":

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

@ -7,6 +7,7 @@
],
"booktype" : "chorded",
"lang" : "french",
"encoding": "utf8",
"authwords" : {
"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."""
from contextlib import contextmanager
import glob
import fnmatch
import importlib
import logging
import os
import posixpath
import re
import sys
LOGGER = logging.getLogger(__name__)
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.
@ -71,29 +70,35 @@ def chdir(path):
else:
yield
def load_plugins(datadirs, subdir, variable, error):
"""Load all content plugins, and return a dictionary of those plugins.
def load_plugins(datadirs, root_modules, keyword):
"""Load all 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
directories of `datadirs`. It contains a dictionary `keyword`. The return
value is the union of the dictionaries of the loaded 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.
- datadirs: List of directories in which plugins are to be searched.
- 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
os.path.join(datadir, "python", *root_modules)
for datadir in datadirs
]
+ [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 +107,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')])
)
for (key, value) in getattr(plugin, variable, {}).items():
if key in plugins:
LOGGER.warning(
error.format(
filename=relpath(name),
key=key,
)
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
del sys.path[-1]
continue
plugins[key] = value
return plugins

13
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

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
*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

13
patacrep/latex/syntax.py

@ -245,12 +245,17 @@ 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(
silent_yacc(module=Parser(filename)).parse(
string,
content,
lexer=SongLexer().lexer,
).metadata
)

59
patacrep/songs/__init__.py

@ -8,7 +8,8 @@ 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
LOGGER = logging.getLogger(__name__)
@ -99,6 +100,7 @@ class Song(Content):
self.fullpath = os.path.join(datadir, subpath)
self.datadir = datadir
self.encoding = config["encoding"]
self.config = config
if datadir:
# Only songs in datadirs are cached
@ -123,15 +125,15 @@ class Song(Content):
self.fullpath
))
# Default values
self.data = {}
# Data extraction from the latex song
self.titles = []
self.languages = []
self.authors = []
self.data = {}
self.cached = None
self.parse(config)
# Parsing and data processing
self.parse()
# Post processing of data
self.datadir = datadir
self.subpath = subpath
self.unprefixed_titles = [
unprefixed_title(
title,
@ -140,17 +142,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()
@ -169,32 +166,16 @@ 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 tex(self, output): # pylint: disable=no-self-use, unused-argument
"""Return the LaTeX code rendering this 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.
This function is to be defined by subclasses.
Arguments:
- output: Name of the output file.
"""
return ''
def parse(self):
"""Parse file `self.fullpath`.
raise NotImplementedError()
This function is to be defined by subclasses.
def parse(self, config): # pylint: disable=no-self-use
"""Parse song.
It set the following attributes:
@ -208,10 +189,7 @@ class Song(Content):
- cached: additional data that will be cached. Thus, data stored in
this attribute must be picklable.
"""
self.data = {}
self.titles = []
self.languages = []
self.authors = []
raise NotImplementedError()
def unprefixed_title(title, prefixes):
"""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:
return match.group(2)
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
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 = [

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",
"Topic :: Utilities",
],
platforms=["GNU/Linux", "Windows", "MacOsX"]
platforms=["GNU/Linux", "Windows", "MacOsX"],
test_suite="patacrep.test.suite",
long_description = open("README.rst", "r").read(),
)

Loading…
Cancel
Save