mirror of https://github.com/patacrep/patacrep.git
Oliverpool
9 years ago
31 changed files with 843 additions and 152 deletions
@ -0,0 +1,103 @@ |
|||||
|
"""Raw songbook utilities""" |
||||
|
|
||||
|
import logging |
||||
|
import os |
||||
|
import sys |
||||
|
import yaml |
||||
|
|
||||
|
from patacrep import encoding |
||||
|
from patacrep.build import config_model |
||||
|
from patacrep.utils import DictOfDict |
||||
|
from patacrep.songs import DataSubpath |
||||
|
import patacrep |
||||
|
|
||||
|
LOGGER = logging.getLogger() |
||||
|
|
||||
|
def open_songbook(filename): |
||||
|
"""Open songbook, and return a raw songbook object. |
||||
|
|
||||
|
:param str filename: Filename of the yaml songbook. |
||||
|
:rvalue: dict |
||||
|
:return: Songbook, as a dictionary. |
||||
|
""" |
||||
|
if os.path.exists(filename + ".yaml") and not os.path.exists(filename): |
||||
|
filename += ".yaml" |
||||
|
|
||||
|
try: |
||||
|
with patacrep.encoding.open_read(filename) as songbook_file: |
||||
|
user_songbook = yaml.load(songbook_file) |
||||
|
if 'encoding' in user_songbook.get('book', []): |
||||
|
with encoding.open_read( |
||||
|
filename, |
||||
|
encoding=user_songbook['book']['encoding'] |
||||
|
) as songbook_file: |
||||
|
user_songbook = yaml.load(songbook_file) |
||||
|
except Exception as error: # pylint: disable=broad-except |
||||
|
raise patacrep.errors.SongbookError(str(error)) |
||||
|
|
||||
|
songbook = _add_songbook_defaults(user_songbook) |
||||
|
|
||||
|
songbook['_filepath'] = filename |
||||
|
songbook['_basename'] = os.path.basename(filename)[:-len(".yaml")] |
||||
|
|
||||
|
# Gathering datadirs |
||||
|
songbook['_datadir'] = list(_iter_absolute_datadirs(songbook)) |
||||
|
if 'datadir' in songbook['book']: |
||||
|
del songbook['book']['datadir'] |
||||
|
|
||||
|
songbook['_songdir'] = [ |
||||
|
DataSubpath(path, 'songs') |
||||
|
for path in songbook['_datadir'] |
||||
|
] |
||||
|
|
||||
|
|
||||
|
return songbook |
||||
|
|
||||
|
def _add_songbook_defaults(user_songbook): |
||||
|
""" Adds the defaults values to the songbook if missing from |
||||
|
the user songbook |
||||
|
|
||||
|
Priority: |
||||
|
- User values |
||||
|
- Default values of the user lang (if set) |
||||
|
- Default english values |
||||
|
""" |
||||
|
|
||||
|
# Merge the default and user configs |
||||
|
locale_default = config_model('default') |
||||
|
# Initialize with default in english |
||||
|
default_songbook = locale_default.get('en', {}) |
||||
|
default_songbook = DictOfDict(default_songbook) |
||||
|
|
||||
|
if 'lang' in user_songbook.get('book', []): |
||||
|
# Update default with current lang |
||||
|
lang = user_songbook['book']['lang'] |
||||
|
default_songbook.update(locale_default.get(lang, {})) |
||||
|
# Update default with user_songbook |
||||
|
default_songbook.update(user_songbook) |
||||
|
|
||||
|
return dict(default_songbook) |
||||
|
|
||||
|
def _iter_absolute_datadirs(raw_songbook): |
||||
|
"""Iterate on the absolute datadirs of the raw songbook |
||||
|
|
||||
|
Appends the songfile dir at the end |
||||
|
""" |
||||
|
datadir = raw_songbook.get('book', {}).get('datadir') |
||||
|
filepath = raw_songbook['_filepath'] |
||||
|
|
||||
|
if datadir is None: |
||||
|
datadir = [] |
||||
|
elif isinstance(datadir, str): |
||||
|
datadir = [datadir] |
||||
|
basedir = os.path.dirname(os.path.abspath(filepath)) |
||||
|
|
||||
|
for path in datadir: |
||||
|
abspath = os.path.join(basedir, path) |
||||
|
if os.path.exists(abspath) and os.path.isdir(abspath): |
||||
|
yield abspath |
||||
|
else: |
||||
|
LOGGER.warning( |
||||
|
"Ignoring non-existent datadir '{}'.".format(path) |
||||
|
) |
||||
|
yield basedir |
@ -0,0 +1,114 @@ |
|||||
|
#!/bin/env python3 |
||||
|
|
||||
|
"""Command line client to :mod:`tools`""" |
||||
|
|
||||
|
import argparse |
||||
|
import logging |
||||
|
import operator |
||||
|
import os |
||||
|
import pkgutil |
||||
|
import re |
||||
|
import sys |
||||
|
|
||||
|
import patacrep |
||||
|
|
||||
|
# Logging configuration |
||||
|
logging.basicConfig(level=logging.INFO) |
||||
|
LOGGER = logging.getLogger("patatools") |
||||
|
|
||||
|
def _execlp(program, args): |
||||
|
"""Call :func:`os.execlp`, adding `program` as the first argument to itself.""" |
||||
|
return os.execlp(program, program, *args) |
||||
|
|
||||
|
def _iter_subcommands(): |
||||
|
"""Iterate over subcommands. |
||||
|
|
||||
|
The objects returned are tuples of: |
||||
|
- the name of the command; |
||||
|
- its description; |
||||
|
- the function to call to execute the subcommand. |
||||
|
""" |
||||
|
subcommands = [] |
||||
|
|
||||
|
# Get python subcommands |
||||
|
path = [os.path.join(item, "patacrep", "tools") for item in sys.path] |
||||
|
prefix = "patacrep.tools." |
||||
|
module_re = re.compile(r'{}(?P<subcommand>[^\.]*)\.__main__'.format(prefix)) |
||||
|
for module_loader, name, _ in pkgutil.walk_packages(path, prefix): |
||||
|
match = module_re.match(name) |
||||
|
if match: |
||||
|
module = module_loader.find_module(match.string).load_module() |
||||
|
if hasattr(module, "SUBCOMMAND_DESCRIPTION"): |
||||
|
subcommands.append(match.groupdict()['subcommand']) |
||||
|
yield ( |
||||
|
match.groupdict()['subcommand'], |
||||
|
getattr(module, "SUBCOMMAND_DESCRIPTION"), |
||||
|
module.main, |
||||
|
) |
||||
|
|
||||
|
class ArgumentParser(argparse.ArgumentParser): |
||||
|
"""Proxy class to circumvent an :mod:`argparse` bug. |
||||
|
|
||||
|
Contrarily to what documented, the `argparse.REMAINDER |
||||
|
<https://docs.python.org/3/library/argparse.html#nargs>`_ `nargs` setting |
||||
|
does not include the remainder arguments if the first one begins with `-`. |
||||
|
|
||||
|
This bug is reperted as `17050 <https://bugs.python.org/issue17050>`_. This |
||||
|
class can be deleted once this bug has been fixed. |
||||
|
""" |
||||
|
|
||||
|
def parse_args(self, args=None, namespace=None): |
||||
|
if args is None: |
||||
|
args = sys.argv[1:] |
||||
|
subcommands = [command[0] for command in set(_iter_subcommands())] |
||||
|
if len(args) > 0: |
||||
|
if args[0] in subcommands: |
||||
|
args = [args[0], "--"] + args[1:] |
||||
|
|
||||
|
value = super().parse_args(args, namespace) |
||||
|
|
||||
|
if hasattr(value, 'remainder'): |
||||
|
value.remainder = value.remainder[1:] |
||||
|
return value |
||||
|
|
||||
|
|
||||
|
def commandline_parser(): |
||||
|
"""Return a command line parser.""" |
||||
|
|
||||
|
parser = ArgumentParser( |
||||
|
prog="patatools", |
||||
|
description=( |
||||
|
"Miscellaneous tools for patacrep." |
||||
|
), |
||||
|
formatter_class=argparse.RawTextHelpFormatter, |
||||
|
) |
||||
|
|
||||
|
parser.add_argument( |
||||
|
'--version', |
||||
|
help='Show version', |
||||
|
action='version', |
||||
|
version='%(prog)s ' + patacrep.__version__ |
||||
|
) |
||||
|
|
||||
|
subparsers = parser.add_subparsers( |
||||
|
title="Subcommands", |
||||
|
description="List of available subcommands.", |
||||
|
) |
||||
|
subparsers.required = True |
||||
|
subparsers.dest = "subcommand" |
||||
|
|
||||
|
for command, message, function in sorted(_iter_subcommands(), key=operator.itemgetter(0)): |
||||
|
sub1 = subparsers.add_parser(command, help=message, add_help=False) |
||||
|
sub1.add_argument('remainder', nargs=argparse.REMAINDER) |
||||
|
sub1.set_defaults(function=function) |
||||
|
|
||||
|
return parser |
||||
|
|
||||
|
def main(args): |
||||
|
"""Main function""" |
||||
|
parser = commandline_parser() |
||||
|
args = parser.parse_args(args[1:]) |
||||
|
args.function(["patatools-{}".format(args.subcommand)] + args.remainder) |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
main(sys.argv) |
@ -0,0 +1,73 @@ |
|||||
|
"""`patatools cache` command: cache manipulation.""" |
||||
|
|
||||
|
import argparse |
||||
|
import logging |
||||
|
import os |
||||
|
import shutil |
||||
|
import sys |
||||
|
import textwrap |
||||
|
|
||||
|
from patacrep import errors |
||||
|
from patacrep.songbook import open_songbook |
||||
|
|
||||
|
LOGGER = logging.getLogger("patatools.cache") |
||||
|
SUBCOMMAND_DESCRIPTION = "Perform operations on cache." |
||||
|
|
||||
|
def filename(name): |
||||
|
"""Check that argument is an existing, readable file name. |
||||
|
|
||||
|
Return the argument for convenience. |
||||
|
""" |
||||
|
if os.path.isfile(name) and os.access(name, os.R_OK): |
||||
|
return name |
||||
|
raise argparse.ArgumentTypeError("Cannot read file '{}'.".format(name)) |
||||
|
|
||||
|
def commandline_parser(): |
||||
|
"""Return a command line parser.""" |
||||
|
|
||||
|
parser = argparse.ArgumentParser( |
||||
|
prog="patatools cache", |
||||
|
description=SUBCOMMAND_DESCRIPTION, |
||||
|
formatter_class=argparse.RawTextHelpFormatter, |
||||
|
) |
||||
|
|
||||
|
subparsers = parser.add_subparsers( |
||||
|
description="", |
||||
|
dest="command", |
||||
|
) |
||||
|
subparsers.required = True |
||||
|
|
||||
|
clean = subparsers.add_parser( |
||||
|
"clean", |
||||
|
description="Delete cache.", |
||||
|
help="Delete cache.", |
||||
|
) |
||||
|
clean.add_argument( |
||||
|
'songbook', |
||||
|
metavar="SONGBOOK", |
||||
|
help=textwrap.dedent("""Songbook file to be used to look for cache path."""), |
||||
|
type=filename, |
||||
|
) |
||||
|
clean.set_defaults(command=do_clean) |
||||
|
|
||||
|
return parser |
||||
|
|
||||
|
def do_clean(namespace): |
||||
|
"""Execute the `patatools cache clean` command.""" |
||||
|
for datadir in open_songbook(namespace.songbook)['_datadir']: |
||||
|
cachedir = os.path.join(datadir, ".cache") |
||||
|
LOGGER.info("Deleting cache directory '{}'...".format(cachedir)) |
||||
|
if os.path.isdir(cachedir): |
||||
|
shutil.rmtree(cachedir) |
||||
|
|
||||
|
def main(args): |
||||
|
"""Main function: run from command line.""" |
||||
|
options = commandline_parser().parse_args(args[1:]) |
||||
|
try: |
||||
|
options.command(options) |
||||
|
except errors.SongbookError as error: |
||||
|
LOGGER.error(str(error)) |
||||
|
sys.exit(1) |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
main(sys.argv) |
@ -0,0 +1,6 @@ |
|||||
|
#! /bin/sh |
||||
|
|
||||
|
# Do not edit this file. This file is just a helper file for development test. |
||||
|
# It is not part of the distributed software. |
||||
|
|
||||
|
python -m patacrep.tools $@ |
@ -0,0 +1 @@ |
|||||
|
.cache |
@ -0,0 +1,67 @@ |
|||||
|
"""Tests of the patatools-cache command.""" |
||||
|
|
||||
|
# pylint: disable=too-few-public-methods |
||||
|
|
||||
|
import os |
||||
|
import shutil |
||||
|
import unittest |
||||
|
|
||||
|
from patacrep.files import chdir |
||||
|
from patacrep.tools.__main__ import main as tools_main |
||||
|
from patacrep.tools.cache.__main__ import main as cache_main |
||||
|
from patacrep.songbook.__main__ import main as songbook_main |
||||
|
|
||||
|
CACHEDIR = os.path.join(os.path.dirname(__file__), "test_cache_datadir", ".cache") |
||||
|
|
||||
|
class TestCache(unittest.TestCase): |
||||
|
"""Test of the "patatools cache" subcommand""" |
||||
|
|
||||
|
def setUp(self): |
||||
|
"""Remove cache.""" |
||||
|
self._remove_cache() |
||||
|
self.assertFalse(os.path.exists(CACHEDIR)) |
||||
|
|
||||
|
def tearDown(self): |
||||
|
"""Remove cache.""" |
||||
|
self._remove_cache() |
||||
|
self.assertFalse(os.path.exists(CACHEDIR)) |
||||
|
|
||||
|
@staticmethod |
||||
|
def _remove_cache(): |
||||
|
"""Delete cache.""" |
||||
|
shutil.rmtree(CACHEDIR, ignore_errors=True) |
||||
|
|
||||
|
def _system(self, main, args): |
||||
|
with chdir(os.path.dirname(__file__)): |
||||
|
try: |
||||
|
main(args) |
||||
|
except SystemExit as systemexit: |
||||
|
self.assertEqual(systemexit.code, 0) |
||||
|
|
||||
|
def test_clean_exists(self): |
||||
|
"""Test of the "patatools cache clean" subcommand""" |
||||
|
for main, args in [ |
||||
|
(tools_main, ["patatools", "cache", "clean", "test_cache.yaml"]), |
||||
|
(cache_main, ["patatools-cache", "clean", "test_cache.yaml"]), |
||||
|
]: |
||||
|
with self.subTest(main=main, args=args): |
||||
|
# First compilation. Ensure that cache exists afterwards |
||||
|
self._system(songbook_main, ["songbook", "--steps", "tex,clean", "test_cache.yaml"]) |
||||
|
self.assertTrue(os.path.exists(CACHEDIR)) |
||||
|
|
||||
|
# Clean cache |
||||
|
self._system(main, args) |
||||
|
|
||||
|
# Ensure that cache does not exist |
||||
|
self.assertFalse(os.path.exists(CACHEDIR)) |
||||
|
|
||||
|
def test_clean_not_exists(self): |
||||
|
"""Test of the "patatools cache clean" subcommand""" |
||||
|
# Clean non-existent cache |
||||
|
for main, args in [ |
||||
|
(tools_main, ["patatools", "cache", "clean", "test_cache.yaml"]), |
||||
|
(cache_main, ["patatools-cache", "clean", "test_cache.yaml"]), |
||||
|
]: |
||||
|
with self.subTest(main=main, args=args): |
||||
|
# Clean cache |
||||
|
self._system(main, args) |
@ -0,0 +1,2 @@ |
|||||
|
book: |
||||
|
datadir: test_cache_datadir |
@ -0,0 +1,123 @@ |
|||||
|
"""Tests of the patatools-convert command.""" |
||||
|
|
||||
|
# pylint: disable=too-few-public-methods |
||||
|
|
||||
|
import contextlib |
||||
|
import glob |
||||
|
import os |
||||
|
import unittest |
||||
|
|
||||
|
from pkg_resources import resource_filename |
||||
|
|
||||
|
from patacrep import files |
||||
|
from patacrep.tools.__main__ import main as tools_main |
||||
|
from patacrep.encoding import open_read |
||||
|
from patacrep.tools.convert.__main__ import main as convert_main |
||||
|
|
||||
|
from .. import dynamic # pylint: disable=unused-import |
||||
|
from .. import logging_reduced |
||||
|
|
||||
|
class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): |
||||
|
"""Test of the "patatools convert" subcommand""" |
||||
|
|
||||
|
@staticmethod |
||||
|
def _system(main, args): |
||||
|
try: |
||||
|
main(args) |
||||
|
except SystemExit as systemexit: |
||||
|
return systemexit.code |
||||
|
return 0 |
||||
|
|
||||
|
def assertConvert(self, basename, in_format, out_format): # pylint: disable=invalid-name |
||||
|
"""Test of the "patatools convert" subcommand""" |
||||
|
sourcename = "{}.{}".format(basename, in_format) |
||||
|
destname = "{}.{}".format(basename, out_format) |
||||
|
controlname = "{}.{}.control".format(sourcename, out_format) |
||||
|
for main, args in [ |
||||
|
(tools_main, ["patatools", "convert"]), |
||||
|
(convert_main, ["patatools-convert"]), |
||||
|
]: |
||||
|
with self.subTest(main=main, args=args): |
||||
|
with self.chdir("test_convert_success"): |
||||
|
with open_read(controlname) as controlfile: |
||||
|
with logging_reduced(): |
||||
|
if os.path.exists(destname): |
||||
|
os.remove(destname) |
||||
|
self.assertEqual( |
||||
|
self._system(main, args + [in_format, out_format, sourcename]), |
||||
|
0, |
||||
|
) |
||||
|
expected = controlfile.read().strip().replace( |
||||
|
"@TEST_FOLDER@", |
||||
|
files.path2posix(resource_filename(__name__, "")), |
||||
|
) |
||||
|
with open_read(destname) as destfile: |
||||
|
self.assertMultiLineEqual( |
||||
|
destfile.read().replace('\r\n', '\n').strip(), |
||||
|
expected.strip(), |
||||
|
) |
||||
|
|
||||
|
def assertFailConvert(self, basename, in_format, out_format): # pylint: disable=invalid-name |
||||
|
"""Test of the "patatools convert" subcommand""" |
||||
|
sourcename = "{}.{}".format(basename, in_format) |
||||
|
destname = "{}.{}".format(basename, out_format) |
||||
|
for main, args in [ |
||||
|
(tools_main, ["patatools", "convert"]), |
||||
|
(convert_main, ["patatools-convert"]), |
||||
|
]: |
||||
|
with self.subTest(main=main, args=args): |
||||
|
with self.chdir("test_convert_failure"): |
||||
|
with logging_reduced(): |
||||
|
if os.path.exists(destname): |
||||
|
os.remove(destname) |
||||
|
self.assertEqual( |
||||
|
self._system(main, args + [in_format, out_format, sourcename]), |
||||
|
1, |
||||
|
) |
||||
|
|
||||
|
@staticmethod |
||||
|
@contextlib.contextmanager |
||||
|
def chdir(*pathlist): |
||||
|
"""Temporary change current directory, relative to this file directory""" |
||||
|
with files.chdir(resource_filename(__name__, ""), *pathlist): |
||||
|
yield |
||||
|
|
||||
|
@classmethod |
||||
|
def _iter_testmethods(cls): |
||||
|
"""Iterate over song files to test.""" |
||||
|
for directory, create_test in [ |
||||
|
("test_convert_success", cls._create_test_success), |
||||
|
("test_convert_failure", cls._create_test_failure), |
||||
|
]: |
||||
|
with cls.chdir(directory): |
||||
|
for control in sorted(glob.glob('*.*.*.control')): |
||||
|
[*base, in_format, out_format, _] = control.split('.') |
||||
|
base = '.'.join(base) |
||||
|
yield ( |
||||
|
"test_{}_{}_{}".format(base, in_format, out_format), |
||||
|
create_test(base, in_format, out_format), |
||||
|
) |
||||
|
|
||||
|
@classmethod |
||||
|
def _create_test_success(cls, base, in_format, out_format): |
||||
|
"""Return a function testing conversion. |
||||
|
|
||||
|
:param str base: Base name of the file to convert. |
||||
|
:param str in_format: Source format. |
||||
|
:param str out_format: Destination format. |
||||
|
""" |
||||
|
test_parse_render = lambda self: self.assertConvert(base, in_format, out_format) |
||||
|
test_parse_render.__doc__ = ( |
||||
|
"Test that '{base}.{in_format}' is correctly converted to '{out_format}'." |
||||
|
).format(base=os.path.basename(base), in_format=in_format, out_format=out_format) |
||||
|
return test_parse_render |
||||
|
|
||||
|
@classmethod |
||||
|
def _create_test_failure(cls, base, in_format, out_format): |
||||
|
"""Return a function testing failing conversions |
||||
|
""" |
||||
|
test_parse_render = lambda self: self.assertFailConvert(base, in_format, out_format) |
||||
|
test_parse_render.__doc__ = ( |
||||
|
"Test that '{base}.{in_format}' raises an error when trying to convert it to '{out_format}'." # pylint: disable=line-too-long |
||||
|
).format(base=os.path.basename(base), in_format=in_format, out_format=out_format) |
||||
|
return test_parse_render |
@ -0,0 +1,122 @@ |
|||||
|
\selectlanguage{english} |
||||
|
|
||||
|
\beginsong{}[ |
||||
|
by={ |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\selectlanguage |
||||
|
\songcolumns |
||||
|
\beginsong |
||||
|
\[&y={Traditionnel},cover={traditionnel},al&um={France}] |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\cover |
||||
|
\gtab |
||||
|
\gtab |
||||
|
\gtab |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\begin |
||||
|
Cheva\\[C]liers de la Table Ronde |
||||
|
Goûtons \\[G7]voir si le vin est \\[C]bon |
||||
|
\rep |
||||
|
\end |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\begin |
||||
|
Goûtons \\[F]voir, \echo |
||||
|
Goûtons \\[C]voir, \echo |
||||
|
Goûtons \\[G7]voir si le vin est bon |
||||
|
\rep |
||||
|
\end |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\begin |
||||
|
S'il est bon, s'il est agréable |
||||
|
J'en boirai jusqu'à mon plaisir |
||||
|
\end |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\begin |
||||
|
J'en boirai cinq à six bouteilles |
||||
|
Et encore, ce n'est pas beaucoup |
||||
|
\end |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\begin |
||||
|
Si je meurs, je veux qu'on m'enterre |
||||
|
Dans une cave où il y a du bon vin |
||||
|
\end |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\begin |
||||
|
Les deux pieds contre la muraille |
||||
|
Et la tête sous le robinet |
||||
|
\end |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\begin |
||||
|
Et les quatre plus grands ivrognes |
||||
|
Porteront les quatre coins du drap |
||||
|
\end |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\begin |
||||
|
Pour donner le discours d'usage |
||||
|
On prendra le bistrot du coin |
||||
|
\end |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\begin |
||||
|
Et si le tonneau se débouche |
||||
|
J'en boirai jusqu'à mon plaisir |
||||
|
\end |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\begin |
||||
|
Et s'il en reste quelques gouttes |
||||
|
Ce sera pour nous rafraîchir |
||||
|
\end |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\begin |
||||
|
Sur ma tombe, je veux qu'on inscrive |
||||
|
\emph |
||||
|
\end |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
\begin{verse} |
||||
|
\endsong |
||||
|
\end{verse} |
||||
|
|
||||
|
|
||||
|
|
||||
|
\endsong |
@ -0,0 +1 @@ |
|||||
|
greensleeves.tsg |
@ -0,0 +1,45 @@ |
|||||
|
{lang: en} |
||||
|
{columns: 2} |
||||
|
{title: Greensleeves} |
||||
|
{title: Un autre sous-titre} |
||||
|
{title: Un sous titre} |
||||
|
{artist: Traditionnel} |
||||
|
{album: Angleterre} |
||||
|
|
||||
|
|
||||
|
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 |
@ -0,0 +1,62 @@ |
|||||
|
\selectlanguage{english} |
||||
|
\songcolumns{2} |
||||
|
|
||||
|
\beginsong{Greensleeves\\ |
||||
|
Un autre sous-titre\\ |
||||
|
Un sous titre}[ |
||||
|
by={ |
||||
|
Traditionnel }, |
||||
|
album={Angleterre}, |
||||
|
] |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
\begin{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{verse} |
||||
|
|
||||
|
|
||||
|
\begin{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{chorus} |
||||
|
|
||||
|
|
||||
|
\begin{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{verse} |
||||
|
|
||||
|
|
||||
|
\begin{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{verse} |
||||
|
|
||||
|
|
||||
|
\begin{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{verse} |
||||
|
|
||||
|
|
||||
|
\begin{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{verse} |
||||
|
|
||||
|
\endsong |
Loading…
Reference in new issue