lydown 0.7.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -14
  3. data/bin/lydown +1 -122
  4. data/lib/lydown.rb +3 -3
  5. data/lib/lydown/cli.rb +4 -0
  6. data/lib/lydown/cli/commands.rb +98 -0
  7. data/lib/lydown/cli/compiler.rb +11 -14
  8. data/lib/lydown/cli/diff.rb +3 -3
  9. data/lib/lydown/cli/output.rb +18 -0
  10. data/lib/lydown/cli/proofing.rb +8 -6
  11. data/lib/lydown/cli/support.rb +34 -0
  12. data/lib/lydown/cli/translation.rb +73 -0
  13. data/lib/lydown/core_ext.rb +1 -1
  14. data/lib/lydown/lilypond.rb +49 -11
  15. data/lib/lydown/parsing.rb +37 -6
  16. data/lib/lydown/parsing/nodes.rb +57 -56
  17. data/lib/lydown/rendering.rb +0 -9
  18. data/lib/lydown/rendering/base.rb +2 -2
  19. data/lib/lydown/rendering/command.rb +2 -2
  20. data/lib/lydown/rendering/comments.rb +1 -1
  21. data/lib/lydown/rendering/figures.rb +14 -14
  22. data/lib/lydown/rendering/lib.ly +22 -0
  23. data/lib/lydown/rendering/lyrics.rb +2 -2
  24. data/lib/lydown/rendering/movement.rb +2 -2
  25. data/lib/lydown/rendering/music.rb +46 -46
  26. data/lib/lydown/rendering/notes.rb +149 -70
  27. data/lib/lydown/rendering/settings.rb +21 -16
  28. data/lib/lydown/rendering/skipping.rb +1 -1
  29. data/lib/lydown/rendering/source_ref.rb +4 -4
  30. data/lib/lydown/rendering/staff.rb +8 -4
  31. data/lib/lydown/rendering/voices.rb +9 -9
  32. data/lib/lydown/templates.rb +9 -0
  33. data/lib/lydown/templates/lilypond_doc.erb +1 -1
  34. data/lib/lydown/templates/movement.erb +9 -1
  35. data/lib/lydown/templates/part.erb +12 -3
  36. data/lib/lydown/translation.rb +17 -0
  37. data/lib/lydown/translation/ripple.rb +30 -0
  38. data/lib/lydown/translation/ripple/nodes.rb +191 -0
  39. data/lib/lydown/translation/ripple/ripple.treetop +100 -0
  40. data/lib/lydown/version.rb +1 -1
  41. data/lib/lydown/work.rb +144 -198
  42. data/lib/lydown/work_context.rb +152 -0
  43. metadata +11 -2
