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 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