rtext 0.2.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.
- 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
|
+
|