rtext 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+