@@ -0,0 +1,100 @@
1
+ grammar Ripple
2
+ include Lydown::Translation::Ripple
3
+
4
+ rule events
5
+ white_space? event* <Root>
6
+ end
7
+
8
+ rule white_space
9
+ [ \t\n]+
10
+ end
11
+
12
+ rule event
13
+ (relative_cmd / curly_brace / key_signature / time_signature /
14
+ full_rest / note / phrasing / named_macro / stop_macro / command) white_space?
15
+ end
16
+
17
+ rule relative_cmd
18
+ '\relative' [\s]+ relative_cmd_note
19
+ end
20
+
21
+ rule relative_cmd_note
22
+ [a-g] accidental? octave* <RelativeCommand>
23
+ end
24
+
25
+ rule curly_brace
26
+ [\{\}]
27
+ end
28
+
29
+ rule key_signature
30
+ '\key' [\s]+ note_head [\s+] key_mode <KeySignature>
31
+ end
32
+
33
+ rule key_mode
34
+ '\major' / '\minor' <KeySignature::Mode>
35
+ end
36
+
37
+ rule note
38
+ note_head duration? expression* <Note>
39
+ end
40
+
41
+ rule note_head
42
+ [a-gr] accidental octave* accidental_flag? <Note::Head>
43
+ end
44
+
45
+ rule octave
46
+ [,']+
47
+ end
48
+
49
+ rule accidental
50
+ ('b' / 's' / 'es')*
51
+ end
52
+
53
+ rule accidental_flag
54
+ [\!\?]+
55
+ end
56
+
57
+ rule duration
58
+ [0-9]+ '.'* <Note::Duration>
59
+ end
60
+
61
+ rule expression
62
+ '\\' [a-zA-Z]+ <Note::Expression>
63
+ end
64
+
65
+ rule time_signature
66
+ '\time' [\s]+ time_signature_content
67
+ end
68
+
69
+ rule time_signature_content
70
+ [0-9\/]+ <TimeSignature>
71
+ end
72
+
73
+ rule full_rest
74
+ 'R' [0-9]+ ('*' [0-9]+)? <FullRest>
75
+ end
76
+
77
+ rule phrasing
78
+ [\(\)\[\]\~] <Phrasing>
79
+ end
80
+
81
+ rule named_macro
82
+ '$' [a-z0-9]+ <NamedMacro>
83
+ end
84
+
85
+ rule stop_macro
86
+ '$$' <NamedMacro::Stop>
87
+ end
88
+
89
+ rule command
90
+ (clef_command / simple_command)
91
+ end
92
+
93
+ rule simple_command
94
+ '\\' [a-zA-Z]+ <Command>
95
+ end
96
+
97
+ rule clef_command
98
+ '\clef ' [a-z0-9_]+ <Command>
99
+ end
100
+ end
@@ -1,3 +1,3 @@
1
1
  module Lydown
2
- VERSION = "0.7.2"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -1,7 +1,6 @@
1
- require 'lydown/core_ext'
2
1
  require 'lydown/templates'
3
-
4
- require 'pp'
2
+ require 'lydown/cli/output'
3
+ require 'parallel'
5
4
 
6
5
  module Lydown
7
6
  # Work is a virtual lilypond document. It can contain multiple movements,
@@ -16,81 +15,13 @@ module Lydown
16
15
  attr_accessor :context
17
16
 
18
17
  def initialize(opts = {})
19
- @context = {}.deep!
20
- reset_context(:work)
21
- @context[:options] = opts.deep_clone
18
+ @context = WorkContext.new(opts)
22
19
 
23
20
  process_work_files if opts[:path]
24
21
  end
25
-
26
- def reset_context(mode)
27
- case mode
28
- when :work
29
- @context[:time] = '4/4'
30
- @context[:tempo] = nil
31
- @context[:cadenza_mode] = nil
32
- @context[:key] = 'c major'
33
- @context[:pickup] = nil
34
- @context[:beaming] = nil
35
- @context[:end_barline] = nil
36
- @context[:part] = nil
37
- when :movement
38
- @context[:part] = nil
39
- end
40
- if @context['process/tuplet_mode']
41
- Lydown::Rendering::TupletDuration.emit_tuplet_end(self)
42
- end
43
- if @context['process/grace_mode']
44
- Lydown::Rendering::Grace.emit_grace_end(self)
45
- end
46
-
47
- if @context['process/voice_selector']
48
- Lydown::Rendering::VoiceSelect.render_voices(self)
49
- end
50
-
51
- Lydown::Rendering::Notes.cleanup_duration_macro(self)
52
-
53
- # reset processing variables
54
- @context['process'] = {
55
- 'duration_values' => ['4'],
56
- 'running_values' => []
57
- }
58
- end
59
-
60
- # Used to bind to instance when rendering templates
61
- def template_binding(locals = {})
62
- b = binding
63
- locals.each {|k, v| b.local_variable_set(k.to_sym, v)}
64
- b
65
- end
66
-
67
- # translate a lydown stream into lilypond
68
- def process(lydown_stream, opts = {})
69
- lydown_stream.each_with_index do |e, idx|
70
- if e[:type]
71
- Lydown::Rendering.translate(self, e, lydown_stream, idx)
72
- else
73
- raise LydownError, "Invalid lydown stream event: #{e.inspect}"
74
- end
75
- end
76
- reset_context(:part) unless opts[:no_reset]
77
- end
78
-
79
- def emit(path, *content)
80
- stream = current_stream(path)
81
-
82
- content.each {|c| stream << c}
83
- end
84
-
85
- def current_stream(subpath)
86
- if @context['process/voice_selector']
87
- path = "process/voices/#{@context['process/voice_selector']}/#{subpath}"
88
- else
89
- movement = @context[:movement]
90
- part = @context[:part]
91
- path = "movements/#{movement}/parts/#{part}/#{subpath}"
92
- end
93
- @context[path] ||= (subpath == :settings) ? {} : ''
22
+
23
+ def translate(stream)
24
+ @context.translate(stream)
94
25
  end
95
26
 
96
27
  def to_lilypond(opts = {})
@@ -105,161 +36,176 @@ module Lydown
105
36
  else
106
37
  @original_context = @context
107
38
  begin
108
- @context = filter_context(opts)
109
- Lydown::Templates.render(:lilypond_doc, self)
39
+ filtered = @context.filter(opts)
40
+ filtered.extend(TemplateBinding)
41
+ Lydown::Templates.render(:lilypond_doc, filtered)
110
42
  ensure
111
43
  @context = @original_context
112
44
  end
113
45
  end
114
46
  end
115
47
 
116
- def filter_context(opts = {})
117
- filtered = @context.deep_clone
48
+ def process_work_files
49
+ path = @context[:options][:path]
50
+ path += '.ld' if File.file?(path + '.ld')
118
51
 
119
- if filtered['movements'].nil? || filtered['movements'].size == 0
120
- # no movements found, so no music
121
- raise LydownError, "No music found"
122
- elsif filtered['movements'].size > 1
123
- # delete default movement if other movements are present
124
- filtered['movements'].delete('')
52
+ if File.file?(path)
53
+ process_file(path)
54
+ elsif File.directory?(path)
55
+ process_directory(path)
56
+ else
57
+ raise LydownError, "Could not read #{path}"
125
58
  end
59
+ end
126
60
 
127
- if opts[:movements]
128
- opts[:movements] = [opts[:movements]] unless opts[:movements].is_a?(Array)
129
- filtered['movements'].select! do |name, m|
130
- opts[:movements].include?(name.to_s)
131
- end
61
+ def process_file(path, prefix = [], opts = {})
62
+ $stderr.puts path
63
+ content = IO.read(path)
64
+ stream = LydownParser.parse(content, {
65
+ filename: File.expand_path(path),
66
+ source: content,
67
+ proof_mode: @context['options/proof_mode']
68
+ })
69
+
70
+ if opts[:line_range]
71
+ Lydown::Rendering.insert_skip_markers(stream, opts[:line_range])
132
72
  end
73
+
74
+ @context.translate(prefix + stream)
75
+ end
76
+
77
+ DEFAULT_BASENAMES = %w{work movement}
133
78
 
134
- if opts[:parts]
135
- opts[:parts] = [opts[:parts]] unless opts[:parts].is_a?(Array)
79
+ def process_directory(path)
80
+ state = {
81
+ streams: {},
82
+ movements: Hash.new {|h, k| h[k] = {}},
83
+ current_movement: nil,
84
+ part_filter: @context[:options][:parts],
85
+ mvmt_filter: @context[:options][:movements]
86
+ }
87
+
88
+ read_directory(path, true, state)
89
+ parse_directory_files(state)
90
+ translate_directory_streams(state)
91
+ end
92
+
93
+ def read_directory(path, recursive, state)
94
+ Dir["#{path}/*"].entries.sort.each do |entry|
95
+ handle_directory_entry(entry, recursive, state)
136
96
  end
137
- filtered['movements'].each do |name, m|
138
- # delete default part if other parts are present
139
- if m['parts'].size > 1
140
- m['parts'].delete('')
97
+ end
98
+
99
+ def handle_directory_entry(entry, recursive, state)
100
+ if File.file?(entry) && (entry =~ /\.ld$/)
101
+ part = File.basename(entry, '.*')
102
+ if part == 'work'
103
+ state[:streams][entry] = nil
104
+ state[:movements][nil][:work] = entry
105
+ elsif part == 'movement'
106
+ state[:streams][entry] = nil
107
+ state[:movements][state[:current_movement]][:movement] = entry
108
+ elsif !skip_part?(part, state)
109
+ state[:streams][entry] = nil
110
+ state[:movements][state[:current_movement]][part] = entry
141
111
  end
142
-
143
- if opts[:parts]
144
- m['parts'].select! do |pname, p|
145
- opts[:parts].include?(pname.to_s)
146
- end
112
+ elsif File.directory?(entry) && recursive
113
+ movement = File.basename(entry)
114
+ unless skip_movement?(movement, state)
115
+ state[:current_movement] = movement
116
+ read_directory(entry, false, state)
147
117
  end
148
118
  end
149
-
150
- filtered
151
119
  end
152
-
153
- def compile(opts = {})
154
- code = to_lilypond(opts)
155
-
156
- Lydown::Lilypond.compile(code, opts)
120
+
121
+ def skip_part?(part, state)
122
+ DEFAULT_BASENAMES.include?(part) ||
123
+ state[:part_filter] && !state[:part_filter].include?(part)
157
124
  end
158
-
159
- def [](path)
160
- context[path]
125
+
126
+ def skip_movement?(mvmt, state)
127
+ state[:mvmt_filter] && !state[:mvmt_filter].include?(mvmt)
161
128
  end
162
129
 
163
- def []=(path, value)
164
- context[path] = value
165
- end
130
+ PARALLEL_PARSE_OPTIONS = {
131
+ progress: {
132
+ title: 'Parse',
133
+ format: Lydown::CLI::PROGRESS_FORMAT
134
+ }
135
+ }
166
136
 
167
- # Helper method to preserve context while processing a file or directory.
168
- # This method is called with a block. After the block is executed, the
169
- # context is restored.
170
- def preserve_context
171
- old_context = @context
172
- new_context = old_context.deep_merge({})
173
- @context = new_context
174
- yield
175
- ensure
176
- @context = old_context
177
- if new_context['movements']
178
- if @context['movements']
179
- @context['movements'].deep_merge! new_context['movements']
180
- else
181
- @context['movements'] = new_context['movements']
182
- end
183
- if @context['movements/01-intro/parts/violino1/settings'] && @context['movements/01-intro/parts/violino1/settings'].keys.include?('pickup')
184
- end
137
+ PARALLEL_PROCESS_OPTIONS = {
138
+ progress: {
139
+ title: 'Process',
140
+ format: Lydown::CLI::PROGRESS_FORMAT
141
+ }
142
+ }
143
+
144
+ def parse_directory_files(state)
145
+ streams = state[:streams]
146
+ proof_mode = @context['options/proof_mode']
147
+ paths = streams.keys
148
+
149
+ processed_streams = Parallel.map(paths, PARALLEL_PARSE_OPTIONS) do |path|
150
+ content = IO.read(path)
151
+ LydownParser.parse(content, {
152
+ filename: File.expand_path(path),
153
+ source: content,
154
+ proof_mode: proof_mode,
155
+ no_progress: true
156
+ })
185
157
  end
158
+ processed_streams.each_with_index {|s, idx| streams[paths[idx]] = s}
186
159
  end
187
160
 
188
- def set_part_context(part)
189
- movement = @context[:movement]
190
- path = "movements/#{movement}/parts/#{part}/settings"
191
-
192
- settings = {}.deep!
193
- settings[:pickup] = @context[:pickup]
194
- settings[:key] = @context[:key]
195
- settings[:tempo] = @context[:tempo]
161
+ def translate_directory_streams(state)
162
+ # Process work file
163
+ if path = state[:movements][nil][:work]
164
+ @context.translate state[:streams][path]
165
+ end
196
166
 
197
- @context[path] = settings
167
+ translate_movement_files(state)
198
168
  end
199
-
200
- def process_work_files
201
- path = @context[:options][:path]
202
- path += '.ld' if File.file?(path + '.ld')
203
-
204
- if File.file?(path)
205
- process_lydown_file(path)
206
- elsif File.directory?(path)
207
- process_directory(path)
208
- else
209
- raise LydownError, "Could not read #{path}"
169
+
170
+ def translate_movement_files(state)
171
+ stream_entries = prepare_work_stream_array(state)
172
+ processed_contexts = Parallel.map(stream_entries, PARALLEL_PROCESS_OPTIONS) do |entry|
173
+ mvmt_stream, stream = *entry
174
+ ctx = @context.clone_for_translation
175
+ ctx.translate(mvmt_stream)
176
+ ctx.translate(stream)
177
+ ctx
210
178
  end
179
+
180
+ processed_contexts.each {|ctx| @context.merge_movements(ctx)}
211
181
  end
182
+
183
+ # An array containing entries for each file/stream to be translated.
184
+ # Each entry in this array is in the form [mvmt_stream, stream]
185
+ def prepare_work_stream_array(state)
186
+ streams = state[:streams]
187
+ movements = state[:movements]
188
+ line_range = @context[:options][:line_range]
189
+ entries = []
190
+
191
+ movements.each do |mvmt, mvmt_files|
192
+ # Construct movement stream, a prefix for the part stream
193
+ mvmt_stream = [{type: :setting, key: 'movement', value: mvmt}]
194
+ if path = movements[mvmt][:movement]
195
+ mvmt_stream += streams[path]
196
+ end
212
197
 
213
- DEFAULT_BASENAMES = %w{work movement}
214
-
215
- def process_directory(path, recursive = true)
216
- preserve_context do
217
- # process work code
218
- process_lydown_file(File.join(path, 'work.ld'))
219
- # process movement specific code
220
- process_lydown_file(File.join(path, 'movement.ld'))
221
-
222
- part_filter = @context[:options][:parts]
223
- mvmt_filter = @context[:options][:movements]
224
-
225
- # Iterate over sorted directory entries
226
- Dir["#{path}/*"].entries.sort.each do |entry|
227
- if File.file?(entry) && (entry =~ /\.ld$/)
228
- part = File.basename(entry, '.*')
229
- skip = part_filter && !part_filter.include?(part)
230
- unless DEFAULT_BASENAMES.include?(part) || skip
231
- preserve_context do
232
- process_lydown_file(entry, [
233
- {type: :setting, key: 'part', value: part}
234
- ], line_range: @context[:options][:line_range])
235
- end
236
- end
237
- elsif File.directory?(entry) && recursive
238
- movement = File.basename(entry)
239
- skip = mvmt_filter && !mvmt_filter.include?(movement)
240
- unless skip
241
- process([{type: :setting, key: 'movement', value: movement}])
242
- process_directory(entry, false)
243
- end
198
+ mvmt_files.each do |part, path|
199
+ unless part.is_a?(Symbol)
200
+ stream = streams[path]
201
+ Lydown::Rendering.insert_skip_markers(stream, line_range) if line_range
202
+ stream.unshift({type: :setting, key: 'part', value: part})
203
+ entries << [mvmt_stream, stream]
244
204
  end
245
205
  end
246
206
  end
207
+ entries
247
208
  end
248
209
 
249
- def process_lydown_file(path, prefix = [], opts = {})
250
- return unless File.file?(path)
251
-
252
- content = IO.read(path)
253
- stream = LydownParser.parse(content, {
254
- filename: File.expand_path(path),
255
- source: content
256
- })
257
-
258
- if opts[:line_range]
259
- Lydown::Rendering.insert_skip_markers(stream, opts[:line_range])
260
- end
261
-
262
- process(prefix + stream)
263
- end
264
210
  end
265
211
  end