rtext 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +4 -0
- data/MIT-LICENSE +20 -0
- data/README +29 -0
- data/RText_Plugin_Implementation_Guide +247 -0
- data/RText_Users_Guide +31 -0
- data/Rakefile +46 -0
- data/lib/rtext/completer.rb +152 -0
- data/lib/rtext/context_element_builder.rb +112 -0
- data/lib/rtext/default_loader.rb +137 -0
- data/lib/rtext/default_service_provider.rb +153 -0
- data/lib/rtext/instantiator.rb +284 -0
- data/lib/rtext/language.rb +257 -0
- data/lib/rtext/parser.rb +251 -0
- data/lib/rtext/serializer.rb +190 -0
- data/lib/rtext/service.rb +182 -0
- data/lib/rtext_plugin/connection_manager.rb +59 -0
- data/test/instantiator_test.rb +931 -0
- data/test/rtext_test.rb +5 -0
- data/test/serializer_test.rb +418 -0
- metadata +84 -0
@@ -0,0 +1,257 @@
|
|
1
|
+
require 'rgen/ecore/ecore'
|
2
|
+
require 'rgen/ecore/ecore_ext'
|
3
|
+
require 'rgen/serializer/opposite_reference_filter'
|
4
|
+
require 'rgen/serializer/qualified_name_provider'
|
5
|
+
|
6
|
+
module RText
|
7
|
+
|
8
|
+
class Language
|
9
|
+
|
10
|
+
# Creates an RText language description for the metamodel described by +root_epackage+
|
11
|
+
# Valid options include:
|
12
|
+
#
|
13
|
+
# :feature_provider
|
14
|
+
# a Proc which receives an EClass and should return a subset of this EClass's features
|
15
|
+
# this can be used to filter and/or reorder the features
|
16
|
+
# note that in most cases, this Proc will have to filter opposite references
|
17
|
+
# default: all features filtered using OppositeReferenceFilter
|
18
|
+
#
|
19
|
+
# :unlabled_arguments
|
20
|
+
# a Proc which receives an EClass and should return this EClass's feature names which are
|
21
|
+
# to be serialized without lables in the given order and before all labled arguments
|
22
|
+
# the features must also occur in :feature_provider if :feature_provider is provided
|
23
|
+
# if unlabled arguments are not part of the current class's features, they will be ignored
|
24
|
+
# default: no unlabled arguments
|
25
|
+
#
|
26
|
+
# :unquoted_arguments
|
27
|
+
# a Proc which receives an EClass and should return this EClass's string typed attribute
|
28
|
+
# names which are to be serialized without quotes. input data my still be quoted.
|
29
|
+
# the serializer will take care to insert quotes if the data is not a valid identifier
|
30
|
+
# the features must also occur in :feature_provider if :feature_provider is provided
|
31
|
+
# default: no unquoted arguments
|
32
|
+
#
|
33
|
+
# :argument_format_provider
|
34
|
+
# a Proc which receives an EAttribute and should return a format specification string
|
35
|
+
# (in sprintf syntax) which will be used by the serializer for integers and floats.
|
36
|
+
# default: if not present or the proc returns nil, then #to_s is used
|
37
|
+
#
|
38
|
+
# :short_class_names
|
39
|
+
# if true, the metamodel is searched for classes by unqualified class name recursively
|
40
|
+
# if false, classes can only be found in the root package, not in subpackages
|
41
|
+
# default: true
|
42
|
+
#
|
43
|
+
# :reference_regexp
|
44
|
+
# a Regexp which is used by the tokenizer for identifying references
|
45
|
+
# it must only match at the beginning of a string, i.e. it should start with \A
|
46
|
+
# it must be built in a way that does not match other language constructs
|
47
|
+
# in particular it must not match identifiers (word characters not starting with a digit)
|
48
|
+
# identifiers can always be used where references are expected
|
49
|
+
# default: word characters separated by at least one slash (/)
|
50
|
+
#
|
51
|
+
# :identifier_provider
|
52
|
+
# a Proc which receives an element and its containing element or nil and should return
|
53
|
+
# the element's identifier as a string
|
54
|
+
# the identifier must be unique for the element unless "per_type_identifier" is set to true,
|
55
|
+
# in which case they must be unique for each element of the same type
|
56
|
+
# identifiers may be relative to the given containing element. in this case a globally unique
|
57
|
+
# identifer must be resonstructed by the proc specified using the :reference_qualifier option.
|
58
|
+
# if the containing element is nil, the identifier returned must be globally unique.
|
59
|
+
# default: identifiers calculated by QualifiedNameProvider
|
60
|
+
# in this case options to QualifiedNameProvider may be provided and will be passed through
|
61
|
+
#
|
62
|
+
# :per_type_identifier
|
63
|
+
# if set to true, identifiers may be reused for elements of different type
|
64
|
+
# default: false
|
65
|
+
#
|
66
|
+
# :reference_qualifier
|
67
|
+
# a Proc which receives an element identifier as returned by the identifier provider and
|
68
|
+
# another element which uses this identifier to reference the element.
|
69
|
+
# it must return the globally unique version of the identifier.
|
70
|
+
# in case the received identifier is already globally unique, it must be returned as is.
|
71
|
+
# the received element might only be similar to the original referencing element. the reason
|
72
|
+
# is that this element may need to be constructed using only partially parsable data.
|
73
|
+
# it is garantueed though that the element's chain of containing elements is complete and
|
74
|
+
# that (non-containment) references are resolved as far as possible.
|
75
|
+
# default: no reference qualifier, i.e. all identifiers returned by the identifier provider
|
76
|
+
# must be globally unique
|
77
|
+
#
|
78
|
+
# :line_number_attribute
|
79
|
+
# the name of the attribute which will be used to associate the line number with a model element
|
80
|
+
# default: no line number
|
81
|
+
#
|
82
|
+
# :file_name_attribute
|
83
|
+
# the name of the attribute which will be used to associate the file name with a model element
|
84
|
+
# default: no file name
|
85
|
+
#
|
86
|
+
# :fragment_ref_attribute
|
87
|
+
# the name of the attribute which will be used to associate a model fragment with a model element
|
88
|
+
#
|
89
|
+
# :comment_handler
|
90
|
+
# a Proc which will be invoked when a new element has been instantiated. receives an
|
91
|
+
# element, the comment as a string, and the environment to which the element has been
|
92
|
+
# added to. then environment may be nil. it should add the comment to the element and
|
93
|
+
# return true. if the element can take no comment, it should return false.
|
94
|
+
# default: no handling of comments
|
95
|
+
#
|
96
|
+
# :comment_provider
|
97
|
+
# a Proc which receives an element and should return this element's comment as a string or nil
|
98
|
+
# the Proc may also modify the element to remove information already part of the comment
|
99
|
+
# default: no comments
|
100
|
+
#
|
101
|
+
# :indent_string
|
102
|
+
# the string representing one indent, could be a tab or spaces
|
103
|
+
# default: 2 spaces
|
104
|
+
#
|
105
|
+
# :command_name_provider
|
106
|
+
# a Proc which receives an EClass object and should return an RText command name
|
107
|
+
# default: class name
|
108
|
+
#
|
109
|
+
def initialize(root_epackage, options={})
|
110
|
+
@root_epackage = root_epackage
|
111
|
+
@feature_provider = options[:feature_provider] ||
|
112
|
+
proc { |c| RGen::Serializer::OppositeReferenceFilter.call(c.eAllStructuralFeatures) }
|
113
|
+
@unlabled_arguments = options[:unlabled_arguments]
|
114
|
+
@unquoted_arguments = options[:unquoted_arguments]
|
115
|
+
@argument_format_provider = options[:argument_format_provider]
|
116
|
+
@class_by_command = {}
|
117
|
+
command_name_provider = options[:command_name_provider] || proc{|c| c.name}
|
118
|
+
((!options.has_key?(:short_class_names) || options[:short_class_names]) ?
|
119
|
+
root_epackage.eAllClasses : root_epackage.eClasses).each do |c|
|
120
|
+
next if c.abstract
|
121
|
+
command_name = command_name_provider.call(c)
|
122
|
+
raise "ambiguous command name #{command_name}" if @class_by_command[command_name]
|
123
|
+
@class_by_command[command_name] = c.instanceClass
|
124
|
+
end
|
125
|
+
# there can't be multiple commands for the same class as the command name provider
|
126
|
+
# can only return one command per class
|
127
|
+
@command_by_class = @class_by_command.invert
|
128
|
+
@reference_regexp = options[:reference_regexp] || /\A\w*(\/\w*)+/
|
129
|
+
@identifier_provider = options[:identifier_provider] ||
|
130
|
+
proc { |element, context|
|
131
|
+
@qualified_name_provider ||= RGen::Serializer::QualifiedNameProvider.new(options)
|
132
|
+
@qualified_name_provider.identifier(element)
|
133
|
+
}
|
134
|
+
@reference_qualifier = options[:reference_qualifier]
|
135
|
+
@line_number_attribute = options[:line_number_attribute]
|
136
|
+
@file_name_attribute = options[:file_name_attribute]
|
137
|
+
@fragment_ref_attribute = options[:fragment_ref_attribute]
|
138
|
+
@comment_handler = options[:comment_handler]
|
139
|
+
@comment_provider = options[:comment_provider]
|
140
|
+
@indent_string = options[:indent_string] || " "
|
141
|
+
@per_type_identifier = options[:per_type_identifier]
|
142
|
+
end
|
143
|
+
|
144
|
+
attr_reader :root_epackage
|
145
|
+
attr_reader :reference_regexp
|
146
|
+
attr_reader :identifier_provider
|
147
|
+
attr_reader :line_number_attribute
|
148
|
+
attr_reader :file_name_attribute
|
149
|
+
attr_reader :fragment_ref_attribute
|
150
|
+
attr_reader :comment_handler
|
151
|
+
attr_reader :comment_provider
|
152
|
+
attr_reader :indent_string
|
153
|
+
attr_reader :per_type_identifier
|
154
|
+
|
155
|
+
def class_by_command(command)
|
156
|
+
@class_by_command[command]
|
157
|
+
end
|
158
|
+
|
159
|
+
def command_by_class(clazz)
|
160
|
+
@command_by_class[clazz]
|
161
|
+
end
|
162
|
+
|
163
|
+
def containments(clazz)
|
164
|
+
features(clazz).select{|f| f.is_a?(RGen::ECore::EReference) && f.containment}
|
165
|
+
end
|
166
|
+
|
167
|
+
def non_containments(clazz)
|
168
|
+
features(clazz).reject{|f| f.is_a?(RGen::ECore::EReference) && f.containment}
|
169
|
+
end
|
170
|
+
|
171
|
+
def labled_arguments(clazz)
|
172
|
+
non_containments(clazz) - unlabled_arguments(clazz)
|
173
|
+
end
|
174
|
+
|
175
|
+
def unlabled_arguments(clazz)
|
176
|
+
return [] unless @unlabled_arguments
|
177
|
+
uargs = @unlabled_arguments.call(clazz) || []
|
178
|
+
uargs.collect{|a| non_containments(clazz).find{|f| f.name == a}}.compact
|
179
|
+
end
|
180
|
+
|
181
|
+
def unquoted?(feature)
|
182
|
+
return false unless @unquoted_arguments
|
183
|
+
@unquoted_arguments.call(feature.eContainingClass).include?(feature.name)
|
184
|
+
end
|
185
|
+
|
186
|
+
def argument_format(feature)
|
187
|
+
@argument_format_provider && @argument_format_provider.call(feature)
|
188
|
+
end
|
189
|
+
|
190
|
+
def concrete_types(clazz)
|
191
|
+
([clazz] + clazz.eAllSubTypes).select{|c| !c.abstract}
|
192
|
+
end
|
193
|
+
|
194
|
+
def containments_by_target_type(clazz, type)
|
195
|
+
map = {}
|
196
|
+
clazz.eAllReferences.select{|r| r.containment}.each do |r|
|
197
|
+
concrete_types(r.eType).each {|t| (map[t] ||= []) << r}
|
198
|
+
end
|
199
|
+
([type]+type.eAllSuperTypes).inject([]){|m,t| m + (map[t] || []) }.uniq
|
200
|
+
end
|
201
|
+
|
202
|
+
def feature_by_name(clazz, name)
|
203
|
+
clazz.eAllStructuralFeatures.find{|f| f.name == name}
|
204
|
+
end
|
205
|
+
|
206
|
+
def file_name(element)
|
207
|
+
@file_name_attribute && element.respond_to?(@file_name_attribute) && element.send(@file_name_attribute)
|
208
|
+
end
|
209
|
+
|
210
|
+
def line_number(element)
|
211
|
+
@line_number_attribute && element.respond_to?(@line_number_attribute) && element.send(@line_number_attribute)
|
212
|
+
end
|
213
|
+
|
214
|
+
def fragment_ref(element)
|
215
|
+
@fragment_ref_attribute && element.respond_to?(@fragment_ref_attribute) && element.send(@fragment_ref_attribute)
|
216
|
+
end
|
217
|
+
|
218
|
+
def qualify_reference(identifier, element)
|
219
|
+
if @reference_qualifier
|
220
|
+
@reference_qualifier.call(identifier, element)
|
221
|
+
else
|
222
|
+
identifier
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
private
|
227
|
+
|
228
|
+
def features(clazz)
|
229
|
+
@feature_provider.call(clazz)
|
230
|
+
end
|
231
|
+
|
232
|
+
# caching
|
233
|
+
[ :containments,
|
234
|
+
:non_containments,
|
235
|
+
:unlabled_arguments,
|
236
|
+
:labled_arguments,
|
237
|
+
:unquoted?,
|
238
|
+
:argument_format,
|
239
|
+
:concrete_types,
|
240
|
+
:containments_by_target_type,
|
241
|
+
:feature_by_name
|
242
|
+
].each do |m|
|
243
|
+
ms = m.to_s.sub('?','_')
|
244
|
+
module_eval <<-END
|
245
|
+
alias #{ms}_orig #{m}
|
246
|
+
def #{m}(*args)
|
247
|
+
@#{ms}_cache ||= {}
|
248
|
+
return @#{ms}_cache[args] if @#{ms}_cache.has_key?(args)
|
249
|
+
@#{ms}_cache[args] = #{ms}_orig(*args)
|
250
|
+
end
|
251
|
+
END
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
|
data/lib/rtext/parser.rb
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
module RText
|
2
|
+
|
3
|
+
class Parser
|
4
|
+
|
5
|
+
def initialize(reference_regexp)
|
6
|
+
@reference_regexp = reference_regexp
|
7
|
+
end
|
8
|
+
|
9
|
+
def parse(str, &visitor)
|
10
|
+
@visitor = visitor
|
11
|
+
@tokens = tokenize(str, @reference_regexp)
|
12
|
+
@last_line = @tokens.last && @tokens.last.line
|
13
|
+
while next_token
|
14
|
+
parse_statement(true, true)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# parse a statement with optional leading comment or an unassociated comment
|
19
|
+
def parse_statement(is_root=false, allow_unassociated_comment=false)
|
20
|
+
comments = []
|
21
|
+
comment = parse_comment
|
22
|
+
if (next_token && next_token == :identifier) || !allow_unassociated_comment
|
23
|
+
comments << [ comment, :above] if comment
|
24
|
+
command = consume(:identifier)
|
25
|
+
arg_list = []
|
26
|
+
parse_argument_list(arg_list)
|
27
|
+
element_list = []
|
28
|
+
if next_token == "{"
|
29
|
+
parse_statement_block(element_list, comments)
|
30
|
+
end
|
31
|
+
eol_comment = parse_eol_comment
|
32
|
+
comments << [ eol_comment, :eol ] if eol_comment
|
33
|
+
consume(:newline)
|
34
|
+
@visitor.call(command, arg_list, element_list, comments, is_root)
|
35
|
+
elsif comment
|
36
|
+
# if there is no statement, the comment is non-optional
|
37
|
+
comments << [ comment, :unassociated ]
|
38
|
+
@visitor.call(nil, nil, nil, comments, nil)
|
39
|
+
nil
|
40
|
+
else
|
41
|
+
# die expecting an identifier (next token is not an identifier)
|
42
|
+
consume(:identifier)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def parse_comment
|
47
|
+
result = nil
|
48
|
+
while next_token == :comment
|
49
|
+
result ||= []
|
50
|
+
result << consume(:comment)
|
51
|
+
consume(:newline)
|
52
|
+
end
|
53
|
+
result
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_eol_comment
|
57
|
+
if next_token == :comment
|
58
|
+
consume(:comment)
|
59
|
+
else
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_statement_block(element_list, comments)
|
65
|
+
consume("{")
|
66
|
+
eol_comment = parse_eol_comment
|
67
|
+
comments << [ eol_comment, :eol ] if eol_comment
|
68
|
+
consume(:newline)
|
69
|
+
while next_token && next_token != "}"
|
70
|
+
parse_block_element(element_list, comments)
|
71
|
+
end
|
72
|
+
consume("}")
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse_block_element(element_list, comments)
|
76
|
+
if next_token == :label
|
77
|
+
label = consume(:label)
|
78
|
+
element_list << [label, parse_labeled_block_element(comments)]
|
79
|
+
else
|
80
|
+
statement = parse_statement(false, true)
|
81
|
+
element_list << statement if statement
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_labeled_block_element(comments)
|
86
|
+
if next_token == "["
|
87
|
+
parse_element_list(comments)
|
88
|
+
else
|
89
|
+
eol_comment = parse_eol_comment
|
90
|
+
comments << [ eol_comment, :eol ] if eol_comment
|
91
|
+
consume(:newline)
|
92
|
+
parse_statement
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def parse_element_list(comments)
|
97
|
+
consume("[")
|
98
|
+
eol_comment = parse_eol_comment
|
99
|
+
comments << [ eol_comment, :eol ] if eol_comment
|
100
|
+
consume(:newline)
|
101
|
+
result = []
|
102
|
+
while next_token && next_token != "]"
|
103
|
+
statement = parse_statement(false, true)
|
104
|
+
result << statement if statement
|
105
|
+
end
|
106
|
+
consume("]")
|
107
|
+
eol_comment = parse_eol_comment
|
108
|
+
comments << [ eol_comment, :eol ] if eol_comment
|
109
|
+
consume(:newline)
|
110
|
+
result
|
111
|
+
end
|
112
|
+
|
113
|
+
def parse_argument_list(arg_list)
|
114
|
+
first = true
|
115
|
+
while !["{", :comment, :newline].include?(next_token)
|
116
|
+
consume(",") unless first
|
117
|
+
first = false
|
118
|
+
parse_argument(arg_list)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def parse_argument(arg_list)
|
123
|
+
if next_token == :label
|
124
|
+
label = consume(:label)
|
125
|
+
arg_list << [label, parse_argument_value]
|
126
|
+
else
|
127
|
+
arg_list << parse_argument_value
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def parse_argument_value
|
132
|
+
if next_token == "["
|
133
|
+
parse_argument_value_list
|
134
|
+
else
|
135
|
+
parse_value
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def parse_argument_value_list
|
140
|
+
consume("[")
|
141
|
+
first = true
|
142
|
+
result = []
|
143
|
+
while next_token != "]"
|
144
|
+
consume(",") unless first
|
145
|
+
first = false
|
146
|
+
result << parse_value
|
147
|
+
end
|
148
|
+
consume("]")
|
149
|
+
result
|
150
|
+
end
|
151
|
+
|
152
|
+
def parse_value
|
153
|
+
consume(:identifier, :integer, :float, :string, :boolean, :reference)
|
154
|
+
end
|
155
|
+
|
156
|
+
def next_token
|
157
|
+
@tokens.first && @tokens.first.kind
|
158
|
+
end
|
159
|
+
|
160
|
+
class Error < Exception
|
161
|
+
attr_reader :message, :line
|
162
|
+
def initialize(message, line)
|
163
|
+
@message, @line = message, line
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def consume(*args)
|
168
|
+
t = @tokens.shift
|
169
|
+
if t.nil?
|
170
|
+
raise Error.new("Unexpected end of file, expected #{args.join(", ")}", @last_line)
|
171
|
+
end
|
172
|
+
if args.include?(t.kind)
|
173
|
+
t
|
174
|
+
else
|
175
|
+
if t.kind == :error
|
176
|
+
raise Error.new("Parse error on token '#{t.value}'", t.line)
|
177
|
+
else
|
178
|
+
value = " '#{t.value}'" if t.value
|
179
|
+
raise Error.new("Unexpected #{t.kind}#{value}, expected #{args.join(", ")}", t.line)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
Token = Struct.new(:kind, :value, :line)
|
185
|
+
|
186
|
+
def tokenize(str, reference_regexp)
|
187
|
+
result = []
|
188
|
+
str.split(/\r?\n/).each_with_index do |str, idx|
|
189
|
+
idx += 1
|
190
|
+
if str =~ /^\s*#(.*)/
|
191
|
+
result << Token.new(:comment, $1, idx)
|
192
|
+
else
|
193
|
+
until str.empty?
|
194
|
+
case str
|
195
|
+
when reference_regexp
|
196
|
+
str = $'
|
197
|
+
result << Token.new(:reference, $&, idx)
|
198
|
+
when /\A[-+]?\d+\.\d+(?:e[+-]\d+)?\b/
|
199
|
+
str = $'
|
200
|
+
result << Token.new(:float, $&.to_f, idx)
|
201
|
+
when /\A0[xX][0-9a-fA-F]+\b/
|
202
|
+
str = $'
|
203
|
+
result << Token.new(:integer, $&.to_i(16), idx)
|
204
|
+
when /\A[-+]?\d+\b/
|
205
|
+
str = $'
|
206
|
+
result << Token.new(:integer, $&.to_i, idx)
|
207
|
+
when /\A"((?:[^"\\]|\\.)*)"/
|
208
|
+
str = $'
|
209
|
+
result << Token.new(:string, $1.
|
210
|
+
gsub('\\\\','\\').
|
211
|
+
gsub('\\"','"').
|
212
|
+
gsub('\\n',"\n").
|
213
|
+
gsub('\\r',"\r").
|
214
|
+
gsub('\\t',"\t").
|
215
|
+
gsub('\\f',"\f").
|
216
|
+
gsub('\\b',"\b"), idx)
|
217
|
+
when /\A(?:true|false)\b/
|
218
|
+
str = $'
|
219
|
+
result << Token.new(:boolean, $& == "true", idx)
|
220
|
+
when /\A([a-zA-Z_]\w*)\b(?:\s*:)?/
|
221
|
+
str = $'
|
222
|
+
if $&[-1] == ?:
|
223
|
+
result << Token.new(:label, $1, idx)
|
224
|
+
else
|
225
|
+
result << Token.new(:identifier, $&, idx)
|
226
|
+
end
|
227
|
+
when /\A[\{\}\[\]:,]/
|
228
|
+
str = $'
|
229
|
+
result << Token.new($&, nil, idx)
|
230
|
+
when /\A#(.*)/
|
231
|
+
str = ""
|
232
|
+
result << Token.new(:comment, $1, idx)
|
233
|
+
when /\A\s+/
|
234
|
+
str = $'
|
235
|
+
# ignore
|
236
|
+
when /\A\S+/
|
237
|
+
str = $'
|
238
|
+
result << Token.new(:error, $&, idx)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
result << Token.new(:newline, nil, idx) \
|
243
|
+
unless result.empty? || result.last.kind == :newline
|
244
|
+
end
|
245
|
+
result
|
246
|
+
end
|
247
|
+
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
|