import os CHORD = '
{}
' SYLLB = '
{}
' WRAPP = '
{}
' LINE = '
{}
' STANZA = '''

{verse_name}

{verse_body}
''' METADATA = '''
{metadata}
''' TITLE = '

{title}

' ARTIST = '

As performed by {artist}

' COMPOSER = '

Written by {composer}

' KEY_CAPO = '
{key_capo}
' KEY = '
Key: {key}
' CAPO = '
Capo: {capo}
' COPYRIGHT = '' FOOTER = '' HTML = ''' {title} {body} ''' STYLE_KEYS = [ 'font', 'size', 'is_bold', 'is_centered', 'is_upper', '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' }, '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' }, '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' }, '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' }, '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' } } _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, 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 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 option = options.get(name, HTML_OPTIONS[name]['default']) if option: kwargs[key] = option 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))