lydown 0.9.0 → 0.10.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 +159 -2
- data/lib/lydown.rb +8 -2
- data/lib/lydown/cache.rb +54 -0
- data/lib/lydown/cli.rb +1 -0
- data/lib/lydown/cli/commands.rb +27 -9
- data/lib/lydown/cli/compiler.rb +218 -54
- data/lib/lydown/cli/diff.rb +1 -1
- data/lib/lydown/cli/proofing.rb +3 -3
- data/lib/lydown/cli/signals.rb +23 -0
- data/lib/lydown/cli/support.rb +23 -1
- data/lib/lydown/core_ext.rb +41 -5
- data/lib/lydown/{rendering/defaults.yml → defaults.yml} +3 -3
- data/lib/lydown/errors.rb +3 -0
- data/lib/lydown/lilypond.rb +73 -31
- data/lib/lydown/ly_lib/lib.ly +297 -0
- data/lib/lydown/parsing.rb +1 -2
- data/lib/lydown/parsing/lydown.treetop +16 -10
- data/lib/lydown/parsing/nodes.rb +29 -5
- data/lib/lydown/rendering.rb +32 -6
- data/lib/lydown/rendering/command.rb +79 -2
- data/lib/lydown/rendering/figures.rb +29 -8
- data/lib/lydown/rendering/literal.rb +7 -0
- data/lib/lydown/rendering/movement.rb +61 -0
- data/lib/lydown/rendering/music.rb +37 -5
- data/lib/lydown/rendering/notes.rb +26 -8
- data/lib/lydown/rendering/settings.rb +41 -13
- data/lib/lydown/rendering/skipping.rb +43 -10
- data/lib/lydown/rendering/staff.rb +72 -16
- data/lib/lydown/templates.rb +8 -2
- data/lib/lydown/templates/lilypond_doc.erb +10 -1
- data/lib/lydown/templates/movement.erb +87 -34
- data/lib/lydown/templates/multi_voice.erb +1 -1
- data/lib/lydown/templates/part.erb +83 -55
- data/lib/lydown/templates/variables.erb +38 -0
- data/lib/lydown/version.rb +1 -1
- data/lib/lydown/work.rb +39 -26
- data/lib/lydown/work_context.rb +252 -14
- metadata +138 -8
- data/lib/lydown/rendering/lib.ly +0 -88
data/lib/lydown/parsing.rb
CHANGED
@@ -23,7 +23,6 @@ class LydownParser
|
|
23
23
|
|
24
24
|
unless ast
|
25
25
|
error_msg = format_parser_error(source, parser, opts)
|
26
|
-
$stderr.puts error_msg
|
27
26
|
raise LydownError, error_msg
|
28
27
|
else
|
29
28
|
stream = []
|
@@ -54,7 +53,7 @@ class LydownParser
|
|
54
53
|
end
|
55
54
|
|
56
55
|
def self.format_parser_error(source, parser, opts)
|
57
|
-
msg = opts[:filename] ? "#{opts[:filename]}: " : ""
|
56
|
+
msg = opts[:filename] ? "#{Pathname.relative_pwd(opts[:filename])}: " : ""
|
58
57
|
if opts[:nice_error]
|
59
58
|
msg << "Unexpected character at line #{parser.failure_line} column #{parser.failure_column}:\n"
|
60
59
|
else
|
@@ -8,10 +8,10 @@ grammar Lydown
|
|
8
8
|
music_stream / lyrics_stream
|
9
9
|
end
|
10
10
|
rule music_stream
|
11
|
-
'=music' white_space? [\n] music ([\n] !stream_breaker music)*
|
11
|
+
'=' white_space? 'music' white_space? [\n] music ([\n] !stream_breaker music)*
|
12
12
|
end
|
13
13
|
rule lyrics_stream
|
14
|
-
'=lyrics' stream_idx? white_space? [\n] lyrics_content
|
14
|
+
'=' white_space? 'lyrics' stream_idx? white_space? [\n] lyrics_content
|
15
15
|
([\n] !stream_breaker lyrics_content)* <Lyrics>
|
16
16
|
end
|
17
17
|
rule stream_breaker
|
@@ -47,7 +47,7 @@ grammar Lydown
|
|
47
47
|
end
|
48
48
|
rule event
|
49
49
|
(inline_command / inline_lyrics / voice_selector / barline / source_ref /
|
50
|
-
grace_duration / duration /
|
50
|
+
grace_duration / duration / standalone_figures / chord / note / rest /
|
51
51
|
silence / phrasing / tie) white_space*
|
52
52
|
end
|
53
53
|
rule barline
|
@@ -66,7 +66,10 @@ grammar Lydown
|
|
66
66
|
number '%' (number '/' number)? <TupletValue>
|
67
67
|
end
|
68
68
|
rule duration_value
|
69
|
-
|
69
|
+
duration_number dots* multiplier? <DurationValue>
|
70
|
+
end
|
71
|
+
rule duration_number
|
72
|
+
[0-9]+ / 'l'
|
70
73
|
end
|
71
74
|
rule number
|
72
75
|
[0-9]+
|
@@ -100,14 +103,17 @@ grammar Lydown
|
|
100
103
|
'<' note white_space* (note white_space*)* '>' expression* <Chord>
|
101
104
|
end
|
102
105
|
rule expression
|
103
|
-
(expression_shorthand /
|
106
|
+
(expression_shorthand / expression_string / expression_longhand) <Note::Expression>
|
104
107
|
end
|
105
108
|
|
106
109
|
rule expression_shorthand
|
107
110
|
[\_\.`]
|
108
111
|
end
|
109
112
|
rule expression_longhand
|
110
|
-
'\\' [
|
113
|
+
'\\' [_\^]? [a-zA-Z]+
|
114
|
+
end
|
115
|
+
rule expression_string
|
116
|
+
'\\' '_'? [<>\|]? string
|
111
117
|
end
|
112
118
|
rule string
|
113
119
|
'"' ('\"' / !'"' .)* '"'
|
@@ -116,7 +122,7 @@ grammar Lydown
|
|
116
122
|
'<' figures_component? (white_space? figures_component)* '>'
|
117
123
|
end
|
118
124
|
rule figures_component
|
119
|
-
(
|
125
|
+
([_\-\.] / [#bh] / ([1-9] [\+\-\!\\'`]*)) <FiguresComponent>
|
120
126
|
end
|
121
127
|
rule standalone_figures
|
122
128
|
duration_value? figures <StandAloneFigures>
|
@@ -125,10 +131,10 @@ grammar Lydown
|
|
125
131
|
[rR] multiplier* rest_expression* <Rest>
|
126
132
|
end
|
127
133
|
rule rest_expression
|
128
|
-
(
|
134
|
+
(expression_string / expression_longhand) <Note::Expression>
|
129
135
|
end
|
130
136
|
rule silence
|
131
|
-
[
|
137
|
+
[sS] multiplier* <Silence>
|
132
138
|
end
|
133
139
|
rule note_head
|
134
140
|
[a-g@] octave* accidental* <Note::Head>
|
@@ -185,7 +191,7 @@ grammar Lydown
|
|
185
191
|
'\\' '!'? inline_command_key (':' inline_command_argument)* <Command>
|
186
192
|
end
|
187
193
|
rule inline_command_key
|
188
|
-
[a-zA-
|
194
|
+
[\<\>\|\\]? [a-zA-Z_\-\.0-9]+ <Command::Key>
|
189
195
|
end
|
190
196
|
rule inline_command_argument
|
191
197
|
(string / [^\s\t\n\:]+) <Command::Argument>
|
data/lib/lydown/parsing/nodes.rb
CHANGED
@@ -11,6 +11,16 @@ module Lydown::Parsing
|
|
11
11
|
stream
|
12
12
|
end
|
13
13
|
|
14
|
+
def each_child(ele = nil, &block)
|
15
|
+
ele ||= elements
|
16
|
+
return unless ele
|
17
|
+
|
18
|
+
ele.each do |e|
|
19
|
+
block[e] if e.respond_to?(:to_stream)
|
20
|
+
each_child(e.elements, &block) if e.elements
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
14
24
|
def to_stream(stream = [], opts = {})
|
15
25
|
_to_stream(self, stream, opts)
|
16
26
|
stream
|
@@ -178,7 +188,15 @@ module Lydown::Parsing
|
|
178
188
|
chord = event_hash(stream, opts, {
|
179
189
|
type: :chord, notes: []
|
180
190
|
})
|
181
|
-
|
191
|
+
each_child do |c|
|
192
|
+
if c.is_a?(Note)
|
193
|
+
c.to_stream(chord[:notes], opts)
|
194
|
+
elsif c.is_a?(Note::Expression)
|
195
|
+
c.to_stream(chord, opts)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# _to_stream(self, chord[:notes], opts)
|
182
200
|
stream << chord
|
183
201
|
end
|
184
202
|
end
|
@@ -243,10 +261,9 @@ module Lydown::Parsing
|
|
243
261
|
rest = event_hash(stream, opts, {
|
244
262
|
type: :rest, raw: text_value, head: text_value[0]
|
245
263
|
})
|
246
|
-
if text_value =~ /^R(\*([0-9]+))
|
264
|
+
if text_value =~ /^R(\*([0-9]+))?/
|
247
265
|
rest[:multiplier] = $2 || '1'
|
248
266
|
end
|
249
|
-
|
250
267
|
_to_stream(self, rest, opts)
|
251
268
|
|
252
269
|
stream << rest
|
@@ -255,9 +272,14 @@ module Lydown::Parsing
|
|
255
272
|
|
256
273
|
class Silence < Root
|
257
274
|
def to_stream(stream, opts)
|
258
|
-
|
275
|
+
silence = event_hash(stream, opts, {
|
259
276
|
type: :silence, raw: text_value, head: text_value[0]
|
260
277
|
})
|
278
|
+
if text_value =~ /^S(\*([0-9]+))?/
|
279
|
+
silence[:multiplier] = $2 || '1'
|
280
|
+
end
|
281
|
+
_to_stream(self, silence, opts)
|
282
|
+
stream << silence
|
261
283
|
end
|
262
284
|
end
|
263
285
|
|
@@ -334,13 +356,15 @@ module Lydown::Parsing
|
|
334
356
|
stream << cmd
|
335
357
|
end
|
336
358
|
|
337
|
-
SETTING_KEYS = %w{time key clef}
|
359
|
+
SETTING_KEYS = %w{time key clef pickup mode nomode}
|
360
|
+
NON_EPHEMERAL_KEYS = %w{time key}
|
338
361
|
|
339
362
|
module Key
|
340
363
|
def to_stream(cmd, opts)
|
341
364
|
cmd[:key] = text_value
|
342
365
|
if SETTING_KEYS.include?(text_value)
|
343
366
|
cmd[:type] = :setting
|
367
|
+
cmd[:ephemeral] = !NON_EPHEMERAL_KEYS.include?(cmd[:key])
|
344
368
|
end
|
345
369
|
end
|
346
370
|
end
|
data/lib/lydown/rendering.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'lydown/templates'
|
2
2
|
require 'lydown/work'
|
3
3
|
require 'lydown/rendering/base'
|
4
|
+
require 'lydown/rendering/literal'
|
4
5
|
require 'lydown/rendering/comments'
|
5
6
|
require 'lydown/rendering/lyrics'
|
6
7
|
require 'lydown/rendering/notes'
|
@@ -13,11 +14,7 @@ require 'lydown/rendering/voices'
|
|
13
14
|
require 'lydown/rendering/source_ref'
|
14
15
|
require 'lydown/rendering/skipping'
|
15
16
|
|
16
|
-
require 'yaml'
|
17
|
-
|
18
17
|
module Lydown::Rendering
|
19
|
-
DEFAULTS = YAML.load(IO.read(File.join(File.dirname(__FILE__), 'rendering/defaults.yml'))).deep!
|
20
|
-
|
21
18
|
class << self
|
22
19
|
def translate(work, e, lydown_stream, idx)
|
23
20
|
klass = class_for_event(e)
|
@@ -30,14 +27,43 @@ module Lydown::Rendering
|
|
30
27
|
raise LydownError, "Invalid lydown event: #{e.inspect}"
|
31
28
|
end
|
32
29
|
|
33
|
-
def
|
30
|
+
def default_part_title(part_name)
|
34
31
|
if part_name =~ /^([^\d]+)(\d+)$/
|
35
32
|
"#{$1.titlize} #{$2.to_i.to_roman}"
|
36
33
|
else
|
37
34
|
part_name.titlize
|
38
35
|
end
|
39
36
|
end
|
37
|
+
|
38
|
+
def variable_name_infix(infix)
|
39
|
+
infix.capitalize.gsub(/(\d+)/) {|n| n.to_i.to_roman.upcase}.
|
40
|
+
gsub(/[^a-zA-Z]/, '').gsub(/\s+/, '')
|
41
|
+
end
|
42
|
+
|
43
|
+
VOICE_INDEX = {
|
44
|
+
'1' => 'One',
|
45
|
+
'2' => 'Two',
|
46
|
+
'3' => 'Three',
|
47
|
+
'4' => 'Four'
|
48
|
+
}
|
49
|
+
|
50
|
+
def voice_variable_name_infix(infix)
|
51
|
+
infix.capitalize.gsub(/(\d+)/) {|n| VOICE_INDEX[n]}
|
52
|
+
end
|
53
|
+
|
54
|
+
def variable_name(opts)
|
55
|
+
varname = "%s/%s/%s" % [
|
56
|
+
opts[:movement],
|
57
|
+
opts[:part],
|
58
|
+
opts[:stream]
|
59
|
+
]
|
60
|
+
|
61
|
+
varname << "/#{opts[:voice]}" if opts[:voice]
|
62
|
+
varname << "/#{opts[:idx]}" if opts[:idx]
|
63
|
+
|
64
|
+
"\"#{varname}\""
|
65
|
+
end
|
40
66
|
end
|
41
67
|
end
|
42
68
|
|
43
|
-
LY_LIB_DIR = File.join(File.dirname(__FILE__), '
|
69
|
+
LY_LIB_DIR = File.join(File.dirname(__FILE__), 'ly_lib')
|
@@ -2,16 +2,63 @@ module Lydown::Rendering
|
|
2
2
|
class Command < Base
|
3
3
|
include Notes
|
4
4
|
|
5
|
+
COMMAND_ALIGNMENT = {
|
6
|
+
'<' => '\\right-align',
|
7
|
+
'>' => '\\left-align',
|
8
|
+
'|' => '\\center-align'
|
9
|
+
}
|
10
|
+
|
5
11
|
def translate
|
12
|
+
key = @event[:key]
|
13
|
+
if key =~ /([\<\>\|])([a-zA-Z0-9]+)/
|
14
|
+
@event[:alignment] = COMMAND_ALIGNMENT[$1]
|
15
|
+
key = $2
|
16
|
+
end
|
17
|
+
# Is there a command handler
|
18
|
+
if respond_to?("cmd_#{key}".to_sym)
|
19
|
+
return send("cmd_#{key}".to_sym)
|
20
|
+
end
|
21
|
+
|
6
22
|
if @context['process/duration_macro']
|
7
23
|
add_macro_event(@event[:raw] || cmd_to_lydown(@event))
|
8
24
|
else
|
9
|
-
|
10
|
-
|
25
|
+
arguments = (@event[:arguments] || []).map do |a|
|
26
|
+
format_argument(@event[:key], a)
|
27
|
+
end.join(' ')
|
28
|
+
if @event[:key] =~ /^\\/
|
29
|
+
cmd = format_override_shorthand_command
|
30
|
+
else
|
31
|
+
cmd = "\\#{@event[:key]} #{arguments} "
|
32
|
+
end
|
33
|
+
@context.emit(:music, '\once ') if @event[:once]
|
11
34
|
@context.emit(:music, cmd)
|
12
35
|
end
|
13
36
|
end
|
14
37
|
|
38
|
+
def format_override_shorthand_command
|
39
|
+
key = @event[:key] =~ /^\\(.+)$/ && $1
|
40
|
+
arguments = @event[:arguments].map do |arg|
|
41
|
+
case arg
|
42
|
+
when /^[0-9\.]+$/
|
43
|
+
"##{arg}"
|
44
|
+
when /^[tf]$/
|
45
|
+
"###{arg}"
|
46
|
+
else
|
47
|
+
arg
|
48
|
+
end
|
49
|
+
end
|
50
|
+
"\\override #{key} = #{arguments.join(' ')} "
|
51
|
+
end
|
52
|
+
|
53
|
+
def format_argument(command_key, argument)
|
54
|
+
case command_key
|
55
|
+
when 'tempo', 'mark'
|
56
|
+
"\"#{argument}\""
|
57
|
+
else
|
58
|
+
argument
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
15
62
|
def cmd_to_lydown(event)
|
16
63
|
cmd = "\\#{event[:key]}"
|
17
64
|
if event[:arguments]
|
@@ -20,5 +67,35 @@ module Lydown::Rendering
|
|
20
67
|
end
|
21
68
|
cmd
|
22
69
|
end
|
70
|
+
|
71
|
+
def cmd_instr
|
72
|
+
return unless (@context.render_mode == :score)
|
73
|
+
markup = Staff.inline_part_title(
|
74
|
+
@context,
|
75
|
+
part: @context[:part],
|
76
|
+
name: @event[:arguments] && @event[:arguments][0],
|
77
|
+
alignment: @event[:alignment]
|
78
|
+
)
|
79
|
+
@context.emit(:music, markup)
|
80
|
+
end
|
81
|
+
|
82
|
+
def cmd_tempo
|
83
|
+
unless @event[:arguments] && @event[:arguments].size == 1
|
84
|
+
raise LydownError, "Invalid or missing tempo argument"
|
85
|
+
end
|
86
|
+
|
87
|
+
tempo = @event[:arguments].first
|
88
|
+
if tempo =~ /^\((.+)\)$/
|
89
|
+
format = @context['options/format']
|
90
|
+
return unless (format == :midi) || (format == :mp3)
|
91
|
+
tempo = $1
|
92
|
+
end
|
93
|
+
|
94
|
+
@context.emit(:music, "\\tempo #{tempo} ")
|
95
|
+
end
|
96
|
+
|
97
|
+
def cmd_partBreak
|
98
|
+
@context.emit(:music, "\\break ") if (@context.render_mode == :part)
|
99
|
+
end
|
23
100
|
end
|
24
101
|
end
|
@@ -1,21 +1,45 @@
|
|
1
1
|
module Lydown::Rendering
|
2
2
|
module Figures
|
3
|
+
BLANK_EXTENDER_START = '<->'
|
4
|
+
BLANK_EXTENDER_STOP = '<.>'
|
5
|
+
BLANK_EXTENDER = '<_>'
|
6
|
+
|
7
|
+
EXTENDERS_ON = "\\bassFigureExtendersOn "
|
8
|
+
EXTENDERS_OFF = "\\bassFigureExtendersOff "
|
9
|
+
|
3
10
|
def add_figures(figures, value)
|
11
|
+
# Add fill-in silences to catch up with music stream
|
4
12
|
if @context['process/running_values']
|
13
|
+
silence_figures = @event[:tenue] ?
|
14
|
+
@context['process/last_figures'] :
|
15
|
+
(@context['process/blank_extender_mode'] ? '<_>' : 's')
|
16
|
+
|
5
17
|
@context['process/running_values'].each do |v|
|
6
|
-
silence =
|
18
|
+
silence = silence_figures
|
7
19
|
if v != @context['process/last_figures_value']
|
8
|
-
silence
|
20
|
+
silence = silence + v
|
9
21
|
@context['process/last_figures_value'] = v
|
10
22
|
end
|
11
23
|
@context.emit(:figures, "#{silence} ")
|
12
24
|
end
|
13
|
-
@context['process/running_values'] =
|
25
|
+
@context['process/running_values'] = nil
|
14
26
|
end
|
15
27
|
|
16
28
|
figures = lilypond_figures(figures)
|
29
|
+
if figures == BLANK_EXTENDER_START
|
30
|
+
@context['process/blank_extender_mode'] = true
|
31
|
+
figures = BLANK_EXTENDER
|
32
|
+
@context.emit(:figures, EXTENDERS_ON)
|
33
|
+
elsif figures == BLANK_EXTENDER_STOP
|
34
|
+
@context['process/blank_extender_mode'] = false
|
35
|
+
figures = BLANK_EXTENDER
|
36
|
+
@event[:figure_extenders_off] = true
|
37
|
+
else
|
38
|
+
@context['process/last_figures'] = figures
|
39
|
+
end
|
40
|
+
|
17
41
|
if value != @context['process/last_figures_value']
|
18
|
-
figures
|
42
|
+
figures = figures + value
|
19
43
|
@context['process/last_figures_value'] = value
|
20
44
|
end
|
21
45
|
|
@@ -49,7 +73,7 @@ module Lydown::Rendering
|
|
49
73
|
unless next_event
|
50
74
|
# if next figures event is not found, check if there is a tenue, and
|
51
75
|
# add extenders off flag
|
52
|
-
@event[:figure_extenders_off] =
|
76
|
+
@event[:figure_extenders_off] = @event[:tenue]
|
53
77
|
return
|
54
78
|
end
|
55
79
|
|
@@ -80,9 +104,6 @@ module Lydown::Rendering
|
|
80
104
|
'`' => '\\\\',
|
81
105
|
"'" => "/"
|
82
106
|
}
|
83
|
-
EXTENDERS_ON = "\\bassFigureExtendersOn "
|
84
|
-
EXTENDERS_OFF = "\\bassFigureExtendersOff "
|
85
|
-
|
86
107
|
HIDDEN_FORMAT = "\\once \\override BassFigure #'implicit = ##t"
|
87
108
|
|
88
109
|
def next_figures_event
|
@@ -15,5 +15,66 @@ module Lydown::Rendering
|
|
15
15
|
|
16
16
|
title
|
17
17
|
end
|
18
|
+
|
19
|
+
def self.tacet?(context, name)
|
20
|
+
context["movements/#{name}/parts"].empty?
|
21
|
+
end
|
22
|
+
|
23
|
+
PAGE_BREAKS = {
|
24
|
+
'before' => {before: true},
|
25
|
+
'after' => {after: true},
|
26
|
+
'before and after' => {before: true, after: true},
|
27
|
+
'blank page before' => {blank_page_before: true}
|
28
|
+
}
|
29
|
+
|
30
|
+
def self.page_breaks(context, opts)
|
31
|
+
setting = case context.render_mode
|
32
|
+
when :score
|
33
|
+
context.get_setting('score/page_break', opts)
|
34
|
+
when :part
|
35
|
+
part = opts[:part] || context[:part] ||
|
36
|
+
(context['options/parts'] ? context['options/parts'][0] : '')
|
37
|
+
context.get_setting(:page_break, opts.merge(part: part)) ||
|
38
|
+
context.get_setting('parts/page_break', opts)
|
39
|
+
else
|
40
|
+
{}
|
41
|
+
end
|
42
|
+
|
43
|
+
PAGE_BREAKS[setting] || {}
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.include_files(context, opts)
|
47
|
+
(context.get_setting(:includes, opts) || []).map do |fn|
|
48
|
+
case File.extname(fn)
|
49
|
+
when '.ely'
|
50
|
+
Lydown::Templates.render(fn, context)
|
51
|
+
else
|
52
|
+
"\\include \"#{fn}\""
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Groups movements by bookparts. Whenever a movement requires a page break
|
58
|
+
# before, a new group is created
|
59
|
+
def self.bookparts(context, opts)
|
60
|
+
groups = []; current_group = []
|
61
|
+
context[:movements].keys.each do |movement|
|
62
|
+
breaks = page_breaks(context, opts.merge(movement: movement))
|
63
|
+
if breaks[:before] || breaks[:blank_page_before]
|
64
|
+
groups << current_group unless current_group.empty?
|
65
|
+
current_group = []
|
66
|
+
end
|
67
|
+
current_group << movement
|
68
|
+
end
|
69
|
+
groups << current_group unless current_group.empty?
|
70
|
+
|
71
|
+
groups
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.hide_bar_numbers?(context, opts)
|
75
|
+
context.get_setting(:bar_numbers, opts) == 'hide'
|
76
|
+
end
|
77
|
+
|
78
|
+
|
18
79
|
end
|
19
80
|
end
|