clef 0.1.0 → 1.0.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.
- checksums.yaml +4 -4
- data/README.md +77 -90
- data/Rakefile +21 -1
- data/exe/clef +21 -0
- data/lib/clef/compiler.rb +107 -4
- data/lib/clef/core/chord.rb +9 -3
- data/lib/clef/core/duration.rb +7 -3
- data/lib/clef/core/key_signature.rb +43 -36
- data/lib/clef/core/measure.rb +14 -10
- data/lib/clef/core/metadata.rb +52 -0
- data/lib/clef/core/note.rb +50 -4
- data/lib/clef/core/pitch.rb +73 -4
- data/lib/clef/core/rest.rb +11 -3
- data/lib/clef/core/score.rb +148 -9
- data/lib/clef/core/staff.rb +13 -3
- data/lib/clef/core/staff_group.rb +8 -2
- data/lib/clef/core/tempo.rb +5 -0
- data/lib/clef/core/tuplet.rb +48 -0
- data/lib/clef/core/validation.rb +39 -0
- data/lib/clef/core/voice.rb +21 -5
- data/lib/clef/engraving/font_manager.rb +1 -1
- data/lib/clef/engraving/glyph_table.rb +18 -3
- data/lib/clef/engraving/style.rb +41 -2
- data/lib/clef/ir/moment.rb +2 -2
- data/lib/clef/ir/music_tree.rb +2 -2
- data/lib/clef/ir/timeline.rb +25 -5
- data/lib/clef/layout/beam_layout.rb +2 -2
- data/lib/clef/layout/item.rb +26 -0
- data/lib/clef/layout/spacing.rb +6 -4
- data/lib/clef/layout/stem.rb +10 -6
- data/lib/clef/layout/system_layout.rb +71 -0
- data/lib/clef/midi/channel_map.rb +5 -2
- data/lib/clef/midi/exporter.rb +316 -38
- data/lib/clef/notation/dynamic.rb +5 -0
- data/lib/clef/notation/lyric.rb +33 -1
- data/lib/clef/parser/dsl.rb +249 -58
- data/lib/clef/parser/lilypond_lexer.rb +43 -3
- data/lib/clef/parser/lilypond_parser.rb +231 -17
- data/lib/clef/plugins/base.rb +24 -4
- data/lib/clef/plugins/registry.rb +80 -10
- data/lib/clef/renderer/base.rb +2 -2
- data/lib/clef/renderer/drawing_context.rb +26 -0
- data/lib/clef/renderer/notation_helpers.rb +92 -1
- data/lib/clef/renderer/pdf_renderer.rb +487 -82
- data/lib/clef/renderer/svg_renderer.rb +510 -97
- data/lib/clef/version.rb +1 -1
- data/lib/clef.rb +60 -7
- data/sig/clef.rbs +292 -0
- metadata +14 -5
data/lib/clef/parser/dsl.rb
CHANGED
|
@@ -5,11 +5,33 @@ module Clef
|
|
|
5
5
|
module DSL
|
|
6
6
|
class Error < StandardError; end
|
|
7
7
|
|
|
8
|
+
module BlockEvaluation
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def evaluate_block(target, &block)
|
|
12
|
+
return unless block
|
|
13
|
+
return block.call(target) if block.arity == 1
|
|
14
|
+
|
|
15
|
+
target.instance_eval(&block)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def method_missing(name, *_args, &_block)
|
|
19
|
+
raise Error, "invalid DSL method: #{name}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
8
27
|
class ScoreBuilder
|
|
28
|
+
include BlockEvaluation
|
|
29
|
+
|
|
9
30
|
attr_reader :score
|
|
10
31
|
|
|
11
|
-
def initialize
|
|
32
|
+
def initialize(plugins: Clef.plugins)
|
|
12
33
|
@score = Clef::Core::Score.new
|
|
34
|
+
@score.plugins = plugins
|
|
13
35
|
@default_group = Clef::Core::StaffGroup.new([], bracket_type: :none)
|
|
14
36
|
score.add_staff_group(@default_group)
|
|
15
37
|
end
|
|
@@ -36,38 +58,30 @@ module Clef
|
|
|
36
58
|
# @param clef [Symbol]
|
|
37
59
|
def staff(id, name: nil, clef: :treble, &block)
|
|
38
60
|
staff = Clef::Core::Staff.new(id, name: name, clef: Clef::Core::Clef.new(clef))
|
|
39
|
-
|
|
40
|
-
|
|
61
|
+
evaluate_block(StaffBuilder.new(staff), &block)
|
|
62
|
+
score.add_staff(staff)
|
|
41
63
|
staff
|
|
42
|
-
rescue NoMethodError => e
|
|
43
|
-
raise Error, "invalid DSL method in staff block: #{e.name}"
|
|
44
64
|
end
|
|
45
65
|
|
|
46
66
|
# @param bracket_type [Symbol]
|
|
47
67
|
def staff_group(bracket_type, &block)
|
|
68
|
+
raise Error, "staff_group requires a block" unless block
|
|
69
|
+
|
|
48
70
|
group = Clef::Core::StaffGroup.new([], bracket_type: bracket_type)
|
|
49
|
-
GroupBuilder.new(group)
|
|
71
|
+
evaluate_block(GroupBuilder.new(group), &block)
|
|
50
72
|
score.add_staff_group(group)
|
|
51
73
|
group
|
|
52
|
-
rescue NoMethodError => e
|
|
53
|
-
raise Error, "invalid DSL method in staff_group block: #{e.name}"
|
|
54
74
|
end
|
|
55
75
|
|
|
56
76
|
# @return [Clef::Core::Score]
|
|
57
77
|
def build
|
|
58
78
|
score
|
|
59
79
|
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def build_staff(staff, &block)
|
|
64
|
-
return unless block
|
|
65
|
-
|
|
66
|
-
StaffBuilder.new(staff).instance_eval(&block)
|
|
67
|
-
end
|
|
68
80
|
end
|
|
69
81
|
|
|
70
82
|
class GroupBuilder
|
|
83
|
+
include BlockEvaluation
|
|
84
|
+
|
|
71
85
|
def initialize(group)
|
|
72
86
|
@group = group
|
|
73
87
|
end
|
|
@@ -77,13 +91,15 @@ module Clef
|
|
|
77
91
|
# @param clef [Symbol]
|
|
78
92
|
def staff(id, name: nil, clef: :treble, &block)
|
|
79
93
|
staff = Clef::Core::Staff.new(id, name: name, clef: Clef::Core::Clef.new(clef))
|
|
80
|
-
StaffBuilder.new(staff)
|
|
94
|
+
evaluate_block(StaffBuilder.new(staff), &block)
|
|
81
95
|
@group.add_staff(staff)
|
|
82
96
|
staff
|
|
83
97
|
end
|
|
84
98
|
end
|
|
85
99
|
|
|
86
100
|
class StaffBuilder
|
|
101
|
+
include BlockEvaluation
|
|
102
|
+
|
|
87
103
|
def initialize(staff)
|
|
88
104
|
@staff = staff
|
|
89
105
|
@current_measure = nil
|
|
@@ -91,7 +107,7 @@ module Clef
|
|
|
91
107
|
@lyrics = []
|
|
92
108
|
end
|
|
93
109
|
|
|
94
|
-
# @param tonic [Symbol, Clef::Core::Pitch]
|
|
110
|
+
# @param tonic [Symbol, String, Clef::Core::Pitch]
|
|
95
111
|
# @param mode [Symbol]
|
|
96
112
|
def key(tonic, mode = :major)
|
|
97
113
|
@staff.key_signature = Clef::Core::KeySignature.new(tonic, mode)
|
|
@@ -103,34 +119,54 @@ module Clef
|
|
|
103
119
|
@staff.time_signature = Clef::Core::TimeSignature.new(numerator, denominator)
|
|
104
120
|
end
|
|
105
121
|
|
|
122
|
+
# @param program [Integer]
|
|
123
|
+
def instrument(program)
|
|
124
|
+
@staff.set_metadata(:midi_program, program)
|
|
125
|
+
end
|
|
126
|
+
|
|
106
127
|
# @param id [Symbol]
|
|
107
128
|
def voice(id = :default, &block)
|
|
108
129
|
measure = ensure_measure
|
|
109
130
|
voice = measure.voice(id)
|
|
110
131
|
return voice unless block
|
|
111
132
|
|
|
112
|
-
VoiceBuilder.new(voice)
|
|
133
|
+
evaluate_block(VoiceBuilder.new(voice), &block)
|
|
134
|
+
validate_measure_overflow!(measure)
|
|
113
135
|
voice
|
|
114
|
-
rescue NoMethodError => e
|
|
115
|
-
raise Error, "invalid DSL method in voice block: #{e.name}"
|
|
116
136
|
end
|
|
117
137
|
|
|
118
138
|
# @param lilypond_string [String]
|
|
119
139
|
def play(lilypond_string)
|
|
120
140
|
segments = split_measures(lilypond_string)
|
|
141
|
+
voice_builder = nil
|
|
121
142
|
segments.each_with_index do |segment, idx|
|
|
122
143
|
measure = ensure_measure
|
|
123
|
-
VoiceBuilder.new(measure.voice(:default))
|
|
144
|
+
voice_builder ||= VoiceBuilder.new(measure.voice(:default))
|
|
145
|
+
voice_builder.use_voice(measure.voice(:default)).notes(segment)
|
|
146
|
+
validate_measure_overflow!(measure)
|
|
124
147
|
advance_measure if idx < segments.length - 1
|
|
125
148
|
end
|
|
126
149
|
end
|
|
127
150
|
|
|
151
|
+
def bar
|
|
152
|
+
advance_measure
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @param number [Integer, nil]
|
|
156
|
+
def measure(number = nil, &block)
|
|
157
|
+
target_number = number || @next_measure_number
|
|
158
|
+
@current_measure = existing_measure(target_number) || new_measure(target_number)
|
|
159
|
+
evaluate_block(self, &block)
|
|
160
|
+
@current_measure
|
|
161
|
+
ensure
|
|
162
|
+
advance_measure
|
|
163
|
+
end
|
|
164
|
+
|
|
128
165
|
# @param voice_id [Symbol]
|
|
129
166
|
# @param text [String]
|
|
130
167
|
def lyrics(voice_id, text)
|
|
131
168
|
@lyrics << Clef::Notation::Lyric.new(voice_id, text)
|
|
132
|
-
@staff.
|
|
133
|
-
@staff.metadata[:lyrics] = @lyrics
|
|
169
|
+
@staff.set_metadata(:lyrics, @lyrics)
|
|
134
170
|
end
|
|
135
171
|
|
|
136
172
|
private
|
|
@@ -138,12 +174,20 @@ module Clef
|
|
|
138
174
|
def ensure_measure
|
|
139
175
|
return @current_measure if @current_measure
|
|
140
176
|
|
|
141
|
-
@current_measure =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
@
|
|
146
|
-
|
|
177
|
+
@current_measure = new_measure(@next_measure_number)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def existing_measure(number)
|
|
181
|
+
@staff.measures.find { |measure| measure.number == number }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def new_measure(number)
|
|
185
|
+
measure = Clef::Core::Measure.new(number, time_signature: @staff.time_signature)
|
|
186
|
+
measure.key_signature = @staff.key_signature
|
|
187
|
+
measure.clef = @staff.clef
|
|
188
|
+
@staff.add_measure(measure)
|
|
189
|
+
@next_measure_number = measure.number + 1
|
|
190
|
+
measure
|
|
147
191
|
end
|
|
148
192
|
|
|
149
193
|
def advance_measure
|
|
@@ -153,11 +197,33 @@ module Clef
|
|
|
153
197
|
def split_measures(input)
|
|
154
198
|
input.to_s.split("|").map(&:strip).reject(&:empty?)
|
|
155
199
|
end
|
|
200
|
+
|
|
201
|
+
def validate_measure_overflow!(measure)
|
|
202
|
+
ids = measure.overflowing_voice_ids
|
|
203
|
+
return if ids.empty?
|
|
204
|
+
|
|
205
|
+
raise Error, "measure #{measure.number} voice #{ids.join(", ")} exceeds time signature length"
|
|
206
|
+
end
|
|
156
207
|
end
|
|
157
208
|
|
|
158
209
|
class VoiceBuilder
|
|
210
|
+
include BlockEvaluation
|
|
211
|
+
|
|
159
212
|
def initialize(voice)
|
|
160
213
|
@voice = voice
|
|
214
|
+
@last_duration = Clef::Core::Duration.quarter
|
|
215
|
+
@pending_tie = false
|
|
216
|
+
@pending_articulations = []
|
|
217
|
+
@open_slur = false
|
|
218
|
+
@open_beam = false
|
|
219
|
+
@last_note = nil
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# @param voice [Clef::Core::Voice]
|
|
223
|
+
# @return [VoiceBuilder]
|
|
224
|
+
def use_voice(voice)
|
|
225
|
+
@voice = voice
|
|
226
|
+
self
|
|
161
227
|
end
|
|
162
228
|
|
|
163
229
|
# @param pitch_str [String]
|
|
@@ -174,7 +240,7 @@ module Clef
|
|
|
174
240
|
# @param opts [Hash]
|
|
175
241
|
def rest(duration_sym, **opts)
|
|
176
242
|
duration = Clef::Core::Duration.new(duration_sym, dots: opts.fetch(:dots, 0))
|
|
177
|
-
@voice.add(Clef::Core::Rest.new(duration))
|
|
243
|
+
@voice.add(Clef::Core::Rest.new(duration, kind: opts.fetch(:kind, :visible), measures: opts.fetch(:measures, 1)))
|
|
178
244
|
end
|
|
179
245
|
|
|
180
246
|
# @param pitch_strs [Array<String>]
|
|
@@ -186,6 +252,18 @@ module Clef
|
|
|
186
252
|
@voice.add(Clef::Core::Chord.new(pitches, duration))
|
|
187
253
|
end
|
|
188
254
|
|
|
255
|
+
# @param type [Symbol]
|
|
256
|
+
def dynamic(type)
|
|
257
|
+
@voice.add(Clef::Notation::Dynamic.new(type))
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# @param beat_unit [Symbol, Clef::Core::Duration]
|
|
261
|
+
# @param bpm [Integer]
|
|
262
|
+
def tempo(beat_unit:, bpm:)
|
|
263
|
+
duration = beat_unit.is_a?(Clef::Core::Duration) ? beat_unit : Clef::Core::Duration.new(beat_unit)
|
|
264
|
+
@voice.add(Clef::Core::Tempo.new(beat_unit: duration, bpm: bpm))
|
|
265
|
+
end
|
|
266
|
+
|
|
189
267
|
# @param lilypond_string [String]
|
|
190
268
|
def notes(lilypond_string)
|
|
191
269
|
parse_tokens(lilypond_string).each { |token| add_token(token) }
|
|
@@ -193,71 +271,184 @@ module Clef
|
|
|
193
271
|
|
|
194
272
|
# @param actual [Integer]
|
|
195
273
|
# @param normal [Integer]
|
|
196
|
-
def tuplet(actual, normal)
|
|
274
|
+
def tuplet(actual, normal, &block)
|
|
197
275
|
raise ArgumentError, "tuplet values must be positive" unless actual.positive? && normal.positive?
|
|
198
|
-
|
|
199
276
|
return unless block_given?
|
|
200
277
|
|
|
201
|
-
|
|
278
|
+
voice = Clef::Core::Voice.new(id: @voice.id)
|
|
279
|
+
evaluate_block(self.class.new(voice), &block)
|
|
280
|
+
@voice.add(Clef::Core::Tuplet.new(actual, normal, voice.elements))
|
|
202
281
|
end
|
|
203
282
|
|
|
204
283
|
private
|
|
205
284
|
|
|
206
285
|
def parse_tokens(input)
|
|
207
|
-
|
|
286
|
+
Clef::Parser::LilypondLexer.new.tokenize_with_locations(input)
|
|
208
287
|
end
|
|
209
288
|
|
|
210
289
|
def add_token(token)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
290
|
+
value = token.to_s
|
|
291
|
+
return if value == "|"
|
|
292
|
+
return start_slur if value == "("
|
|
293
|
+
return end_slur if value == ")"
|
|
294
|
+
return start_beam if value == "["
|
|
295
|
+
return end_beam if value == "]"
|
|
296
|
+
return tie_next if value == "~"
|
|
297
|
+
return add_pending_articulation(value) if articulation_token?(value)
|
|
298
|
+
return add_command_token(value) if value.start_with?("\\")
|
|
299
|
+
|
|
300
|
+
if value.start_with?("r")
|
|
301
|
+
add_rest_token(value)
|
|
302
|
+
elsif value.start_with?("<")
|
|
303
|
+
add_chord_token(value)
|
|
217
304
|
else
|
|
218
|
-
add_note_token(
|
|
305
|
+
add_note_token(value)
|
|
219
306
|
end
|
|
220
|
-
rescue
|
|
221
|
-
raise Error, "failed to parse token '#{token}
|
|
307
|
+
rescue => e
|
|
308
|
+
raise Error, "failed to parse token '#{value}'#{token_location(token)}: #{e.message}"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def token_location(token)
|
|
312
|
+
return "" unless token.respond_to?(:line) && token.respond_to?(:column)
|
|
313
|
+
|
|
314
|
+
" at line #{token.line}, column #{token.column}"
|
|
222
315
|
end
|
|
223
316
|
|
|
224
317
|
def add_rest_token(token)
|
|
225
|
-
match = /\Ar(\d
|
|
318
|
+
match = /\Ar(\d*)(\.*)\z/.match(token)
|
|
226
319
|
raise ArgumentError, "invalid rest token" unless match
|
|
227
320
|
|
|
228
|
-
duration =
|
|
321
|
+
duration = duration_from_match(match[1], match[2])
|
|
229
322
|
@voice.add(Clef::Core::Rest.new(duration))
|
|
230
323
|
end
|
|
231
324
|
|
|
232
325
|
def add_chord_token(token)
|
|
233
|
-
|
|
326
|
+
token, tied = split_tie_suffix(token)
|
|
327
|
+
match = /\A<([^>]+)>(\d*)(\.*)\z/.match(token)
|
|
234
328
|
raise ArgumentError, "invalid chord token" unless match
|
|
235
329
|
|
|
236
330
|
pitches = match[1].split(/\s+/).map { |value| parse_pitch(value) }
|
|
237
|
-
duration =
|
|
331
|
+
duration = duration_from_match(match[2], match[3])
|
|
238
332
|
@voice.add(Clef::Core::Chord.new(pitches, duration))
|
|
333
|
+
tie_next if tied
|
|
239
334
|
end
|
|
240
335
|
|
|
241
336
|
def add_note_token(token)
|
|
242
|
-
|
|
337
|
+
token, tied = split_tie_suffix(token)
|
|
338
|
+
token, articulations = split_articulation_suffixes(token)
|
|
339
|
+
@pending_articulations.concat(articulations)
|
|
340
|
+
match = /\A([a-g](?:isis|eses|is|es)?[',]*)(\d*)(\.*)\z/.match(token)
|
|
243
341
|
raise ArgumentError, "invalid note token" unless match
|
|
244
342
|
|
|
245
343
|
pitch = parse_pitch(match[1])
|
|
246
|
-
duration =
|
|
247
|
-
|
|
344
|
+
duration = duration_from_match(match[2], match[3])
|
|
345
|
+
note = Clef::Core::Note.new(pitch, duration,
|
|
346
|
+
articulations: consume_articulations,
|
|
347
|
+
tied: consume_tie_state)
|
|
348
|
+
note.slur_start = consume_slur_start
|
|
349
|
+
note.beam_start = consume_beam_start
|
|
350
|
+
@voice.add(note)
|
|
351
|
+
@last_note = note
|
|
352
|
+
tie_next if tied
|
|
248
353
|
end
|
|
249
354
|
|
|
250
355
|
def parse_pitch(value)
|
|
251
|
-
|
|
356
|
+
Clef::Core::Pitch.parse_any(value)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def duration_from_match(number, dots)
|
|
360
|
+
duration = if number.empty?
|
|
361
|
+
@last_duration
|
|
362
|
+
else
|
|
363
|
+
Clef::Core::Duration.from_lilypond(number.to_i, dots.length)
|
|
364
|
+
end
|
|
365
|
+
@last_duration = duration
|
|
366
|
+
duration
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def split_tie_suffix(token)
|
|
370
|
+
return [token.delete_suffix("~"), true] if token.end_with?("~")
|
|
371
|
+
|
|
372
|
+
[token, false]
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def split_articulation_suffixes(token)
|
|
376
|
+
articulations = []
|
|
377
|
+
loop do
|
|
378
|
+
suffix = {"-." => :staccato, "->" => :accent, "--" => :tenuto}.find { |marker, _| token.end_with?(marker) }
|
|
379
|
+
break unless suffix
|
|
380
|
+
|
|
381
|
+
marker, articulation = suffix
|
|
382
|
+
token = token.delete_suffix(marker)
|
|
383
|
+
articulations << articulation
|
|
384
|
+
end
|
|
385
|
+
[token, articulations.reverse]
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def articulation_token?(token)
|
|
389
|
+
%w[-. -> --].include?(token)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def add_pending_articulation(token)
|
|
393
|
+
articulation = {"-." => :staccato, "->" => :accent, "--" => :tenuto}.fetch(token)
|
|
394
|
+
if @last_note
|
|
395
|
+
@last_note.add_articulation(articulation)
|
|
396
|
+
else
|
|
397
|
+
@pending_articulations << articulation
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def add_command_token(token)
|
|
402
|
+
dynamic_name = token.delete_prefix("\\").to_sym
|
|
403
|
+
return dynamic(dynamic_name) if Clef::Notation::Dynamic::TYPES.include?(dynamic_name)
|
|
404
|
+
|
|
405
|
+
raise ArgumentError, "unsupported command token: #{token}"
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def consume_articulations
|
|
409
|
+
@pending_articulations.tap { @pending_articulations = [] }
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def tie_next
|
|
413
|
+
@last_note.tied = :start if @last_note
|
|
414
|
+
@pending_tie = true
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def consume_tie_state
|
|
418
|
+
return false unless @pending_tie
|
|
419
|
+
|
|
420
|
+
@pending_tie = false
|
|
421
|
+
:stop
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def start_slur
|
|
425
|
+
@open_slur = true
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def end_slur
|
|
429
|
+
@last_note.slur_end = true if @last_note
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def start_beam
|
|
433
|
+
@open_beam = true
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def end_beam
|
|
437
|
+
@last_note.beam_end = true if @last_note
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def consume_slur_start
|
|
441
|
+
return false unless @open_slur
|
|
442
|
+
|
|
443
|
+
@open_slur = false
|
|
444
|
+
true
|
|
252
445
|
end
|
|
253
446
|
|
|
254
|
-
def
|
|
255
|
-
|
|
256
|
-
return nil unless match
|
|
447
|
+
def consume_beam_start
|
|
448
|
+
return false unless @open_beam
|
|
257
449
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
Clef::Core::Pitch.new(note_name, match[3].to_i, alteration: alteration)
|
|
450
|
+
@open_beam = false
|
|
451
|
+
true
|
|
261
452
|
end
|
|
262
453
|
end
|
|
263
454
|
end
|
|
@@ -3,19 +3,59 @@
|
|
|
3
3
|
module Clef
|
|
4
4
|
module Parser
|
|
5
5
|
class LilypondLexer
|
|
6
|
-
|
|
6
|
+
Token = Struct.new(:value, :line, :column, keyword_init: true) do
|
|
7
|
+
def to_s
|
|
8
|
+
value
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def match?(*args)
|
|
12
|
+
value.match?(*args)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ==(other)
|
|
16
|
+
value == other || super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
TOKEN_REGEX = /
|
|
21
|
+
\\[a-zA-Z]+ |
|
|
22
|
+
<< | >> |
|
|
23
|
+
\{\} | \{ | \} |
|
|
24
|
+
<[^>]+>\d*\.*~? |
|
|
25
|
+
[a-g](?:isis|eses|is|es)?[',]*\d*\.*~? |
|
|
26
|
+
r\d*\.* |
|
|
27
|
+
-- | -> | -\. |
|
|
28
|
+
[~()\[\]|]
|
|
29
|
+
/mx
|
|
7
30
|
|
|
8
31
|
# @param input [String]
|
|
9
32
|
# @return [Array<String>]
|
|
10
33
|
def tokenize(input)
|
|
34
|
+
tokenize_with_locations(input).map(&:value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param input [String]
|
|
38
|
+
# @return [Array<Token>]
|
|
39
|
+
def tokenize_with_locations(input)
|
|
11
40
|
sanitized = strip_comments(input)
|
|
12
|
-
sanitized.scan
|
|
41
|
+
sanitized.enum_for(:scan, TOKEN_REGEX).map do
|
|
42
|
+
value = Regexp.last_match[0]
|
|
43
|
+
line, column = location_for(sanitized, Regexp.last_match.begin(0))
|
|
44
|
+
Token.new(value: value, line: line, column: column)
|
|
45
|
+
end
|
|
13
46
|
end
|
|
14
47
|
|
|
15
48
|
private
|
|
16
49
|
|
|
17
50
|
def strip_comments(input)
|
|
18
|
-
input.to_s.each_line.map { |line| line.sub(/%.*/, "") }.join
|
|
51
|
+
input.to_s.gsub(/%\{.*?%\}/m, "").each_line.map { |line| line.sub(/%.*/, "") }.join
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def location_for(input, offset)
|
|
55
|
+
prefix = input[0...offset]
|
|
56
|
+
line = prefix.count("\n") + 1
|
|
57
|
+
column = offset - (prefix.rindex("\n") || -1)
|
|
58
|
+
[line, column]
|
|
19
59
|
end
|
|
20
60
|
end
|
|
21
61
|
end
|