songbookize 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|