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.
@@ -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
+
@@ -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
+