lydown 0.12.4 → 0.14.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 +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
|