lydown 0.7.2 → 0.9.0

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