textmate_grammar 0.0.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/lib/textmate_grammar/generated/grammar.rb +32 -0
- data/lib/textmate_grammar/generated/rule.rb +144 -0
- data/lib/textmate_grammar/grammar.rb +670 -0
- data/lib/textmate_grammar/grammar_plugin.rb +189 -0
- data/lib/textmate_grammar/import_patterns.rb +14 -0
- data/lib/textmate_grammar/linters/flat_includes.rb +32 -0
- data/lib/textmate_grammar/linters/includes_then_tag_as.rb +48 -0
- data/lib/textmate_grammar/linters/standard_naming.rb +226 -0
- data/lib/textmate_grammar/linters/start_match_empty.rb +49 -0
- data/lib/textmate_grammar/linters/tests.rb +19 -0
- data/lib/textmate_grammar/linters/unused_unresolved.rb +9 -0
- data/lib/textmate_grammar/pattern_extensions/look_ahead_for.rb +32 -0
- data/lib/textmate_grammar/pattern_extensions/look_ahead_to_avoid.rb +31 -0
- data/lib/textmate_grammar/pattern_extensions/look_behind_for.rb +31 -0
- data/lib/textmate_grammar/pattern_extensions/look_behind_to_avoid.rb +31 -0
- data/lib/textmate_grammar/pattern_extensions/lookaround_pattern.rb +169 -0
- data/lib/textmate_grammar/pattern_extensions/match_result_of.rb +67 -0
- data/lib/textmate_grammar/pattern_extensions/maybe.rb +50 -0
- data/lib/textmate_grammar/pattern_extensions/one_of.rb +107 -0
- data/lib/textmate_grammar/pattern_extensions/one_or_more_of.rb +42 -0
- data/lib/textmate_grammar/pattern_extensions/or_pattern.rb +55 -0
- data/lib/textmate_grammar/pattern_extensions/placeholder.rb +102 -0
- data/lib/textmate_grammar/pattern_extensions/recursively_match.rb +76 -0
- data/lib/textmate_grammar/pattern_extensions/zero_or_more_of.rb +50 -0
- data/lib/textmate_grammar/pattern_variations/base_pattern.rb +870 -0
- data/lib/textmate_grammar/pattern_variations/legacy_pattern.rb +61 -0
- data/lib/textmate_grammar/pattern_variations/pattern.rb +9 -0
- data/lib/textmate_grammar/pattern_variations/pattern_range.rb +233 -0
- data/lib/textmate_grammar/pattern_variations/repeatable_pattern.rb +204 -0
- data/lib/textmate_grammar/regex_operator.rb +182 -0
- data/lib/textmate_grammar/regex_operators/alternation.rb +24 -0
- data/lib/textmate_grammar/regex_operators/concat.rb +23 -0
- data/lib/textmate_grammar/stdlib/common.rb +20 -0
- data/lib/textmate_grammar/tokens.rb +110 -0
- data/lib/textmate_grammar/transforms/add_ending.rb +25 -0
- data/lib/textmate_grammar/transforms/bailout.rb +92 -0
- data/lib/textmate_grammar/transforms/fix_repeated_tag_as.rb +75 -0
- data/lib/textmate_grammar/transforms/resolve_placeholders.rb +121 -0
- data/lib/textmate_grammar/util.rb +198 -0
- data/lib/textmate_grammar.rb +4 -0
- metadata +85 -0
@@ -0,0 +1,870 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Provides a base class to simplify the writing of complex regular expressions rules
|
5
|
+
# This class completely handles capture numbers and provides convenience methods for
|
6
|
+
# many common Regexp operations
|
7
|
+
#
|
8
|
+
# @note Users should not normally directly instantiate this class
|
9
|
+
#
|
10
|
+
class PatternBase
|
11
|
+
# @return [PatternBase] The next pattern in the linked list of patterns
|
12
|
+
attr_accessor :next_pattern
|
13
|
+
# @return [String,PatternBase] The pattern to match
|
14
|
+
attr_accessor :match
|
15
|
+
# @return [Hash] The processed arguments
|
16
|
+
attr_accessor :arguments
|
17
|
+
# @return [Hash] The original arguments passed into initialize
|
18
|
+
attr_accessor :original_arguments
|
19
|
+
|
20
|
+
#
|
21
|
+
# does @arguments contain any attributes that require this pattern be captured?
|
22
|
+
#
|
23
|
+
# @return [Boolean] if this PatternBase needs to capture
|
24
|
+
#
|
25
|
+
def needs_to_capture?
|
26
|
+
capturing_attributes = [
|
27
|
+
:tag_as,
|
28
|
+
:reference,
|
29
|
+
:includes,
|
30
|
+
]
|
31
|
+
puts @match.class unless @arguments.is_a? Hash
|
32
|
+
|
33
|
+
!(@arguments.keys & capturing_attributes).empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Can the capture be optimized out
|
38
|
+
#
|
39
|
+
# When the pattern has nothing after it then its capture can instead become
|
40
|
+
# capture group 0
|
41
|
+
#
|
42
|
+
# @return [Boolean] can this capture become capture group 0
|
43
|
+
#
|
44
|
+
def optimize_outer_group?
|
45
|
+
needs_to_capture? and @next_pattern.nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Appends pattern to the linked list of patterns
|
50
|
+
#
|
51
|
+
# @param [PatternBase] pattern the pattern to append
|
52
|
+
#
|
53
|
+
# @return [self]
|
54
|
+
#
|
55
|
+
# @see insert
|
56
|
+
#
|
57
|
+
def insert!(pattern)
|
58
|
+
last = self
|
59
|
+
last = last.next_pattern while last.next_pattern
|
60
|
+
last.next_pattern = pattern
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# Append pattern to a copy of the linked list of patterns
|
66
|
+
#
|
67
|
+
# @param [PatternBase] pattern the pattern to append
|
68
|
+
#
|
69
|
+
# @return [PatternBase] a copy of self with pattern appended
|
70
|
+
#
|
71
|
+
def insert(pattern)
|
72
|
+
new_pattern = __deep_clone__
|
73
|
+
new_pattern.insert!(pattern).freeze
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Adds a capture group if needed
|
78
|
+
#
|
79
|
+
# @param [String] regex_as_string the pattern as a string
|
80
|
+
#
|
81
|
+
# @return [String] the pattern, potentially with a capture group
|
82
|
+
#
|
83
|
+
def add_capture_group_if_needed(regex_as_string)
|
84
|
+
regex_as_string = "(#{regex_as_string})" if needs_to_capture?
|
85
|
+
regex_as_string
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# Uses a block to transform all Patterns in the list
|
90
|
+
#
|
91
|
+
# @param [Boolean] map_includes should include patterns be mapped?
|
92
|
+
# @yield [self] invokes the block with self for modification
|
93
|
+
#
|
94
|
+
# @return [self]
|
95
|
+
#
|
96
|
+
def map!(map_includes = false, &block)
|
97
|
+
yield self
|
98
|
+
if @match.is_a? PatternBase
|
99
|
+
if @match.frozen?
|
100
|
+
puts "frozen @match"
|
101
|
+
puts @match.inspect
|
102
|
+
end
|
103
|
+
@match = @match.map!(map_includes, &block)
|
104
|
+
end
|
105
|
+
if @next_pattern.is_a? PatternBase
|
106
|
+
if @next_pattern.frozen?
|
107
|
+
puts "frozen @next_pattern"
|
108
|
+
puts @next_pattern.inspect
|
109
|
+
end
|
110
|
+
@next_pattern = @next_pattern.map!(map_includes, &block)
|
111
|
+
end
|
112
|
+
map_includes!(&block) if map_includes
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
#
|
117
|
+
# (see #map!)
|
118
|
+
#
|
119
|
+
# @return [PatternBase] a transformed copy of self
|
120
|
+
#
|
121
|
+
def map(map_includes = false, &block)
|
122
|
+
__deep_clone__.map!(map_includes, &block).freeze
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Call the block for each pattern in the list
|
127
|
+
#
|
128
|
+
# @param [Boolean] each_includes should include patterns be called?
|
129
|
+
# @yield [self] invokes the block with self
|
130
|
+
#
|
131
|
+
# @return [void]
|
132
|
+
#
|
133
|
+
def each(each_includes = false, &block)
|
134
|
+
yield self
|
135
|
+
@match.each(each_includes, &block) if @match.is_a? PatternBase
|
136
|
+
@next_pattern.each(each_includes, &block) if @next_pattern.is_a? PatternBase
|
137
|
+
|
138
|
+
return unless each_includes
|
139
|
+
return unless @arguments[:includes].is_a? Array
|
140
|
+
|
141
|
+
@arguments[:includes].each do |s|
|
142
|
+
next unless s.is_a? Pattern
|
143
|
+
|
144
|
+
s.each(true, &block)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# Uses a block to transform all Patterns in all includes
|
150
|
+
# @api private
|
151
|
+
# @note only for use by map!
|
152
|
+
#
|
153
|
+
# @yield [self] invokes the block with the includes for modification
|
154
|
+
#
|
155
|
+
# @return [void]
|
156
|
+
#
|
157
|
+
def map_includes!(&block)
|
158
|
+
return unless @arguments[:includes].is_a? Array
|
159
|
+
|
160
|
+
@arguments[:includes].map! do |s|
|
161
|
+
if s.is_a? PatternBase
|
162
|
+
if s.frozen?
|
163
|
+
puts "frozen s"
|
164
|
+
puts s.inspect
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
next s.map!(true, &block) if s.is_a? PatternBase
|
169
|
+
|
170
|
+
next s
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
#
|
175
|
+
# Uses block to recursively transform includes
|
176
|
+
#
|
177
|
+
# @yield [PatternBase,Symbol,Regexp,String] invokes the block with each include to transform
|
178
|
+
#
|
179
|
+
# @return [PatternBase] a copy of self with transformed includes
|
180
|
+
#
|
181
|
+
def transform_includes(&block)
|
182
|
+
map(true) do |s|
|
183
|
+
s.arguments[:includes].map!(&block) if s.arguments[:includes].is_a? Array
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
#
|
188
|
+
# Uses block to recursively transform tag_as
|
189
|
+
#
|
190
|
+
# @yield [String] Invokes the block to with each tag_as to transform
|
191
|
+
#
|
192
|
+
# @return [PatternBase] a copy of self with transformed tag_as
|
193
|
+
#
|
194
|
+
def transform_tag_as(&block)
|
195
|
+
__deep_clone__.map! do |s|
|
196
|
+
s.arguments[:tag_as] = block.call(s.arguments[:tag_as]) if s.arguments[:tag_as]
|
197
|
+
next unless s.arguments[:includes].is_a?(Array)
|
198
|
+
|
199
|
+
s.arguments[:includes].map! do |i|
|
200
|
+
next i unless i.is_a? PatternBase
|
201
|
+
|
202
|
+
i.transform_tag_as(&block)
|
203
|
+
end
|
204
|
+
end.freeze
|
205
|
+
end
|
206
|
+
|
207
|
+
#
|
208
|
+
# Construct a new pattern
|
209
|
+
#
|
210
|
+
# @overload initialize(pattern)
|
211
|
+
# matches an exact pattern
|
212
|
+
# @param pattern [PatternBase, Regexp, String] the pattern to match
|
213
|
+
# @overload initialize(opts)
|
214
|
+
# @param opts [Hash] options
|
215
|
+
# @option opts [PatternBase, Regexp, String] :match the pattern to match
|
216
|
+
# @option opts [String] :tag_as what to tag this pattern as
|
217
|
+
# @option opts [Array<PatternBase, Symbol>] :includes pattern includes
|
218
|
+
# @option opts [String] :reference a name for this pattern can be referred to in
|
219
|
+
# earlier or later parts of the pattern list, or in tag_as
|
220
|
+
# @option opts [Array<String>] :should_fully_match string that this pattern should
|
221
|
+
# fully match
|
222
|
+
# @option opts [Array<String>] :should_partial_match string that this pattern should
|
223
|
+
# partially match
|
224
|
+
# @option opts [Array<String>] :should_not_fully_match string that this pattern should
|
225
|
+
# not fully match
|
226
|
+
# @option opts [Array<String>] :should_not_partial_match string that this pattern should
|
227
|
+
# not partially match
|
228
|
+
# @option opts [Enumerator, Integer] :at_most match up to N times, nil to match any
|
229
|
+
# number of times
|
230
|
+
# @option opts [Enumerator, Integer] :at_least match no fewer than N times, nil to
|
231
|
+
# match any number of times
|
232
|
+
# @option opts [Enumerator, Integer] :how_many_times match exactly N times
|
233
|
+
# @option opts [Array<String>] :word_cannot_be_any_of list of wordlike string that
|
234
|
+
# the pattern should not match (this is a qualifier not a unit test)
|
235
|
+
# @option opts [Boolean] :dont_back_track? can this pattern backtrack
|
236
|
+
# @note Plugins may provide additional options
|
237
|
+
# @note all options except :match are optional
|
238
|
+
# @overload initialize(opts, deep_clone, original)
|
239
|
+
# makes a copy of PatternBase
|
240
|
+
# @param opts [Hash] the original patterns @arguments with match
|
241
|
+
# @param deep_clone [:deep_clone] identifies as a deep_clone construction
|
242
|
+
# @param original [Hash] the original patterns @original_arguments
|
243
|
+
# @api private
|
244
|
+
# @note this should only be called by __deep_clone__, however subclasses must be
|
245
|
+
# able to accept this form
|
246
|
+
#
|
247
|
+
def initialize(*arguments)
|
248
|
+
if arguments.length > 1 && arguments[1] == :deep_clone
|
249
|
+
@arguments = arguments[0]
|
250
|
+
@match = @arguments[:match]
|
251
|
+
@arguments.delete(:match)
|
252
|
+
@original_arguments = arguments[2]
|
253
|
+
@next_pattern = nil
|
254
|
+
return
|
255
|
+
end
|
256
|
+
|
257
|
+
if arguments.length > 1
|
258
|
+
# PatternBase was likely constructed like `PatternBase.new(/foo/, option: bar)`
|
259
|
+
puts "PatternBase#new() expects a single Regexp, String, or Hash"
|
260
|
+
puts "PatternBase#new() was provided with multiple arguments"
|
261
|
+
puts "arguments:"
|
262
|
+
puts arguments
|
263
|
+
raise "See error above"
|
264
|
+
end
|
265
|
+
@next_pattern = nil
|
266
|
+
arg1 = arguments[0]
|
267
|
+
arg1 = {match: arg1} unless arg1.is_a? Hash
|
268
|
+
@original_arguments = arg1.clone
|
269
|
+
if arg1[:match].is_a? String
|
270
|
+
arg1[:match] = Regexp.escape(arg1[:match]).gsub("/", "\\/")
|
271
|
+
@match = arg1[:match]
|
272
|
+
elsif arg1[:match].is_a? Regexp
|
273
|
+
raise_if_regex_has_capture_group arg1[:match]
|
274
|
+
@match = arg1[:match].inspect[1..-2] # convert to string and remove the slashes
|
275
|
+
elsif arg1[:match].is_a? PatternBase
|
276
|
+
@match = arg1[:match]
|
277
|
+
else
|
278
|
+
puts <<-HEREDOC.remove_indent
|
279
|
+
Pattern.new() must be constructed with a String, Regexp, or Pattern
|
280
|
+
Provided arguments: #{@original_arguments}
|
281
|
+
HEREDOC
|
282
|
+
raise "See error above"
|
283
|
+
end
|
284
|
+
# ensure that includes is either nil or a flat array
|
285
|
+
if arg1[:includes]
|
286
|
+
arg1[:includes] = [arg1[:includes]] unless arg1[:includes].is_a? Array
|
287
|
+
arg1[:includes] = arg1[:includes].flatten
|
288
|
+
end
|
289
|
+
arg1.delete(:match)
|
290
|
+
@arguments = arg1
|
291
|
+
end
|
292
|
+
|
293
|
+
# attempts to provide a memorable name for a pattern
|
294
|
+
# @return [String]
|
295
|
+
def name
|
296
|
+
return @arguments[:reference] unless @arguments[:reference].nil?
|
297
|
+
return @arguments[:tag_as] unless @arguments[:tag_as].nil?
|
298
|
+
|
299
|
+
to_s
|
300
|
+
end
|
301
|
+
|
302
|
+
#
|
303
|
+
# converts a PatternBase to a Hash representing a textmate rule
|
304
|
+
#
|
305
|
+
# @return [Hash] The pattern as a textmate grammar rule
|
306
|
+
#
|
307
|
+
def to_tag
|
308
|
+
output = {
|
309
|
+
match: evaluate,
|
310
|
+
}
|
311
|
+
|
312
|
+
output[:captures] = convert_group_attributes_to_captures(collect_group_attributes)
|
313
|
+
if optimize_outer_group?
|
314
|
+
# optimize captures by removing outermost
|
315
|
+
output[:match] = output[:match][1..-2]
|
316
|
+
output[:name] = output[:captures]["0"][:name]
|
317
|
+
output[:captures]["0"].delete(:name)
|
318
|
+
output[:captures].reject! { |_, v| !v || v.empty? }
|
319
|
+
end
|
320
|
+
output.reject! { |_, v| !v || v.empty? }
|
321
|
+
output
|
322
|
+
end
|
323
|
+
|
324
|
+
#
|
325
|
+
# evaluates the pattern into a string suitable for inserting into a
|
326
|
+
# grammar or constructing a Regexp.
|
327
|
+
#
|
328
|
+
# @param [Hash] groups if groups is nil consider this PatternBase to be the top_level
|
329
|
+
# when a pattern is top_level, group numbers and back references are relative
|
330
|
+
# to that pattern
|
331
|
+
#
|
332
|
+
# @return [String] the complete pattern
|
333
|
+
#
|
334
|
+
def evaluate(groups = nil, fixup_refereces: false)
|
335
|
+
top_level = groups.nil?
|
336
|
+
groups = collect_group_attributes if top_level
|
337
|
+
evaluate_array = ['']
|
338
|
+
|
339
|
+
pat = self
|
340
|
+
while pat.is_a? PatternBase
|
341
|
+
evaluate_array << pat.evaluate_operator
|
342
|
+
evaluate_array << pat.do_evaluate_self(groups)
|
343
|
+
pat = pat.next_pattern
|
344
|
+
end
|
345
|
+
|
346
|
+
self_evaluate = RegexOperator.evaluate(evaluate_array)
|
347
|
+
self_evaluate = fixup_regex_references(groups, self_evaluate) if top_level || fixup_refereces
|
348
|
+
self_evaluate
|
349
|
+
end
|
350
|
+
|
351
|
+
#
|
352
|
+
# converts a pattern to a Regexp
|
353
|
+
#
|
354
|
+
# @param [Hash] groups if groups is nil consider this PatternBase to be the top_level
|
355
|
+
# when a pattern is top_level, group numbers and back references are relative
|
356
|
+
# to that pattern
|
357
|
+
#
|
358
|
+
# @return [Regexp] the pattern as a Regexp
|
359
|
+
#
|
360
|
+
def to_r(groups = nil)
|
361
|
+
with_no_warnings { Regexp.new(evaluate(groups)) }
|
362
|
+
end
|
363
|
+
|
364
|
+
#
|
365
|
+
# Displays the PatternBase as you would write it in code
|
366
|
+
#
|
367
|
+
# @param [Integer] depth the current nesting depth
|
368
|
+
# @param [Boolean] top_level is this a top level pattern or is it being chained
|
369
|
+
#
|
370
|
+
# @return [String] The pattern as a string
|
371
|
+
#
|
372
|
+
def to_s(depth = 0, top_level = true)
|
373
|
+
# TODO: make this method easier to understand
|
374
|
+
|
375
|
+
# rubocop:disable Metrics/LineLength
|
376
|
+
begin
|
377
|
+
plugins = Grammar.plugins
|
378
|
+
plugins.reject! { |p| (@original_arguments.keys & p.class.options).empty? }
|
379
|
+
|
380
|
+
regex_as_string =
|
381
|
+
case @original_arguments[:match]
|
382
|
+
when PatternBase then @original_arguments[:match].to_s(depth + 2, true)
|
383
|
+
when Regexp then @original_arguments[:match].inspect
|
384
|
+
when String then "/" + Regexp.escape(@original_arguments[:match]) + "/"
|
385
|
+
end
|
386
|
+
indent = " " * depth
|
387
|
+
output = indent + do_get_to_s_name(top_level)
|
388
|
+
# basic pattern information
|
389
|
+
output += "\n#{indent} match: " + regex_as_string.lstrip
|
390
|
+
output += ",\n#{indent} tag_as: \"" + @arguments[:tag_as] + '"' if @arguments[:tag_as]
|
391
|
+
output += ",\n#{indent} reference: \"" + @arguments[:reference] + '"' if @arguments[:reference]
|
392
|
+
# unit tests
|
393
|
+
output += ",\n#{indent} should_fully_match: " + @arguments[:should_fully_match].to_s if @arguments[:should_fully_match]
|
394
|
+
output += ",\n#{indent} should_not_fully_match: " + @arguments[:should_not_fully_match].to_s if @arguments[:should_not_fully_match]
|
395
|
+
output += ",\n#{indent} should_partially_match: " + @arguments[:should_partially_match].to_s if @arguments[:should_partially_match]
|
396
|
+
output += ",\n#{indent} should_not_partially_match: " + @arguments[:should_not_partially_match].to_s if @arguments[:should_not_partially_match]
|
397
|
+
|
398
|
+
output += ",\n#{indent} includes: " + @arguments[:includes].to_s if @arguments[:includes]
|
399
|
+
# add any linter/transform configurations
|
400
|
+
plugins.each { |p| output += p.display_options(indent + " ", @original_arguments) }
|
401
|
+
# subclass, ending and recursive
|
402
|
+
output += do_add_attributes(indent)
|
403
|
+
output += ",\n#{indent})"
|
404
|
+
output += @next_pattern.to_s(depth, false).lstrip if @next_pattern
|
405
|
+
output
|
406
|
+
rescue
|
407
|
+
return @original_arguments.to_s
|
408
|
+
end
|
409
|
+
# rubocop:enable Metrics/LineLength
|
410
|
+
end
|
411
|
+
|
412
|
+
#
|
413
|
+
# Runs the unit tests, recursively
|
414
|
+
#
|
415
|
+
# @return [Boolean] If all test passed return true, otherwise false
|
416
|
+
#
|
417
|
+
def run_tests
|
418
|
+
pass = [
|
419
|
+
run_self_tests,
|
420
|
+
]
|
421
|
+
|
422
|
+
# run related unit tests
|
423
|
+
pass << @match.run_tests if @match.is_a? PatternBase
|
424
|
+
pass << @next_pattern.run_tests if @next_pattern.is_a? PatternBase
|
425
|
+
if @arguments[:includes].is_a? Array
|
426
|
+
@arguments[:includes]&.each { |inc| pass << inc.run_tests if inc.is_a? PatternBase }
|
427
|
+
elsif @arguments[:includes].is_a? PatternBase
|
428
|
+
pass << @arguments[:includes].run_tests
|
429
|
+
end
|
430
|
+
pass.none?(&:!)
|
431
|
+
end
|
432
|
+
|
433
|
+
#
|
434
|
+
# Runs the unit tests for self
|
435
|
+
#
|
436
|
+
# @return [Boolean] If all test passed return true, otherwise false
|
437
|
+
#
|
438
|
+
def run_self_tests
|
439
|
+
pass = [true]
|
440
|
+
|
441
|
+
# some patterns are not able to be evaluated
|
442
|
+
# do not attempt to unless required
|
443
|
+
return true unless [
|
444
|
+
:should_fully_match,
|
445
|
+
:should_not_fully_match,
|
446
|
+
:should_partially_match,
|
447
|
+
:should_not_partially_match,
|
448
|
+
].any? { |k| @arguments.include? k }
|
449
|
+
|
450
|
+
copy = __deep_clone_self__
|
451
|
+
test_regex = copy.to_r
|
452
|
+
test_fully_regex = wrap_with_anchors(copy).to_r
|
453
|
+
|
454
|
+
warn = lambda do |symbol|
|
455
|
+
puts [
|
456
|
+
"",
|
457
|
+
"When testing the pattern #{test_regex.inspect}. The unit test for #{symbol} failed.",
|
458
|
+
"The unit test has the following patterns:",
|
459
|
+
"#{@arguments[symbol].to_yaml}",
|
460
|
+
"The Failing pattern is below:",
|
461
|
+
"#{self}",
|
462
|
+
].join("\n")
|
463
|
+
end
|
464
|
+
if @arguments[:should_fully_match].is_a? Array
|
465
|
+
unless @arguments[:should_fully_match].all? { |test| test =~ test_fully_regex }
|
466
|
+
warn.call :should_fully_match
|
467
|
+
pass << false
|
468
|
+
end
|
469
|
+
end
|
470
|
+
if @arguments[:should_not_fully_match].is_a? Array
|
471
|
+
unless @arguments[:should_not_fully_match].none? { |test| test =~ test_fully_regex }
|
472
|
+
warn.call :should_not_fully_match
|
473
|
+
pass << false
|
474
|
+
end
|
475
|
+
end
|
476
|
+
if @arguments[:should_partially_match].is_a? Array
|
477
|
+
unless @arguments[:should_partially_match].all? { |test| test =~ test_regex }
|
478
|
+
warn.call :should_partially_match
|
479
|
+
pass << false
|
480
|
+
end
|
481
|
+
end
|
482
|
+
if @arguments[:should_not_partially_match].is_a? Array
|
483
|
+
unless @arguments[:should_not_partially_match].none? { |test| test =~ test_regex }
|
484
|
+
warn.call :should_not_partially_match
|
485
|
+
pass << false
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
pass.none?(&:!)
|
490
|
+
end
|
491
|
+
|
492
|
+
#
|
493
|
+
# To aid in Linters all Patterns support start_pattern which return the pattern
|
494
|
+
# for initial match, for a single match pattern that is itself
|
495
|
+
#
|
496
|
+
# @return [self] This pattern
|
497
|
+
#
|
498
|
+
def start_pattern
|
499
|
+
self
|
500
|
+
end
|
501
|
+
|
502
|
+
#
|
503
|
+
# Gets the patterns Hashcode
|
504
|
+
#
|
505
|
+
# @return [Integer] the Hashcode
|
506
|
+
#
|
507
|
+
def hash
|
508
|
+
# TODO: find a better hash code
|
509
|
+
# PatternBase.new("abc") == PatternBase.new(PatternBase.new("abc"))
|
510
|
+
# but PatternBase.new("abc").hash != PatternBase.new(PatternBase.new("abc")).hash
|
511
|
+
@match.hash
|
512
|
+
end
|
513
|
+
|
514
|
+
#
|
515
|
+
# Checks for equality
|
516
|
+
# A pattern is considered equal to another pattern if the result of tag_as is equivalent
|
517
|
+
#
|
518
|
+
# @param [PatternBase] other the pattern to compare
|
519
|
+
#
|
520
|
+
# @return [Boolean] true if other is a PatternBase and to_tag is equivalent, false otherwise
|
521
|
+
#
|
522
|
+
def eql?(other)
|
523
|
+
return false unless other.is_a? PatternBase
|
524
|
+
|
525
|
+
to_tag == other.to_tag
|
526
|
+
end
|
527
|
+
|
528
|
+
# (see #eql?)
|
529
|
+
def ==(other)
|
530
|
+
eql? other
|
531
|
+
end
|
532
|
+
|
533
|
+
#
|
534
|
+
# Construct a new pattern and append to the end
|
535
|
+
#
|
536
|
+
# @param [PatternBase] pattern options (see #initialize for options)
|
537
|
+
# @see #initialize
|
538
|
+
#
|
539
|
+
# @return [PatternBase] a copy of self with a pattern inserted
|
540
|
+
#
|
541
|
+
def then(pattern)
|
542
|
+
unless pattern.is_a?(PatternBase) && pattern.next_pattern.nil?
|
543
|
+
pattern = Pattern.new(pattern)
|
544
|
+
end
|
545
|
+
insert(pattern)
|
546
|
+
end
|
547
|
+
# other methods added by subclasses
|
548
|
+
|
549
|
+
#
|
550
|
+
# evaluates @match
|
551
|
+
# @note optionally override when inheriting
|
552
|
+
# @note by default this optionally adds a capture group
|
553
|
+
#
|
554
|
+
# @param [Hash] groups group attributes
|
555
|
+
#
|
556
|
+
# @return [String] the result of evaluating @match
|
557
|
+
#
|
558
|
+
def do_evaluate_self(groups)
|
559
|
+
match = @match
|
560
|
+
match = match.evaluate(groups) if match.is_a? PatternBase
|
561
|
+
add_capture_group_if_needed(match)
|
562
|
+
end
|
563
|
+
|
564
|
+
#
|
565
|
+
# Returns the operator to use when evaluating
|
566
|
+
#
|
567
|
+
# @return [RegexOperator] the operator to use
|
568
|
+
#
|
569
|
+
def evaluate_operator
|
570
|
+
ConcatOperator.new
|
571
|
+
end
|
572
|
+
|
573
|
+
#
|
574
|
+
# return a string of any additional attributes that need to be added to the #to_s output
|
575
|
+
# indent is a string with the amount of space the parent block is indented, attributes
|
576
|
+
# are indented 2 more spaces
|
577
|
+
# called by #to_s
|
578
|
+
#
|
579
|
+
# @param [String] indent the spaces to indent with
|
580
|
+
#
|
581
|
+
# @return [String] the attributes to add
|
582
|
+
#
|
583
|
+
def do_add_attributes(indent) # rubocop:disable Lint/UnusedMethodArgument
|
584
|
+
""
|
585
|
+
end
|
586
|
+
|
587
|
+
#
|
588
|
+
# What is the name of the method that the user would call
|
589
|
+
# top_level is if a freestanding or chaining function is called
|
590
|
+
# called by #to_s
|
591
|
+
#
|
592
|
+
# @param [Boolean] top_level is this top_level or chained
|
593
|
+
#
|
594
|
+
# @return [String] the name of the method
|
595
|
+
#
|
596
|
+
def do_get_to_s_name(top_level)
|
597
|
+
top_level ? "Pattern.new(" : ".then("
|
598
|
+
end
|
599
|
+
|
600
|
+
# (see string_single_entity)
|
601
|
+
def single_entity?
|
602
|
+
string_single_entity? evaluate
|
603
|
+
end
|
604
|
+
|
605
|
+
# does this pattern contain no capturing groups
|
606
|
+
def groupless?
|
607
|
+
collect_group_attributes == []
|
608
|
+
end
|
609
|
+
|
610
|
+
# create a copy of this pattern that contains no groups
|
611
|
+
# @return [PatternBase]
|
612
|
+
def groupless
|
613
|
+
__deep_clone__.map! do |s|
|
614
|
+
s.arguments.delete(:tag_as)
|
615
|
+
s.arguments.delete(:reference)
|
616
|
+
s.arguments.delete(:includes)
|
617
|
+
raise "unable to remove capture" if s.needs_to_capture?
|
618
|
+
end.freeze
|
619
|
+
end
|
620
|
+
|
621
|
+
#
|
622
|
+
# Retags all tags_as
|
623
|
+
#
|
624
|
+
# @param [Hash] args retag options
|
625
|
+
# @option [Boolean] :all (true) should all tags be kept
|
626
|
+
# @option [Boolean] :keep (true) should all tags be kept
|
627
|
+
# @option [String] :append a string to append to all tags (implies :keep)
|
628
|
+
# @option [String] tag_as maps from an old tag_as to a new tag_as
|
629
|
+
# @option [String] reference maps from reference to a new tag_as
|
630
|
+
#
|
631
|
+
# @return [PatternBase] a copy of self retagged
|
632
|
+
#
|
633
|
+
def reTag(args)
|
634
|
+
__deep_clone__.map! do |s|
|
635
|
+
# tags are keep unless `all: false` or `keep: false`, and append is not a string
|
636
|
+
discard_tag = (args[:all] == false || args[:keep] == false)
|
637
|
+
discard_tag = false if args[:append].is_a? String
|
638
|
+
|
639
|
+
args.each do |key, tag|
|
640
|
+
if [s.arguments[:tag_as], s.arguments[:reference]].include? key
|
641
|
+
s.arguments[:tag_as] = tag
|
642
|
+
discard_tag = false
|
643
|
+
end
|
644
|
+
end
|
645
|
+
|
646
|
+
if args[:append].is_a?(String) && s.arguments[:tag_as]
|
647
|
+
s.arguments[:tag_as] = s.arguments[:tag_as] + "." + args[:append]
|
648
|
+
end
|
649
|
+
|
650
|
+
s.arguments.delete(:tag_as) if discard_tag
|
651
|
+
end.freeze
|
652
|
+
end
|
653
|
+
|
654
|
+
#
|
655
|
+
# Collects information about the capture groups
|
656
|
+
#
|
657
|
+
# @api private
|
658
|
+
#
|
659
|
+
# @param [Integer] next_group the next group number to use
|
660
|
+
#
|
661
|
+
# @return [Array<Hash>] group attributes
|
662
|
+
#
|
663
|
+
def collect_group_attributes(next_group = optimize_outer_group? ? 0 : 1)
|
664
|
+
groups = do_collect_self_groups(next_group)
|
665
|
+
next_group += groups.length
|
666
|
+
if @match.is_a? PatternBase
|
667
|
+
new_groups = @match.collect_group_attributes(next_group)
|
668
|
+
groups.concat(new_groups)
|
669
|
+
next_group += new_groups.length
|
670
|
+
end
|
671
|
+
if @next_pattern.is_a? PatternBase
|
672
|
+
new_groups = @next_pattern.collect_group_attributes(next_group)
|
673
|
+
groups.concat(new_groups)
|
674
|
+
end
|
675
|
+
groups
|
676
|
+
end
|
677
|
+
|
678
|
+
#
|
679
|
+
# Collect group information about self
|
680
|
+
#
|
681
|
+
# @param [Integer] next_group The next group number to use
|
682
|
+
#
|
683
|
+
# @return [Array<Hash>] group attributes
|
684
|
+
#
|
685
|
+
def do_collect_self_groups(next_group)
|
686
|
+
groups = []
|
687
|
+
groups << {group: next_group}.merge(@arguments) if needs_to_capture?
|
688
|
+
groups
|
689
|
+
end
|
690
|
+
|
691
|
+
#
|
692
|
+
# Displays the Pattern for inspection
|
693
|
+
#
|
694
|
+
# @return [String] A representation of the pattern
|
695
|
+
#
|
696
|
+
def inspect
|
697
|
+
super.split(" ")[0] + " match:" + @match.inspect + ">"
|
698
|
+
end
|
699
|
+
|
700
|
+
#
|
701
|
+
# Convert group references into backreferences
|
702
|
+
#
|
703
|
+
# @api private
|
704
|
+
#
|
705
|
+
# @param [Hash] groups group information for the pattern
|
706
|
+
# @param [String] self_regex the pattern as string
|
707
|
+
#
|
708
|
+
# @return [String] the fixed up regex_string
|
709
|
+
#
|
710
|
+
def fixup_regex_references(groups, self_regex)
|
711
|
+
# rubocop:disable Metrics/LineLength
|
712
|
+
references = {}
|
713
|
+
# convert all references to group numbers
|
714
|
+
groups.each do |group|
|
715
|
+
references[group[:reference]] = group[:group] unless group[:reference].nil?
|
716
|
+
end
|
717
|
+
|
718
|
+
# convert back references
|
719
|
+
self_regex = self_regex.gsub(/\(\?\#\[:backreference:([^\\]+?):\]\)/) do
|
720
|
+
match_reference = Regexp.last_match(1)
|
721
|
+
if references[match_reference].nil?
|
722
|
+
raise "\nWhen processing the matchResultOf:#{match_reference}, I couldn't find the group it was referencing"
|
723
|
+
end
|
724
|
+
|
725
|
+
# if the reference does exist, then replace it with it's number
|
726
|
+
"\\#{references[match_reference]}"
|
727
|
+
end
|
728
|
+
|
729
|
+
# check for a subroutine to the Nth group, replace it with `\N`
|
730
|
+
self_regex = self_regex.gsub(/\(\?\#\[:subroutine:([^\\]+?):\]\)/) do
|
731
|
+
match_reference = Regexp.last_match(1)
|
732
|
+
if references[match_reference].nil?
|
733
|
+
raise "\nWhen processing the recursivelyMatch:#{match_reference}, I couldn't find the group it was referencing"
|
734
|
+
end
|
735
|
+
|
736
|
+
# if the reference does exist, then replace it with it's number
|
737
|
+
"\\g<#{references[match_reference]}>"
|
738
|
+
end
|
739
|
+
# rubocop:enable Metrics/LineLength
|
740
|
+
self_regex
|
741
|
+
end
|
742
|
+
|
743
|
+
#
|
744
|
+
# Scrambles references of self
|
745
|
+
# This method provides a way to rename all references
|
746
|
+
# both actual references and references to references will be scrambled in
|
747
|
+
# some one to one mapping, all references that were unique before remain unique
|
748
|
+
#
|
749
|
+
# This must be idempotent, calling this repeatedly must have references be as if it
|
750
|
+
# was called only once, even if the pattern is cloned between calls
|
751
|
+
# this is because it may be called a different number of times depending on the nest
|
752
|
+
# level of the patterns
|
753
|
+
#
|
754
|
+
# @return [void] nothing
|
755
|
+
#
|
756
|
+
def self_scramble_references
|
757
|
+
scramble = lambda do |name|
|
758
|
+
return name if name.start_with?("__scrambled__")
|
759
|
+
|
760
|
+
"__scrambled__" + name
|
761
|
+
end
|
762
|
+
|
763
|
+
tag_as = @arguments[:tag_as]
|
764
|
+
reference = @arguments[:reference]
|
765
|
+
@arguments[:tag_as] = scramble.call(tag_as) if tag_as.is_a? String
|
766
|
+
@arguments[:reference] = scramble.call(reference) if reference.is_a? String
|
767
|
+
end
|
768
|
+
|
769
|
+
#
|
770
|
+
# Converts group attributes into a captures hash
|
771
|
+
#
|
772
|
+
# @api private
|
773
|
+
#
|
774
|
+
# @param [Hash] groups group attributes
|
775
|
+
#
|
776
|
+
# @return [Hash] capture hash
|
777
|
+
#
|
778
|
+
def convert_group_attributes_to_captures(groups)
|
779
|
+
captures = {}
|
780
|
+
|
781
|
+
groups.each do |group|
|
782
|
+
output = {}
|
783
|
+
output[:name] = group[:tag_as] unless group[:tag_as].nil?
|
784
|
+
if group[:includes].is_a? Array
|
785
|
+
output[:patterns] = convert_includes_to_patterns(group[:includes])
|
786
|
+
elsif !group[:includes].nil?
|
787
|
+
output[:patterns] = convert_includes_to_patterns([group[:includes]])
|
788
|
+
end
|
789
|
+
captures[group[:group].to_s] = output
|
790
|
+
end
|
791
|
+
# replace $match and $reference() with the appropriate capture number
|
792
|
+
captures.each do |key, value|
|
793
|
+
next if value[:name].nil?
|
794
|
+
|
795
|
+
value[:name] = value[:name].gsub(/\$(?:match|reference\((.+)\))/) do |match|
|
796
|
+
next ("$" + key) if match == "$match"
|
797
|
+
|
798
|
+
reference_group = groups.detect do |group|
|
799
|
+
group[:reference] == Regexp.last_match(1)
|
800
|
+
end
|
801
|
+
"$" + reference_group[:group].to_s
|
802
|
+
end
|
803
|
+
end
|
804
|
+
end
|
805
|
+
|
806
|
+
#
|
807
|
+
# converts an includes array into a patterns array
|
808
|
+
#
|
809
|
+
# @api private
|
810
|
+
#
|
811
|
+
# @param [Array<PatternBase, Symbol>] includes an includes array
|
812
|
+
#
|
813
|
+
# @return [Array<Hash>] a patterns array
|
814
|
+
#
|
815
|
+
def convert_includes_to_patterns(includes)
|
816
|
+
includes = [includes] unless includes.is_a? Array
|
817
|
+
patterns = includes.flatten.map do |rule|
|
818
|
+
next {include: rule} if rule.is_a?(String) && rule.start_with?("source.", "text.")
|
819
|
+
next {include: rule.to_s} if [:$self, :$base].include? rule
|
820
|
+
next {include: "##{rule}"} if rule.is_a? Symbol
|
821
|
+
|
822
|
+
rule = PatternBase.new(rule) unless rule.is_a? PatternBase
|
823
|
+
rule.to_tag
|
824
|
+
end
|
825
|
+
patterns
|
826
|
+
end
|
827
|
+
|
828
|
+
#
|
829
|
+
# Deeply clone self
|
830
|
+
#
|
831
|
+
# @return [PatternBase] a copy of self
|
832
|
+
#
|
833
|
+
def __deep_clone__
|
834
|
+
__deep_clone_self__.insert! @next_pattern.__deep_clone__
|
835
|
+
end
|
836
|
+
|
837
|
+
#
|
838
|
+
# Deeply clones self, without its next_pattern
|
839
|
+
#
|
840
|
+
# @return [PatternBase] a copy of self
|
841
|
+
#
|
842
|
+
def __deep_clone_self__
|
843
|
+
options = @arguments.__deep_clone__
|
844
|
+
options[:match] = @match.__deep_clone__
|
845
|
+
self.class.new(options, :deep_clone, @original_arguments)
|
846
|
+
end
|
847
|
+
|
848
|
+
#
|
849
|
+
# Raise an error if regex contains a capturing group
|
850
|
+
#
|
851
|
+
# @param [Regexp] regex the regexp to test
|
852
|
+
# @param [Integer] check the group to check for
|
853
|
+
#
|
854
|
+
# @return [void]
|
855
|
+
#
|
856
|
+
def raise_if_regex_has_capture_group(regex, check = 1)
|
857
|
+
# this will throw a RegexpError if there are no capturing groups
|
858
|
+
_ignore = with_no_warnings { /#{regex}#{"\\" + check.to_s}/ }
|
859
|
+
# at this point @match contains a capture group, complain
|
860
|
+
raise <<-HEREDOC.remove_indent
|
861
|
+
|
862
|
+
There is a pattern that is being constructed from a regular expression
|
863
|
+
with a capturing group. This is not allowed, as the group cannot be tracked
|
864
|
+
The bad pattern is
|
865
|
+
#{self}
|
866
|
+
HEREDOC
|
867
|
+
rescue RegexpError # rubocop: disable Lint/HandleExceptions
|
868
|
+
# no capture groups present, purposely do nothing
|
869
|
+
end
|
870
|
+
end
|