535 lines
16 KiB
Python
535 lines
16 KiB
Python
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 name == 'default':
|
|
styles.append('html, body, * {')
|
|
else:
|
|
styles.append('.{name} {{'.format(name=name))
|
|
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))
|