Various updates, including a new text renderer
- Add a new text file renderer - Update the HTML renderer to include some basic formatting options - Expand the CLI to specify a renderer and pass parameters to the selected renderer - Fix a bug where trailing chords were being swallowed
This commit is contained in:
parent
41394d559c
commit
161dbf9864
@ -1,17 +1,33 @@
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from chordpro.base import Song
|
||||
from chordpro.render.html import render
|
||||
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)
|
||||
output = render(song)
|
||||
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)
|
||||
|
@ -7,6 +7,7 @@ from chordpro.constants import KNOWN_DIRECTIVES, KNOWN_VERSE_TYPES
|
||||
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 = {}
|
||||
SYLLABLE_EXCEPTIONS = {
|
||||
@ -71,7 +72,7 @@ class Word(object):
|
||||
4. Add chord to syllable
|
||||
"""
|
||||
word_parts = []
|
||||
chords = [''] # Due to regex, chords will always be 1 behind, so create an empty item to bump them along
|
||||
chords = ['']
|
||||
match = CHORD_WORD.match(word)
|
||||
while match:
|
||||
word_parts.append(match.group(1))
|
||||
@ -100,18 +101,41 @@ class Word(object):
|
||||
sylls = [whole_word]
|
||||
for syll in sylls:
|
||||
syllable = Syllable(syll)
|
||||
for i, part in enumerate(word_parts):
|
||||
can_consume = False
|
||||
for idx, part in enumerate(word_parts):
|
||||
if part.startswith(syll):
|
||||
syllable.chord = chords[i]
|
||||
can_consume = True
|
||||
syllable.chord = chords[idx]
|
||||
break
|
||||
self.syllables.append(syllable)
|
||||
if can_consume:
|
||||
word_parts = word_parts[idx + 1:]
|
||||
chords = chords[idx + 1:]
|
||||
# Process any left over chords, they're trailing chords
|
||||
for chord in chords:
|
||||
self.syllables.append(Syllable('', chord))
|
||||
|
||||
|
||||
class Line(object):
|
||||
|
||||
def __init__(self, line=None):
|
||||
if line:
|
||||
self.words = [Word(word) for word in line.split(' ')]
|
||||
words = line.split(' ')
|
||||
# Split trailing chords into their own "words"
|
||||
last_word = words[-1]
|
||||
trailing_chords = []
|
||||
for part in last_word.split('['):
|
||||
if not part:
|
||||
continue
|
||||
if part.split(']')[-1] == '':
|
||||
trailing_chords.append('[{}]'.format(part.split(']')[0]))
|
||||
# remove chords from last word
|
||||
for chord in trailing_chords:
|
||||
last_word = last_word.replace(chord, '')
|
||||
# replace last word, and append trailing chords as separate words
|
||||
words[-1] = last_word
|
||||
words.extend(trailing_chords)
|
||||
self.words = [Word(word) for word in words]
|
||||
else:
|
||||
self.words = []
|
||||
|
||||
@ -132,7 +156,7 @@ class Verse(object):
|
||||
match = START_OF.match(line)
|
||||
verse = cls(match.group(1))
|
||||
if len(match.groups()) > 1:
|
||||
verse.title = match.group(3)
|
||||
verse.title = match.group(3) or match.group(1).title()
|
||||
return verse
|
||||
|
||||
@staticmethod
|
||||
@ -146,6 +170,18 @@ class Verse(object):
|
||||
return match is not None
|
||||
return match.group(1) == type_
|
||||
|
||||
@staticmethod
|
||||
def is_chorus_marker(line):
|
||||
return line.strip().startswith('{chorus')
|
||||
|
||||
@staticmethod
|
||||
def get_chorus_from_marker(line):
|
||||
match = CHORUS_MARKER.match(line)
|
||||
if not match:
|
||||
return None
|
||||
if len(match.groups()) > 1:
|
||||
return match.group(2)
|
||||
|
||||
|
||||
class Metadata(object):
|
||||
|
||||
@ -170,6 +206,7 @@ class Song(object):
|
||||
def parse(self, filename):
|
||||
self.metadata = Metadata()
|
||||
self.verses = []
|
||||
self.verse_order = []
|
||||
with open(filename) as song_file:
|
||||
is_verse = False
|
||||
current_verse = None
|
||||
@ -179,6 +216,7 @@ class Song(object):
|
||||
elif Verse.is_start_of_verse(line):
|
||||
is_verse = True
|
||||
current_verse = Verse.parse(line)
|
||||
self.verse_order.append(current_verse)
|
||||
elif Verse.is_end_of_verse(line):
|
||||
if not Verse.is_end_of_verse(line, current_verse.type_):
|
||||
raise MismatchedVerseType(line_number, current_verse.type_)
|
||||
@ -187,3 +225,9 @@ class Song(object):
|
||||
current_verse = None
|
||||
elif is_verse:
|
||||
current_verse.add_line(line.strip())
|
||||
elif Verse.is_chorus_marker(line):
|
||||
chorus_name = Verse.get_chorus_from_marker(line)
|
||||
for verse in self.verses[::-1]:
|
||||
if verse.title == chorus_name or verse.type_ == "chorus":
|
||||
self.verse_order.append(verse)
|
||||
break
|
||||
|
@ -13,21 +13,28 @@ HTML = '''<html>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
.verse-name {{ font-weight: bold; margin-bottom: 0; text-transform: uppercase; }}
|
||||
h4.verse-name {{ font-weight: normal; }}
|
||||
.verse-name {{ margin-bottom: 0; }}
|
||||
.verse-body {{ padding-left: 2rem; }}
|
||||
.chord {{ font-weight: bold; text-align: center; }}
|
||||
.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):
|
||||
def render(song, verse_upper=False, verse_name_bold=False, chord_bold=False):
|
||||
"""Render a song to HTML"""
|
||||
rendered_verses = []
|
||||
for verse in song.verses:
|
||||
for verse in song.verse_order:
|
||||
rendered_lines = []
|
||||
for line in verse.lines:
|
||||
rendered_chords = []
|
||||
@ -45,4 +52,11 @@ def render(song):
|
||||
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)
|
||||
return HTML.format(title=title, body=body)
|
||||
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)
|
||||
|
32
src/chordpro/renderers/text.py
Normal file
32
src/chordpro/renderers/text.py
Normal file
@ -0,0 +1,32 @@
|
||||
import os
|
||||
|
||||
|
||||
def render(song, verse_upper=False):
|
||||
"""Render a song to a text file"""
|
||||
nl = os.linesep
|
||||
bl = nl + nl
|
||||
rendered_verses = []
|
||||
for verse in song.verse_order:
|
||||
verse_title = verse.title.upper() if verse_upper else verse.title
|
||||
rendered_lines = [verse_title + ':']
|
||||
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)
|
||||
padding = max(len(syllable.syllable), len(syllable.chord or ''))
|
||||
if is_last_syllable and not is_last_word:
|
||||
padding += 1
|
||||
elif syllable.chord and not syllable.syllable:
|
||||
padding += 1
|
||||
rendered_chords.append('{chord:^{padding}}'.format(chord=syllable.chord or '', padding=padding))
|
||||
rendered_syllables.append('{syllable:<{padding}}'.format(syllable=syllable.syllable,
|
||||
padding=padding))
|
||||
rendered_lines.append(''.join(rendered_chords))
|
||||
rendered_lines.append(''.join(rendered_syllables))
|
||||
rendered_verses.append(nl.join(rendered_lines))
|
||||
title = song.metadata.get('title') or 'Song'
|
||||
artist = song.metadata.get('artist') or song.metadata.get('composer') or ''
|
||||
return nl.join([title, artist, '', bl.join(rendered_verses)]) + nl
|
Loading…
Reference in New Issue
Block a user