songbookize 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Creator/capo.rb +53 -0
- data/Creator/html_formatter.rb +75 -0
- data/Creator/latex_formatter.rb +113 -0
- data/Creator/midi_formatter.rb +25 -0
- data/Creator/parser.rb +150 -0
- data/Creator/slideable_song.slim +134 -0
- data/Creator/song.rb +52 -0
- data/Creator/song.slim +82 -0
- data/Creator/songbook.slim +121 -0
- data/Creator/tv.slim +33 -0
- data/LICENSE +427 -0
- data/Rakefile +95 -0
- data/Readme.md +16 -0
- data/bin/bookize +6 -0
- data/template.tex +161 -0
- metadata +59 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e5804912b54b4f7e9eb0e75b4ed611e7a7d94aa4
|
4
|
+
data.tar.gz: 4472d45c0fa525c07e2b88a7aecf15da0b8d77a1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3be94c1a03624d912a90f56bbd2d6639b249e227c03732fda99a0407d9932716410a897a4084dacd32a2fc2e2ae7a7bffb19f1e9dd0a0b887ce0003521f172eb
|
7
|
+
data.tar.gz: 6df99920eaf27c4c96bcdff1717ecd0fff62d1131532d96d536a64394327283ba67906bb3dc25fcc8d7667f0bc750fa5aeec761ca0433f3b0033814cf16e4243
|
data/Creator/capo.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
class Song
|
2
|
+
def capo!(fret)
|
3
|
+
capo_melody!(fret) unless melody.nil? or melody.empty?
|
4
|
+
verses.each{|v| v.capo!(fret)}
|
5
|
+
%[key K].each{|k| info[k] = Song.capo(info[k], fret) if info.include?(k)}
|
6
|
+
return self
|
7
|
+
end
|
8
|
+
|
9
|
+
OCTAVE ||= %w[A Bb B C Db D Eb E F Gb G Ab]
|
10
|
+
def self.capo(chord, fret)
|
11
|
+
idx = OCTAVE.index(chord[0])
|
12
|
+
range = 0
|
13
|
+
if idx.nil? then
|
14
|
+
idx = OCTAVE.index(chord[0..1])
|
15
|
+
range = 0..1
|
16
|
+
end
|
17
|
+
raise StandardError.new("Unknown Chords #{chord}!") if idx.nil?
|
18
|
+
|
19
|
+
chord[range] = OCTAVE[(idx + fret) % OCTAVE.size]
|
20
|
+
return chord
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def capo_melody!(fret)
|
25
|
+
Open3.popen2("abc2abc - -t #{fret}") do |i,o,t|
|
26
|
+
i.print(melody.to_s)
|
27
|
+
i.close
|
28
|
+
t.join
|
29
|
+
self.melody = o.read
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Song
|
35
|
+
class Verse
|
36
|
+
def capo!(fret)
|
37
|
+
lines.each{|l| l.capo!(fret)}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Line
|
42
|
+
def capo!(fret)
|
43
|
+
log.debug self
|
44
|
+
log.debug chords
|
45
|
+
return if chords.nil? or chords.empty?
|
46
|
+
chords.each do |chord, pos|
|
47
|
+
Song.capo(chord, fret)
|
48
|
+
end
|
49
|
+
log.debug self
|
50
|
+
log.debug chords
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'json'
|
3
|
+
require 'date'
|
4
|
+
require 'slim'
|
5
|
+
require 'ostruct'
|
6
|
+
require 'kramdown'
|
7
|
+
require_relative 'song'
|
8
|
+
|
9
|
+
class Song
|
10
|
+
class Line
|
11
|
+
def to_html
|
12
|
+
return lyrics.gsub("'", "’").strip.each_line.map{|l| %|<span class="lyricline">#{l}</span>|}.join("\n") if chords.nil? or chords.empty?
|
13
|
+
return %|<span class="chords">#{raw_chords}</span><span class="chordlyrics">#{lyrics.gsub("'", "’").strip}</span>|
|
14
|
+
|
15
|
+
return res.gsub("'", "’")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Verse
|
20
|
+
def to_html
|
21
|
+
lstr = lines.map(&:to_html).join("\n")
|
22
|
+
if chorus_indicator?
|
23
|
+
lstr = "<div class='chorusindicator'>[Chorus]</div>"
|
24
|
+
elsif chorus?
|
25
|
+
lstr = "<span class='chorusindicator'>Chorus:</span>\n<div class='chorus'>#{lstr}</div>"
|
26
|
+
end
|
27
|
+
|
28
|
+
return lstr
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Melody
|
33
|
+
def to_html
|
34
|
+
sub(/^([^:]*)$/, "#{other_info.join("\n")}\\1").to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_html
|
39
|
+
verses.map(&:to_html).join("\n\n")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class SongBook
|
44
|
+
def json_info
|
45
|
+
info = {}
|
46
|
+
info['songs'] = songs.reject(&:hidden?).map(&:title)
|
47
|
+
info['songInfo'] = songs.reject(&:hidden?).map do |song|
|
48
|
+
Hash[%w[title slug key author desc emoji].map{|f| [f, song.__send__(f.to_sym)]}]
|
49
|
+
end
|
50
|
+
info['date'] = DateTime.now
|
51
|
+
info['date_format'] = DateTime.now.strftime("%B %-d, %Y")
|
52
|
+
|
53
|
+
return info.to_json
|
54
|
+
end
|
55
|
+
|
56
|
+
def jsonp_info
|
57
|
+
return %|window.songBookInfo = #{json_info};|
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_html(dir, opts = {})
|
61
|
+
scope = OpenStruct.new(book: self)
|
62
|
+
log.info "Creating songbook in #{dir}"
|
63
|
+
File.write("#{dir}/songbook.html", Slim::Template.new(File.join(File.dirname(__FILE__),'songbook.slim')).render(scope))
|
64
|
+
File.write("#{dir}/tv.html", Slim::Template.new(File.join(File.dirname(__FILE__),'tv.slim')).render(scope))
|
65
|
+
log.debug "Options: #{opts}"
|
66
|
+
songs.each do |song|
|
67
|
+
next if opts.has_key?(:only) and not opts[:only].include?(song.slug)
|
68
|
+
log.debug("Creating song page for #{song.title}")
|
69
|
+
scope = OpenStruct.new(song: song)
|
70
|
+
File.write("#{dir}/#{song.slug}.html", Slim::Template.new(File.join(File.dirname(__FILE__),'song.slim')).render(scope))
|
71
|
+
File.write("#{dir}/#{song.slug}_slideable.html", Slim::Template.new(File.join(File.dirname(__FILE__),'slideable_song.slim')).render(scope))
|
72
|
+
end
|
73
|
+
FileUtils.cp Dir["#{File.dirname(__FILE__)}/resources/*"], "#{dir}/"
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require 'open3'
|
3
|
+
require_relative 'song.rb'
|
4
|
+
|
5
|
+
class Song
|
6
|
+
class Line
|
7
|
+
def to_latex
|
8
|
+
return lyrics.gsub("'", "’") if chords.nil? or chords.empty?
|
9
|
+
offset = 0
|
10
|
+
res = lyrics.dup
|
11
|
+
chords.each do |chord, pos|
|
12
|
+
cmd = %|\\ch{#{chord}}|
|
13
|
+
res = res.ljust(pos + offset + 1).insert(pos + offset, cmd)
|
14
|
+
offset += cmd.length
|
15
|
+
end
|
16
|
+
|
17
|
+
return res.gsub("'", "’")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Verse
|
22
|
+
def to_latex
|
23
|
+
lstr = lines.collect{|l| "#{l.to_latex} \\\\\\*"}.join("\n")
|
24
|
+
return "" if lstr.strip.empty?
|
25
|
+
lstr = "\\chorus\n" + lstr if chorus?
|
26
|
+
template =
|
27
|
+
"\\begin{songverse}
|
28
|
+
#{lstr}
|
29
|
+
\\end{songverse}"
|
30
|
+
return template
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_latex
|
35
|
+
parts = []
|
36
|
+
case
|
37
|
+
when short? then parts << "\\newshortsong"
|
38
|
+
when song? then parts << "\\newsong"
|
39
|
+
when tune? then parts << "\\newtune"
|
40
|
+
end
|
41
|
+
parts << "\\section{#{title}}"
|
42
|
+
parts << "\\songdesc{#{desc}}\n\n" if desc
|
43
|
+
parts << "\\#{(melody.nil? or melody.empty?) ? "nm" : ""}songinfo{#{"Key of #{key}"}}{#{author}}"
|
44
|
+
parts << "\\melody{#{slug}001.eps}" unless melody.nil? or melody.empty?
|
45
|
+
# parts << "\\begin{abc}[name=#{title.downcase.gsub(/[^\w]/, "")}]\n#{melody}\\end{abc}" unless melody.nil? or melody.empty?
|
46
|
+
# parts << "\\begin{lilypond}[quoted,staffsize=26]\n#{lilypond}\\end{lilypond}" unless melody.nil? or melody.empty?
|
47
|
+
|
48
|
+
unless verses.empty? then
|
49
|
+
parts << "\\begin{lyrics}"
|
50
|
+
parts << verses.collect{|v| v.to_latex}.join("\n")
|
51
|
+
parts << "\\end{lyrics}"
|
52
|
+
end
|
53
|
+
|
54
|
+
return parts.join("\n")
|
55
|
+
end
|
56
|
+
|
57
|
+
def eps!
|
58
|
+
return nil if melody.nil? or melody.empty?
|
59
|
+
Open3.popen2("abcm2ps -E -O #{slug} -") do |i,o,t|
|
60
|
+
other_info = []
|
61
|
+
# other_info << "%%textfont Times-Italic 14"
|
62
|
+
# other_info << "%%text Key of #{self.key}"
|
63
|
+
# other_info << "%%textoption right"
|
64
|
+
# other_info << "%%text #{self.author}"
|
65
|
+
i.print(melody.sub(/^([^:]*)$/, "#{other_info.join("\n")}\\1").to_s)
|
66
|
+
i.close
|
67
|
+
t.join
|
68
|
+
end
|
69
|
+
return "#{slug}001.eps"
|
70
|
+
end
|
71
|
+
|
72
|
+
def preview_latex
|
73
|
+
Dir.chdir("/tmp/") { eps! }
|
74
|
+
File.write("/tmp/preview.tex", SongBook.templatize(to_latex))
|
75
|
+
|
76
|
+
Dir.chdir("/tmp") do
|
77
|
+
return unless system("xelatex -shell-escape /tmp/preview.tex")
|
78
|
+
end
|
79
|
+
spawn("evince /tmp/preview.pdf")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class SongBook
|
84
|
+
def to_latex(options = {})
|
85
|
+
SongBook.templatize(songs.reject{|s| s.hidden?}.sort_by do |s|
|
86
|
+
#[s.song? ? 0 : 1, s.short? ? 1 :0, File.ctime(s.file)]
|
87
|
+
s.order
|
88
|
+
end.collect{|s| s.to_latex}.join("\n"), options.merge(@info))
|
89
|
+
end
|
90
|
+
|
91
|
+
def build(options = {})
|
92
|
+
filename = options.fetch(:filename, "preview")
|
93
|
+
File.write("#{filename}.tex", to_latex(options.merge(preface:self.preface)))
|
94
|
+
2.times { raise StandardError.new("Could not build #{filename}") unless system("xelatex -shell-escape #{filename}.tex") }
|
95
|
+
end
|
96
|
+
|
97
|
+
def preview(options = {})
|
98
|
+
build(options)
|
99
|
+
spawn("evince #{filename}.pdf")
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.templatize(str, options={})
|
103
|
+
template = File.read(options[:template] || "#{File.dirname(__FILE__)}/../template.tex")
|
104
|
+
if options[:booklet]
|
105
|
+
template.gsub!('documentclass[letterpaper]{article}', 'documentclass[letterpaper]{book}')
|
106
|
+
template.gsub!('usepackage[noprint,1to1]{booklet}', 'usepackage[print,1to1]{booklet}')
|
107
|
+
end
|
108
|
+
template.sub("SONGGOESHERE", str)
|
109
|
+
.sub("PREFACEGOESHERE", options[:preface].to_s)
|
110
|
+
.sub("TITLEGOESHERE", options.fetch(:title, "Untitled Songbook"))
|
111
|
+
.sub("SUBTITLEGOESHERE", options.fetch(:subtitle, ""))
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Song
|
2
|
+
def to_midi
|
3
|
+
return nil if melody.nil? or melody.empty?
|
4
|
+
Open3.popen2("abc2midi - -o #{slug}.mid") do |i,o,t|
|
5
|
+
other_info = []
|
6
|
+
# other_info << "%%textfont Times-Italic 14"
|
7
|
+
# other_info << "%%text Key of #{self.key}"
|
8
|
+
# other_info << "%%textoption right"
|
9
|
+
# other_info << "%%text #{self.author}"
|
10
|
+
i.print(melody.sub(/^([^:]*)$/, "#{other_info.join("\n")}\\1").to_s)
|
11
|
+
i.close
|
12
|
+
t.join
|
13
|
+
end
|
14
|
+
return "#{slug}.mid"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class SongBook
|
19
|
+
def to_midi(dir)
|
20
|
+
FileUtils.mkdir_p dir
|
21
|
+
Dir.chdir(dir) do
|
22
|
+
songs.each(&:to_midi)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/Creator/parser.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'logger'
|
3
|
+
require 'yaml'
|
4
|
+
require 'open-uri'
|
5
|
+
require_relative 'song'
|
6
|
+
|
7
|
+
Encoding.default_internal = Encoding::UTF_8
|
8
|
+
Encoding.default_external = Encoding::UTF_8
|
9
|
+
|
10
|
+
def log; $logger ||= Logger.new(STDOUT); return $logger; end
|
11
|
+
|
12
|
+
class Song
|
13
|
+
def self.split_chords(line)
|
14
|
+
return line.enum_for(:scan, /\b\w+\b/).map{[Regexp.last_match[0], Regexp.last_match.begin(0)]}
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse_abc(f = nil)
|
18
|
+
f ||= "#{File.dirname(file)}/#{slug}.abc"
|
19
|
+
return log.debug("No such file #{f}") unless File.exist?(f)
|
20
|
+
remove_sections = %w[T Q C N]
|
21
|
+
self.melody = File.read(f)
|
22
|
+
self.file = File.absolute_path(f) if file.nil?
|
23
|
+
self.info.merge!(self.melody.each_line.map{|l|l.split(":")}.reject{|i|i.size != 2}.inject({}){|h,k| h[k[0]]=k[1].strip; h})
|
24
|
+
self.melody = self.melody.each_line.reject{|l| remove_sections.include?(l.split(":").first)}.join
|
25
|
+
return self
|
26
|
+
rescue
|
27
|
+
log "Could not parse ABC for #{f}"
|
28
|
+
raise
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse_song_chords(f)
|
32
|
+
state = :none
|
33
|
+
last_key = :text
|
34
|
+
current_line = Song::Line.new
|
35
|
+
self.file = File.absolute_path(f)
|
36
|
+
File.open(f).each_line do |raw_line|
|
37
|
+
line = raw_line.strip
|
38
|
+
# log.debug("#{state}: #{line[0..20]}")
|
39
|
+
case (state)
|
40
|
+
when :none then
|
41
|
+
if line == "---" then
|
42
|
+
log.debug "Starting header"
|
43
|
+
state = :header
|
44
|
+
next
|
45
|
+
end
|
46
|
+
|
47
|
+
if line.start_with?("Additional Verses") then
|
48
|
+
next
|
49
|
+
end
|
50
|
+
|
51
|
+
unless line.empty? then
|
52
|
+
log.debug "Starting new verse"
|
53
|
+
state = :verse
|
54
|
+
self.verses << Song::Verse.new
|
55
|
+
redo
|
56
|
+
end
|
57
|
+
when :header then
|
58
|
+
if line == "---" then
|
59
|
+
log.debug "Ending header"
|
60
|
+
state = :none
|
61
|
+
next
|
62
|
+
end
|
63
|
+
|
64
|
+
key, value = line.scan(/(\w*):(.*)/).flatten
|
65
|
+
log.debug("k: #{key}, v:#{value}, lk: #{last_key}")
|
66
|
+
if key.nil? or value.nil? then
|
67
|
+
info[last_key] += " " + line.strip
|
68
|
+
elsif last_key
|
69
|
+
info[key] = value.strip
|
70
|
+
last_key = key
|
71
|
+
end
|
72
|
+
when :verse then
|
73
|
+
if line.empty? then
|
74
|
+
state = :none
|
75
|
+
next
|
76
|
+
end
|
77
|
+
|
78
|
+
if line.match(/^\[?[Cc]horus:?\]?$/) then
|
79
|
+
self.verses.last.chorus = true
|
80
|
+
next
|
81
|
+
end
|
82
|
+
|
83
|
+
if raw_line.gsub('\r', '').match(/[ \t]{2}/) then
|
84
|
+
current_line.raw_chords = raw_line.gsub('\t', ' ').gsub('\r', '').gsub('\n', '')
|
85
|
+
current_line.chords = Song.split_chords(raw_line.gsub('\t', ' '))
|
86
|
+
# log.debug "Chords #{current_line.chords}"
|
87
|
+
else
|
88
|
+
current_line.lyrics = line
|
89
|
+
self.verses.last.lines << current_line
|
90
|
+
current_line = Song::Line.new
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
return self
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.parse(f)
|
99
|
+
s = Song.new
|
100
|
+
s.parse_song_chords(f)
|
101
|
+
s.parse_abc
|
102
|
+
return s
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class SongBook
|
107
|
+
def parse(dir)
|
108
|
+
if File.exist?("#{dir}/../songbook.yml") then
|
109
|
+
log.debug "Reading info file"
|
110
|
+
@info = Hash[YAML.load(File.read("#{dir}/../songbook.yml")).map{|(k,v)| [k.to_sym,v]}]
|
111
|
+
else
|
112
|
+
raise StandardError.new("No such file #{dir}/songbook.yml")
|
113
|
+
end
|
114
|
+
|
115
|
+
Dir["#{dir}/*.chords"].each do |f|
|
116
|
+
songs << Song.parse(f)
|
117
|
+
end
|
118
|
+
|
119
|
+
Dir["#{dir}/*.abc"].reject{|a| File.exist?("#{dir}/#{File.basename(a, ".abc")}.chords")}.each do |f|
|
120
|
+
songs << Song.new.parse_abc(f)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Allow include: syntax in songbook.yml to include songs from other songbooks
|
124
|
+
@info.fetch(:include, {}).each do |inc_dir, song_list|
|
125
|
+
Dir["#{inc_dir}/*.chords"].each do |f|
|
126
|
+
new_song = Song.parse(f)
|
127
|
+
songs << new_song if song_list.nil? or song_list.empty? or song_list.include?(new_song.slug)
|
128
|
+
end
|
129
|
+
Dir["#{inc_dir}/*.abc"].reject{|a| File.exist?("#{inc_dir}/#{File.basename(a, ".abc")}.chords")}.each do |f|
|
130
|
+
new_song = Song.new.parse_abc(f)
|
131
|
+
songs << new_song if song_list.nil? or song_list.empty? or song_list.include?(new_song.slug)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
if File.exist?("#{dir}/order.yml") then
|
136
|
+
# Looks better in the yml file as ##: slug
|
137
|
+
order = Hash[YAML.load(File.read("#{dir}/order.yml")).map{|k,v| [v, k]}] || {}
|
138
|
+
max = order.values.max || 0
|
139
|
+
songs.each{|s| s.order = order.fetch(s.slug, max += 1) || max += 1}
|
140
|
+
songs.sort_by!(&:order)
|
141
|
+
songs.each_with_index{|s, i| s.order = i + 1}
|
142
|
+
end
|
143
|
+
|
144
|
+
return self
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.parse(dir)
|
148
|
+
return SongBook.new.parse(dir)
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
doctype 1.1
|
2
|
+
html
|
3
|
+
head
|
4
|
+
title=song.title
|
5
|
+
meta charset="UTF-8"
|
6
|
+
style type="text/css"==open(URI.encode("https://fonts.googleapis.com/css?family=Cousine|Lora")).read
|
7
|
+
script src="abcjs_basic_2.3-min.js"
|
8
|
+
script src="abcjs_plugin_2.3-min.js"
|
9
|
+
script src="https://d3js.org/d3.v4.min.js"
|
10
|
+
javascript:
|
11
|
+
window.ABCJS.plugin.auto_render_threshold = 100;
|
12
|
+
window.ABCJS.plugin.hide_abc = true;
|
13
|
+
sass:
|
14
|
+
body
|
15
|
+
max-width: 1000px
|
16
|
+
margin: auto
|
17
|
+
padding-top: 50px
|
18
|
+
font-family: 'Junge'
|
19
|
+
background-color: white
|
20
|
+
color: black
|
21
|
+
overflow: visible
|
22
|
+
html
|
23
|
+
overflow-x: hidden
|
24
|
+
.lyrics
|
25
|
+
line-height: 140%
|
26
|
+
font-size: 140%
|
27
|
+
overflow-y: hidden
|
28
|
+
transform-origin: top
|
29
|
+
clear: both
|
30
|
+
font-family: 'Cousine'
|
31
|
+
margin: auto
|
32
|
+
width: 100%
|
33
|
+
padding-top: 1em
|
34
|
+
position: absolute
|
35
|
+
left: 0
|
36
|
+
overflow: visible
|
37
|
+
white-space: pre
|
38
|
+
|
39
|
+
.chordlyrics
|
40
|
+
font-family: 'Cousine', monospace
|
41
|
+
|
42
|
+
.lyricline
|
43
|
+
display: inline-block
|
44
|
+
white-space: pre-wrap
|
45
|
+
text-indent: -2em
|
46
|
+
padding-left: 2em
|
47
|
+
|
48
|
+
.verse
|
49
|
+
position: absolute
|
50
|
+
width: 40%
|
51
|
+
|
52
|
+
.abcrendered
|
53
|
+
margin: auto
|
54
|
+
h2
|
55
|
+
text-align: center
|
56
|
+
.order
|
57
|
+
position: relative
|
58
|
+
top: 10
|
59
|
+
left: 0
|
60
|
+
float: right
|
61
|
+
font-size: 26px
|
62
|
+
font-family: 'Cousine', sans
|
63
|
+
.chords
|
64
|
+
font-family: 'Cousine', monospace
|
65
|
+
font-weight: bold
|
66
|
+
color: #08c
|
67
|
+
.chorusindicator
|
68
|
+
color: #0a5
|
69
|
+
font-weight: bold
|
70
|
+
text-align: center
|
71
|
+
display: block
|
72
|
+
.chorus
|
73
|
+
color: #083
|
74
|
+
.author, .key
|
75
|
+
font-family: 'Lora', serif
|
76
|
+
font-size: 140%
|
77
|
+
text-align: center
|
78
|
+
font-style: italic
|
79
|
+
|
80
|
+
.author
|
81
|
+
float: right
|
82
|
+
margin-right: 120px
|
83
|
+
.key
|
84
|
+
float: left
|
85
|
+
margin-left: 120px
|
86
|
+
.desc
|
87
|
+
clear: both
|
88
|
+
margin-bottom: 2em
|
89
|
+
.melody
|
90
|
+
background: none
|
91
|
+
border: none
|
92
|
+
clear: both
|
93
|
+
|
94
|
+
.order=song.order
|
95
|
+
a name=song.slug
|
96
|
+
h2="#{song.title}"
|
97
|
+
.info
|
98
|
+
.desc=song.desc
|
99
|
+
.author=song.author
|
100
|
+
.key="Key of #{song.key}"
|
101
|
+
- unless song.melody.nil? or song.melody.empty?
|
102
|
+
pre.melody=song.melody
|
103
|
+
- unless song.verses.empty?
|
104
|
+
.lyrics
|
105
|
+
script=="window.verses = #{song.verses.each_with_index.map { |v, i| next_verse = song.verses[i + 1]; (v.chorus? && !v.chorus_indicator? ? %|<span class="chorusindicator">[CHORUS:]\n</span>| : "") + v.lines.map(&:to_html).join("\n") + ((next_verse && next_verse.chorus_indicator? && !v.chorus?) ? %|\n<span class="chorusindicator">[CHORUS]</span>| : "")}.reject(&:empty?).to_json}"
|
106
|
+
|
107
|
+
coffee:
|
108
|
+
window.verse_index = 0
|
109
|
+
updateVerses = ->
|
110
|
+
v = d3.select('.lyrics').selectAll('.verse').data(verses)
|
111
|
+
v.enter().append('div')
|
112
|
+
.attr('class', 'verse')
|
113
|
+
.style('left', "3000px")
|
114
|
+
.html((d) -> d)
|
115
|
+
|
116
|
+
d3.select('.lyrics').selectAll('.verse').transition().duration(500)
|
117
|
+
.style 'left', (d, i) ->
|
118
|
+
switch
|
119
|
+
when i < verse_index then "-100%"
|
120
|
+
when i == verse_index then "5%"
|
121
|
+
when i == verse_index + 1 then "55%"
|
122
|
+
else "100%"
|
123
|
+
|
124
|
+
updateVerses()
|
125
|
+
$("body").keydown (e) ->
|
126
|
+
console.log(e.key)
|
127
|
+
switch e.key
|
128
|
+
when "ArrowLeft"
|
129
|
+
verse_index-- if verse_index > 0
|
130
|
+
when "ArrowRight", " "
|
131
|
+
verse_index++ if verse_index < verses.length - 1
|
132
|
+
when "Backspace"
|
133
|
+
window.history.back()
|
134
|
+
updateVerses()
|
data/Creator/song.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
class Song
|
3
|
+
attr_accessor :info, :file, :verses, :melody, :order
|
4
|
+
def initialize
|
5
|
+
@verses = []
|
6
|
+
@info = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def title; (info["title"] || info["T"]).to_s.gsub("'", "’"); end
|
10
|
+
def desc; (info["desc"] || info["N"]).to_s.gsub("'", "’"); end
|
11
|
+
def key; (info["key"] || info["K"]).to_s.gsub("maj", "").gsub("min", "m"); end
|
12
|
+
def author; info["author"] || info["composer"] || info["C"] || "Traditional"; end
|
13
|
+
def emoji; info.fetch("emoji", "").strip.split(""); end
|
14
|
+
|
15
|
+
def slug; File.basename(file, File.extname(file)); end
|
16
|
+
|
17
|
+
def song?; return !tune?; end
|
18
|
+
def tune?; return verses.empty?; end
|
19
|
+
def hidden?; info['skip'] == "true"; end
|
20
|
+
def short?; info['short'] == 'true'; end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Song
|
24
|
+
class Verse
|
25
|
+
attr_accessor :lines, :chords, :chorus
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@lines = []
|
29
|
+
end
|
30
|
+
def chorus?; @chorus; end
|
31
|
+
def chorus_indicator?; chorus? and lines.empty?; end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Line
|
35
|
+
attr_accessor :lyrics, :chords, :raw_chords
|
36
|
+
end
|
37
|
+
|
38
|
+
class Melody
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class SongBook
|
43
|
+
attr_accessor :songs, :preface, :info
|
44
|
+
def initialize
|
45
|
+
@songs = []
|
46
|
+
end
|
47
|
+
|
48
|
+
def title; info[:title]; end
|
49
|
+
def subtitle; info[:subtitle]; end
|
50
|
+
|
51
|
+
def file_safe_title; title.gsub(/[^\w]/,""); end
|
52
|
+
end
|
data/Creator/song.slim
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
html
|
2
|
+
head
|
3
|
+
title=song.title
|
4
|
+
meta charset="UTF-8"
|
5
|
+
/ style type="text/css"==open(URI.encode("https://fonts.googleapis.com/css?family=Bitter:400,700|Muli:300,400,i|Montserrat:400,700|Fira+Mono|Junge")).read
|
6
|
+
script src="abcjs_basic_2.3-min.js"
|
7
|
+
script src="abcjs_plugin_2.3-min.js"
|
8
|
+
javascript:
|
9
|
+
window.ABCJS.plugin.auto_render_threshold = 100;
|
10
|
+
window.ABCJS.plugin.hide_abc = true;
|
11
|
+
sass:
|
12
|
+
body
|
13
|
+
max-width: 1000px
|
14
|
+
margin: auto
|
15
|
+
padding-top: 50px
|
16
|
+
font-family: 'Junge'
|
17
|
+
.lyrics
|
18
|
+
line-height: 140%
|
19
|
+
font-size: 140%
|
20
|
+
overflow-y: hidden
|
21
|
+
transform-origin: top
|
22
|
+
clear: both
|
23
|
+
font-family: 'Junge'
|
24
|
+
margin: auto
|
25
|
+
width: fit-content
|
26
|
+
padding-top: 1em
|
27
|
+
|
28
|
+
.chordlyrics
|
29
|
+
font-family: 'Fira Mono', monospace
|
30
|
+
.abcrendered
|
31
|
+
margin: auto
|
32
|
+
h2
|
33
|
+
text-align: center
|
34
|
+
.order
|
35
|
+
position: relative
|
36
|
+
top: 10
|
37
|
+
left: 0
|
38
|
+
float: right
|
39
|
+
font-size: 26px
|
40
|
+
font-family: 'Fira Mono', sans
|
41
|
+
.chords
|
42
|
+
font-family: 'Fira Mono', monospace
|
43
|
+
font-weight: bold
|
44
|
+
color: #08c
|
45
|
+
.chorusindicator
|
46
|
+
color: #0a5
|
47
|
+
font-weight: bold
|
48
|
+
text-align: center
|
49
|
+
.chorus
|
50
|
+
color: #083
|
51
|
+
.author, .key
|
52
|
+
font-family: 'Lora', serif
|
53
|
+
font-size: 140%
|
54
|
+
text-align: center
|
55
|
+
font-style: italic
|
56
|
+
|
57
|
+
.author
|
58
|
+
float: right
|
59
|
+
margin-right: 120px
|
60
|
+
.key
|
61
|
+
float: left
|
62
|
+
margin-left: 120px
|
63
|
+
.desc
|
64
|
+
clear: both
|
65
|
+
margin-bottom: 2em
|
66
|
+
.melody
|
67
|
+
background: none
|
68
|
+
border: none
|
69
|
+
clear: both
|
70
|
+
body
|
71
|
+
.order=song.order
|
72
|
+
a name=song.slug
|
73
|
+
h2="#{song.title}"
|
74
|
+
.info
|
75
|
+
.desc=song.desc
|
76
|
+
.author=song.author
|
77
|
+
.key="Key of #{song.key}"
|
78
|
+
- unless song.melody.nil? or song.melody.empty?
|
79
|
+
pre.melody=song.melody
|
80
|
+
- unless song.verses.empty?
|
81
|
+
pre.melody
|
82
|
+
.lyrics==song.to_html
|