lydown 0.12.4 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +41 -1
- data/bin/lydown +2 -0
- data/lib/lydown.rb +6 -2
- data/lib/lydown/cache.rb +5 -1
- data/lib/lydown/cli.rb +1 -1
- data/lib/lydown/cli/commands.rb +7 -11
- data/lib/lydown/cli/compiler.rb +5 -0
- data/lib/lydown/cli/proofing.rb +2 -2
- data/lib/lydown/cli/repl.rb +1 -1
- data/lib/lydown/cli/support.rb +0 -33
- data/lib/lydown/defaults.yml +50 -8
- data/lib/lydown/inverso.rb +84 -0
- data/lib/lydown/lilypond.rb +76 -10
- data/lib/lydown/ly_lib/lib.ly +140 -127
- data/lib/lydown/parsing/lydown.treetop +25 -12
- data/lib/lydown/parsing/nodes.rb +55 -19
- data/lib/lydown/rendering.rb +72 -1
- data/lib/lydown/rendering/base.rb +21 -0
- data/lib/lydown/rendering/command.rb +53 -0
- data/lib/lydown/rendering/layout.rb +83 -0
- data/lib/lydown/rendering/lyrics.rb +1 -1
- data/lib/lydown/rendering/markup.rb +23 -0
- data/lib/lydown/rendering/movement.rb +7 -4
- data/lib/lydown/rendering/music.rb +35 -29
- data/lib/lydown/rendering/notes.rb +75 -41
- data/lib/lydown/rendering/repeats.rb +27 -0
- data/lib/lydown/rendering/settings.rb +36 -9
- data/lib/lydown/rendering/skipping.rb +10 -2
- data/lib/lydown/rendering/staff.rb +38 -31
- data/lib/lydown/rendering/voices.rb +1 -1
- data/lib/lydown/templates.rb +8 -8
- data/lib/lydown/templates/layout.rb +40 -0
- data/lib/lydown/templates/lilypond_doc.rb +95 -0
- data/lib/lydown/templates/movement.rb +188 -0
- data/lib/lydown/templates/multi_voice.rb +25 -0
- data/lib/lydown/templates/part.rb +146 -0
- data/lib/lydown/templates/variables.rb +43 -0
- data/lib/lydown/translation/ripple.rb +1 -1
- data/lib/lydown/translation/ripple/nodes.rb +51 -2
- data/lib/lydown/translation/ripple/ripple.treetop +87 -10
- data/lib/lydown/version.rb +1 -1
- data/lib/lydown/work.rb +19 -2
- data/lib/lydown/work_context.rb +10 -2
- metadata +12 -8
- data/lib/lydown/cli/installer.rb +0 -175
- data/lib/lydown/templates/lilypond_doc.erb +0 -34
- data/lib/lydown/templates/movement.erb +0 -118
- data/lib/lydown/templates/multi_voice.erb +0 -16
- data/lib/lydown/templates/part.erb +0 -118
- data/lib/lydown/templates/variables.erb +0 -43
@@ -0,0 +1,23 @@
|
|
1
|
+
module Lydown::Rendering
|
2
|
+
module Markup
|
3
|
+
def self.convert(str)
|
4
|
+
convert_line_breaks(str)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.convert_styling(str)
|
8
|
+
str.
|
9
|
+
gsub(/__([^_]+)__/) {|m| "\\bold {#{$1} }" }.
|
10
|
+
gsub(/_([^_]+)_/) {|m| "\\italic {#{$1} }" }
|
11
|
+
gsub(/\^([^\^]+)\^/) {|m| "\\smallCaps {#{$1} }" }
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.convert_line_breaks(str)
|
15
|
+
if str =~/\n/
|
16
|
+
lines = str.lines.map {|s| "\\fill-line { \"#{s.chomp}\" }"}
|
17
|
+
"\\markup \\column { #{lines.join(' ')} }"
|
18
|
+
else
|
19
|
+
"\"#{str}\""
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -3,7 +3,9 @@ module Lydown::Rendering
|
|
3
3
|
def self.movement_title(context, name)
|
4
4
|
return nil if name.nil? || name.empty?
|
5
5
|
|
6
|
-
if
|
6
|
+
if t = context.get_setting('movement_title', movement: name)
|
7
|
+
title = t
|
8
|
+
elsif name =~ /^(?:([0-9]+)([a-z]*))\-(.+)$/
|
7
9
|
title = "#{$1.to_i}#{$2}. #{$3.capitalize}"
|
8
10
|
else
|
9
11
|
title = name
|
@@ -24,7 +26,8 @@ module Lydown::Rendering
|
|
24
26
|
'before' => {before: true},
|
25
27
|
'after' => {after: true},
|
26
28
|
'before and after' => {before: true, after: true},
|
27
|
-
'blank page before' => {blank_page_before: true}
|
29
|
+
'blank page before' => {blank_page_before: true},
|
30
|
+
'bookpart before' => {bookpart_before: true}
|
28
31
|
}
|
29
32
|
|
30
33
|
def self.page_breaks(context, opts)
|
@@ -32,8 +35,8 @@ module Lydown::Rendering
|
|
32
35
|
when :score
|
33
36
|
context.get_setting('score/page_break', opts)
|
34
37
|
when :part
|
35
|
-
part = opts[:part] || context[:part] ||
|
36
|
-
|
38
|
+
part = opts[:part] || context[:part] || context['render_opts/parts']
|
39
|
+
|
37
40
|
context.get_setting(:page_break, opts.merge(part: part)) ||
|
38
41
|
context.get_setting('parts/page_break', opts)
|
39
42
|
else
|
@@ -10,8 +10,25 @@ module Lydown::Rendering
|
|
10
10
|
'6' => '16',
|
11
11
|
'3' => '32'
|
12
12
|
}
|
13
|
-
|
13
|
+
|
14
14
|
class Duration < Base
|
15
|
+
def self.full_bar_value(time)
|
16
|
+
r = Rational(time)
|
17
|
+
case r.numerator
|
18
|
+
when 1
|
19
|
+
r.denominator.to_s
|
20
|
+
when 3
|
21
|
+
case r.denominator
|
22
|
+
when 1
|
23
|
+
"\\breve."
|
24
|
+
else
|
25
|
+
"#{r.denominator / 2}."
|
26
|
+
end
|
27
|
+
else
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
15
32
|
def translate
|
16
33
|
Notes.cleanup_duration_macro(@context)
|
17
34
|
|
@@ -32,6 +49,7 @@ module Lydown::Rendering
|
|
32
49
|
@context['process/figures_duration_value'] = value
|
33
50
|
else
|
34
51
|
@context['process/duration_values'] = [value]
|
52
|
+
@context['process/cross_bar_dotting'] = @event[:cross_bar_dotting]
|
35
53
|
@context['process/tuplet_mode'] = nil
|
36
54
|
@context['process/duration_macro'] = nil unless @context['process/macro_group']
|
37
55
|
end
|
@@ -184,23 +202,11 @@ module Lydown::Rendering
|
|
184
202
|
class Rest < Base
|
185
203
|
include Notes
|
186
204
|
|
187
|
-
def full_bar_value(time)
|
188
|
-
r = Rational(time)
|
189
|
-
case r.numerator
|
190
|
-
when 1
|
191
|
-
r.denominator.to_s
|
192
|
-
when 3
|
193
|
-
"#{r.denominator / 2}."
|
194
|
-
else
|
195
|
-
nil
|
196
|
-
end
|
197
|
-
end
|
198
|
-
|
199
205
|
def translate
|
200
206
|
translate_expressions
|
201
207
|
|
202
208
|
if @event[:multiplier]
|
203
|
-
value = full_bar_value(@context.get_current_setting(:time))
|
209
|
+
value = Duration.full_bar_value(@context.get_current_setting(:time))
|
204
210
|
@context['process/duration_macro'] = nil unless @context['process/macro_group']
|
205
211
|
if value
|
206
212
|
@event[:rest_value] = "#{value}*#{@event[:multiplier]}"
|
@@ -221,21 +227,9 @@ module Lydown::Rendering
|
|
221
227
|
class Silence < Base
|
222
228
|
include Notes
|
223
229
|
|
224
|
-
def full_bar_value(time)
|
225
|
-
r = Rational(time)
|
226
|
-
case r.numerator
|
227
|
-
when 1
|
228
|
-
r.denominator.to_s
|
229
|
-
when 3
|
230
|
-
"#{r.denominator / 2}."
|
231
|
-
else
|
232
|
-
nil
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
230
|
def translate
|
237
231
|
if @event[:multiplier]
|
238
|
-
value = full_bar_value(@context.get_current_setting(:time))
|
232
|
+
value = Duration.full_bar_value(@context.get_current_setting(:time))
|
239
233
|
@context['process/duration_macro'] = nil unless @context['process/macro_group']
|
240
234
|
if value
|
241
235
|
@event[:rest_value] = "#{value}*#{@event[:multiplier]}"
|
@@ -285,10 +279,22 @@ module Lydown::Rendering
|
|
285
279
|
end
|
286
280
|
|
287
281
|
class Barline < Base
|
282
|
+
|
283
|
+
LILYPOND_BARLINES = {
|
284
|
+
'|:' => '.|:',
|
285
|
+
':|' => ':|.',
|
286
|
+
'|\''=> '\halfBarline',
|
287
|
+
'?|' => ''
|
288
|
+
}
|
289
|
+
|
288
290
|
def translate
|
289
291
|
barline = @event[:barline]
|
290
|
-
barline =
|
291
|
-
|
292
|
+
barline = LILYPOND_BARLINES[barline] || barline
|
293
|
+
if barline =~ /^\\/
|
294
|
+
@context.emit(:music, "#{barline} ")
|
295
|
+
else
|
296
|
+
@context.emit(:music, "\\bar \"#{barline}\" ")
|
297
|
+
end
|
292
298
|
end
|
293
299
|
end
|
294
300
|
end
|
@@ -40,7 +40,7 @@ module Lydown::Rendering
|
|
40
40
|
def self.calc_accidentals_map(accidentals)
|
41
41
|
accidentals.inject({}) { |h, a| h[a[0]] = (a[1] == '+') ? 1 : -1; h}
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
ACCIDENTAL_VALUES = {
|
45
45
|
'+' => 1,
|
46
46
|
'-' => -1,
|
@@ -51,26 +51,26 @@ module Lydown::Rendering
|
|
51
51
|
def self.lilypond_note_name(note, key_signature = 'c major')
|
52
52
|
# if the natural sign (h) is used, no need to calculate the note name
|
53
53
|
return $1 if note =~ /([a-g])h/
|
54
|
-
|
54
|
+
|
55
55
|
value = 0
|
56
56
|
# accidental value from note
|
57
57
|
note = note.gsub(/[\-\+#ß]/) { |c| value += ACCIDENTAL_VALUES[c]; '' }
|
58
|
-
|
58
|
+
|
59
59
|
# add key signature value
|
60
60
|
value += accidentals_for_key_signature(key_signature)[note] || 0
|
61
61
|
|
62
62
|
note + (value >= 0 ? 'is' * value : 'es' * -value)
|
63
63
|
end
|
64
|
-
|
64
|
+
|
65
65
|
def self.chromatic_to_diatonic(note, key_signature = 'c major')
|
66
66
|
note =~ /([a-g])([\+\-]*)/
|
67
67
|
diatonic_note = $1
|
68
68
|
chromatic_value = $2.count('+') - $2.count('-')
|
69
|
-
|
69
|
+
|
70
70
|
key_accidentals = accidentals_for_key_signature(key_signature)
|
71
71
|
diatonic_value = key_accidentals[diatonic_note] || 0
|
72
72
|
value = chromatic_value - diatonic_value
|
73
|
-
|
73
|
+
|
74
74
|
"#{diatonic_note}#{value >= 0 ? '+' * value : '-' * -value}"
|
75
75
|
end
|
76
76
|
|
@@ -84,35 +84,35 @@ module Lydown::Rendering
|
|
84
84
|
lilypond_note_name(note, key)
|
85
85
|
end
|
86
86
|
end
|
87
|
-
|
87
|
+
|
88
88
|
module Octaves
|
89
89
|
DIATONICS = %w{a b c d e f g}
|
90
90
|
|
91
|
-
# calculates the octave markers needed to put a first note in the right
|
91
|
+
# calculates the octave markers needed to put a first note in the right
|
92
92
|
# octave. In lydown, octaves are relative (i.e. lilypond's relative mode).
|
93
|
-
# But the first note gives the octave to start on, rather than a relative
|
93
|
+
# But the first note gives the octave to start on, rather than a relative
|
94
94
|
# note to c (or any other reference note).
|
95
95
|
#
|
96
|
-
# In that manner, d' is d above middle c, g'' is g an octave and fifth
|
96
|
+
# In that manner, d' is d above middle c, g'' is g an octave and fifth
|
97
97
|
# above middle c, a is a a below middle c, and eß, is great e flat.
|
98
|
-
#
|
98
|
+
#
|
99
99
|
# The return value is a string with octave markers for relative mode,
|
100
100
|
# based on the refence note
|
101
101
|
def self.relative_octave(note, ref_note = 'c')
|
102
102
|
note_diatonic, ref_diatonic = note[0], ref_note[0]
|
103
103
|
raise LydownError, "Invalid note #{note}" unless DIATONICS.index(note_diatonic)
|
104
104
|
raise LydownError, "Invalid reference note #{ref_note}" unless DIATONICS.index(ref_diatonic)
|
105
|
-
|
105
|
+
|
106
106
|
# calculate diatonic interval
|
107
107
|
note_array = DIATONICS.rotate(DIATONICS.index(ref_diatonic))
|
108
108
|
interval = note_array.index(note_diatonic)
|
109
109
|
|
110
|
-
# calculate octave interval and
|
110
|
+
# calculate octave interval and
|
111
111
|
octave_value = note.count("'") - note.count(',')
|
112
112
|
ref_value = ref_note.count("'") - ref_note.count(',')
|
113
113
|
octave_interval = octave_value - ref_value
|
114
114
|
octave_interval += 1 if interval >= 4
|
115
|
-
|
115
|
+
|
116
116
|
# generate octave markers
|
117
117
|
octave_interval >= 0 ? "'" * octave_interval : "," * -octave_interval
|
118
118
|
end
|
@@ -121,17 +121,17 @@ module Lydown::Rendering
|
|
121
121
|
note_diatonic, ref_diatonic = note[0], ref_note[0]
|
122
122
|
raise LydownError, "Invalid note #{note}" unless DIATONICS.index(note_diatonic)
|
123
123
|
raise LydownError, "Invalid reference note #{ref_note}" unless DIATONICS.index(ref_diatonic)
|
124
|
-
|
124
|
+
|
125
125
|
# calculate diatonic interval
|
126
126
|
note_array = DIATONICS.rotate(DIATONICS.index(ref_diatonic))
|
127
127
|
interval = note_array.index(note_diatonic)
|
128
128
|
|
129
|
-
# calculate octave interval and
|
129
|
+
# calculate octave interval and
|
130
130
|
note_value = note.count("'") - note.count(',')
|
131
131
|
ref_value = ref_note.count("'") - ref_note.count(',')
|
132
132
|
octave_interval = ref_value + note_value
|
133
133
|
octave_interval -= 1 if interval >= 4
|
134
|
-
|
134
|
+
|
135
135
|
# generate octave markers
|
136
136
|
octave_interval >= 0 ? "'" * octave_interval : "," * -octave_interval
|
137
137
|
end
|
@@ -142,19 +142,19 @@ module Lydown::Rendering
|
|
142
142
|
|
143
143
|
def add_note(event, options = {})
|
144
144
|
@context.set_setting(:got_music, true)
|
145
|
-
|
145
|
+
|
146
146
|
return add_macro_note(event) if @context['process/duration_macro']
|
147
|
-
|
147
|
+
|
148
148
|
# calculate relative octave markers for first note
|
149
149
|
unless @context['process/first_note'] || event[:head] =~ /^[rsR]/
|
150
150
|
note = event[:head] + (event[:octave] || '')
|
151
151
|
event[:octave] = Lydown::Rendering::Octaves.relative_octave(note)
|
152
152
|
@context['process/first_note'] = note
|
153
153
|
end
|
154
|
-
|
154
|
+
|
155
155
|
if event[:head] == '@'
|
156
156
|
# replace repeating note head
|
157
|
-
event[:head] = @context['process/last_note_head']
|
157
|
+
event[:head] = @context['process/last_note_head']
|
158
158
|
else
|
159
159
|
@context['process/last_note_head'] = event[:head]
|
160
160
|
end
|
@@ -174,7 +174,7 @@ module Lydown::Rendering
|
|
174
174
|
@context['process/running_values'] << value
|
175
175
|
end
|
176
176
|
end
|
177
|
-
|
177
|
+
|
178
178
|
# only add the value if different than the last used
|
179
179
|
if options[:no_value] || (value == @context['process/last_value'])
|
180
180
|
value = ''
|
@@ -190,7 +190,7 @@ module Lydown::Rendering
|
|
190
190
|
code = lilypond_note(event, options.merge(value: value))
|
191
191
|
@context.emit(event[:stream] || :music, code)
|
192
192
|
end
|
193
|
-
|
193
|
+
|
194
194
|
def add_chord(event, options = {})
|
195
195
|
value = @context['process/duration_values'].first
|
196
196
|
@context['process/duration_values'].rotate!
|
@@ -214,16 +214,20 @@ module Lydown::Rendering
|
|
214
214
|
else
|
215
215
|
@context['process/last_value'] = value
|
216
216
|
end
|
217
|
-
|
217
|
+
|
218
218
|
notes = event[:notes].map do |note|
|
219
219
|
lilypond_note(note)
|
220
220
|
end
|
221
|
-
|
221
|
+
|
222
222
|
options = options.merge(value: value)
|
223
223
|
@context.emit(event[:stream] || :music, lilypond_chord(event, notes, options))
|
224
224
|
end
|
225
|
-
|
225
|
+
|
226
226
|
def lilypond_note(event, options = {})
|
227
|
+
if @context['process/cross_bar_dotting']
|
228
|
+
return cross_bar_dot_lilypond_note(event, options)
|
229
|
+
end
|
230
|
+
|
227
231
|
head = Accidentals.translate_note_name(@context, event[:head])
|
228
232
|
if options[:head_only]
|
229
233
|
head
|
@@ -235,7 +239,7 @@ module Lydown::Rendering
|
|
235
239
|
accidental_flag = event[:accidental_flag]
|
236
240
|
prefix = ''
|
237
241
|
end
|
238
|
-
|
242
|
+
|
239
243
|
[
|
240
244
|
prefix,
|
241
245
|
head,
|
@@ -249,6 +253,37 @@ module Lydown::Rendering
|
|
249
253
|
end
|
250
254
|
end
|
251
255
|
|
256
|
+
TRANSPARENT_TIE = "\\once \\override Tie #'transparent = ##t"
|
257
|
+
TRANSPARENT_NOTE = <<EOF
|
258
|
+
\\once \\override NoteHead #'transparent = ##t
|
259
|
+
\\once \\override Dots #'extra-offset = #'(-1.3 . 0)
|
260
|
+
\\once \\override Stem #'transparent = ##t
|
261
|
+
EOF
|
262
|
+
|
263
|
+
def cross_bar_dot_lilypond_note(event, options)
|
264
|
+
@context['process/cross_bar_dotting'] = nil
|
265
|
+
|
266
|
+
original_duration = @context['process/duration_values'][0]
|
267
|
+
original_duration =~ /([0-9]+)(\.+)/
|
268
|
+
value, dots = $1, $2
|
269
|
+
|
270
|
+
main_note = lilypond_note(event, options.merge(value: value))
|
271
|
+
|
272
|
+
cross_bar_note_head = lilypond_note(event, options.merge(head_only: true))
|
273
|
+
cross_bar_note = "#{cross_bar_note_head}#{original_duration}*0"
|
274
|
+
|
275
|
+
silence = "s#{value.to_i * 2} "
|
276
|
+
|
277
|
+
[
|
278
|
+
TRANSPARENT_TIE,
|
279
|
+
main_note,
|
280
|
+
'~',
|
281
|
+
TRANSPARENT_NOTE,
|
282
|
+
cross_bar_note,
|
283
|
+
silence
|
284
|
+
].join(' ')
|
285
|
+
end
|
286
|
+
|
252
287
|
def lilypond_chord(event, notes, options = {})
|
253
288
|
[
|
254
289
|
'<',
|
@@ -309,7 +344,7 @@ module Lydown::Rendering
|
|
309
344
|
event[:figures] ? "<#{event[:figures].join}>" : '',
|
310
345
|
event[:expressions] ? event[:expressions].join + ' ' : ''
|
311
346
|
]
|
312
|
-
|
347
|
+
|
313
348
|
# replace place holder and repeaters in macro group with actual note
|
314
349
|
@context['process/macro_group'].gsub!(/[_∞]/) do |match|
|
315
350
|
case match
|
@@ -325,7 +360,7 @@ module Lydown::Rendering
|
|
325
360
|
# correct
|
326
361
|
@context['process/macro_filename'] = event[:filename]
|
327
362
|
@context['process/macro_source'] = event[:source]
|
328
|
-
|
363
|
+
|
329
364
|
# increment group note count
|
330
365
|
@context['process/macro_group_note_count'] ||= 0
|
331
366
|
@context['process/macro_group_note_count'] += 1
|
@@ -335,20 +370,19 @@ module Lydown::Rendering
|
|
335
370
|
Notes.add_duration_macro_group(@context, @context['process/macro_group'])
|
336
371
|
end
|
337
372
|
end
|
338
|
-
|
373
|
+
|
339
374
|
# emits the current macro group up to the first placeholder character.
|
340
|
-
# this method is called
|
375
|
+
# this method is called
|
341
376
|
def self.cleanup_duration_macro(context)
|
342
377
|
return unless context['process/macro_group_note_count'] &&
|
343
378
|
context['process/macro_group_note_count'] > 0
|
344
|
-
|
379
|
+
|
345
380
|
# truncate macro group up until first placeholder
|
346
|
-
group = context['process/macro_group'].sub(/_.*$/, '')
|
381
|
+
group = context['process/macro_group'].sub(/(?<!\<)_.*$/, '')
|
347
382
|
|
348
|
-
# Refrain from adding
|
349
383
|
add_duration_macro_group(context, group)
|
350
384
|
end
|
351
|
-
|
385
|
+
|
352
386
|
def self.add_duration_macro_group(context, group)
|
353
387
|
opts = (context[:options] || {}).merge({
|
354
388
|
filename: context['process/macro_filename'],
|
@@ -367,7 +401,7 @@ module Lydown::Rendering
|
|
367
401
|
# restore macro
|
368
402
|
context['process/duration_macro'] = macro
|
369
403
|
end
|
370
|
-
|
404
|
+
|
371
405
|
def add_macro_event(code)
|
372
406
|
case @context['process/macro_group']
|
373
407
|
when nil
|
@@ -389,7 +423,7 @@ module Lydown::Rendering
|
|
389
423
|
'>' => 'left-align',
|
390
424
|
'|' => 'center-align'
|
391
425
|
}
|
392
|
-
|
426
|
+
|
393
427
|
DYNAMICS = %w{
|
394
428
|
pppp ppp pp p mp mf f ff fff ffff fp sf sff sp spp sfz rfz
|
395
429
|
}
|
@@ -421,19 +455,19 @@ module Lydown::Rendering
|
|
421
455
|
gsub(/__([^_]+)__/) {|m| "\\bold { #{$1} }" }.
|
422
456
|
gsub(/_([^_]+)_/) {|m| "\\italic { #{$1} }" }
|
423
457
|
end
|
424
|
-
|
458
|
+
|
425
459
|
TEXTMATE_URL = "txmt://open?url=file://%s&line=%d&column=%d"
|
426
|
-
|
460
|
+
|
427
461
|
ADD_LINK_COMMAND = '\once \override NoteHead.after-line-breaking =
|
428
462
|
#(add-link "%s") '
|
429
|
-
|
463
|
+
|
430
464
|
def note_event_url_link(event)
|
431
465
|
url = TEXTMATE_URL % [
|
432
466
|
File.expand_path(event[:filename]).uri_escape,
|
433
467
|
event[:line],
|
434
468
|
event[:column]
|
435
469
|
]
|
436
|
-
|
470
|
+
|
437
471
|
ADD_LINK_COMMAND % [url]
|
438
472
|
end
|
439
473
|
end
|