Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
|
e55294c714 | |
|
034b73496b | |
|
20f7aee4f3 | |
|
cdfabbc95e | |
|
d151c203bc | |
|
afeba5f9b1 | |
|
00e3a1fa4f | |
|
2bcfdaf0de | |
|
f8145b4c43 | |
|
ae3d3ef330 | |
|
a551fdf465 | |
|
80e74a77d4 | |
|
61b1cda0f6 | |
|
dc5eedb8f4 | |
|
ba2e2e5ee9 | |
|
7541b88101 |
30
README.rst
30
README.rst
|
@ -1,8 +1,10 @@
|
||||||
python-chordpro
|
igitar
|
||||||
===============
|
===============
|
||||||
|
|
||||||
``python-chordpro`` is a ChordPro parser, written in Python. The main difference between this module
|
**igitar** *[ee-gee-tahr]* isiXhosa, meaning guitar
|
||||||
and other similar libraries is that ``python-chordpro`` parses ChordPro files down to the syllable
|
|
||||||
|
``igitar`` is a ChordPro parser, written in Python. The main difference between this module
|
||||||
|
and other similar libraries is that ``igitar`` parses ChordPro files down to the syllable
|
||||||
level, enabling finer-grained control of the formatted output.
|
level, enabling finer-grained control of the formatted output.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
@ -13,16 +15,18 @@ level, enabling finer-grained control of the formatted output.
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|
||||||
You can use ``pip`` to install ``python-chordpro``::
|
You can use ``pip`` to install ``igitar``:
|
||||||
|
|
||||||
$ pip install python-chordpro
|
.. code-block:: shell-session
|
||||||
|
|
||||||
|
$ pip install igitar
|
||||||
|
|
||||||
Example Usage
|
Example Usage
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from chordpro import Song
|
from igitar import Song
|
||||||
|
|
||||||
song = Song('path/to/song.chordpro')
|
song = Song('path/to/song.chordpro')
|
||||||
|
|
||||||
|
@ -32,11 +36,11 @@ Example Usage
|
||||||
Rendering
|
Rendering
|
||||||
---------
|
---------
|
||||||
|
|
||||||
``python-chordpro`` comes with two renders, HTML and Text.
|
``igitar`` comes with two renders, HTML and Text.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from chordpro.renderers.html import render
|
from igitar.renderers.html import render
|
||||||
|
|
||||||
print(render(song))
|
print(render(song))
|
||||||
|
|
||||||
|
@ -44,14 +48,16 @@ Rendering
|
||||||
Command Line Interface
|
Command Line Interface
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
``python-chordpro`` also ships with a built-in command line interface which will read a ChordPro
|
``igitar`` also ships with a built-in command line interface which will read a ChordPro
|
||||||
file and then render it using either the text or HTML renderer.
|
file and then render it using either the text or HTML renderer.
|
||||||
|
|
||||||
For example::
|
For example:
|
||||||
|
|
||||||
$ python-chordpro path/to/song.chordpro -f text -o song.txt
|
.. code-block:: shell-session
|
||||||
|
|
||||||
|
$ igitar path/to/song.chordpro -f text -o song.txt
|
||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
|
||||||
``python-chordpro`` is licensed under the MIT license. See the LICENSE file for more information.
|
``igitar`` is licensed under the MIT license. See the LICENSE file for more information.
|
||||||
|
|
11
setup.cfg
11
setup.cfg
|
@ -1,11 +1,11 @@
|
||||||
[metadata]
|
[metadata]
|
||||||
name = python-chordpro
|
name = igitar
|
||||||
author = Raoul Snyman
|
author = Raoul Snyman
|
||||||
author_email = raoul@snyman.info
|
author_email = raoul@snyman.info
|
||||||
description = A ChordPro parser, written in Python
|
description = A ChordPro parser, written in Python
|
||||||
long_description = file:README.rst
|
long_description = file:README.rst
|
||||||
long_description_content_type = text/x-rst
|
long_description_content_type = text/x-rst
|
||||||
url = https://git.snyman.info/raoul/python-chordpro
|
url = https://git.snyman.info/raoul/igitar
|
||||||
license = MIT
|
license = MIT
|
||||||
classifiers =
|
classifiers =
|
||||||
Development Status :: 3 - Alpha
|
Development Status :: 3 - Alpha
|
||||||
|
@ -18,7 +18,10 @@ classifiers =
|
||||||
Programming Language :: Python :: 3.9
|
Programming Language :: Python :: 3.9
|
||||||
Programming Language :: Python :: 3.10
|
Programming Language :: Python :: 3.10
|
||||||
Topic :: Utilities
|
Topic :: Utilities
|
||||||
keywords = music, chords, guitar
|
keywords =
|
||||||
|
music
|
||||||
|
chords
|
||||||
|
guitar
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
package_dir =
|
package_dir =
|
||||||
|
@ -35,7 +38,7 @@ where = src
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
python-chordpro = chordpro
|
igitar = igitar.cli:main
|
||||||
|
|
||||||
[bdist_wheel]
|
[bdist_wheel]
|
||||||
universal = 1
|
universal = 1
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
from chordpro.base import Song # noqa
|
|
|
@ -1,35 +0,0 @@
|
||||||
from argparse import ArgumentParser
|
|
||||||
|
|
||||||
from chordpro.base import Song
|
|
||||||
from chordpro.renderers.html import render as render_html
|
|
||||||
from chordpro.renderers.text import render as render_text
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
parser = ArgumentParser()
|
|
||||||
parser.add_argument('input', metavar='FILENAME', help='Input ChordPro file')
|
|
||||||
parser.add_argument('-o', '--output', metavar='FILENAME', help='Output to a file')
|
|
||||||
parser.add_argument('-f', '--format', metavar='FORMAT', choices=['html', 'text'], default='html',
|
|
||||||
help='Output format')
|
|
||||||
parser.add_argument('-p', '--param', metavar='KEY=VALUE', action='append', help='Parameter to send to renderer')
|
|
||||||
args = parser.parse_args()
|
|
||||||
render_params = {kv.split('=')[0]: kv.split('=')[-1] for kv in args.param} if args.param else {}
|
|
||||||
# Prepare the params
|
|
||||||
for key, value in render_params.items():
|
|
||||||
if value.lower() == "true":
|
|
||||||
render_params[key] = True
|
|
||||||
elif value.lower() == "false":
|
|
||||||
render_params[key] = False
|
|
||||||
elif value.isdigit():
|
|
||||||
render_params[key] = int(value)
|
|
||||||
|
|
||||||
song = Song(args.input)
|
|
||||||
if args.format == 'text':
|
|
||||||
output = render_text(song, **render_params)
|
|
||||||
else:
|
|
||||||
output = render_html(song, **render_params)
|
|
||||||
if args.output:
|
|
||||||
with open(args.output, 'w') as html_file:
|
|
||||||
html_file.write(output)
|
|
||||||
else:
|
|
||||||
print(output)
|
|
|
@ -1,62 +0,0 @@
|
||||||
CHORD = '<td class="chord">{}</td>'
|
|
||||||
SYLLB = '<td class="syllable">{}</td>'
|
|
||||||
LINE = '<table class="line" border="0" cellpadding="0" cellspacing="0"><tr class="chords-line">{}</tr><tr ' \
|
|
||||||
'class="lyrics-line">{}</tr></table>'
|
|
||||||
VERSE = '''<section class="verse">
|
|
||||||
<h4 class="verse-name {verse_type}">{verse_name}</h4>
|
|
||||||
<div class="verse-body">
|
|
||||||
{verse_body}
|
|
||||||
</div>
|
|
||||||
</section>'''
|
|
||||||
TITLE = '<section class="metadata"><h1 class="title">{title}</h1><h3 class="artist">{artist}</h3></section>'
|
|
||||||
HTML = '''<html>
|
|
||||||
<head>
|
|
||||||
<title>{title}</title>
|
|
||||||
<style>
|
|
||||||
h4.verse-name {{ font-weight: normal; }}
|
|
||||||
.verse-name {{ margin-bottom: 0; }}
|
|
||||||
.verse-body {{ padding-left: 2rem; }}
|
|
||||||
.chord {{ text-align: center; }}
|
|
||||||
{styles}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{body}
|
|
||||||
</body>
|
|
||||||
</html>'''
|
|
||||||
STYLES = {
|
|
||||||
'verse_name_bold': '.verse-name { font-weight: bold !important; }',
|
|
||||||
'verse_upper': '.verse-name { text-transform: uppercase; }',
|
|
||||||
'chord_bold': '.chord { font-weight: bold; }'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def render(song, verse_upper=False, verse_name_bold=False, chord_bold=False):
|
|
||||||
"""Render a song to HTML"""
|
|
||||||
rendered_verses = []
|
|
||||||
for verse in song.verse_order:
|
|
||||||
rendered_lines = []
|
|
||||||
for line in verse.lines:
|
|
||||||
rendered_chords = []
|
|
||||||
rendered_syllables = []
|
|
||||||
for word_counter, word in enumerate(line.words):
|
|
||||||
is_last_word = (word_counter + 1) == len(line.words)
|
|
||||||
for syll_counter, syllable in enumerate(word.syllables):
|
|
||||||
is_last_syllable = (syll_counter + 1) == len(word.syllables)
|
|
||||||
rendered_chords.append(CHORD.format(syllable.chord or ' '))
|
|
||||||
rendered_syllables.append(SYLLB.format(
|
|
||||||
syllable.syllable + (' ' if is_last_syllable and not is_last_word else '')))
|
|
||||||
rendered_lines.append(LINE.format(''.join(rendered_chords), ''.join(rendered_syllables)))
|
|
||||||
rendered_verses.append(VERSE.format(verse_type=verse.type_ or '', verse_name=verse.title,
|
|
||||||
verse_body='\n'.join(rendered_lines)))
|
|
||||||
title = song.metadata.get('title') or 'Song'
|
|
||||||
metadata = TITLE.format(title=title, artist=song.metadata.get('artist') or song.metadata.get('composer') or '')
|
|
||||||
body = metadata + '\n' + '\n'.join(rendered_verses)
|
|
||||||
styles = ''
|
|
||||||
if verse_name_bold:
|
|
||||||
styles += STYLES['verse_name_bold'] + '\n'
|
|
||||||
if verse_upper:
|
|
||||||
styles += STYLES['verse_upper'] + '\n'
|
|
||||||
if chord_bold:
|
|
||||||
styles += STYLES['chord_bold'] + '\n'
|
|
||||||
return HTML.format(title=title, body=body, styles=styles)
|
|
|
@ -0,0 +1 @@
|
||||||
|
from igitar.base import Song # noqa
|
|
@ -0,0 +1,2 @@
|
||||||
|
from igitar.cli import main
|
||||||
|
main()
|
|
@ -1,14 +1,8 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from hyphen import Hyphenator
|
from hyphen import Hyphenator
|
||||||
|
|
||||||
from chordpro.constants import KNOWN_DIRECTIVES, KNOWN_VERSE_TYPES
|
from igitar.constants import BRIDGE_MARKER, CHORD_WORD, CHORUS_MARKER, DIRECTIVE, END_OF, KNOWN_DIRECTIVES, START_OF, \
|
||||||
|
VERSE_MARKER
|
||||||
|
|
||||||
DIRECTIVE = re.compile(r'\{(.*?): *(.*?)\}')
|
|
||||||
START_OF = re.compile(r'\{start_of_(' + '|'.join(KNOWN_VERSE_TYPES) + r')(: *(.*?))?\}')
|
|
||||||
END_OF = re.compile(r'\{end_of_(' + '|'.join(KNOWN_VERSE_TYPES) + r')\}')
|
|
||||||
CHORUS_MARKER = re.compile(r'\{chorus(: *(.*?))?\}')
|
|
||||||
CHORD_WORD = re.compile(r'(.*?)\[(.*?)\]')
|
|
||||||
HYPHEN_CACHE = {}
|
HYPHEN_CACHE = {}
|
||||||
SYLLABLE_EXCEPTIONS = {
|
SYLLABLE_EXCEPTIONS = {
|
||||||
'outer': ['out', 'er']
|
'outer': ['out', 'er']
|
||||||
|
@ -36,18 +30,13 @@ class Directive(object):
|
||||||
return None
|
return None
|
||||||
for known_directive in KNOWN_DIRECTIVES:
|
for known_directive in KNOWN_DIRECTIVES:
|
||||||
if match.group(1) in known_directive:
|
if match.group(1) in known_directive:
|
||||||
self.directive = match.group(1)
|
self.directive = known_directive[0]
|
||||||
self.info = match.group(2)
|
self.info = match.group(2)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_directive(line):
|
def is_directive(line):
|
||||||
"""Check if a line in a file contains a directive"""
|
"""Check if a line in a file contains a directive"""
|
||||||
match = DIRECTIVE.match(line)
|
return DIRECTIVE.match(line) is not None
|
||||||
if match:
|
|
||||||
for known_directive in KNOWN_DIRECTIVES:
|
|
||||||
if match.group(1) in known_directive:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class Syllable(object):
|
class Syllable(object):
|
||||||
|
@ -170,6 +159,18 @@ class Verse(object):
|
||||||
return match is not None
|
return match is not None
|
||||||
return match.group(1) == type_
|
return match.group(1) == type_
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_verse_marker(line):
|
||||||
|
return line.strip().startswith('{verse')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_verse_from_marker(line):
|
||||||
|
match = VERSE_MARKER.match(line)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
if len(match.groups()) > 1:
|
||||||
|
return match.group(2)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_chorus_marker(line):
|
def is_chorus_marker(line):
|
||||||
return line.strip().startswith('{chorus')
|
return line.strip().startswith('{chorus')
|
||||||
|
@ -182,6 +183,18 @@ class Verse(object):
|
||||||
if len(match.groups()) > 1:
|
if len(match.groups()) > 1:
|
||||||
return match.group(2)
|
return match.group(2)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_bridge_marker(line):
|
||||||
|
return line.strip().startswith('{bridge')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_bridge_from_marker(line):
|
||||||
|
match = BRIDGE_MARKER.match(line)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
if len(match.groups()) > 1:
|
||||||
|
return match.group(2)
|
||||||
|
|
||||||
|
|
||||||
class Metadata(object):
|
class Metadata(object):
|
||||||
|
|
||||||
|
@ -201,33 +214,60 @@ class Song(object):
|
||||||
def __init__(self, filename=None):
|
def __init__(self, filename=None):
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
if self.filename:
|
if self.filename:
|
||||||
self.parse(self.filename)
|
with open(filename) as song_file:
|
||||||
|
self.parse(song_file.read())
|
||||||
|
|
||||||
def parse(self, filename):
|
def parse(self, text):
|
||||||
self.metadata = Metadata()
|
self.metadata = Metadata()
|
||||||
self.verses = []
|
self.verses = []
|
||||||
self.verse_order = []
|
self.verse_order = []
|
||||||
with open(filename) as song_file:
|
is_verse = False
|
||||||
is_verse = False
|
current_verse = None
|
||||||
current_verse = None
|
for line_number, line in enumerate(text.splitlines()):
|
||||||
for line_number, line in enumerate(song_file):
|
if Directive.is_directive(line):
|
||||||
if Directive.is_directive(line):
|
self.metadata.add(Directive(line))
|
||||||
self.metadata.add(Directive(line))
|
elif Verse.is_start_of_verse(line):
|
||||||
elif Verse.is_start_of_verse(line):
|
is_verse = True
|
||||||
is_verse = True
|
current_verse = Verse.parse(line)
|
||||||
current_verse = Verse.parse(line)
|
self.verse_order.append(current_verse)
|
||||||
self.verse_order.append(current_verse)
|
elif Verse.is_end_of_verse(line):
|
||||||
elif Verse.is_end_of_verse(line):
|
if not Verse.is_end_of_verse(line, current_verse.type_):
|
||||||
if not Verse.is_end_of_verse(line, current_verse.type_):
|
raise MismatchedVerseType(line_number, current_verse.type_)
|
||||||
raise MismatchedVerseType(line_number, current_verse.type_)
|
self.verses.append(current_verse)
|
||||||
self.verses.append(current_verse)
|
is_verse = False
|
||||||
is_verse = False
|
current_verse = None
|
||||||
current_verse = None
|
elif is_verse:
|
||||||
elif is_verse:
|
current_verse.add_line(line.strip())
|
||||||
current_verse.add_line(line.strip())
|
elif Verse.is_verse_marker(line):
|
||||||
elif Verse.is_chorus_marker(line):
|
verse_name = Verse.get_verse_from_marker(line)
|
||||||
chorus_name = Verse.get_chorus_from_marker(line)
|
for verse in self.verses[::-1]:
|
||||||
for verse in self.verses[::-1]:
|
if verse.type_ != 'verse':
|
||||||
if verse.title == chorus_name or verse.type_ == "chorus":
|
continue
|
||||||
self.verse_order.append(verse)
|
if verse_name and verse.title == verse_name:
|
||||||
break
|
self.verse_order.append(verse)
|
||||||
|
break
|
||||||
|
elif not verse_name:
|
||||||
|
self.verse_order.append(verse)
|
||||||
|
break
|
||||||
|
elif Verse.is_chorus_marker(line):
|
||||||
|
chorus_name = Verse.get_chorus_from_marker(line)
|
||||||
|
for verse in self.verses[::-1]:
|
||||||
|
if verse.type_ != 'chorus':
|
||||||
|
continue
|
||||||
|
if chorus_name and verse.title == chorus_name:
|
||||||
|
self.verse_order.append(verse)
|
||||||
|
break
|
||||||
|
elif not chorus_name:
|
||||||
|
self.verse_order.append(verse)
|
||||||
|
break
|
||||||
|
elif Verse.is_bridge_marker(line):
|
||||||
|
bridge_name = Verse.get_bridge_from_marker(line)
|
||||||
|
for verse in self.verses[::-1]:
|
||||||
|
if verse.type_ != 'bridge':
|
||||||
|
continue
|
||||||
|
elif bridge_name and verse.title == bridge_name:
|
||||||
|
self.verse_order.append(verse)
|
||||||
|
break
|
||||||
|
elif not bridge_name:
|
||||||
|
self.verse_order.append(verse)
|
||||||
|
break
|
|
@ -1,6 +1,6 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from chordpro.constants import CHORD_SUFFIXES, ENGLISH_NOTES, GERMAN_NOTES, NEOLATIN_NOTES
|
from igitar.constants import CHORD_SUFFIXES, ENGLISH_NOTES, GERMAN_NOTES, NEOLATIN_NOTES
|
||||||
|
|
||||||
_chord_cache = {}
|
_chord_cache = {}
|
||||||
_line_cache = {}
|
_line_cache = {}
|
|
@ -0,0 +1,67 @@
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
|
from igitar.base import Song
|
||||||
|
from igitar.renderers.html import render as html_render, get_options as html_get_options
|
||||||
|
from igitar.renderers.text import render as text_render, get_options as text_get_options
|
||||||
|
|
||||||
|
|
||||||
|
def get_args():
|
||||||
|
"""Parse the command line arguments"""
|
||||||
|
parser = ArgumentParser()
|
||||||
|
subparsers = parser.add_subparsers(title='commands', dest='command')
|
||||||
|
|
||||||
|
subparsers.add_parser('list', aliases=['list-commands', 'l'], help='List the commands available')
|
||||||
|
subparsers.add_parser('html', aliases=['html-params', 'h'], help='List the parameters available for HTML rendering')
|
||||||
|
subparsers.add_parser('text', aliases=['text-params', 't'], help='List the parameters available for text rendering')
|
||||||
|
|
||||||
|
parser_render = subparsers.add_parser('render', aliases=['r'], help='Render a ChordPro file to HTML or text')
|
||||||
|
parser_render.add_argument('input', metavar='FILENAME', help='Input ChordPro file')
|
||||||
|
parser_render.add_argument('-o', '--output', metavar='FILENAME', help='Output to a file')
|
||||||
|
parser_render.add_argument('-f', '--format', metavar='FORMAT', choices=['html', 'text'], default='html',
|
||||||
|
help='Output format')
|
||||||
|
parser_render.add_argument('-p', '--param', metavar='KEY=VALUE', action='append',
|
||||||
|
help='Parameter to send to renderer')
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""The command line entrypoint"""
|
||||||
|
args = get_args()
|
||||||
|
if args.command in ['list', 'list-commands', 'l']:
|
||||||
|
print('Commands')
|
||||||
|
print('')
|
||||||
|
print(' list List all available commands')
|
||||||
|
print(' render Render a ChordPro file to HTML or text')
|
||||||
|
print(' html List the parameters available for HTML rendering')
|
||||||
|
print(' text List the parameters available for text rendering')
|
||||||
|
elif args.command in ['html', 'html-params', 'h']:
|
||||||
|
print('HTML parameters')
|
||||||
|
print('')
|
||||||
|
for param, details in html_get_options().items():
|
||||||
|
print(' {:<12} {}'.format(param, details['description']))
|
||||||
|
elif args.command in ['text', 'text-params', 't']:
|
||||||
|
print('Text parameters')
|
||||||
|
print('')
|
||||||
|
for param, details in text_get_options().items():
|
||||||
|
print(' {:<12} {}'.format(param, details['description']))
|
||||||
|
elif args.command == 'render':
|
||||||
|
render_params = {kv.split('=')[0]: kv.split('=')[-1] for kv in args.param} if args.param else {}
|
||||||
|
# Prepare the params
|
||||||
|
for key, value in render_params.items():
|
||||||
|
if value.lower() == "true":
|
||||||
|
render_params[key] = True
|
||||||
|
elif value.lower() == "false":
|
||||||
|
render_params[key] = False
|
||||||
|
elif value.isdigit():
|
||||||
|
render_params[key] = int(value)
|
||||||
|
|
||||||
|
song = Song(args.input)
|
||||||
|
if args.format == 'text':
|
||||||
|
output = text_render(song, options=render_params)
|
||||||
|
else:
|
||||||
|
output = html_render(song, options=render_params)
|
||||||
|
if args.output:
|
||||||
|
with open(args.output, 'w') as output_file:
|
||||||
|
output_file.write(output)
|
||||||
|
else:
|
||||||
|
print(output)
|
|
@ -1,3 +1,5 @@
|
||||||
|
import re
|
||||||
|
|
||||||
KNOWN_DIRECTIVES = [
|
KNOWN_DIRECTIVES = [
|
||||||
('title', 't'),
|
('title', 't'),
|
||||||
('subtitle', 'st'),
|
('subtitle', 'st'),
|
||||||
|
@ -13,7 +15,8 @@ KNOWN_DIRECTIVES = [
|
||||||
('tempo',),
|
('tempo',),
|
||||||
('duration',),
|
('duration',),
|
||||||
('capo',),
|
('capo',),
|
||||||
('meta',)
|
('meta',),
|
||||||
|
('comment', 'c')
|
||||||
]
|
]
|
||||||
KNOWN_VERSE_TYPES = [
|
KNOWN_VERSE_TYPES = [
|
||||||
'verse',
|
'verse',
|
||||||
|
@ -27,3 +30,11 @@ GERMAN_NOTES = '[CDEFGAH]'
|
||||||
NEOLATIN_NOTES = '(Do|Re|Mi|Fa|Sol|La|Si)'
|
NEOLATIN_NOTES = '(Do|Re|Mi|Fa|Sol|La|Si)'
|
||||||
CHORD_SUFFIXES = '(b|bb)?(#)?(m|maj7|maj|min7|min|sus)?(1|2|3|4|5|6|7|8|9)?'
|
CHORD_SUFFIXES = '(b|bb)?(#)?(m|maj7|maj|min7|min|sus)?(1|2|3|4|5|6|7|8|9)?'
|
||||||
SLIM_CHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
|
SLIM_CHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
|
||||||
|
DIRECTIVE = re.compile(r'\{(' + '|'.join([d for t in KNOWN_DIRECTIVES for d in t]) + r'): *(.*?)\}')
|
||||||
|
START_OF = re.compile(r'\{start_of_(' + '|'.join(KNOWN_VERSE_TYPES) + r')(: *(.*?))?\}')
|
||||||
|
END_OF = re.compile(r'\{end_of_(' + '|'.join(KNOWN_VERSE_TYPES) + r')\}')
|
||||||
|
CHORUS_MARKER = re.compile(r'\{chorus(: *(.*?))?\}')
|
||||||
|
VERSE_MARKER = re.compile(r'\{verse(: *(.*?))?\}')
|
||||||
|
BRIDGE_MARKER = re.compile(r'\{bridge(: *(.*?))?\}')
|
||||||
|
CHORD_WORD = re.compile(r'(.*?)\[(.*?)\]')
|
||||||
|
CHORD = re.compile(r'\[(.*?)\]')
|
|
@ -0,0 +1,530 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
CHORD = '<div class="chord">{}</div>'
|
||||||
|
SYLLB = '<div class="syllable">{}</div>'
|
||||||
|
WRAPP = '<div class="wrapper">{}</div>'
|
||||||
|
LINE = '<div class="line">{}</div>'
|
||||||
|
STANZA = '''<section class="stanza-section {verse_type}-section">
|
||||||
|
<h2 class="stanza-heading {verse_type}-heading">{verse_name}</h2>
|
||||||
|
<div class="stanza {verse_type}">
|
||||||
|
{verse_body}
|
||||||
|
</div>
|
||||||
|
</section>'''
|
||||||
|
METADATA = '''<section class="metadata">
|
||||||
|
{metadata}
|
||||||
|
</section>'''
|
||||||
|
TITLE = '<h1 class="title">{title}</h1>'
|
||||||
|
ARTIST = '<p class="artist">As performed by {artist}</p>'
|
||||||
|
COMPOSER = '<p class="composer">Written by {composer}</p>'
|
||||||
|
KEY_CAPO = '<div class="key-capo">{key_capo}</div>'
|
||||||
|
KEY = '<div class="key">Key: {key}</div>'
|
||||||
|
CAPO = '<div class="capo">Capo: {capo}</div>'
|
||||||
|
COPYRIGHT = '<p class="copyright">{copyright}</p>'
|
||||||
|
FOOTER = '<footer class="footer">{footer}</footer>'
|
||||||
|
HTML = '''<html>
|
||||||
|
<head>
|
||||||
|
<title>{title}</title>
|
||||||
|
<style>
|
||||||
|
h1.title, h2.stanza-heading, p.composer, p.artist {{ font-weight: normal; margin-top: 0; margin-bottom: 0; }}
|
||||||
|
.metadata, .stanza-section {{ margin-bottom: 1.8rem; page-break-inside: avoid; }}
|
||||||
|
.key-capo {{ padding: 0.5rem 1rem; border: 1px solid #000; border-radius: 2px; float: right; clear: right; }}
|
||||||
|
.key-capo div {{ margin-top: 0.5rem; margin-bottom: 0.5rem; }}
|
||||||
|
.key-capo div:first-child {{ margin-top: 0; }}
|
||||||
|
.key-capo div:last-child {{ margin-bottom: 0; }}
|
||||||
|
footer, .footer {{ position: absolute; bottom: 0; height: 2rem; }}
|
||||||
|
.wrapper {{ display: inline-block !important; }}
|
||||||
|
{styles}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{body}
|
||||||
|
</body>
|
||||||
|
</html>'''
|
||||||
|
STYLE_KEYS = [
|
||||||
|
'font',
|
||||||
|
'size',
|
||||||
|
'is_bold',
|
||||||
|
'is_centered',
|
||||||
|
'is_upper',
|
||||||
|
'is_hidden',
|
||||||
|
'indent'
|
||||||
|
]
|
||||||
|
HTML_OPTIONS = {
|
||||||
|
'default_font': {
|
||||||
|
'description': 'The default font to use.',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'default'
|
||||||
|
},
|
||||||
|
'default_size': {
|
||||||
|
'description': 'The default size to use.',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'default'
|
||||||
|
},
|
||||||
|
'title_font': {
|
||||||
|
'description': 'The font for the title',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'title'
|
||||||
|
},
|
||||||
|
'title_size': {
|
||||||
|
'description': 'The size of the title in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 14,
|
||||||
|
'group': 'title'
|
||||||
|
},
|
||||||
|
'title_is_bold': {
|
||||||
|
'description': 'Set to True to make the title bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': True,
|
||||||
|
'group': 'title'
|
||||||
|
},
|
||||||
|
'title_is_centered': {
|
||||||
|
'description': 'Center the title on the page',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'title'
|
||||||
|
},
|
||||||
|
'title_is_upper': {
|
||||||
|
'description': 'Force the title to be uppercase',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'title'
|
||||||
|
},
|
||||||
|
'composer_font': {
|
||||||
|
'description': 'The font for the composer',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'composer'
|
||||||
|
},
|
||||||
|
'composer_size': {
|
||||||
|
'description': 'The size of the composer in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'composer'
|
||||||
|
},
|
||||||
|
'composer_is_bold': {
|
||||||
|
'description': 'Set to True to make the composer bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'composer'
|
||||||
|
},
|
||||||
|
'composer_is_centered': {
|
||||||
|
'description': 'Center the composer on the page',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'composer'
|
||||||
|
},
|
||||||
|
'composer_is_upper': {
|
||||||
|
'description': 'Force the composer to be uppercase',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'composer'
|
||||||
|
},
|
||||||
|
'artist_font': {
|
||||||
|
'description': 'The font for the artist',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'artist'
|
||||||
|
},
|
||||||
|
'artist_size': {
|
||||||
|
'description': 'The size of the artist in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'artist'
|
||||||
|
},
|
||||||
|
'artist_is_bold': {
|
||||||
|
'description': 'Set to True to make the artist bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'artist'
|
||||||
|
},
|
||||||
|
'artist_is_centered': {
|
||||||
|
'description': 'Center the artist on the page',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'artist'
|
||||||
|
},
|
||||||
|
'artist_is_upper': {
|
||||||
|
'description': 'Force the artist to be uppercase',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'artist'
|
||||||
|
},
|
||||||
|
'copyright_font': {
|
||||||
|
'description': 'The font for the copyright',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'copyright'
|
||||||
|
},
|
||||||
|
'copyright_size': {
|
||||||
|
'description': 'The size of the copyright in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'copyright'
|
||||||
|
},
|
||||||
|
'copyright_is_bold': {
|
||||||
|
'description': 'Set to True to make the copyright bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'copyright'
|
||||||
|
},
|
||||||
|
'copyright_is_upper': {
|
||||||
|
'description': 'Force the copyright to be uppercase',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'copyright'
|
||||||
|
},
|
||||||
|
'stanza_font': {
|
||||||
|
'description': 'The font for the stanzas',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'stanza'
|
||||||
|
},
|
||||||
|
'stanza_size': {
|
||||||
|
'description': 'The size of the stanza in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'stanza'
|
||||||
|
},
|
||||||
|
'stanza_is_bold': {
|
||||||
|
'description': 'Set to True to make the stanza bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'stanza'
|
||||||
|
},
|
||||||
|
'stanza_indent': {
|
||||||
|
'description': 'How much to indent the stanza',
|
||||||
|
'type': int,
|
||||||
|
'default': 4,
|
||||||
|
'group': 'stanza'
|
||||||
|
},
|
||||||
|
'stanza_heading_font': {
|
||||||
|
'description': 'The font for the stanza headings',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'stanza_heading'
|
||||||
|
},
|
||||||
|
'stanza_heading_size': {
|
||||||
|
'description': 'The size of the stanza heading in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'stanza_heading'
|
||||||
|
},
|
||||||
|
'stanza_heading_is_bold': {
|
||||||
|
'description': 'Set to True to make the stanza heading bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': True,
|
||||||
|
'group': 'stanza_heading'
|
||||||
|
},
|
||||||
|
'stanza_heading_is_upper': {
|
||||||
|
'description': 'Force the stanza heading to be uppercase',
|
||||||
|
'type': bool,
|
||||||
|
'default': True,
|
||||||
|
'group': 'stanza_heading'
|
||||||
|
},
|
||||||
|
'stanza_heading_is_hidden': {
|
||||||
|
'description': 'Hide the stanza headings',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'stanza_heading'
|
||||||
|
},
|
||||||
|
'verse_font': {
|
||||||
|
'description': 'The font for the verses',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'verse'
|
||||||
|
},
|
||||||
|
'verse_size': {
|
||||||
|
'description': 'The size of the verse in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'verse'
|
||||||
|
},
|
||||||
|
'verse_is_bold': {
|
||||||
|
'description': 'Set to True to make the verse bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'verse'
|
||||||
|
},
|
||||||
|
'verse_indent': {
|
||||||
|
'description': 'How much to indent the verse',
|
||||||
|
'type': int,
|
||||||
|
'default': 4,
|
||||||
|
'group': 'verse'
|
||||||
|
},
|
||||||
|
'verse_heading_font': {
|
||||||
|
'description': 'The font for the verse headings',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'verse_heading'
|
||||||
|
},
|
||||||
|
'verse_heading_size': {
|
||||||
|
'description': 'The size of the verse heading in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'verse_heading'
|
||||||
|
},
|
||||||
|
'verse_heading_is_bold': {
|
||||||
|
'description': 'Set to True to make the verse heading bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': True,
|
||||||
|
'group': 'verse_heading'
|
||||||
|
},
|
||||||
|
'verse_heading_is_upper': {
|
||||||
|
'description': 'Force the verse heading to be uppercase',
|
||||||
|
'type': bool,
|
||||||
|
'default': True,
|
||||||
|
'group': 'verse_heading'
|
||||||
|
},
|
||||||
|
'verse_heading_is_hidden': {
|
||||||
|
'description': 'Hide the verse headings',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'verse_heading'
|
||||||
|
},
|
||||||
|
'chorus_font': {
|
||||||
|
'description': 'The font for the choruses',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'chorus'
|
||||||
|
},
|
||||||
|
'chorus_size': {
|
||||||
|
'description': 'The size of the chorus in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'chorus'
|
||||||
|
},
|
||||||
|
'chorus_is_bold': {
|
||||||
|
'description': 'Set to True to make the chorus bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'chorus'
|
||||||
|
},
|
||||||
|
'chorus_indent': {
|
||||||
|
'description': 'How much to indent the chorus',
|
||||||
|
'type': int,
|
||||||
|
'default': 4,
|
||||||
|
'group': 'chorus'
|
||||||
|
},
|
||||||
|
'chorus_heading_font': {
|
||||||
|
'description': 'The font for the chorus headings',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'chorus_heading'
|
||||||
|
},
|
||||||
|
'chorus_heading_size': {
|
||||||
|
'description': 'The size of the chorus heading in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'chorus_heading'
|
||||||
|
},
|
||||||
|
'chorus_heading_is_bold': {
|
||||||
|
'description': 'Set to True to make the chorus heading bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': True,
|
||||||
|
'group': 'chorus_heading'
|
||||||
|
},
|
||||||
|
'chorus_heading_is_upper': {
|
||||||
|
'description': 'Force the chorus heading to be uppercase',
|
||||||
|
'type': bool,
|
||||||
|
'default': True,
|
||||||
|
'group': 'chorus_heading'
|
||||||
|
},
|
||||||
|
'chorus_heading_is_hidden': {
|
||||||
|
'description': 'Hide the chorus headings',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'chorus_heading'
|
||||||
|
},
|
||||||
|
'bridge_font': {
|
||||||
|
'description': 'The font for the bridge',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'bridge'
|
||||||
|
},
|
||||||
|
'bridge_size': {
|
||||||
|
'description': 'The size of the bridge in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'bridge'
|
||||||
|
},
|
||||||
|
'bridge_is_bold': {
|
||||||
|
'description': 'Set to True to make the bridge bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'bridge'
|
||||||
|
},
|
||||||
|
'bridge_indent': {
|
||||||
|
'description': 'How much to indent the bridge',
|
||||||
|
'type': int,
|
||||||
|
'default': 4,
|
||||||
|
'group': 'bridge'
|
||||||
|
},
|
||||||
|
'bridge_heading_font': {
|
||||||
|
'description': 'The font for the bridge headings',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'bridge_heading'
|
||||||
|
},
|
||||||
|
'bridge_heading_size': {
|
||||||
|
'description': 'The size of the bridge heading in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'bridge_heading'
|
||||||
|
},
|
||||||
|
'bridge_heading_is_bold': {
|
||||||
|
'description': 'Set to True to make the bridge heading bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': True,
|
||||||
|
'group': 'bridge_heading'
|
||||||
|
},
|
||||||
|
'bridge_heading_is_upper': {
|
||||||
|
'description': 'Force the bridge heading to be uppercase',
|
||||||
|
'type': bool,
|
||||||
|
'default': True,
|
||||||
|
'group': 'bridge_heading'
|
||||||
|
},
|
||||||
|
'bridge_heading_is_hidden': {
|
||||||
|
'description': 'Hide the bridge headings',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'bridge_heading'
|
||||||
|
},
|
||||||
|
'chord_font': {
|
||||||
|
'description': 'The font for the chord',
|
||||||
|
'type': str,
|
||||||
|
'default': None,
|
||||||
|
'group': 'chord'
|
||||||
|
},
|
||||||
|
'chord_size': {
|
||||||
|
'description': 'The size of the chord in pt',
|
||||||
|
'type': int,
|
||||||
|
'default': 12,
|
||||||
|
'group': 'chord'
|
||||||
|
},
|
||||||
|
'chord_is_bold': {
|
||||||
|
'description': 'Set to True to make the chord bold',
|
||||||
|
'type': bool,
|
||||||
|
'default': True,
|
||||||
|
'group': 'chord'
|
||||||
|
},
|
||||||
|
'chord_is_centered': {
|
||||||
|
'description': 'Make chords centered over syllables',
|
||||||
|
'type': bool,
|
||||||
|
'default': True,
|
||||||
|
'group': 'chord'
|
||||||
|
},
|
||||||
|
'chord_is_hidden': {
|
||||||
|
'description': 'Hide all the chords',
|
||||||
|
'type': bool,
|
||||||
|
'default': False,
|
||||||
|
'group': 'chord'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_OPTION_GROUPS = []
|
||||||
|
|
||||||
|
|
||||||
|
def get_options(group=None):
|
||||||
|
"""Return the options supported by the HTML renderer"""
|
||||||
|
if not group:
|
||||||
|
return HTML_OPTIONS
|
||||||
|
else:
|
||||||
|
return {key: value for key, value in HTML_OPTIONS.items() if value['group'] == group}
|
||||||
|
|
||||||
|
|
||||||
|
def get_option_groups():
|
||||||
|
if not _OPTION_GROUPS:
|
||||||
|
for option in HTML_OPTIONS.values():
|
||||||
|
if option['group'] not in _OPTION_GROUPS:
|
||||||
|
_OPTION_GROUPS.append(option['group'])
|
||||||
|
return _OPTION_GROUPS
|
||||||
|
|
||||||
|
|
||||||
|
def make_style(name, font=None, size=None, is_bold=None, is_centered=None, is_upper=None, is_hidden=False, indent=None):
|
||||||
|
"""Build a CSS style"""
|
||||||
|
styles = []
|
||||||
|
if font:
|
||||||
|
styles.append(' font-family: \'{font}\';'.format(font=font))
|
||||||
|
if size:
|
||||||
|
styles.append(' font-size: {size}pt;'.format(size=size))
|
||||||
|
if is_bold:
|
||||||
|
styles.append(' font-weight: bold !important;')
|
||||||
|
if is_centered:
|
||||||
|
styles.append(' text-align: center;')
|
||||||
|
if is_upper:
|
||||||
|
styles.append(' text-transform: uppercase;')
|
||||||
|
if is_hidden:
|
||||||
|
styles.append(' display: none !important;')
|
||||||
|
styles.append(' visibility: hidden !important;')
|
||||||
|
if indent:
|
||||||
|
styles.append(' margin-left: {indent}rem;'.format(indent=indent))
|
||||||
|
if styles:
|
||||||
|
if name == 'default':
|
||||||
|
styles.insert(0, 'html, body, * {')
|
||||||
|
else:
|
||||||
|
styles.insert(0, '.{name} {{'.format(name=name))
|
||||||
|
styles.append('}')
|
||||||
|
return os.linesep.join(styles)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_option_styles(options):
|
||||||
|
styles = []
|
||||||
|
for group in get_option_groups():
|
||||||
|
kwargs = {}
|
||||||
|
for key in STYLE_KEYS:
|
||||||
|
name = '{group}_{key}'.format(group=group, key=key)
|
||||||
|
if name not in HTML_OPTIONS:
|
||||||
|
continue
|
||||||
|
value = options.get(name, HTML_OPTIONS[name]['default'])
|
||||||
|
if value is not None:
|
||||||
|
kwargs[key] = value
|
||||||
|
styles.append(make_style(group.replace('_', '-'), **kwargs))
|
||||||
|
return styles
|
||||||
|
|
||||||
|
|
||||||
|
def render(song, options=None, extra_styles=None):
|
||||||
|
"""Render a song to HTML"""
|
||||||
|
nl = os.linesep
|
||||||
|
styles = options and generate_option_styles(options) or []
|
||||||
|
if extra_styles:
|
||||||
|
styles.append(extra_styles)
|
||||||
|
rendered_verses = []
|
||||||
|
for verse in song.verse_order:
|
||||||
|
rendered_lines = []
|
||||||
|
for line in verse.lines:
|
||||||
|
rendered_syllables = []
|
||||||
|
for word_counter, word in enumerate(line.words):
|
||||||
|
is_last_word = (word_counter + 1) == len(line.words)
|
||||||
|
for syll_counter, syllable in enumerate(word.syllables):
|
||||||
|
is_last_syllable = (syll_counter + 1) == len(word.syllables)
|
||||||
|
rendered_chord = CHORD.format(syllable.chord or ' ')
|
||||||
|
rendered_syllable = SYLLB.format(
|
||||||
|
(syllable.syllable or ' ') +
|
||||||
|
(' ' if is_last_syllable and not is_last_word else ''))
|
||||||
|
rendered_syllables.append(WRAPP.format(rendered_chord + rendered_syllable))
|
||||||
|
rendered_lines.append(LINE.format(''.join(rendered_syllables)))
|
||||||
|
rendered_verses.append(STANZA.format(verse_type=verse.type_ or '', verse_name=verse.title,
|
||||||
|
verse_body=nl.join(rendered_lines)))
|
||||||
|
metadata_lines = []
|
||||||
|
if song.metadata.get('key') or song.metadata.get('capo'):
|
||||||
|
key_lines = []
|
||||||
|
if song.metadata.get('key'):
|
||||||
|
key_lines.append(KEY.format(key=song.metadata.get('key')))
|
||||||
|
if song.metadata.get('capo'):
|
||||||
|
key_lines.append(CAPO.format(capo=song.metadata.get('capo')))
|
||||||
|
if key_lines:
|
||||||
|
metadata_lines.append(KEY_CAPO.format(key_capo=''.join(key_lines)))
|
||||||
|
metadata_lines.append(TITLE.format(title=song.metadata.get('title') or 'Song'))
|
||||||
|
if song.metadata.get('composer'):
|
||||||
|
metadata_lines.append(COMPOSER.format(composer=song.metadata.get('composer')))
|
||||||
|
if song.metadata.get('artist'):
|
||||||
|
metadata_lines.append(ARTIST.format(artist=song.metadata.get('artist')))
|
||||||
|
metadata = METADATA.format(metadata=nl.join(metadata_lines))
|
||||||
|
footer = ''
|
||||||
|
# if song.metadata.get('copyright'):
|
||||||
|
# copyright = COPYRIGHT.format(copyright=song.metadata.get('copyright'))
|
||||||
|
# footer = FOOTER.format(footer=copyright)
|
||||||
|
body = metadata + nl + nl.join(rendered_verses) + nl + footer
|
||||||
|
return HTML.format(title=song.metadata.get('title') or 'Song', body=body, styles=nl.join(styles))
|
|
@ -1,6 +1,13 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
TEXT_OPTIONS = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_options(group=None):
|
||||||
|
return TEXT_OPTIONS
|
||||||
|
|
||||||
|
|
||||||
def render(song, verse_upper=False):
|
def render(song, verse_upper=False):
|
||||||
"""Render a song to a text file"""
|
"""Render a song to a text file"""
|
||||||
nl = os.linesep
|
nl = os.linesep
|
Loading…
Reference in New Issue