songbookize 0.0.1

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