gherkin 1.0.30 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -0
- data/History.txt +19 -0
- data/Rakefile +4 -4
- data/VERSION.yml +2 -2
- data/features/feature_parser.feature +11 -0
- data/features/json_formatter.feature +238 -0
- data/features/pretty_formatter.feature +9 -0
- data/features/step_definitions/gherkin_steps.rb +1 -1
- data/features/step_definitions/json_formatter_steps.rb +32 -0
- data/features/step_definitions/pretty_formatter_steps.rb +24 -23
- data/features/support/env.rb +3 -3
- data/lib/gherkin/formatter/json_formatter.rb +82 -0
- data/lib/gherkin/formatter/pretty_formatter.rb +73 -78
- data/lib/gherkin/i18n.rb +22 -18
- data/lib/gherkin/i18n.yml +9 -9
- data/lib/gherkin/i18n_lexer.rb +2 -2
- data/lib/gherkin/parser/event.rb +6 -6
- data/lib/gherkin/parser/filter_listener.rb +5 -1
- data/lib/gherkin/parser/formatter_listener.rb +113 -0
- data/lib/gherkin/parser/json_parser.rb +102 -0
- data/lib/gherkin/parser/parser.rb +10 -2
- data/lib/gherkin/parser/row.rb +15 -0
- data/lib/gherkin/rubify.rb +2 -0
- data/lib/gherkin/tools/files.rb +1 -1
- data/lib/gherkin/tools/reformat.rb +1 -2
- data/lib/gherkin/tools/stats.rb +1 -1
- data/lib/gherkin/tools/stats_listener.rb +5 -5
- data/ragel/lexer.c.rl.erb +41 -12
- data/ragel/lexer.java.rl.erb +26 -17
- data/ragel/lexer.rb.rl.erb +10 -5
- data/ragel/lexer_common.rl.erb +6 -6
- data/spec/gherkin/c_lexer_spec.rb +2 -2
- data/spec/gherkin/fixtures/complex.js +105 -0
- data/spec/gherkin/formatter/argument_spec.rb +3 -3
- data/spec/gherkin/formatter/colors_spec.rb +3 -4
- data/spec/gherkin/formatter/pretty_formatter_spec.rb +21 -50
- data/spec/gherkin/i18n_lexer_spec.rb +6 -6
- data/spec/gherkin/i18n_spec.rb +16 -9
- data/spec/gherkin/java_lexer_spec.rb +1 -2
- data/spec/gherkin/output_stream_string_io.rb +24 -0
- data/spec/gherkin/parser/filter_listener_spec.rb +16 -9
- data/spec/gherkin/parser/formatter_listener_spec.rb +134 -0
- data/spec/gherkin/parser/json_parser_spec.rb +129 -0
- data/spec/gherkin/parser/parser_spec.rb +9 -9
- data/spec/gherkin/parser/tag_expression_spec.rb +8 -8
- data/spec/gherkin/rb_lexer_spec.rb +1 -1
- data/spec/gherkin/sexp_recorder.rb +21 -1
- data/spec/gherkin/shared/{lexer_spec.rb → lexer_group.rb} +172 -102
- data/spec/gherkin/shared/{py_string_spec.rb → py_string_group.rb} +21 -17
- data/spec/gherkin/shared/{row_spec.rb → row_group.rb} +36 -19
- data/spec/gherkin/shared/{tags_spec.rb → tags_group.rb} +13 -9
- data/spec/spec_helper.rb +18 -38
- data/tasks/bench.rake +3 -3
- data/tasks/compile.rake +13 -14
- data/tasks/rspec.rake +6 -11
- metadata +42 -28
- data/features/pretty_printer.feature +0 -14
- data/spec/gherkin/csharp_lexer_spec.rb +0 -20
data/lib/gherkin/parser/event.rb
CHANGED
@@ -1,11 +1,6 @@
|
|
1
1
|
module Gherkin
|
2
2
|
module Parser
|
3
3
|
class Event < Array
|
4
|
-
def initialize(*args)
|
5
|
-
super
|
6
|
-
self[1] = self[1].to_a if event == :row # Special JRuby handling
|
7
|
-
end
|
8
|
-
|
9
4
|
def event
|
10
5
|
self[0]
|
11
6
|
end
|
@@ -24,7 +19,12 @@ module Gherkin
|
|
24
19
|
end
|
25
20
|
|
26
21
|
def replay(listener)
|
27
|
-
|
22
|
+
begin
|
23
|
+
listener.__send__(event, *args)
|
24
|
+
rescue ArgumentError => e
|
25
|
+
e.message << "\nListener: #{listener.class}, args: #{args.inspect}"
|
26
|
+
raise e
|
27
|
+
end
|
28
28
|
end
|
29
29
|
|
30
30
|
private
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'gherkin/native'
|
2
|
+
require 'gherkin/parser/row'
|
3
|
+
|
4
|
+
module Gherkin
|
5
|
+
module Parser
|
6
|
+
# Adapter from the "raw" Gherkin <tt>Listener</tt> API
|
7
|
+
# to the slightly more high-level <tt>Formatter</tt> API,
|
8
|
+
# which is easier to implement (less state to keep track of).
|
9
|
+
class FormatterListener
|
10
|
+
native_impl('gherkin')
|
11
|
+
|
12
|
+
def initialize(formatter)
|
13
|
+
@formatter = formatter
|
14
|
+
@comments = []
|
15
|
+
@tags = []
|
16
|
+
@table = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def location(uri, offset)
|
20
|
+
@uri = uri
|
21
|
+
@offset = offset
|
22
|
+
end
|
23
|
+
|
24
|
+
def comment(content, line)
|
25
|
+
@comments << content
|
26
|
+
end
|
27
|
+
|
28
|
+
def tag(name, line)
|
29
|
+
@tags << name
|
30
|
+
end
|
31
|
+
|
32
|
+
def feature(keyword, name, description, line)
|
33
|
+
@formatter.feature(grab_comments!, grab_tags!, keyword, name, description, @uri)
|
34
|
+
end
|
35
|
+
|
36
|
+
def background(keyword, name, description, line)
|
37
|
+
@formatter.background(grab_comments!, keyword, name, description, line)
|
38
|
+
end
|
39
|
+
|
40
|
+
def scenario(keyword, name, description, line)
|
41
|
+
replay_step_or_examples
|
42
|
+
@formatter.scenario(grab_comments!, grab_tags!, keyword, name, description, line)
|
43
|
+
end
|
44
|
+
|
45
|
+
def scenario_outline(keyword, name, description, line)
|
46
|
+
replay_step_or_examples
|
47
|
+
@formatter.scenario_outline(grab_comments!, grab_tags!, keyword, name, description, line)
|
48
|
+
end
|
49
|
+
|
50
|
+
def examples(keyword, name, description, line)
|
51
|
+
replay_step_or_examples
|
52
|
+
@examples = [grab_comments!, grab_tags!, keyword, name, description, line]
|
53
|
+
end
|
54
|
+
|
55
|
+
def step(keyword, name, line)
|
56
|
+
replay_step_or_examples
|
57
|
+
@step = [grab_comments!, keyword, name, line]
|
58
|
+
end
|
59
|
+
|
60
|
+
def row(cells, line)
|
61
|
+
@table ||= []
|
62
|
+
@table << Row.new(cells, grab_comments!, line)
|
63
|
+
end
|
64
|
+
|
65
|
+
def py_string(py_string, line)
|
66
|
+
@py_string = py_string
|
67
|
+
end
|
68
|
+
|
69
|
+
def eof
|
70
|
+
replay_step_or_examples
|
71
|
+
@formatter.eof
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def grab_comments!
|
77
|
+
comments = @comments
|
78
|
+
@comments = []
|
79
|
+
comments
|
80
|
+
end
|
81
|
+
|
82
|
+
def grab_tags!
|
83
|
+
tags = @tags
|
84
|
+
@tags = []
|
85
|
+
tags
|
86
|
+
end
|
87
|
+
|
88
|
+
def grab_table!
|
89
|
+
table = @table
|
90
|
+
@table = nil
|
91
|
+
table
|
92
|
+
end
|
93
|
+
|
94
|
+
def grab_py_string!
|
95
|
+
py_string = @py_string
|
96
|
+
@py_string = nil
|
97
|
+
py_string
|
98
|
+
end
|
99
|
+
|
100
|
+
def replay_step_or_examples
|
101
|
+
if(@step)
|
102
|
+
multiline_arg = grab_py_string! || grab_table!
|
103
|
+
@formatter.step(*(@step + [multiline_arg, nil, nil, nil, nil]))
|
104
|
+
@step = nil
|
105
|
+
end
|
106
|
+
if(@examples)
|
107
|
+
@formatter.examples(*(@examples + [grab_table!]))
|
108
|
+
@examples = nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'gherkin/i18n'
|
3
|
+
|
4
|
+
module Gherkin
|
5
|
+
module Parser
|
6
|
+
class JSONParser
|
7
|
+
attr_reader :i18n_language
|
8
|
+
|
9
|
+
def initialize(listener)
|
10
|
+
@listener = listener
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(src)
|
14
|
+
feature = JSON.parse(src)
|
15
|
+
|
16
|
+
@i18n_language = Gherkin::I18n.get(feature["language"] || "en" )
|
17
|
+
|
18
|
+
tags_for(feature)
|
19
|
+
@listener.feature(keyword_for("feature", feature), feature["name"], line_for(feature)) if feature["name"]
|
20
|
+
|
21
|
+
if feature["background"]
|
22
|
+
@listener.background(keyword_for("background", feature["background"]), feature["background"]["name"], line_for(feature["background"]))
|
23
|
+
steps_for(feature["background"])
|
24
|
+
end
|
25
|
+
|
26
|
+
feature["elements"].each do |feature_element|
|
27
|
+
parse_element(feature_element)
|
28
|
+
end if feature["elements"]
|
29
|
+
|
30
|
+
@listener.eof
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def parse_element(feature_element)
|
36
|
+
case feature_element["type"]
|
37
|
+
when "Scenario" then parse_scenario(feature_element)
|
38
|
+
when "Scenario Outline" then parse_outline(feature_element)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_outline(scenario_outline)
|
43
|
+
tags_for(scenario_outline)
|
44
|
+
@listener.scenario_outline(keyword_for("scenario_outline", scenario_outline), scenario_outline["name"], line_for(scenario_outline) )
|
45
|
+
steps_for(scenario_outline)
|
46
|
+
scenario_outline["examples"].each do |examples|
|
47
|
+
tags_for(examples)
|
48
|
+
@listener.examples(keyword_for("examples", examples), examples["name"], line_for(examples))
|
49
|
+
rows_for(examples)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_scenario(scenario)
|
54
|
+
tags_for(scenario)
|
55
|
+
@listener.scenario(keyword_for("scenario", scenario), scenario["name"], line_for(scenario))
|
56
|
+
steps_for(scenario)
|
57
|
+
end
|
58
|
+
|
59
|
+
def tags_for(element)
|
60
|
+
element["tags"].each do |tag|
|
61
|
+
@listener.tag(tag, 0)
|
62
|
+
end if element["tags"]
|
63
|
+
end
|
64
|
+
|
65
|
+
def steps_for(element)
|
66
|
+
element["steps"].each do |step|
|
67
|
+
@listener.step(keyword_for("given",step), step["name"], line_for(step))
|
68
|
+
py_string_for(step)
|
69
|
+
rows_for(step)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def py_string_for(element)
|
74
|
+
@listener.py_string(element["py_string"], 0) if element["py_string"]
|
75
|
+
end
|
76
|
+
|
77
|
+
def rows_for(element)
|
78
|
+
element["table"].each do |row|
|
79
|
+
@listener.row(cells_for(row), 0)
|
80
|
+
end if element["table"]
|
81
|
+
end
|
82
|
+
|
83
|
+
def cells_for(row)
|
84
|
+
row["cells"].inject([]) { |col, ele| col << ele["text"] }
|
85
|
+
end
|
86
|
+
|
87
|
+
def line_for(element)
|
88
|
+
if element["line"]
|
89
|
+
element["line"]
|
90
|
+
elsif element["file_colon_line"]
|
91
|
+
element["file_colon_line"].split(':')[1].to_i
|
92
|
+
else
|
93
|
+
0
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def keyword_for(gherkin_keyword, element)
|
98
|
+
element["keyword"] || i18n_language.keywords(gherkin_keyword).reject { |kw| kw == "* " }.first
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -20,11 +20,15 @@ module Gherkin
|
|
20
20
|
push_machine(@machine_name)
|
21
21
|
end
|
22
22
|
|
23
|
+
def location(uri, offset)
|
24
|
+
@listener.location(uri, offset)
|
25
|
+
end
|
26
|
+
|
23
27
|
# Doesn't yet fall back to super
|
24
28
|
def method_missing(method, *args)
|
25
29
|
# TODO: Catch exception and call super
|
26
30
|
if(event(method.to_s, args[-1]))
|
27
|
-
@listener.
|
31
|
+
@listener.__send__(method, *args)
|
28
32
|
end
|
29
33
|
if method == :eof
|
30
34
|
pop_machine
|
@@ -117,7 +121,8 @@ module Gherkin
|
|
117
121
|
def transition_table(name)
|
118
122
|
state_machine_reader = StateMachineReader.new
|
119
123
|
lexer = Gherkin::I18n.new('en').lexer(state_machine_reader)
|
120
|
-
|
124
|
+
machine = File.dirname(__FILE__) + "/#{name}.txt"
|
125
|
+
lexer.scan(File.read(machine), machine, 0)
|
121
126
|
state_machine_reader.rows
|
122
127
|
end
|
123
128
|
|
@@ -128,6 +133,9 @@ module Gherkin
|
|
128
133
|
@rows = []
|
129
134
|
end
|
130
135
|
|
136
|
+
def location(uri, offset)
|
137
|
+
end
|
138
|
+
|
131
139
|
def row(row, line_number)
|
132
140
|
@rows << row
|
133
141
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'gherkin/native'
|
2
|
+
|
3
|
+
module Gherkin
|
4
|
+
module Parser
|
5
|
+
class Row
|
6
|
+
native_impl('gherkin')
|
7
|
+
|
8
|
+
attr_reader :cells, :comments, :line
|
9
|
+
|
10
|
+
def initialize(cells, comments, line)
|
11
|
+
@cells, @comments, @line = cells, comments, line
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/gherkin/rubify.rb
CHANGED
@@ -2,6 +2,8 @@ module Gherkin
|
|
2
2
|
module Rubify
|
3
3
|
if defined?(JRUBY_VERSION)
|
4
4
|
# Translate Java objects to Ruby.
|
5
|
+
# This is especially important to convert java.util.List coming
|
6
|
+
# from Java and back to a Ruby Array.
|
5
7
|
def rubify(o)
|
6
8
|
if Java.java.util.Collection === o || Array === o
|
7
9
|
o.map{|e| rubify(e)}
|
data/lib/gherkin/tools/files.rb
CHANGED
data/lib/gherkin/tools/stats.rb
CHANGED
@@ -10,7 +10,7 @@ module Gherkin
|
|
10
10
|
each do |f|
|
11
11
|
parser = Gherkin::Parser::Parser.new(listener, true)
|
12
12
|
lexer = Gherkin::I18nLexer.new(parser)
|
13
|
-
lexer.scan(IO.read(f))
|
13
|
+
lexer.scan(IO.read(f), f, 0)
|
14
14
|
end
|
15
15
|
puts "Features: #{listener.features}"
|
16
16
|
puts "Scenarios: #{listener.scenarios}"
|
@@ -20,21 +20,21 @@ module Gherkin
|
|
20
20
|
def comment(content, line)
|
21
21
|
end
|
22
22
|
|
23
|
-
def feature(keyword, name, line)
|
23
|
+
def feature(keyword, name, description, line)
|
24
24
|
@features += 1
|
25
25
|
end
|
26
26
|
|
27
|
-
def background(keyword, name, line)
|
27
|
+
def background(keyword, name, description, line)
|
28
28
|
end
|
29
29
|
|
30
|
-
def scenario(keyword, name, line)
|
30
|
+
def scenario(keyword, name, description, line)
|
31
31
|
@scenarios += 1
|
32
32
|
end
|
33
33
|
|
34
|
-
def scenario_outline(keyword, name, line)
|
34
|
+
def scenario_outline(keyword, name, description, line)
|
35
35
|
end
|
36
36
|
|
37
|
-
def examples(keyword, name, line)
|
37
|
+
def examples(keyword, name, description, line)
|
38
38
|
end
|
39
39
|
|
40
40
|
def step(keyword, name, line)
|
data/ragel/lexer.c.rl.erb
CHANGED
@@ -66,7 +66,7 @@ static VALUE rb_eGherkinLexingError;
|
|
66
66
|
#define PTR_TO(P) (data + lexer->P)
|
67
67
|
|
68
68
|
#define STORE_KW_END_CON(EVENT) \
|
69
|
-
|
69
|
+
store_multiline_kw_con(listener, # EVENT, \
|
70
70
|
PTR_TO(keyword_start), LEN(keyword_start, PTR_TO(keyword_end - 1)), \
|
71
71
|
PTR_TO(content_start), LEN(content_start, PTR_TO(content_end)), \
|
72
72
|
lexer->current_line, lexer->eol); \
|
@@ -129,7 +129,7 @@ static VALUE rb_eGherkinLexingError;
|
|
129
129
|
store_kw_con(listener, "step",
|
130
130
|
PTR_TO(keyword_start), LEN(keyword_start, PTR_TO(keyword_end)),
|
131
131
|
PTR_TO(content_start), LEN(content_start, p),
|
132
|
-
lexer->current_line
|
132
|
+
lexer->current_line);
|
133
133
|
}
|
134
134
|
|
135
135
|
action store_comment_content {
|
@@ -251,31 +251,59 @@ strip_i(VALUE str, VALUE ary)
|
|
251
251
|
}
|
252
252
|
|
253
253
|
static VALUE
|
254
|
-
multiline_strip(VALUE text
|
254
|
+
multiline_strip(VALUE text)
|
255
255
|
{
|
256
256
|
VALUE map = rb_ary_new();
|
257
257
|
VALUE split = rb_str_split(text, "\n");
|
258
258
|
|
259
259
|
rb_iterate(rb_each, split, strip_i, map);
|
260
260
|
|
261
|
-
return
|
262
|
-
eol == CRLF_FLAG ? CRLF : LF ));
|
261
|
+
return split;
|
263
262
|
}
|
264
263
|
|
265
264
|
static void
|
266
265
|
store_kw_con(VALUE listener, const char * event_name,
|
267
266
|
const char * keyword_at, size_t keyword_length,
|
268
267
|
const char * at, size_t length,
|
269
|
-
int current_line
|
268
|
+
int current_line)
|
270
269
|
{
|
271
270
|
VALUE con = Qnil, kw = Qnil;
|
272
271
|
kw = ENCODED_STR_NEW(keyword_at, keyword_length);
|
273
272
|
con = ENCODED_STR_NEW(at, length);
|
274
|
-
con = multiline_strip(con, eol);
|
275
273
|
rb_funcall(con, rb_intern("strip!"), 0);
|
276
274
|
rb_funcall(listener, rb_intern(event_name), 3, kw, con, INT2FIX(current_line));
|
277
275
|
}
|
278
276
|
|
277
|
+
static void
|
278
|
+
store_multiline_kw_con(VALUE listener, const char * event_name,
|
279
|
+
const char * keyword_at, size_t keyword_length,
|
280
|
+
const char * at, size_t length,
|
281
|
+
int current_line, int eol)
|
282
|
+
{
|
283
|
+
VALUE con = Qnil, kw = Qnil, name = Qnil, desc = Qnil;
|
284
|
+
|
285
|
+
kw = ENCODED_STR_NEW(keyword_at, keyword_length);
|
286
|
+
con = ENCODED_STR_NEW(at, length);
|
287
|
+
|
288
|
+
VALUE split = multiline_strip(con);
|
289
|
+
|
290
|
+
name = rb_funcall(split, rb_intern("shift"), 0);
|
291
|
+
desc = rb_ary_join(split, rb_str_new2( \
|
292
|
+
eol == CRLF_FLAG ? CRLF : LF ));
|
293
|
+
|
294
|
+
if( name == Qnil )
|
295
|
+
{
|
296
|
+
name = rb_str_new2("");
|
297
|
+
}
|
298
|
+
if( rb_funcall(desc, rb_intern("size"), 0) == 0)
|
299
|
+
{
|
300
|
+
desc = rb_str_new2("");
|
301
|
+
}
|
302
|
+
rb_funcall(name, rb_intern("strip!"), 0);
|
303
|
+
rb_funcall(desc, rb_intern("strip!"), 0);
|
304
|
+
rb_funcall(listener, rb_intern(event_name), 4, kw, name, desc, INT2FIX(current_line));
|
305
|
+
}
|
306
|
+
|
279
307
|
static void
|
280
308
|
store_attr(VALUE listener, const char * attr_type,
|
281
309
|
const char * at, size_t length,
|
@@ -307,7 +335,7 @@ store_pystring_content(VALUE listener,
|
|
307
335
|
static void
|
308
336
|
raise_lexer_error(const char * at, int line)
|
309
337
|
{
|
310
|
-
rb_raise(rb_eGherkinLexingError, "Lexing error on line %d: '%s'.", line, at);
|
338
|
+
rb_raise(rb_eGherkinLexingError, "Lexing error on line %d: '%s'. See http://wiki.github.com/aslakhellesoy/gherkin/lexingerror for more information.", line, at);
|
311
339
|
}
|
312
340
|
|
313
341
|
static int
|
@@ -361,12 +389,14 @@ static VALUE CLexer_init(VALUE self, VALUE listener)
|
|
361
389
|
return self;
|
362
390
|
}
|
363
391
|
|
364
|
-
static VALUE CLexer_scan(VALUE self, VALUE input)
|
392
|
+
static VALUE CLexer_scan(VALUE self, VALUE input, VALUE uri, VALUE offset)
|
365
393
|
{
|
394
|
+
VALUE listener = rb_iv_get(self, "@listener");
|
395
|
+
rb_funcall(listener, rb_intern("location"), 2, uri, offset);
|
396
|
+
|
366
397
|
lexer_state *lexer = NULL;
|
367
398
|
DATA_GET(self, lexer_state, lexer);
|
368
399
|
|
369
|
-
|
370
400
|
VALUE input_copy = rb_str_dup(input);
|
371
401
|
|
372
402
|
rb_str_append(input_copy, rb_str_new2("\n%_FEATURE_END_%"));
|
@@ -384,7 +414,6 @@ static VALUE CLexer_scan(VALUE self, VALUE input)
|
|
384
414
|
const char *p, *pe, *eof;
|
385
415
|
int cs = 0;
|
386
416
|
|
387
|
-
VALUE listener = rb_iv_get(self, "@listener");
|
388
417
|
VALUE current_row = Qnil;
|
389
418
|
|
390
419
|
p = data;
|
@@ -420,6 +449,6 @@ void Init_gherkin_lexer_<%= @i18n.underscored_iso_code %>()
|
|
420
449
|
cI18nLexer = rb_define_class_under(mCLexer, "<%= @i18n.underscored_iso_code.capitalize %>", rb_cObject);
|
421
450
|
rb_define_alloc_func(cI18nLexer, CLexer_alloc);
|
422
451
|
rb_define_method(cI18nLexer, "initialize", CLexer_init, 1);
|
423
|
-
rb_define_method(cI18nLexer, "scan", CLexer_scan,
|
452
|
+
rb_define_method(cI18nLexer, "scan", CLexer_scan, 3);
|
424
453
|
}
|
425
454
|
|