rtext 0.4.0 → 0.5.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 +20 -0
- data/{README → README.rdoc} +5 -1
- data/RText_Protocol +444 -0
- data/Rakefile +10 -10
- data/lib/rtext/completer.rb +32 -26
- data/lib/rtext/context_builder.rb +113 -59
- data/lib/rtext/default_loader.rb +73 -8
- data/lib/rtext/default_service_provider.rb +30 -14
- data/lib/rtext/frontend/config.rb +58 -0
- data/lib/rtext/frontend/connector.rb +233 -0
- data/lib/rtext/frontend/connector_manager.rb +81 -0
- data/lib/rtext/frontend/context.rb +56 -0
- data/lib/rtext/generic.rb +13 -0
- data/lib/rtext/instantiator.rb +30 -7
- data/lib/rtext/language.rb +54 -27
- data/lib/rtext/link_detector.rb +57 -0
- data/lib/rtext/message_helper.rb +77 -0
- data/lib/rtext/parser.rb +19 -68
- data/lib/rtext/serializer.rb +18 -3
- data/lib/rtext/service.rb +182 -118
- data/lib/rtext/tokenizer.rb +102 -0
- data/test/completer_test.rb +327 -70
- data/test/context_builder_test.rb +671 -91
- data/test/instantiator_test.rb +153 -0
- data/test/integration/backend.out +10 -0
- data/test/integration/crash_on_request_editor.rb +12 -0
- data/test/integration/ecore_editor.rb +50 -0
- data/test/integration/frontend.log +25138 -0
- data/test/integration/model/invalid_encoding.invenc +2 -0
- data/test/integration/model/test.crash_on_request +18 -0
- data/test/integration/model/test.crashing_backend +18 -0
- data/test/integration/model/test.dont_open_socket +0 -0
- data/test/integration/model/test.invalid_cmd_line +0 -0
- data/test/integration/model/test.not_in_rtext +0 -0
- data/test/integration/model/test_large_with_errors.ect3 +43523 -0
- data/test/integration/model/test_metamodel.ect +18 -0
- data/test/integration/model/test_metamodel2.ect +5 -0
- data/test/integration/model/test_metamodel_error.ect2 +3 -0
- data/test/integration/model/test_metamodel_ok.ect2 +18 -0
- data/test/integration/test.rb +684 -0
- data/test/link_detector_test.rb +276 -0
- data/test/message_helper_test.rb +118 -0
- data/test/rtext_test.rb +4 -1
- data/test/serializer_test.rb +96 -1
- data/test/tokenizer_test.rb +125 -0
- metadata +36 -10
- data/RText_Plugin_Implementation_Guide +0 -268
- data/lib/rtext_plugin/connection_manager.rb +0 -59
@@ -0,0 +1,56 @@
|
|
1
|
+
module RText
|
2
|
+
module Frontend
|
3
|
+
|
4
|
+
module Context
|
5
|
+
|
6
|
+
# lines; all lines from the beginning up to and including the current line
|
7
|
+
def self.extract(lines)
|
8
|
+
non_ignored_lines = 0
|
9
|
+
array_nesting = 0
|
10
|
+
block_nesting = 0
|
11
|
+
last_element_line = 0
|
12
|
+
result = []
|
13
|
+
lines.reverse.each_with_index do |l, i|
|
14
|
+
if i == 0
|
15
|
+
result.unshift(l)
|
16
|
+
else
|
17
|
+
l = l.strip
|
18
|
+
if l.size == 0 || l[0..0] == "#"
|
19
|
+
# ignore empty lines and comments
|
20
|
+
else
|
21
|
+
non_ignored_lines += 1
|
22
|
+
case l[-1..-1]
|
23
|
+
when "{"
|
24
|
+
if block_nesting > 0
|
25
|
+
block_nesting -= 1
|
26
|
+
elsif block_nesting == 0
|
27
|
+
result.unshift(l)
|
28
|
+
last_element_line = non_ignored_lines
|
29
|
+
end
|
30
|
+
when "}"
|
31
|
+
block_nesting += 1
|
32
|
+
when "["
|
33
|
+
if array_nesting > 0
|
34
|
+
array_nesting -= 1
|
35
|
+
elsif array_nesting == 0
|
36
|
+
result.unshift(l)
|
37
|
+
end
|
38
|
+
when "]"
|
39
|
+
array_nesting += 1
|
40
|
+
when ":"
|
41
|
+
# lable directly above element
|
42
|
+
if non_ignored_lines == last_element_line + 1
|
43
|
+
result.unshift(l)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
result
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
data/lib/rtext/instantiator.rb
CHANGED
@@ -36,6 +36,10 @@ class Instantiator
|
|
36
36
|
# :fragment_ref
|
37
37
|
# object which references the fragment being instantiated, will be set on model elements
|
38
38
|
#
|
39
|
+
# :on_progress
|
40
|
+
# a proc which is called twice for each model element:
|
41
|
+
# when the command token is recognized and when the element is instantiated
|
42
|
+
#
|
39
43
|
def instantiate(str, options={})
|
40
44
|
@line_numbers = {}
|
41
45
|
@env = options[:env]
|
@@ -44,6 +48,7 @@ class Instantiator
|
|
44
48
|
@root_elements = options[:root_elements] || []
|
45
49
|
@file_name = options[:file_name]
|
46
50
|
@fragment_ref = options[:fragment_ref]
|
51
|
+
@on_progress_proc = options[:on_progress]
|
47
52
|
@context_class_stack = []
|
48
53
|
parser = Parser.new(@lang.reference_regexp)
|
49
54
|
@root_elements.clear
|
@@ -64,15 +69,13 @@ class Instantiator
|
|
64
69
|
unassociated_comments(args[3])
|
65
70
|
end
|
66
71
|
end,
|
72
|
+
:on_command_token => @on_progress_proc && lambda do
|
73
|
+
@on_progress_proc.call
|
74
|
+
end,
|
67
75
|
:problems => parser_problems)
|
68
76
|
parser_problems.each do |p|
|
69
77
|
problem(p.message, p.line)
|
70
78
|
end
|
71
|
-
if @unresolved_refs
|
72
|
-
@unresolved_refs.each do |ur|
|
73
|
-
ur.proxy.targetIdentifier = @lang.qualify_reference(ur.proxy.targetIdentifier, ur.element)
|
74
|
-
end
|
75
|
-
end
|
76
79
|
end
|
77
80
|
|
78
81
|
private
|
@@ -83,8 +86,9 @@ class Instantiator
|
|
83
86
|
end
|
84
87
|
end
|
85
88
|
|
86
|
-
def create_element(command, arg_list, element_list, comments, is_root)
|
89
|
+
def create_element(command, arg_list, element_list, comments, annotation, is_root)
|
87
90
|
clazz = @context_class_stack.last
|
91
|
+
@on_progress_proc.call if @on_progress_proc
|
88
92
|
if !@lang.has_command(command.value)
|
89
93
|
problem("Unknown command '#{command.value}'", command.line)
|
90
94
|
return
|
@@ -132,6 +136,7 @@ class Instantiator
|
|
132
136
|
comments.each do |c|
|
133
137
|
handle_comment(c, element)
|
134
138
|
end
|
139
|
+
handle_annotation(annotation, element) if annotation
|
135
140
|
element
|
136
141
|
end
|
137
142
|
|
@@ -209,7 +214,13 @@ class Instantiator
|
|
209
214
|
end
|
210
215
|
expected_kind = expected_token_kind(feature)
|
211
216
|
value.each do |v|
|
212
|
-
if
|
217
|
+
if v.kind == :generic
|
218
|
+
if @lang.generics_enabled
|
219
|
+
element.setOrAddGeneric(feature.name, v.value)
|
220
|
+
else
|
221
|
+
problem("Generic value not allowed", line)
|
222
|
+
end
|
223
|
+
elsif !expected_kind.include?(v.kind)
|
213
224
|
problem("Argument '#{name}' can not take a #{v.kind}, expected #{expected_kind.join(", ")}", line)
|
214
225
|
elsif feature.eType.is_a?(RGen::ECore::EEnum)
|
215
226
|
if !feature.eType.eLiterals.name.include?(v.value)
|
@@ -250,6 +261,18 @@ class Instantiator
|
|
250
261
|
end
|
251
262
|
end
|
252
263
|
|
264
|
+
def handle_annotation(annotation_desc, element)
|
265
|
+
if @lang.annotation_handler
|
266
|
+
annotation = annotation_desc.collect{|c| c.value}.join("\n")
|
267
|
+
success = @lang.annotation_handler.call(annotation, element, @env)
|
268
|
+
if !success
|
269
|
+
problem("Annotation not allowed", line_number(element))
|
270
|
+
end
|
271
|
+
else
|
272
|
+
problem("Annotation not allowed", line_number(element))
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
253
276
|
def is_labeled(a)
|
254
277
|
a.is_a?(Array) && a[0].respond_to?(:kind) && a[0].kind == :label
|
255
278
|
end
|
data/lib/rtext/language.rb
CHANGED
@@ -44,12 +44,14 @@ class Language
|
|
44
44
|
# default: word characters separated by at least one slash (/)
|
45
45
|
#
|
46
46
|
# :identifier_provider
|
47
|
-
# a Proc which receives an element
|
48
|
-
# the
|
47
|
+
# a Proc which receives an element, its containing element, the feature through which the
|
48
|
+
# element is referenced and the index position of the reference within the feature's values.
|
49
|
+
# the latter 3 argumnets may be nil. it should return the element's identifier as a string.
|
49
50
|
# the identifier must be unique for the element unless "per_type_identifier" is set to true,
|
50
|
-
# in which case they must be unique for each element of the same type
|
51
|
-
# identifiers may be relative to the given containing element
|
52
|
-
#
|
51
|
+
# in which case they must be unique for each element of the same type.
|
52
|
+
# identifiers may be relative to the given containing element, depending on the given
|
53
|
+
# feature and index position. in this case a globally unique
|
54
|
+
# identifier must be resonstructed by the proc specified using the :reference_qualifier option.
|
53
55
|
# if the containing element is nil, the identifier returned must be globally unique.
|
54
56
|
# default: identifiers calculated by QualifiedNameProvider
|
55
57
|
# in this case options to QualifiedNameProvider may be provided and will be passed through
|
@@ -59,16 +61,11 @@ class Language
|
|
59
61
|
# default: false
|
60
62
|
#
|
61
63
|
# :reference_qualifier
|
62
|
-
# a Proc which receives
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
# is that this element may need to be constructed using only partially parsable data.
|
68
|
-
# it is garantueed though that the element's chain of containing elements is complete and
|
69
|
-
# that (non-containment) references are resolved as far as possible.
|
70
|
-
# default: no reference qualifier, i.e. all identifiers returned by the identifier provider
|
71
|
-
# must be globally unique
|
64
|
+
# a Proc which receives RGen unresolved references and either a FragmentedModel or a ModelFragment.
|
65
|
+
# it should modify the unresolved references' targetIdentifiers to make them globally unique.
|
66
|
+
# the Proc is called for each fragment after it as been loaded and for the overall model.
|
67
|
+
# this can be used to qualify context specific references returned by the identifier provider.
|
68
|
+
# default: no reference qualifier
|
72
69
|
#
|
73
70
|
# :root_classes
|
74
71
|
# an Array of EClass objects representing classes which can be used on root level
|
@@ -98,6 +95,18 @@ class Language
|
|
98
95
|
# the Proc may also modify the element to remove information already part of the comment
|
99
96
|
# default: no comments
|
100
97
|
#
|
98
|
+
# :annotation_handler
|
99
|
+
# a Proc which will be invoked when a new element has been instantiated. receives
|
100
|
+
# the annotation as a string, the element and the environment to which the element has been added to.
|
101
|
+
# the environment may be nil. it may change the model or otherwise use the annotated information.
|
102
|
+
# if the element can take no annotation, it should return false, otherwise true.
|
103
|
+
# default: no handling of annotations
|
104
|
+
#
|
105
|
+
# :annotation_provider
|
106
|
+
# a Proc which receives an element and should return this element's annotation as a string or nil.
|
107
|
+
# the Proc may also modify the element to remove information already part of the annotation.
|
108
|
+
# default: no annotations
|
109
|
+
#
|
101
110
|
# :indent_string
|
102
111
|
# the string representing one indent, could be a tab or spaces
|
103
112
|
# default: 2 spaces
|
@@ -106,10 +115,22 @@ class Language
|
|
106
115
|
# a Proc which receives an EClass object and should return an RText command name
|
107
116
|
# default: class name
|
108
117
|
#
|
118
|
+
# :backward_ref_attribute
|
119
|
+
# a Proc which receives an EClass object and should return the name of this class's
|
120
|
+
# feature which is used to represent backward references (i.e. for following the backward
|
121
|
+
# reference, the user must click on a value of this attribute)
|
122
|
+
# a value of nil means that the command name is used to follow the backward reference
|
123
|
+
# default: nil (command name)
|
124
|
+
#
|
125
|
+
# :enable_generics
|
126
|
+
# if set to true, generics (<value>) are allowed, otherwise forbidden
|
127
|
+
# default: false
|
128
|
+
#
|
109
129
|
def initialize(root_epackage, options={})
|
110
130
|
@root_epackage = root_epackage
|
111
131
|
@feature_provider = options[:feature_provider] ||
|
112
|
-
proc { |c| RGen::Serializer::OppositeReferenceFilter.call(c.eAllStructuralFeatures)
|
132
|
+
proc { |c| RGen::Serializer::OppositeReferenceFilter.call(c.eAllStructuralFeatures).
|
133
|
+
reject{|f| f.derived} }
|
113
134
|
@unlabled_arguments = options[:unlabled_arguments]
|
114
135
|
@unquoted_arguments = options[:unquoted_arguments]
|
115
136
|
@argument_format_provider = options[:argument_format_provider]
|
@@ -118,31 +139,45 @@ class Language
|
|
118
139
|
setup_commands(root_epackage, command_name_provider)
|
119
140
|
@reference_regexp = options[:reference_regexp] || /\A\w*(\/\w*)+/
|
120
141
|
@identifier_provider = options[:identifier_provider] ||
|
121
|
-
proc { |element, context|
|
142
|
+
proc { |element, context, feature, index|
|
122
143
|
@qualified_name_provider ||= RGen::Serializer::QualifiedNameProvider.new(options)
|
123
|
-
|
144
|
+
name_attribute = options[:attribute_name] || "name"
|
145
|
+
if element.is_a?(RGen::MetamodelBuilder::MMProxy) || element.respond_to?(name_attribute)
|
146
|
+
@qualified_name_provider.identifier(element)
|
147
|
+
else
|
148
|
+
nil
|
149
|
+
end
|
124
150
|
}
|
125
|
-
@reference_qualifier = options[:reference_qualifier]
|
151
|
+
@reference_qualifier = options[:reference_qualifier] || proc{|urefs, model_or_fragment| }
|
126
152
|
@line_number_attribute = options[:line_number_attribute]
|
127
153
|
@file_name_attribute = options[:file_name_attribute]
|
128
154
|
@fragment_ref_attribute = options[:fragment_ref_attribute]
|
129
155
|
@comment_handler = options[:comment_handler]
|
130
156
|
@comment_provider = options[:comment_provider]
|
157
|
+
@annotation_handler = options[:annotation_handler]
|
158
|
+
@annotation_provider = options[:annotation_provider]
|
131
159
|
@indent_string = options[:indent_string] || " "
|
132
160
|
@per_type_identifier = options[:per_type_identifier]
|
161
|
+
@backward_ref_attribute = options[:backward_ref_attribute] || proc{|c| nil}
|
162
|
+
@generics_enabled = options[:enable_generics]
|
133
163
|
end
|
134
164
|
|
135
165
|
attr_reader :root_epackage
|
136
166
|
attr_reader :root_classes
|
137
167
|
attr_reader :reference_regexp
|
138
168
|
attr_reader :identifier_provider
|
169
|
+
attr_reader :reference_qualifier
|
139
170
|
attr_reader :line_number_attribute
|
140
171
|
attr_reader :file_name_attribute
|
141
172
|
attr_reader :fragment_ref_attribute
|
142
173
|
attr_reader :comment_handler
|
143
174
|
attr_reader :comment_provider
|
175
|
+
attr_reader :annotation_handler
|
176
|
+
attr_reader :annotation_provider
|
144
177
|
attr_reader :indent_string
|
145
178
|
attr_reader :per_type_identifier
|
179
|
+
attr_reader :backward_ref_attribute
|
180
|
+
attr_reader :generics_enabled
|
146
181
|
|
147
182
|
def class_by_command(command, context_class)
|
148
183
|
map = @class_by_command[context_class]
|
@@ -212,14 +247,6 @@ class Language
|
|
212
247
|
@fragment_ref_attribute && element.respond_to?(@fragment_ref_attribute) && element.send(@fragment_ref_attribute)
|
213
248
|
end
|
214
249
|
|
215
|
-
def qualify_reference(identifier, element)
|
216
|
-
if @reference_qualifier
|
217
|
-
@reference_qualifier.call(identifier, element)
|
218
|
-
else
|
219
|
-
identifier
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
250
|
private
|
224
251
|
|
225
252
|
def setup_commands(root_epackage, command_name_provider)
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'rtext/tokenizer'
|
2
|
+
require 'rtext/context_builder'
|
3
|
+
|
4
|
+
module RText
|
5
|
+
|
6
|
+
class LinkDetector
|
7
|
+
include RText::Tokenizer
|
8
|
+
|
9
|
+
def initialize(lang)
|
10
|
+
@lang = lang
|
11
|
+
end
|
12
|
+
|
13
|
+
LinkDesc = Struct.new(:element, :feature, :index, :backward, :value, :scol, :ecol)
|
14
|
+
|
15
|
+
# column numbers start at 1
|
16
|
+
def detect(lines, column)
|
17
|
+
# make sure there is a space at the end of the line
|
18
|
+
# otherwise an important attribute value (e.g. the local name) might be missing in the
|
19
|
+
# context model since the context builder removes it as "just being completed"
|
20
|
+
lines.last.concat(" ")
|
21
|
+
current_line = lines.last
|
22
|
+
context = ContextBuilder.build_context(@lang, lines, lines.last.size)
|
23
|
+
tokens = tokenize(lines.last, @lang.reference_regexp)
|
24
|
+
token = tokens.find{|t| t.scol && t.scol <= column && t.ecol && t.ecol >= column}
|
25
|
+
if context && context.element && token &&
|
26
|
+
[:reference, :integer, :string, :identifier].include?(token.kind)
|
27
|
+
if column > 1
|
28
|
+
line_prefix = lines.last[0..column-2]
|
29
|
+
else
|
30
|
+
line_prefix = ""
|
31
|
+
end
|
32
|
+
context2 = ContextBuilder.build_context(@lang, lines, line_prefix.size)
|
33
|
+
# for command prefixes nested below a lable, the context element and feature are
|
34
|
+
# the parent element and feature, this is not what's expected here
|
35
|
+
if line_prefix =~ /^\s*\w*$/
|
36
|
+
context2.element = nil
|
37
|
+
context2.feature = nil
|
38
|
+
end
|
39
|
+
bwref_attr = @lang.backward_ref_attribute.call(context.element.class.ecore)
|
40
|
+
if bwref_attr
|
41
|
+
is_backward = (context2.feature && context2.feature.name == bwref_attr)
|
42
|
+
else
|
43
|
+
is_backward = (token.kind == :identifier && line_prefix =~ /^\s*\w*$/)
|
44
|
+
end
|
45
|
+
is_backward = is_backward ? true : false
|
46
|
+
if context2.element && context2.feature
|
47
|
+
index = context2.element.getGenericAsArray(context2.feature.name).size
|
48
|
+
end
|
49
|
+
LinkDesc.new(context.element, context2.feature, index, is_backward, token.value, token.scol, token.ecol)
|
50
|
+
else
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module RText
|
4
|
+
|
5
|
+
module MessageHelper
|
6
|
+
|
7
|
+
def serialize_message(obj)
|
8
|
+
each_json_object_string(obj) do |s|
|
9
|
+
s.force_encoding("binary")
|
10
|
+
bytes = s.bytes.to_a
|
11
|
+
s.clear
|
12
|
+
bytes.each do |b|
|
13
|
+
if b >= 128 || b == 0x25 # %
|
14
|
+
s << "%#{b.to_s(16)}".force_encoding("binary")
|
15
|
+
else
|
16
|
+
s << b.chr("binary")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
# there are no non ascii-7-bit characters left
|
20
|
+
s.force_encoding("utf-8")
|
21
|
+
end
|
22
|
+
json = JSON(obj)
|
23
|
+
# the JSON method outputs data in UTF-8 encoding
|
24
|
+
# the RText protocol expects message lengths measured in bytes
|
25
|
+
# there shouldn't be any non-ascii-7-bit characters, though, so json.size would also be ok
|
26
|
+
"#{json.bytesize}#{json}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def extract_message(data)
|
30
|
+
# interpret input data as binary
|
31
|
+
data.force_encoding("binary")
|
32
|
+
obj = nil
|
33
|
+
if data =~ /^(\d+)\{/
|
34
|
+
length_length = $1.size
|
35
|
+
length = $1.to_i
|
36
|
+
if data.size >= length_length + length
|
37
|
+
data.slice!(0..(length_length-1))
|
38
|
+
json = data.slice!(0..length-1)
|
39
|
+
# there shouldn't be any non ascii-7-bit characters, transcode just to be sure
|
40
|
+
# encode from binary to utf-8 with :undef => :replace turns all non-ascii-7-bit bytes
|
41
|
+
# into the replacement character (\uFFFD)
|
42
|
+
json.encode!("utf-8", :undef => :replace)
|
43
|
+
obj = JSON(json)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
if obj
|
47
|
+
each_json_object_string(obj) do |s|
|
48
|
+
# change encoding back to binary
|
49
|
+
# there could still be replacement characters (\uFFFD), turn them into "?"
|
50
|
+
s.encode!("binary", :undef => :replace)
|
51
|
+
s.gsub!(/%[0-9a-fA-F][0-9a-fA-F]/){|m| m[1..2].to_i(16).chr("binary")}
|
52
|
+
s.force_encoding("binary")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
obj
|
56
|
+
end
|
57
|
+
|
58
|
+
def each_json_object_string(object, &block)
|
59
|
+
if object.is_a?(Hash)
|
60
|
+
object.each_pair do |k, v|
|
61
|
+
# don't call block for hash keys
|
62
|
+
each_json_object_string(v, &block)
|
63
|
+
end
|
64
|
+
elsif object.is_a?(Array)
|
65
|
+
object.each do |v|
|
66
|
+
each_json_object_string(v, &block)
|
67
|
+
end
|
68
|
+
elsif object.is_a?(String)
|
69
|
+
yield(object)
|
70
|
+
else
|
71
|
+
# ignore
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
data/lib/rtext/parser.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
+
require 'rtext/generic'
|
2
|
+
require 'rtext/tokenizer'
|
3
|
+
|
1
4
|
module RText
|
2
5
|
|
3
6
|
class Parser
|
7
|
+
include RText::Tokenizer
|
4
8
|
|
5
9
|
Problem = Struct.new(:message, :line)
|
6
10
|
|
@@ -14,7 +18,7 @@ class Parser
|
|
14
18
|
@problems = options[:problems] || []
|
15
19
|
@non_consume_count = 0
|
16
20
|
@consume_problem_reported = false
|
17
|
-
@tokens = tokenize(str, @reference_regexp)
|
21
|
+
@tokens = tokenize(str, @reference_regexp, :on_command_token => options[:on_command_token])
|
18
22
|
@last_line = @tokens.last && @tokens.last.line
|
19
23
|
#@debug = true
|
20
24
|
begin
|
@@ -29,6 +33,7 @@ class Parser
|
|
29
33
|
def parse_statement(is_root=false, allow_unassociated_comment=false)
|
30
34
|
comments = []
|
31
35
|
comment = parse_comment
|
36
|
+
annotation = parse_annotation
|
32
37
|
if (next_token && next_token == :identifier) || !allow_unassociated_comment
|
33
38
|
comments << [ comment, :above] if comment
|
34
39
|
command = consume(:identifier)
|
@@ -43,7 +48,7 @@ class Parser
|
|
43
48
|
eol_comment = parse_eol_comment
|
44
49
|
comments << [ eol_comment, :eol ] if eol_comment
|
45
50
|
consume(:newline)
|
46
|
-
@asc_visitor.call(command, arg_list, element_list, comments, is_root)
|
51
|
+
@asc_visitor.call(command, arg_list, element_list, comments, annotation, is_root)
|
47
52
|
else
|
48
53
|
discard_until(:newline)
|
49
54
|
nil
|
@@ -51,7 +56,7 @@ class Parser
|
|
51
56
|
elsif comment
|
52
57
|
# if there is no statement, the comment is non-optional
|
53
58
|
comments << [ comment, :unassociated ]
|
54
|
-
@asc_visitor.call(nil, nil, nil, comments, nil)
|
59
|
+
@asc_visitor.call(nil, nil, nil, comments, nil, nil)
|
55
60
|
nil
|
56
61
|
else
|
57
62
|
# die expecting an identifier (next token is not an identifier)
|
@@ -71,6 +76,16 @@ class Parser
|
|
71
76
|
result
|
72
77
|
end
|
73
78
|
|
79
|
+
def parse_annotation
|
80
|
+
result = nil
|
81
|
+
while next_token == :annotation
|
82
|
+
result ||= []
|
83
|
+
result << consume(:annotation)
|
84
|
+
consume(:newline)
|
85
|
+
end
|
86
|
+
result
|
87
|
+
end
|
88
|
+
|
74
89
|
def parse_eol_comment
|
75
90
|
if next_token == :comment
|
76
91
|
consume(:comment)
|
@@ -169,7 +184,7 @@ class Parser
|
|
169
184
|
result
|
170
185
|
end
|
171
186
|
|
172
|
-
AnyValue = [:identifier, :integer, :float, :string, :boolean, :reference]
|
187
|
+
AnyValue = [:identifier, :integer, :float, :string, :boolean, :reference, :generic]
|
173
188
|
|
174
189
|
def parse_value
|
175
190
|
consume(*AnyValue)
|
@@ -238,70 +253,6 @@ class Parser
|
|
238
253
|
end
|
239
254
|
end
|
240
255
|
|
241
|
-
Token = Struct.new(:kind, :value, :line)
|
242
|
-
|
243
|
-
def tokenize(str, reference_regexp)
|
244
|
-
result = []
|
245
|
-
str.split(/\r?\n/).each_with_index do |str, idx|
|
246
|
-
idx += 1
|
247
|
-
if str =~ /^\s*#(.*)/
|
248
|
-
result << Token.new(:comment, $1, idx)
|
249
|
-
else
|
250
|
-
until str.empty?
|
251
|
-
case str
|
252
|
-
when reference_regexp
|
253
|
-
str = $'
|
254
|
-
result << Token.new(:reference, $&, idx)
|
255
|
-
when /\A[-+]?\d+\.\d+(?:e[+-]\d+)?\b/
|
256
|
-
str = $'
|
257
|
-
result << Token.new(:float, $&.to_f, idx)
|
258
|
-
when /\A0[xX][0-9a-fA-F]+\b/
|
259
|
-
str = $'
|
260
|
-
result << Token.new(:integer, $&.to_i(16), idx)
|
261
|
-
when /\A[-+]?\d+\b/
|
262
|
-
str = $'
|
263
|
-
result << Token.new(:integer, $&.to_i, idx)
|
264
|
-
when /\A"((?:[^"\\]|\\.)*)"/
|
265
|
-
str = $'
|
266
|
-
result << Token.new(:string, $1.
|
267
|
-
gsub('\\\\','\\').
|
268
|
-
gsub('\\"','"').
|
269
|
-
gsub('\\n',"\n").
|
270
|
-
gsub('\\r',"\r").
|
271
|
-
gsub('\\t',"\t").
|
272
|
-
gsub('\\f',"\f").
|
273
|
-
gsub('\\b',"\b"), idx)
|
274
|
-
when /\A(?:true|false)\b/
|
275
|
-
str = $'
|
276
|
-
result << Token.new(:boolean, $& == "true", idx)
|
277
|
-
when /\A([a-zA-Z_]\w*)\b(?:\s*:)?/
|
278
|
-
str = $'
|
279
|
-
if $&[-1] == ?:
|
280
|
-
result << Token.new(:label, $1, idx)
|
281
|
-
else
|
282
|
-
result << Token.new(:identifier, $&, idx)
|
283
|
-
end
|
284
|
-
when /\A[\{\}\[\]:,]/
|
285
|
-
str = $'
|
286
|
-
result << Token.new($&, nil, idx)
|
287
|
-
when /\A#(.*)/
|
288
|
-
str = ""
|
289
|
-
result << Token.new(:comment, $1, idx)
|
290
|
-
when /\A\s+/
|
291
|
-
str = $'
|
292
|
-
# ignore
|
293
|
-
when /\A\S+/
|
294
|
-
str = $'
|
295
|
-
result << Token.new(:error, $&, idx)
|
296
|
-
end
|
297
|
-
end
|
298
|
-
end
|
299
|
-
result << Token.new(:newline, nil, idx) \
|
300
|
-
unless result.empty? || result.last.kind == :newline
|
301
|
-
end
|
302
|
-
result
|
303
|
-
end
|
304
|
-
|
305
256
|
end
|
306
257
|
|
307
258
|
end
|