forked from openlp/openlp
More work on chords
This commit is contained in:
parent
530f119938
commit
611c970eb0
@ -27,6 +27,7 @@ OpenLP work.
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import math
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from PyQt5 import QtCore, QtGui, Qt, QtWidgets
|
||||
@ -314,6 +315,63 @@ def expand_tags(text):
|
||||
return text
|
||||
|
||||
|
||||
def expand_and_align_chords_in_line(match):
|
||||
"""
|
||||
Expand the chords in the line and align them using whitespaces.
|
||||
NOTE: There is equivalent javascript code in chords.js, in the updateSlide function. Make sure to update both!
|
||||
|
||||
:param match:
|
||||
:return: The line with expanded html-chords
|
||||
"""
|
||||
slimchars = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
|
||||
whitespaces = ''
|
||||
chordlen = 0
|
||||
taillen = 0
|
||||
chord = match.group(1)
|
||||
tail = match.group(2)
|
||||
remainder = match.group(3)
|
||||
end = match.group(4)
|
||||
print('chord: %s, tail: %s, remainder: %s, end: %s' % (chord, tail, remainder, end))
|
||||
for chord_char in chord:
|
||||
if chord_char not in slimchars:
|
||||
chordlen += 2
|
||||
else:
|
||||
chordlen += 1
|
||||
for tail_char in tail:
|
||||
if tail_char not in slimchars:
|
||||
taillen += 2
|
||||
else:
|
||||
taillen += 1
|
||||
for remainder_char in remainder:
|
||||
if remainder_char not in slimchars:
|
||||
taillen += 2
|
||||
else:
|
||||
taillen += 1
|
||||
if chordlen >= taillen and end is None:
|
||||
if tail:
|
||||
if not remainder:
|
||||
print()
|
||||
for c in range(math.ceil((chordlen - taillen) / 2) + 1):
|
||||
whitespaces += '_'
|
||||
else:
|
||||
for c in range(chordlen - taillen + 2):
|
||||
whitespaces += ' '
|
||||
else:
|
||||
if not remainder:
|
||||
for c in range(math.floor((chordlen - taillen) / 2)):
|
||||
whitespaces += '_'
|
||||
else:
|
||||
for c in range(chordlen - taillen + 1):
|
||||
whitespaces += ' '
|
||||
else:
|
||||
if not tail and remainder and remainder[0] == ' ':
|
||||
for c in range(chordlen):
|
||||
whitespaces += ' '
|
||||
if whitespaces:
|
||||
whitespaces = '<span class="ws">' + whitespaces + '</span>'
|
||||
return '<span class="chord"><span><strong>' + chord + '</strong></span></span>' + tail + whitespaces + remainder
|
||||
|
||||
|
||||
def expand_chords(text):
|
||||
"""
|
||||
Expand ChordPro tags
|
||||
@ -322,21 +380,22 @@ def expand_chords(text):
|
||||
"""
|
||||
text_lines = text.split('{br}')
|
||||
expanded_text_lines = []
|
||||
chords_on_last_line = False
|
||||
chords_on_prev_line = False
|
||||
for line in text_lines:
|
||||
# If a ChordPro is detected in the line, replace it with a html-span tag and wrap the line in a span tag.
|
||||
if '[' in line and ']' in line:
|
||||
if chords_on_last_line:
|
||||
if chords_on_prev_line:
|
||||
new_line = '<span class="chordline">'
|
||||
else:
|
||||
new_line = '<span class="chordline firstchordline">'
|
||||
chords_on_last_line = True
|
||||
new_line += re.sub(r'(.*?)\[(.+?)\](.*?)',
|
||||
r'\1<span class="chord"><span><strong>\2</strong></span></span>\3', line)
|
||||
chords_on_prev_line = True
|
||||
new_line += re.sub(r'\[(.+?)\]([\u0080-\uFFFF,\w]*)([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?', expand_and_align_chords_in_line, line)
|
||||
#new_line += re.sub(r'(.*?)\[(.+?)\](.*?)',
|
||||
# r'\1<span class="chord"><span><strong>\2</strong></span></span>\3', line)
|
||||
new_line += '</span>'
|
||||
expanded_text_lines.append(new_line)
|
||||
else:
|
||||
chords_on_last_line = False
|
||||
chords_on_prev_line = False
|
||||
expanded_text_lines.append(line)
|
||||
return '{br}'.join(expanded_text_lines)
|
||||
|
||||
|
@ -43,8 +43,8 @@
|
||||
#plus,
|
||||
#minus {
|
||||
display: inline-block;
|
||||
width: 3vw;
|
||||
line-height: 3vw;
|
||||
width: 1.2em;
|
||||
line-height: 1.2em;
|
||||
vertical-align: middle;
|
||||
color: white;
|
||||
background-color: gray;
|
||||
|
@ -178,32 +178,43 @@ window.OpenLP = {
|
||||
var transposeValue = getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0]);
|
||||
var chordclass=/class="[a-z\s]*chord[a-z\s]*"\s*style="display:\s?none"/g;
|
||||
var chordclassshow='class="chord"';
|
||||
var regchord=/<span class="chord"><span><strong>([\(\w#b♭\+\*\d/\)-]+)<\/strong><\/span><\/span>([\u0080-\uFFFF,\w]*)([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(<br>)?/g;
|
||||
var replaceChords=function(mstr,$1,$2,$3,$4) {
|
||||
var v='', w='';
|
||||
var $1len = 0, $2len = 0, slimchars='fiíIÍjlĺľrtť.,;/ ()|"\'!:\\';
|
||||
$1 = transposeChord($1, transposeValue, OpenLP.chordNotation);
|
||||
for (var i = 0; i < $1.length; i++) if (slimchars.indexOf($1.charAt(i)) === -1) {$1len += 2;} else {$1len += 1;}
|
||||
for (var i = 0; i < $2.length; i++) if (slimchars.indexOf($2.charAt(i)) === -1) {$2len += 2;} else {$2len += 1;}
|
||||
for (var i = 0; i < $3.length; i++) if (slimchars.indexOf($2.charAt(i)) === -1) {$2len += 2;} else {$2len += 1;}
|
||||
if ($1len >= $2len && !$4) {
|
||||
if ($2.length){
|
||||
if (!$3.length) {
|
||||
for (c = 0; c < Math.ceil(($1len - $2len) / 2) + 1; c++) {w += '_';}
|
||||
var regchord=/<span class="chord"><span><strong>([\(\w#b♭\+\*\d/\)-]+)<\/strong><\/span><\/span>([\u0080-\uFFFF,\w]*)(<span class="ws">.+?<\/span>)?([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(<br>)?/g;
|
||||
// NOTE: There is equivalent python code in openlp/core/lib/__init__.py, in the expand_and_align_chords_in_line function. Make sure to update both!
|
||||
var replaceChords=function(mstr,$chord,$tail,$skips,$remainder,$end) {
|
||||
var v='';
|
||||
var w='';
|
||||
var $chordlen = 0;
|
||||
var $taillen = 0;
|
||||
var slimchars='fiíIÍjlĺľrtť.,;/ ()|"\'!:\\';
|
||||
// Transpose chord as dictated by the transpose value in local storage
|
||||
$chord = transposeChord($chord, transposeValue, OpenLP.chordNotation);
|
||||
// Replace any padding '_' added to tail
|
||||
$tail = $tail.replace(/_+$/, '')
|
||||
console.log('chord: ' +$chord +', tail: ' + $tail + ', remainder: ' + $remainder +', end: ' + $end +', match: ' + mstr)
|
||||
for (var i = 0; i < $chord.length; i++) if (slimchars.indexOf($chord.charAt(i)) === -1) {$chordlen += 2;} else {$chordlen += 1;}
|
||||
for (var i = 0; i < $tail.length; i++) if (slimchars.indexOf($tail.charAt(i)) === -1) {$taillen += 2;} else {$taillen += 1;}
|
||||
for (var i = 0; i < $remainder.length; i++) if (slimchars.indexOf($tail.charAt(i)) === -1) {$taillen += 2;} else {$taillen += 1;}
|
||||
if ($chordlen >= $taillen && !$end) {
|
||||
if ($tail.length){
|
||||
if (!$remainder.length) {
|
||||
for (c = 0; c < Math.ceil(($chordlen - $taillen) / 2) + 1; c++) {w += '_';}
|
||||
} else {
|
||||
for (c = 0; c < $1len - $2len + 2; c++) {w += ' ';}
|
||||
for (c = 0; c < $chordlen - $taillen + 2; c++) {w += ' ';}
|
||||
}
|
||||
} else {
|
||||
if (!$3.length) {
|
||||
for (c = 0; c < Math.floor(($1len - $2len) / 2) + 1; c++) {w += '_';}
|
||||
if (!$remainder.length) {
|
||||
for (c = 0; c < Math.floor(($chordlen - $taillen) / 2) + 1; c++) {w += '_';}
|
||||
} else {
|
||||
for (c = 0; c < $1len - $2len + 1; c++) {w += ' ';}
|
||||
for (c = 0; c < $chordlen - $taillen + 1; c++) {w += ' ';}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (!$2 && $3.charAt(0) == ' ') {for (c = 0; c < $1len; c++) {w += ' ';}}
|
||||
if (!$tail && $remainder.charAt(0) == ' ') {for (c = 0; c < $chordlen; c++) {w += ' ';}}
|
||||
}
|
||||
return $.grep(['<span class="chord"><span><strong>', $1, '</strong></span>', $2, w, $3, '</span>', $4], Boolean).join('');
|
||||
if (w!='') {
|
||||
w='<span class="ws">' + w + '</span>';
|
||||
}
|
||||
return $.grep(['<span class="chord"><span><strong>', $chord, '</strong></span></span>', $tail, w, $remainder, $end], Boolean).join('');
|
||||
};
|
||||
$("#verseorder span").removeClass("currenttag");
|
||||
$("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag");
|
||||
|
@ -26,7 +26,7 @@ import re
|
||||
from lxml import objectify
|
||||
from lxml.etree import Error, LxmlError
|
||||
|
||||
from openlp.core.common import translate
|
||||
from openlp.core.common import translate, Settings
|
||||
from openlp.plugins.songs.lib import VerseType
|
||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||
from openlp.plugins.songs.lib.ui import SongStrings
|
||||
@ -87,7 +87,7 @@ class OpenSongImport(SongImport):
|
||||
All verses are imported and tagged appropriately.
|
||||
|
||||
Guitar chords can be provided "above" the lyrics (the line is preceded by a period "."), and one or more "_" can
|
||||
be used to signify long-drawn-out words. Chords and "_" are removed by this importer. For example::
|
||||
be used to signify long-drawn-out words. For example::
|
||||
|
||||
. A7 Bm
|
||||
1 Some____ Words
|
||||
@ -195,14 +195,34 @@ class OpenSongImport(SongImport):
|
||||
lyrics = str(root.lyrics)
|
||||
else:
|
||||
lyrics = ''
|
||||
chords = []
|
||||
for this_line in lyrics.split('\n'):
|
||||
if not this_line.strip():
|
||||
continue
|
||||
# skip this line if it is a comment
|
||||
if this_line.startswith(';'):
|
||||
continue
|
||||
# skip guitar chords and page and column breaks
|
||||
if this_line.startswith('.') or this_line.startswith('---') or this_line.startswith('-!!'):
|
||||
# skip page and column breaks
|
||||
if this_line.startswith('---') or this_line.startswith('-!!'):
|
||||
continue
|
||||
# guitar chords marker
|
||||
if this_line.startswith('.'):
|
||||
# Find the position of the chords so they can be inserted in the lyrics
|
||||
chords = []
|
||||
this_line = this_line[1:]
|
||||
chord = ''
|
||||
i = 0
|
||||
while i < len(this_line):
|
||||
if this_line[i] != ' ':
|
||||
chord_pos = i
|
||||
chord += this_line[i]
|
||||
i += 1
|
||||
while i < len(this_line) and this_line[i] != ' ':
|
||||
chord += this_line[i]
|
||||
i += 1
|
||||
chords.append((chord_pos, chord))
|
||||
chord = ''
|
||||
i += 1
|
||||
continue
|
||||
# verse/chorus/etc. marker
|
||||
if this_line.startswith('['):
|
||||
@ -228,12 +248,19 @@ class OpenSongImport(SongImport):
|
||||
# number at start of line.. it's verse number
|
||||
if this_line[0].isdigit():
|
||||
verse_num = this_line[0]
|
||||
this_line = this_line[1:].strip()
|
||||
this_line = this_line[1:]
|
||||
verses.setdefault(verse_tag, {})
|
||||
verses[verse_tag].setdefault(verse_num, {})
|
||||
if inst not in verses[verse_tag][verse_num]:
|
||||
verses[verse_tag][verse_num][inst] = []
|
||||
our_verse_order.append([verse_tag, verse_num, inst])
|
||||
# If chords exists insert them
|
||||
if chords and not Settings().value('songs/disable chords import'):
|
||||
offset = 0
|
||||
for (column, chord) in chords:
|
||||
this_line = '{pre}[{chord}]{post}'.format(pre=this_line[:offset+column], chord=chord,
|
||||
post=this_line[offset+column:])
|
||||
offset += len(chord) + 2
|
||||
# Tidy text and remove the ____s from extended words
|
||||
this_line = self.tidy_text(this_line)
|
||||
this_line = this_line.replace('_', '')
|
||||
|
@ -26,8 +26,9 @@ exproted from Lyrix."""
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
from openlp.core.common import translate
|
||||
from openlp.core.common import translate, Settings
|
||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||
from openlp.plugins.songs.lib.db import AuthorType
|
||||
|
||||
@ -113,7 +114,11 @@ class VideoPsalmImport(SongImport):
|
||||
if 'Memo3' in song:
|
||||
self.add_comment(song['Memo3'])
|
||||
for verse in song['Verses']:
|
||||
self.add_verse(verse['Text'], 'v')
|
||||
verse_text = verse['Text']
|
||||
# Strip out chords if set up to
|
||||
if not Settings().value('songs/disable chords import'):
|
||||
verse_text = re.sub(r'\[\w.*?\]', '', verse_text)
|
||||
self.add_verse(verse_text, 'v')
|
||||
if not self.finish():
|
||||
self.log_error('Could not import {title}'.format(title=self.title))
|
||||
except Exception as e:
|
||||
|
@ -61,7 +61,7 @@ import re
|
||||
|
||||
from lxml import etree, objectify
|
||||
|
||||
from openlp.core.common import translate
|
||||
from openlp.core.common import translate, Settings
|
||||
from openlp.core.common.versionchecker import get_application_version
|
||||
from openlp.core.lib import FormattingTags
|
||||
from openlp.plugins.songs.lib import VerseType, clean_song
|
||||
@ -154,7 +154,7 @@ class OpenLyrics(object):
|
||||
OpenLP does not support the attribute *lang*.
|
||||
|
||||
``<chord>``
|
||||
This property is not supported.
|
||||
This property is fully supported.
|
||||
|
||||
``<comments>``
|
||||
The ``<comments>`` property is fully supported. But comments in lyrics are not supported.
|
||||
@ -334,7 +334,7 @@ class OpenLyrics(object):
|
||||
:return: the lyrics with the converted chords
|
||||
"""
|
||||
# Process chords.
|
||||
new_text = re.sub(r'\[(..?.?)\]', r'<chord name="\1"/>', text)
|
||||
new_text = re.sub(r'\[(\w.*?)\]', r'<chord name="\1"/>', text)
|
||||
return new_text
|
||||
|
||||
def _get_missing_tags(self, text):
|
||||
@ -607,8 +607,7 @@ class OpenLyrics(object):
|
||||
|
||||
def _process_lines_mixed_content(self, element, newlines=True):
|
||||
"""
|
||||
Converts the xml text with mixed content to OpenLP representation. Chords are skipped and formatting tags are
|
||||
converted.
|
||||
Converts the xml text with mixed content to OpenLP representation. Chords and formatting tags are converted.
|
||||
|
||||
:param element: The property object (lxml.etree.Element).
|
||||
:param newlines: The switch to enable/disable processing of line breaks <br/>. The <br/> is used since
|
||||
@ -620,13 +619,14 @@ class OpenLyrics(object):
|
||||
# TODO: Verify format() with template variables
|
||||
if element.tag == NSMAP % 'comment':
|
||||
if element.tail:
|
||||
# Append tail text at chord element.
|
||||
# Append tail text at comment element.
|
||||
text += element.tail
|
||||
return text
|
||||
# Convert chords to ChordPro format which OpenLP uses internally
|
||||
# TODO: Verify format() with template variables
|
||||
elif element.tag == NSMAP % 'chord':
|
||||
text += '[{chord}]'.format(chord=element.get('name'))
|
||||
if not Settings().value('songs/disable chords import'):
|
||||
text += '[{chord}]'.format(chord=element.get('name'))
|
||||
if element.tail:
|
||||
# Append tail text at chord element.
|
||||
text += element.tail
|
||||
@ -679,7 +679,7 @@ class OpenLyrics(object):
|
||||
text = self._process_lines_mixed_content(element)
|
||||
# OpenLyrics version <= 0.7 contains <line> elements to represent lines. First child element is tested.
|
||||
else:
|
||||
# Loop over the "line" elements removing comments and chords.
|
||||
# Loop over the "line" elements removing comments
|
||||
for line in element:
|
||||
# Skip comment lines.
|
||||
# TODO: Verify format() with template variables
|
||||
|
@ -42,10 +42,16 @@ class TestOpenSongFileImport(SongImportTestHelper):
|
||||
self.importer_module_name = 'opensong'
|
||||
super(TestOpenSongFileImport, self).__init__(*args, **kwargs)
|
||||
|
||||
def test_song_import(self):
|
||||
@patch('openlp.plugins.songs.lib.importers.opensong.Settings')
|
||||
def test_song_import(self, mocked_settings):
|
||||
"""
|
||||
Test that loading an OpenSong file works correctly on various files
|
||||
"""
|
||||
# Mock out the settings - always return False
|
||||
mocked_returned_settings = MagicMock()
|
||||
mocked_returned_settings.value.return_value = False
|
||||
mocked_settings.return_value = mocked_returned_settings
|
||||
# Do the test import
|
||||
self.file_import([os.path.join(TEST_PATH, 'Amazing Grace')],
|
||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
|
||||
self.file_import([os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer')],
|
||||
|
@ -25,6 +25,7 @@ This module contains tests for the VideoPsalm song importer.
|
||||
import os
|
||||
|
||||
from tests.helpers.songfileimport import SongImportTestHelper
|
||||
from tests.functional import patch, MagicMock
|
||||
|
||||
TEST_PATH = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'videopsalmsongs'))
|
||||
@ -37,9 +38,15 @@ class TestVideoPsalmFileImport(SongImportTestHelper):
|
||||
self.importer_module_name = 'videopsalm'
|
||||
super(TestVideoPsalmFileImport, self).__init__(*args, **kwargs)
|
||||
|
||||
def test_song_import(self):
|
||||
@patch('openlp.plugins.songs.lib.importers.videopsalm.Settings')
|
||||
def test_song_import(self, mocked_settings):
|
||||
"""
|
||||
Test that loading an VideoPsalm file works correctly on various files
|
||||
"""
|
||||
# Mock out the settings - always return False
|
||||
mocked_returned_settings = MagicMock()
|
||||
mocked_returned_settings.value.return_value = False
|
||||
mocked_settings.return_value = mocked_returned_settings
|
||||
# Do the test import
|
||||
self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold.json'),
|
||||
self.load_external_result_data(os.path.join(TEST_PATH, 'as-safe-a-stronghold.json')))
|
||||
|
@ -19,23 +19,23 @@
|
||||
"verse_order_list": [],
|
||||
"verses": [
|
||||
[
|
||||
"Amazing grace! How sweet the sound!\nThat saved a wretch like me!\nI once was lost, but now am found;\nWas blind, but now I see.",
|
||||
"A[D]mazing [D7]grace! How [G]sweet the [D]sound!\nThat [Bm]saved a [E]wretch like [A]me![A7]\nI [D]once was [D7]lost, but [G]now am [D]found;\nWas [Bm]blind, but [A]now I [G]see.[D]",
|
||||
"v1"
|
||||
],
|
||||
[
|
||||
"'Twas grace that taught my heart to fear,\nAnd grace my fears relieved.\nHow precious did that grace appear,\nThe hour I first believed.",
|
||||
"'Twas [D]grace that [D7]taught my [G]heart to [D]fear,\nAnd [Bm]grace my [E]fears re[A]lieved.[A7]\nHow [D]precious [D7]did that [G]grace ap[D]pear,\nThe [Bm]hour I [A]first be[G]lieved.[D]",
|
||||
"v2"
|
||||
],
|
||||
[
|
||||
"The Lord has promised good to me,\nHis Word my hope secures.\nHe will my shield and portion be\nAs long as life endures.",
|
||||
"The [D]Lord has [D7]promised [G]good to [D]me,\nHis [Bm]Word my [E]hope se[A]cures.[A7]\nHe [D]will my [D7]shield and [G]portion [D]be\nAs [Bm]long as [A]life en[G]dures.[D]",
|
||||
"v3"
|
||||
],
|
||||
[
|
||||
"Thro' many dangers, toils and snares\nI have already come.\n'Tis grace that brought me safe thus far,\nAnd grace will lead me home.",
|
||||
"Thro' [D]many [D7]dangers, [G]toils and [D]snares\nI [Bm]have al[E]ready [A]come.[A7]\n'Tis [D]grace that [D7]brought me [G]safe thus [D]far,\nAnd [Bm]grace will [A]lead me [G]home.[D]",
|
||||
"v4"
|
||||
],
|
||||
[
|
||||
"When we've been there ten thousand years,\nBright shining as the sun,\nWe've no less days to sing God's praise,\nThan when we first begun.",
|
||||
"When [D]we've been [D7]there ten [G]thousand [D]years,\nBright [Bm]shining [E]as the [A]sun,[A7]\nWe've [D]no less [D7]days to [G]sing God's [D]praise,\nThan [Bm]when we [A]first be[G]gun.[D]",
|
||||
"v5"
|
||||
]
|
||||
]
|
||||
|
@ -19,24 +19,24 @@
|
||||
"verse_order_list": [],
|
||||
"verses": [
|
||||
[
|
||||
"Amazing grace! How sweet the sound!\nThat saved a wretch like me!\nI once was lost, but now am found;\nWas blind, but now I see.",
|
||||
"A[D]mazing [D7]grace! How [G]sweet the [D]sound!\nThat [Bm]saved a [E]wretch like [A]me![A7]\nI [D]once was [D7]lost, but [G]now am [D]found;\nWas [Bm]blind, but [A]now I [G]see.[D]",
|
||||
"v1"
|
||||
],
|
||||
[
|
||||
"'Twas grace that taught my heart to fear,\nAnd grace my fears relieved.\nHow precious did that grace appear,\nThe hour I first believed.",
|
||||
"'Twas [D]grace that [D7]taught my [G]heart to [D]fear,\nAnd [Bm]grace my [E]fears re[A]lieved.[A7]\nHow [D]precious [D7]did that [G]grace ap[D]pear,\nThe [Bm]hour I [A]first be[G]lieved.[D]",
|
||||
"v2"
|
||||
],
|
||||
[
|
||||
"The Lord has promised good to me,\nHis Word my hope secures.\nHe will my shield and portion be\nAs long as life endures.",
|
||||
"The [D]Lord has [D7]promised [G]good to [D]me,\nHis [Bm]Word my [E]hope se[A]cures.[A7]\nHe [D]will my [D7]shield and [G]portion [D]be\nAs [Bm]long as [A]life en[G]dures.[D]",
|
||||
"v3"
|
||||
],
|
||||
[
|
||||
"Thro' many dangers, toils and snares\nI have already come.\n'Tis grace that brought me safe thus far,\nAnd grace will lead me home.",
|
||||
"Thro' [D]many [D7]dangers, [G]toils and [D]snares\nI [Bm]have al[E]ready [A]come.[A7]\n'Tis [D]grace that [D7]brought me [G]safe thus [D]far,\nAnd [Bm]grace will [A]lead me [G]home.[D]",
|
||||
"v4"
|
||||
],
|
||||
[
|
||||
"When we've been there ten thousand years,\nBright shining as the sun,\nWe've no less days to sing God's praise,\nThan when we first begun.",
|
||||
"When [D]we've been [D7]there ten [G]thousand [D]years,\nBright [Bm]shining [E]as the [A]sun,[A7]\nWe've [D]no less [D7]days to [G]sing God's [D]praise,\nThan [Bm]when we [A]first be[G]gun.[D]",
|
||||
"v5"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user