ruby_grammar_builder 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/lib/textmate_grammar/generated/grammar.rb +32 -0
  4. data/lib/textmate_grammar/generated/rule.rb +144 -0
  5. data/lib/textmate_grammar/grammar.rb +670 -0
  6. data/lib/textmate_grammar/grammar_plugin.rb +189 -0
  7. data/lib/textmate_grammar/import_patterns.rb +14 -0
  8. data/lib/textmate_grammar/linters/flat_includes.rb +32 -0
  9. data/lib/textmate_grammar/linters/includes_then_tag_as.rb +48 -0
  10. data/lib/textmate_grammar/linters/standard_naming.rb +226 -0
  11. data/lib/textmate_grammar/linters/start_match_empty.rb +49 -0
  12. data/lib/textmate_grammar/linters/tests.rb +19 -0
  13. data/lib/textmate_grammar/linters/unused_unresolved.rb +9 -0
  14. data/lib/textmate_grammar/pattern_extensions/look_ahead_for.rb +32 -0
  15. data/lib/textmate_grammar/pattern_extensions/look_ahead_to_avoid.rb +31 -0
  16. data/lib/textmate_grammar/pattern_extensions/look_behind_for.rb +31 -0
  17. data/lib/textmate_grammar/pattern_extensions/look_behind_to_avoid.rb +31 -0
  18. data/lib/textmate_grammar/pattern_extensions/lookaround_pattern.rb +169 -0
  19. data/lib/textmate_grammar/pattern_extensions/match_result_of.rb +67 -0
  20. data/lib/textmate_grammar/pattern_extensions/maybe.rb +50 -0
  21. data/lib/textmate_grammar/pattern_extensions/one_of.rb +107 -0
  22. data/lib/textmate_grammar/pattern_extensions/one_or_more_of.rb +42 -0
  23. data/lib/textmate_grammar/pattern_extensions/or_pattern.rb +55 -0
  24. data/lib/textmate_grammar/pattern_extensions/placeholder.rb +102 -0
  25. data/lib/textmate_grammar/pattern_extensions/recursively_match.rb +76 -0
  26. data/lib/textmate_grammar/pattern_extensions/zero_or_more_of.rb +50 -0
  27. data/lib/textmate_grammar/pattern_variations/base_pattern.rb +870 -0
  28. data/lib/textmate_grammar/pattern_variations/legacy_pattern.rb +61 -0
  29. data/lib/textmate_grammar/pattern_variations/pattern.rb +9 -0
  30. data/lib/textmate_grammar/pattern_variations/pattern_range.rb +233 -0
  31. data/lib/textmate_grammar/pattern_variations/repeatable_pattern.rb +204 -0
  32. data/lib/textmate_grammar/regex_operator.rb +182 -0
  33. data/lib/textmate_grammar/regex_operators/alternation.rb +24 -0
  34. data/lib/textmate_grammar/regex_operators/concat.rb +23 -0
  35. data/lib/textmate_grammar/stdlib/common.rb +20 -0
  36. data/lib/textmate_grammar/tokens.rb +110 -0
  37. data/lib/textmate_grammar/transforms/add_ending.rb +25 -0
  38. data/lib/textmate_grammar/transforms/bailout.rb +92 -0
  39. data/lib/textmate_grammar/transforms/fix_repeated_tag_as.rb +75 -0
  40. data/lib/textmate_grammar/transforms/resolve_placeholders.rb +121 -0
  41. data/lib/textmate_grammar/util.rb +198 -0
  42. data/lib/textmate_grammar.rb +4 -0
  43. metadata +85 -0
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./base_pattern.rb"
4
+
5
+ #
6
+ # LegacyPattern allows for a hash to be treated as a Pattern
7
+ # It implements the minimum required to be sucessfully generated by the grammar
8
+ #
9
+ class LegacyPattern < PatternBase
10
+ def initialize(hash)
11
+ super("placeholder")
12
+ @hash = hash.transform_keys(&:to_sym)
13
+ end
14
+
15
+ # LegacyPattern cannot be evaluated
16
+ def evaluate(*_ignored)
17
+ raise "LegacyPattern cannot be used as a part of a Pattern"
18
+ end
19
+
20
+ # LegacyPattern cannot be chained
21
+ def insert!(_pattern)
22
+ raise "LegacyPattern cannot be used as a part of a Pattern"
23
+ end
24
+
25
+ #
26
+ # (see PatternBase#to_tag)
27
+ #
28
+ # @return [Hash] The hash it was constructed with
29
+ #
30
+ def to_tag
31
+ @hash
32
+ end
33
+
34
+ #
35
+ # (see PatternBase#run_tests)
36
+ #
37
+ def run_tests
38
+ true
39
+ end
40
+
41
+ #
42
+ # (see PatternBase#map!)
43
+ #
44
+ def map!(*)
45
+ self
46
+ end
47
+
48
+ #
49
+ # (see PatternBase#start_pattern)
50
+ #
51
+ def start_pattern
52
+ ""
53
+ end
54
+
55
+ #
56
+ # (see PatternBase#__deep_clone__)
57
+ #
58
+ def __deep_clone__
59
+ self.class.new(@hash)
60
+ end
61
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./repeatable_pattern.rb"
4
+
5
+ #
6
+ # Pattern is a class alias for RepeatablePattern
7
+ #
8
+ # class Pattern # <- encase people ctrl+F for "class Pattern"
9
+ Pattern = RepeatablePattern
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./base_pattern.rb"
4
+
5
+ #
6
+ # Provides the ability to create begin/end and begin/while rules
7
+ #
8
+ class PatternRange < PatternBase
9
+ attr_reader :start_pattern
10
+
11
+ #
12
+ # Creates a new PatternRange
13
+ #
14
+ # @param [Hash] arguments options
15
+ # @option arguments [PatternBase,Regexp,String] :start_pattern the start pattern
16
+ # @option arguments [PatternBase,Regexp,String] :end_pattern the end pattern
17
+ # @option arguments [PatternBase,Regexp,String] :while_pattern the while pattern
18
+ # @option arguments [String] :tag_as the tag for this pattern
19
+ # @option arguments [String] :tag_contents_as the tag for contents of this pattern
20
+ # @option arguments [String] :tag_start_as the tag for the start pattern
21
+ # @option arguments [String] :tag_end_as the tag for the end pattern
22
+ # @option arguments [String] :tag_while_as the tag for the continuation pattern
23
+ #
24
+ # Plugins may add additional options
25
+ # @note exactly one of :end_pattern or :while_pattern is required
26
+ #
27
+ def initialize(arguments)
28
+ @original_arguments = arguments
29
+ @match = nil
30
+ @next_pattern = nil
31
+
32
+ raise "PatternRange.new() expects a hash" unless arguments.is_a? Hash
33
+
34
+ # ensure end_pattern: XOR while_pattern: is provided
35
+ if arguments[:end_pattern].nil? && arguments[:while_pattern].nil?
36
+ raise "one of `while_pattern:` or `end_pattern` must be supplied"
37
+ end
38
+
39
+ if !arguments[:end_pattern].nil? && !arguments[:while_pattern].nil?
40
+ raise "only one of `while_pattern:` or `end_pattern` must be supplied"
41
+ end
42
+
43
+ @start_pattern = arguments[:start_pattern]
44
+ @stop_pattern = arguments[:end_pattern] || arguments[:while_pattern]
45
+
46
+ # convert to patterns if needed
47
+ @start_pattern = PatternBase.new(@start_pattern) unless @start_pattern.is_a? PatternBase
48
+ @stop_pattern = PatternBase.new(@stop_pattern) unless @stop_pattern.is_a? PatternBase
49
+
50
+ # store originals for to_s
51
+ @original_start_pattern = @start_pattern
52
+ @original_stop_pattern = @stop_pattern
53
+
54
+ if arguments[:tag_start_as]
55
+ @start_pattern = PatternBase.new(
56
+ match: @start_pattern,
57
+ tag_as: arguments[:tag_start_as],
58
+ )
59
+ end
60
+
61
+ tag_stop_as = arguments[:tag_end_as] || arguments[:tag_while_as]
62
+
63
+ if tag_stop_as
64
+ @stop_pattern = PatternBase.new(
65
+ match: @stop_pattern,
66
+ tag_as: tag_stop_as,
67
+ )
68
+ end
69
+
70
+ raise "match: is not supported in a PatternRange" if arguments[:match]
71
+
72
+ @stop_type = arguments[:end_pattern] ? :end_pattern : :while_pattern
73
+
74
+ arguments.delete(:start_pattern)
75
+ arguments.delete(:end_pattern)
76
+ arguments.delete(:while_pattern)
77
+
78
+ # ensure that includes is either nil or a flat array
79
+ if arguments[:includes]
80
+ arguments[:includes] = [arguments[:includes]] unless arguments[:includes].is_a? Array
81
+ arguments[:includes] = arguments[:includes].flatten
82
+ end
83
+
84
+ #canonize end_pattern_last
85
+ arguments[:end_pattern_last] = arguments[:apply_end_pattern_last] if arguments[:apply_end_pattern_last]
86
+ arguments[:end_pattern_last] = arguments[:applyEndPatternLast] if arguments[:applyEndPatternLast]
87
+
88
+ arguments.delete(:apply_end_pattern_last)
89
+ arguments.delete(:applyEndPatternLast)
90
+
91
+ @arguments = arguments
92
+ end
93
+
94
+ #
95
+ # (see PatternBase#__deep_clone__)
96
+ #
97
+ def __deep_clone__
98
+ options = @arguments.__deep_clone__
99
+ options[:start_pattern] = @original_start_pattern.__deep_clone__
100
+ if @stop_type == :end_pattern
101
+ options[:end_pattern] = @original_stop_pattern.__deep_clone__
102
+ else
103
+ options[:while_pattern] = @original_stop_pattern.__deep_clone__
104
+ end
105
+ self.class.new(options)
106
+ end
107
+
108
+ #
109
+ # Raises an error to prevent use inside a pattern list
110
+ #
111
+ # @param _ignored ignored
112
+ #
113
+ # @return [void]
114
+ #
115
+ def evaluate(*_ignored)
116
+ raise "PatternRange cannot be used as a part of a Pattern"
117
+ end
118
+
119
+ #
120
+ # Raises an error to prevent use inside a pattern list
121
+ #
122
+ # @param _ignored ignored
123
+ #
124
+ # @return [void]
125
+ #
126
+ def do_evaluate_self(*_ignored)
127
+ raise "PatternRange cannot be used as a part of a Pattern"
128
+ end
129
+
130
+ #
131
+ # Generate a Textmate rule from the PatternRange
132
+ #
133
+ # @return [Hash] The Textmate rule
134
+ #
135
+ def to_tag
136
+ match_key = { end_pattern: "end", while_pattern: "while" }[@stop_type]
137
+ capture_key = { end_pattern: "endCaptures", while_pattern: "whileCaptures" }[@stop_type]
138
+
139
+ start_groups = @start_pattern.collect_group_attributes
140
+ stop_groups = @stop_pattern.collect_group_attributes
141
+
142
+ output = {
143
+ "begin" => @start_pattern.evaluate,
144
+ # this is supposed to be start_groups as back references in end and while
145
+ # refer to the start pattern
146
+ match_key => @stop_pattern.evaluate(start_groups, fixup_refereces: true),
147
+ "beginCaptures" => convert_group_attributes_to_captures(start_groups),
148
+ capture_key => convert_group_attributes_to_captures(stop_groups),
149
+ }
150
+
151
+ output[:name] = @arguments[:tag_as] unless @arguments[:tag_as].nil?
152
+ output[:contentName] = @arguments[:tag_content_as] unless @arguments[:tag_content_as].nil?
153
+
154
+ output["begin"] = output["begin"][1..-2] if @start_pattern.optimize_outer_group?
155
+ output[match_key] = output[match_key][1..-2] if @stop_pattern.optimize_outer_group?
156
+
157
+ if @arguments[:includes].is_a? Array
158
+ output[:patterns] = convert_includes_to_patterns(@arguments[:includes])
159
+ elsif !@arguments[:includes].nil?
160
+ output[:patterns] = convert_includes_to_patterns([@arguments[:includes]])
161
+ end
162
+
163
+ # end_pattern_last
164
+ output["applyEndPatternLast"] = 1 if @arguments[:end_pattern_last]
165
+
166
+ output
167
+ end
168
+
169
+ #
170
+ # Displays this pattern range as source code that would generate it
171
+ #
172
+ # @return [String] The PatternRange as source code
173
+ #
174
+ def to_s
175
+ start_pattern = @original_start_pattern.to_s(2, true)
176
+ stop_pattern = @original_stop_pattern.to_s(2, true)
177
+
178
+ output = "PatternRange.new("
179
+ output += "\n start_pattern: " + start_pattern.lstrip
180
+ output += ",\n #{@stop_type}: " + stop_pattern.lstrip
181
+ [:tag_as, :tag_content_as, :tag_start_as, :tag_end_as, :tag_while_as].each do |tag|
182
+ next if @arguments[tag].nil?
183
+
184
+ output += ",\n #{tag}: \"" + @arguments[tag] + "\"" if @arguments[tag].is_a? String
185
+ end
186
+ output += ",\n includes: " + @arguments[:includes].to_s if @arguments[:includes]
187
+ output += ",\n end_pattern_last: #{@arguments[:end_pattern_last]}" if @arguments[:end_pattern_last]
188
+ output += ",\n)"
189
+
190
+ output
191
+ end
192
+
193
+ #
194
+ # (see PatternBase#map!)
195
+ #
196
+ def map!(map_includes = false, &block)
197
+ yield self
198
+
199
+ @start_pattern.map!(map_includes, &block)
200
+ @stop_pattern.map!(map_includes, &block)
201
+ map_includes!(&block) if map_includes
202
+
203
+ self
204
+ end
205
+
206
+ #
207
+ # (see PatternBase#transform_includes)
208
+ #
209
+ def transform_includes(&block)
210
+ copy = __deep_clone__
211
+ copy.arguments[:includes].map!(&block) if copy.arguments[:includes].is_a? Array
212
+
213
+ copy.map!(true) do |s|
214
+ s.arguments[:includes].map!(&block) if s.arguments[:includes].is_a? Array
215
+ end.freeze
216
+ end
217
+
218
+ #
219
+ # (see PatternBase#run_tests)
220
+ #
221
+ def run_tests
222
+ s = @start_pattern.run_tests
223
+ e = @stop_pattern.run_tests
224
+ s && e
225
+ end
226
+
227
+ #
228
+ # (see PatternBase#inspect)
229
+ #
230
+ def inspect
231
+ super.split(" ")[0] + " start_pattern:" + @start_pattern.inspect + ">"
232
+ end
233
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./base_pattern.rb"
4
+
5
+ #
6
+ # RepeatablePattern provides quantifiers for patterns
7
+ #
8
+ class RepeatablePattern < PatternBase
9
+ # @return [Integer,nil] the minimum amount that can be matched
10
+ attr_accessor :at_least
11
+ # @return [Integer,nil] the maximum amount that can be matched
12
+ attr_accessor :at_most
13
+ # (see PatternBase#initialize)
14
+ def initialize(*arguments)
15
+ super(*arguments)
16
+
17
+ @at_least = nil
18
+ @at_most = nil
19
+ process_quantifiers_from_arguments
20
+ end
21
+
22
+ #
23
+ # sets @at_least and @at_most based on arguments
24
+ #
25
+ # @return [void]
26
+ #
27
+ # @api private
28
+ #
29
+ def process_quantifiers_from_arguments
30
+ # this sets the @at_most and @at_least value based on the arguments
31
+
32
+ #
33
+ # Simplify the quantity down to just :at_least and :at_most
34
+ #
35
+ attributes_clone = @arguments.clone
36
+ # convert Enumerators to numbers
37
+ [:at_least, :at_most, :how_many_times?].each do |each|
38
+ if attributes_clone[each].is_a?(Enumerator)
39
+ attributes_clone[each] = attributes_clone[each].size
40
+ end
41
+ end
42
+
43
+ # canonize dont_back_track? and as_few_as_possible?
44
+ @arguments[:dont_back_track?] ||= @arguments[:possessive?]
45
+ @arguments[:as_few_as_possible?] ||= @arguments[:lazy?]
46
+ if @arguments[:greedy?]
47
+ @arguments[:dont_back_track?] = false
48
+ @arguments[:as_few_as_possible?] = false
49
+ end
50
+ # extract the data
51
+ at_least = attributes_clone[:at_least]
52
+ at_most = attributes_clone[:at_most]
53
+ how_many_times = attributes_clone[:how_many_times?]
54
+ # simplify to at_least and at_most
55
+ at_least = at_most = how_many_times if how_many_times.is_a?(Integer)
56
+
57
+ # check if quantifying is allowed
58
+ # check after everything else in case additional quantifying options
59
+ # are created in the future
60
+ if quantifying_allowed?
61
+ @at_least = at_least
62
+ @at_most = at_most
63
+ # if a quantifying value was set and quantifying is not allowed, raise an error
64
+ # telling the user that its not allowed
65
+ elsif !(at_most.nil? && at_least.nil?)
66
+ raise <<-HEREDOC.remove_indent
67
+
68
+ Inside of the #{name} pattern, there are some quantity arguments like:
69
+ :at_least
70
+ :at_most
71
+ or :how_many_times?
72
+ These are not allowed in this kind of #{do_get_to_s_name}) pattern
73
+ If you did this intentionally please wrap it inside of a Pattern.new()
74
+ ex: #{do_get_to_s_name} Pattern.new( *your_arguments* ) )
75
+ HEREDOC
76
+ end
77
+
78
+ return unless @arguments[:dont_back_track?] && @arguments[:as_few_as_possible?]
79
+
80
+ raise ":dont_back_track? and :as_few_as_possible? cannot both be provided"
81
+ end
82
+
83
+ #
84
+ # converts @at_least and @at_most into the appropriate quantifier
85
+ # this is a simple_quantifier because it does not include atomic-ness
86
+ #
87
+ # @return [String] the quantifier
88
+ #
89
+ def simple_quantifier
90
+ # Generate the ending based on :at_least and :at_most
91
+
92
+ # by default assume no quantifiers
93
+ quantifier = ""
94
+ # if there is no at_least, at_most, or how_many_times, then theres no quantifier
95
+ if @at_least.nil? and @at_most.nil?
96
+ quantifier = ""
97
+ # if there is a quantifier
98
+ else
99
+ # if there's no at_least, then assume at_least = 1
100
+ @at_least = 1 if @at_least.nil?
101
+
102
+ quantifier =
103
+ if @at_least == 1 and @at_most == 1
104
+ # no qualifier
105
+ ""
106
+ elsif @at_least == 0 and @at_most == 1
107
+ # this is just a different way of "maybe"
108
+ "?"
109
+ elsif @at_least == 0 and @at_most.nil?
110
+ # this is just a different way of "zeroOrMoreOf"
111
+ "*"
112
+ elsif @at_least == 1 and @at_most.nil?
113
+ # this is just a different way of "oneOrMoreOf"
114
+ "+"
115
+ elsif @at_least == @at_most
116
+ # exactly N times
117
+ "{#{@at_least}}"
118
+ else
119
+ # if it is more complicated than that, just use a range
120
+ "{#{@at_least},#{@at_most}}"
121
+ end
122
+ end
123
+ # quantifiers can be made possessive without requiring atomic groups
124
+ quantifier += "+" if quantifier != "" && @arguments[:dont_back_track?] == true
125
+ quantifier += "?" if quantifier != "" && @arguments[:as_few_as_possible?] == true
126
+ quantifier
127
+ end
128
+
129
+ #
130
+ # Adds quantifiers to match
131
+ #
132
+ # @param [String, PatternBase] match the pattern to add a quantifier to
133
+ # @param [Array] groups group information, used for evaluating match
134
+ #
135
+ # @return [String] match with quantifiers applied
136
+ #
137
+ def add_quantifier_options_to(match, groups)
138
+ match = match.evaluate(groups) if match.is_a? PatternBase
139
+ quantifier = simple_quantifier
140
+ # check if there are quantifiers
141
+ if quantifier != ""
142
+ # if the match is not a single entity, then it needs to be wrapped
143
+ match = "(?:#{match})" unless string_single_entity?(match)
144
+ # add the quantified ending
145
+ match += quantifier
146
+ elsif @arguments[:dont_back_track?] == true
147
+ # make atomic, which allows arbitrary expression to be prevented from backtracking
148
+ match = "(?>#{match})"
149
+ end
150
+ if @arguments[:word_cannot_be_any_of]
151
+ word_pattern = @arguments[:word_cannot_be_any_of].map { |w| Regexp.escape w }.join "|"
152
+ match = "(?!\\b(?:#{word_pattern})\\b)#{match}"
153
+ end
154
+ match
155
+ end
156
+
157
+ # (see PatternBase#do_evaluate_self)
158
+ def do_evaluate_self(groups)
159
+ add_capture_group_if_needed(add_quantifier_options_to(@match, groups))
160
+ end
161
+
162
+ # controls weather @arguments[:at_most] et. al. set @at_most et. al.
163
+ # @note override when inheriting. Return false unless the subclass allow quantifying
164
+ # @return [Boolean] if quantifying is allowed
165
+ # @note the default implementation returns True
166
+ def quantifying_allowed?
167
+ true
168
+ end
169
+
170
+ #
171
+ # (see PatternBase#do_add_attributes)
172
+ #
173
+ def do_add_attributes(indent)
174
+ # rubocop:disable Metrics/LineLength
175
+ output = ""
176
+ # special #then arguments
177
+ if quantifying_allowed?
178
+ output += ",\n#{indent} at_least: " + @arguments[:at_least].to_s if @arguments[:at_least]
179
+ output += ",\n#{indent} at_most: " + @arguments[:at_most].to_s if @arguments[:at_most]
180
+ output += ",\n#{indent} how_many_times: " + @arguments[:how_many_times].to_s if @arguments[:how_many_times]
181
+ output += ",\n#{indent} word_cannot_be_any_of: " + @arguments[:word_cannot_be_any_of].to_s if @arguments[:word_cannot_be_any_of]
182
+ end
183
+ output += ",\n#{indent} dont_back_track?: " + @arguments[:dont_back_track?].to_s if @arguments[:dont_back_track?]
184
+ output
185
+ # rubocop:enable Metrics/LineLength
186
+ end
187
+
188
+ #
189
+ # Does this pattern potentially rematch any capture groups
190
+ #
191
+ # @note this is used by FixRepeatedTagAs to modify patterns
192
+ # The answer of true is a safe, but expensive to runtime, default
193
+ #
194
+ # @return [Boolean] True if this pattern potentially rematches capture groups
195
+ #
196
+ def self_capture_group_rematch
197
+ # N or more
198
+ return true if @at_most.nil? && !@at_least.nil?
199
+ # up to N
200
+ return true if !@at_most.nil? && @at_most > 1
201
+
202
+ false
203
+ end
204
+ end