Compare commits
No commits in common. "161dbf9864f27aa081d7cd135dfbcce5c73213f2" and "6675f2ccb984c7d5fdd591aba7362ed66d4b515e" have entirely different histories.
161dbf9864
...
6675f2ccb9
@ -1,33 +1,17 @@
|
|||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
from chordpro.base import Song
|
from chordpro.base import Song
|
||||||
from chordpro.renderers.html import render as render_html
|
from chordpro.render.html import render
|
||||||
from chordpro.renderers.text import render as render_text
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = ArgumentParser()
|
parser = ArgumentParser()
|
||||||
parser.add_argument('input', metavar='FILENAME', help='Input ChordPro file')
|
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('-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()
|
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)
|
song = Song(args.input)
|
||||||
if args.format == 'text':
|
output = render(song)
|
||||||
output = render_text(song, **render_params)
|
|
||||||
else:
|
|
||||||
output = render_html(song, **render_params)
|
|
||||||
if args.output:
|
if args.output:
|
||||||
with open(args.output, 'w') as html_file:
|
with open(args.output, 'w') as html_file:
|
||||||
html_file.write(output)
|
html_file.write(output)
|
||||||
|
@ -7,7 +7,6 @@ from chordpro.constants import KNOWN_DIRECTIVES, KNOWN_VERSE_TYPES
|
|||||||
DIRECTIVE = re.compile(r'\{(.*?): *(.*?)\}')
|
DIRECTIVE = re.compile(r'\{(.*?): *(.*?)\}')
|
||||||
START_OF = re.compile(r'\{start_of_(' + '|'.join(KNOWN_VERSE_TYPES) + 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')\}')
|
END_OF = re.compile(r'\{end_of_(' + '|'.join(KNOWN_VERSE_TYPES) + r')\}')
|
||||||
CHORUS_MARKER = re.compile(r'\{chorus(: *(.*?))?\}')
|
|
||||||
CHORD_WORD = re.compile(r'(.*?)\[(.*?)\]')
|
CHORD_WORD = re.compile(r'(.*?)\[(.*?)\]')
|
||||||
HYPHEN_CACHE = {}
|
HYPHEN_CACHE = {}
|
||||||
SYLLABLE_EXCEPTIONS = {
|
SYLLABLE_EXCEPTIONS = {
|
||||||
@ -72,7 +71,7 @@ class Word(object):
|
|||||||
4. Add chord to syllable
|
4. Add chord to syllable
|
||||||
"""
|
"""
|
||||||
word_parts = []
|
word_parts = []
|
||||||
chords = ['']
|
chords = [''] # Due to regex, chords will always be 1 behind, so create an empty item to bump them along
|
||||||
match = CHORD_WORD.match(word)
|
match = CHORD_WORD.match(word)
|
||||||
while match:
|
while match:
|
||||||
word_parts.append(match.group(1))
|
word_parts.append(match.group(1))
|
||||||
@ -101,41 +100,18 @@ class Word(object):
|
|||||||
sylls = [whole_word]
|
sylls = [whole_word]
|
||||||
for syll in sylls:
|
for syll in sylls:
|
||||||
syllable = Syllable(syll)
|
syllable = Syllable(syll)
|
||||||
can_consume = False
|
for i, part in enumerate(word_parts):
|
||||||
for idx, part in enumerate(word_parts):
|
|
||||||
if part.startswith(syll):
|
if part.startswith(syll):
|
||||||
can_consume = True
|
syllable.chord = chords[i]
|
||||||
syllable.chord = chords[idx]
|
|
||||||
break
|
break
|
||||||
self.syllables.append(syllable)
|
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):
|
class Line(object):
|
||||||
|
|
||||||
def __init__(self, line=None):
|
def __init__(self, line=None):
|
||||||
if line:
|
if line:
|
||||||
words = line.split(' ')
|
self.words = [Word(word) for word in 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:
|
else:
|
||||||
self.words = []
|
self.words = []
|
||||||
|
|
||||||
@ -156,7 +132,7 @@ class Verse(object):
|
|||||||
match = START_OF.match(line)
|
match = START_OF.match(line)
|
||||||
verse = cls(match.group(1))
|
verse = cls(match.group(1))
|
||||||
if len(match.groups()) > 1:
|
if len(match.groups()) > 1:
|
||||||
verse.title = match.group(3) or match.group(1).title()
|
verse.title = match.group(3)
|
||||||
return verse
|
return verse
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -170,18 +146,6 @@ 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_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):
|
class Metadata(object):
|
||||||
|
|
||||||
@ -206,7 +170,6 @@ class Song(object):
|
|||||||
def parse(self, filename):
|
def parse(self, filename):
|
||||||
self.metadata = Metadata()
|
self.metadata = Metadata()
|
||||||
self.verses = []
|
self.verses = []
|
||||||
self.verse_order = []
|
|
||||||
with open(filename) as song_file:
|
with open(filename) as song_file:
|
||||||
is_verse = False
|
is_verse = False
|
||||||
current_verse = None
|
current_verse = None
|
||||||
@ -216,7 +179,6 @@ class Song(object):
|
|||||||
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)
|
|
||||||
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_)
|
||||||
@ -225,9 +187,3 @@ 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_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,28 +13,21 @@ HTML = '''<html>
|
|||||||
<head>
|
<head>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<style>
|
<style>
|
||||||
h4.verse-name {{ font-weight: normal; }}
|
.verse-name {{ font-weight: bold; margin-bottom: 0; text-transform: uppercase; }}
|
||||||
.verse-name {{ margin-bottom: 0; }}
|
|
||||||
.verse-body {{ padding-left: 2rem; }}
|
.verse-body {{ padding-left: 2rem; }}
|
||||||
.chord {{ text-align: center; }}
|
.chord {{ font-weight: bold; text-align: center; }}
|
||||||
{styles}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{body}
|
{body}
|
||||||
</body>
|
</body>
|
||||||
</html>'''
|
</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):
|
def render(song):
|
||||||
"""Render a song to HTML"""
|
"""Render a song to HTML"""
|
||||||
rendered_verses = []
|
rendered_verses = []
|
||||||
for verse in song.verse_order:
|
for verse in song.verses:
|
||||||
rendered_lines = []
|
rendered_lines = []
|
||||||
for line in verse.lines:
|
for line in verse.lines:
|
||||||
rendered_chords = []
|
rendered_chords = []
|
||||||
@ -52,11 +45,4 @@ def render(song, verse_upper=False, verse_name_bold=False, chord_bold=False):
|
|||||||
title = song.metadata.get('title') or 'Song'
|
title = song.metadata.get('title') or 'Song'
|
||||||
metadata = TITLE.format(title=title, artist=song.metadata.get('artist') or song.metadata.get('composer') or '')
|
metadata = TITLE.format(title=title, artist=song.metadata.get('artist') or song.metadata.get('composer') or '')
|
||||||
body = metadata + '\n' + '\n'.join(rendered_verses)
|
body = metadata + '\n' + '\n'.join(rendered_verses)
|
||||||
styles = ''
|
return HTML.format(title=title, body=body)
|
||||||
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)
|
|
@ -1,32 +0,0 @@
|
|||||||
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