Support additional markers in the chordpro file, and move to div-based HTML

This commit is contained in:
Raoul Snyman 2021-08-07 01:08:01 -07:00
parent 454c29152d
commit d3f68f109b
3 changed files with 64 additions and 13 deletions

View File

@ -1,7 +1,7 @@
from hyphen import Hyphenator from hyphen import Hyphenator
from chordpro.constants import DIRECTIVE, CHORD_WORD, CHORUS_MARKER, KNOWN_DIRECTIVES, \ from chordpro.constants import BRIDGE_MARKER, CHORD_WORD, CHORUS_MARKER, DIRECTIVE, END_OF, \
START_OF, END_OF KNOWN_DIRECTIVES, START_OF, VERSE_MARKER
HYPHEN_CACHE = {} HYPHEN_CACHE = {}
SYLLABLE_EXCEPTIONS = { SYLLABLE_EXCEPTIONS = {
@ -159,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')
@ -171,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):
@ -214,9 +238,21 @@ class Song(object):
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):
verse_name = Verse.get_verse_from_marker(line)
for verse in self.verses[::-1]:
if verse.title == verse_name or verse.type_ == "verse":
self.verse_order.append(verse)
break
elif Verse.is_chorus_marker(line): elif Verse.is_chorus_marker(line):
chorus_name = Verse.get_chorus_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.title == chorus_name or verse.type_ == "chorus": if verse.title == chorus_name or verse.type_ == "chorus":
self.verse_order.append(verse) self.verse_order.append(verse)
break break
elif Verse.is_bridge_marker(line):
bridge_name = Verse.get_bridge_from_marker(line)
for verse in self.verses[::-1]:
if verse.title == bridge_name or verse.type_ == "bridge":
self.verse_order.append(verse)
break

View File

@ -35,5 +35,6 @@ START_OF = re.compile(r'\{start_of_(' + '|'.join(KNOWN_VERSE_TYPES) + r')(: *(.*
END_OF = re.compile(r'\{end_of_(' + '|'.join(KNOWN_VERSE_TYPES) + r')\}') END_OF = re.compile(r'\{end_of_(' + '|'.join(KNOWN_VERSE_TYPES) + r')\}')
CHORUS_MARKER = re.compile(r'\{chorus(: *(.*?))?\}') CHORUS_MARKER = re.compile(r'\{chorus(: *(.*?))?\}')
VERSE_MARKER = re.compile(r'\{verse: *(.*?)\}') VERSE_MARKER = re.compile(r'\{verse: *(.*?)\}')
BRIDGE_MARKER = re.compile(r'\{bridge: *(.*?)\}')
CHORD_WORD = re.compile(r'(.*?)\[(.*?)\]') CHORD_WORD = re.compile(r'(.*?)\[(.*?)\]')
CHORD = re.compile(r'\[(.*?)\]') CHORD = re.compile(r'\[(.*?)\]')

View File

@ -1,9 +1,16 @@
import os import os
CHORD = '<td class="chord">{}</td>' # Tables -- don't work when rendered to PDF
SYLLB = '<td class="syllable">{}</td>' # CHORD = '<td class="chord">{}</td>'
LINE = '<table class="line" border="0" cellpadding="0" cellspacing="0"><tr class="chords-line">{}</tr><tr ' \ # SYLLB = '<td class="syllable">{}</td>'
'class="lyrics-line">{}</tr></table>' # LINE = '<table class="line" border="0" cellpadding="0" cellspacing="0"><tr class="chords-line">{}</tr><tr ' \
# 'class="lyrics-line">{}</tr></table>'
# Divs
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"> STANZA = '''<section class="stanza-section {verse_type}-section">
<h4 class="stanza-heading {verse_type}-heading">{verse_name}</h4> <h4 class="stanza-heading {verse_type}-heading">{verse_name}</h4>
<div class="stanza {verse_type}"> <div class="stanza {verse_type}">
@ -16,6 +23,9 @@ HTML = '''<html>
<title>{title}</title> <title>{title}</title>
<style> <style>
h1.title, h3.composer, h4.stanza-heading {{ font-weight: normal; }} h1.title, h3.composer, h4.stanza-heading {{ font-weight: normal; }}
h4.stanza-heading {{ margin-bottom: 0; }}
.stanza-section {{ margin-bottom: 2rem; page-break-inside: avoid; }}
.wrapper {{ display: inline-block !important; }}
{styles} {styles}
</style> </style>
</head> </head>
@ -403,26 +413,30 @@ def generate_option_styles(options):
return styles return styles
def render(song, options=None): def render(song, options=None, extra_styles=None):
"""Render a song to HTML""" """Render a song to HTML"""
styles = options and generate_option_styles(options) or [] styles = options and generate_option_styles(options) or []
if extra_styles:
styles.append(extra_styles)
rendered_verses = [] rendered_verses = []
for verse in song.verse_order: for verse in song.verse_order:
rendered_lines = [] rendered_lines = []
for line in verse.lines: for line in verse.lines:
rendered_chords = []
rendered_syllables = [] rendered_syllables = []
for word_counter, word in enumerate(line.words): for word_counter, word in enumerate(line.words):
is_last_word = (word_counter + 1) == len(line.words) is_last_word = (word_counter + 1) == len(line.words)
for syll_counter, syllable in enumerate(word.syllables): for syll_counter, syllable in enumerate(word.syllables):
is_last_syllable = (syll_counter + 1) == len(word.syllables) is_last_syllable = (syll_counter + 1) == len(word.syllables)
rendered_chords.append(CHORD.format(syllable.chord or '&nbsp;')) rendered_chord = CHORD.format(syllable.chord or '&nbsp;')
rendered_syllables.append(SYLLB.format( rendered_syllable = SYLLB.format(
syllable.syllable + ('&nbsp;' if is_last_syllable and not is_last_word else ''))) (syllable.syllable or '&nbsp;') +
rendered_lines.append(LINE.format(''.join(rendered_chords), ''.join(rendered_syllables))) ('&nbsp;' 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, rendered_verses.append(STANZA.format(verse_type=verse.type_ or '', verse_name=verse.title,
verse_body='\n'.join(rendered_lines))) verse_body='\n'.join(rendered_lines)))
title = song.metadata.get('title') or 'Song' title = song.metadata.get('title') or 'Song'
metadata = TITLE.format(title=title, composer=song.metadata.get('artist') or song.metadata.get('composer') or '') metadata = TITLE.format(title=title, composer=song.metadata.get('artist') or
song.metadata.get('composer') or '')
body = metadata + '\n' + '\n'.join(rendered_verses) body = metadata + '\n' + '\n'.join(rendered_verses)
return HTML.format(title=title, body=body, styles=os.linesep.join(styles)) return HTML.format(title=title, body=body, styles=os.linesep.join(styles))