rack-mount 0.0.1 → 0.8.3

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.
Files changed (43) hide show
  1. data/README.rdoc +12 -4
  2. data/lib/rack/mount/analysis/histogram.rb +55 -6
  3. data/lib/rack/mount/analysis/splitting.rb +103 -89
  4. data/lib/rack/mount/code_generation.rb +120 -0
  5. data/lib/rack/mount/generatable_regexp.rb +95 -48
  6. data/lib/rack/mount/multimap.rb +84 -41
  7. data/lib/rack/mount/prefix.rb +13 -8
  8. data/lib/rack/mount/regexp_with_named_groups.rb +27 -7
  9. data/lib/rack/mount/route.rb +75 -18
  10. data/lib/rack/mount/route_set.rb +308 -22
  11. data/lib/rack/mount/strexp/parser.rb +160 -0
  12. data/lib/rack/mount/strexp/tokenizer.rb +83 -0
  13. data/lib/rack/mount/strexp.rb +54 -79
  14. data/lib/rack/mount/utils.rb +65 -174
  15. data/lib/rack/mount/vendor/regin/regin/alternation.rb +40 -0
  16. data/lib/rack/mount/vendor/regin/regin/anchor.rb +4 -0
  17. data/lib/rack/mount/vendor/regin/regin/atom.rb +54 -0
  18. data/lib/rack/mount/vendor/regin/regin/character.rb +51 -0
  19. data/lib/rack/mount/vendor/regin/regin/character_class.rb +50 -0
  20. data/lib/rack/mount/vendor/regin/regin/collection.rb +77 -0
  21. data/lib/rack/mount/vendor/regin/regin/expression.rb +126 -0
  22. data/lib/rack/mount/vendor/regin/regin/group.rb +90 -0
  23. data/lib/rack/mount/vendor/regin/regin/options.rb +55 -0
  24. data/lib/rack/mount/vendor/regin/regin/parser.rb +546 -0
  25. data/lib/rack/mount/vendor/regin/regin/tokenizer.rb +255 -0
  26. data/lib/rack/mount/vendor/regin/regin/version.rb +3 -0
  27. data/lib/rack/mount/vendor/regin/regin.rb +75 -0
  28. data/lib/rack/mount/version.rb +3 -0
  29. data/lib/rack/mount.rb +13 -17
  30. metadata +88 -35
  31. data/lib/rack/mount/analysis/frequency.rb +0 -51
  32. data/lib/rack/mount/const.rb +0 -45
  33. data/lib/rack/mount/exceptions.rb +0 -3
  34. data/lib/rack/mount/generation/route.rb +0 -57
  35. data/lib/rack/mount/generation/route_set.rb +0 -163
  36. data/lib/rack/mount/meta_method.rb +0 -104
  37. data/lib/rack/mount/mixover.rb +0 -47
  38. data/lib/rack/mount/recognition/code_generation.rb +0 -99
  39. data/lib/rack/mount/recognition/route.rb +0 -59
  40. data/lib/rack/mount/recognition/route_set.rb +0 -88
  41. data/lib/rack/mount/vendor/multimap/multimap.rb +0 -466
  42. data/lib/rack/mount/vendor/multimap/multiset.rb +0 -153
  43. data/lib/rack/mount/vendor/multimap/nested_multimap.rb +0 -156
@@ -1,93 +1,68 @@
1
- require 'strscan'
1
+ require 'rack/mount/strexp/parser'
2
2
 
3
3
  module Rack::Mount
4
- class Strexp < Regexp
5
- # Parses segmented string expression and converts it into a Regexp
6
- #
7
- # Strexp.compile('foo')
8
- # # => %r{\Afoo\Z}
9
- #
10
- # Strexp.compile('foo/:bar', {}, ['/'])
11
- # # => %r{\Afoo/(?<bar>[^/]+)\Z}
12
- #
13
- # Strexp.compile(':foo.example.com')
14
- # # => %r{\A(?<foo>.+)\.example\.com\Z}
15
- #
16
- # Strexp.compile('foo/:bar', {:bar => /[a-z]+/}, ['/'])
17
- # # => %r{\Afoo/(?<bar>[a-z]+)\Z}
18
- #
19
- # Strexp.compile('foo(.:extension)')
20
- # # => %r{\Afoo(\.(?<extension>.+))?\Z}
21
- #
22
- # Strexp.compile('src/*files')
23
- # # => %r{\Asrc/(?<files>.+)\Z}
24
- def initialize(str, requirements = {}, separators = [])
25
- return super(str) if str.is_a?(Regexp)
4
+ class Strexp
5
+ class << self
6
+ # Parses segmented string expression and converts it into a Regexp
7
+ #
8
+ # Strexp.compile('foo')
9
+ # # => %r{\Afoo\Z}
10
+ #
11
+ # Strexp.compile('foo/:bar', {}, ['/'])
12
+ # # => %r{\Afoo/(?<bar>[^/]+)\Z}
13
+ #
14
+ # Strexp.compile(':foo.example.com')
15
+ # # => %r{\A(?<foo>.+)\.example\.com\Z}
16
+ #
17
+ # Strexp.compile('foo/:bar', {:bar => /[a-z]+/}, ['/'])
18
+ # # => %r{\Afoo/(?<bar>[a-z]+)\Z}
19
+ #
20
+ # Strexp.compile('foo(.:extension)')
21
+ # # => %r{\Afoo(\.(?<extension>.+))?\Z}
22
+ #
23
+ # Strexp.compile('src/*files')
24
+ # # => %r{\Asrc/(?<files>.+)\Z}
25
+ def compile(str, requirements = {}, separators = [], anchor = true)
26
+ return Regexp.compile(str) if str.is_a?(Regexp)
26
27
 
27
- re = Regexp.escape(str)
28
- requirements = requirements ? requirements.dup : {}
28
+ requirements = requirements ? requirements.dup : {}
29
+ normalize_requirements!(requirements, separators)
29
30
 
30
- normalize_requirements!(requirements, separators)
31
- parse_dynamic_segments!(re, requirements)
32
- parse_optional_segments!(re)
31
+ parser = StrexpParser.new
32
+ parser.anchor = anchor
33
+ parser.requirements = requirements
33
34
 
34
- super("\\A#{re}\\Z")
35
- end
36
-
37
- private
38
- def normalize_requirements!(requirements, separators)
39
- requirements.each do |key, value|
40
- if value.is_a?(Regexp)
41
- if regexp_has_modifiers?(value)
42
- requirements[key] = value
43
- else
44
- requirements[key] = value.source
45
- end
46
- else
47
- requirements[key] = Regexp.escape(value)
48
- end
35
+ begin
36
+ re = parser.scan_str(str)
37
+ rescue Racc::ParseError => e
38
+ raise RegexpError, e.message
49
39
  end
50
- requirements.default ||= separators.any? ?
51
- "[^#{separators.join}]+" : '.+'
52
- requirements
53
- end
54
40
 
55
- def parse_dynamic_segments!(str, requirements)
56
- re, pos, scanner = '', 0, StringScanner.new(str)
57
- while scanner.scan_until(/(:|\\\*)([a-zA-Z_]\w*)/)
58
- pre, pos = scanner.pre_match[pos..-1], scanner.pos
59
- if pre =~ /(.*)\\\\\Z/
60
- re << $1 + scanner.matched
61
- else
62
- name = scanner[2].to_sym
63
- requirement = scanner[1] == ':' ?
64
- requirements[name] : '.+'
65
- re << pre + Const::REGEXP_NAMED_CAPTURE % [name, requirement]
66
- end
67
- end
68
- re << scanner.rest
69
- str.replace(re)
41
+ Regexp.compile(re)
70
42
  end
43
+ alias_method :new, :compile
71
44
 
72
- def parse_optional_segments!(str)
73
- re, pos, scanner = '', 0, StringScanner.new(str)
74
- while scanner.scan_until(/\\\(|\\\)/)
75
- pre, pos = scanner.pre_match[pos..-1], scanner.pos
76
- if pre =~ /(.*)\\\\\Z/
77
- re << $1 + scanner.matched
78
- elsif scanner.matched == '\\('
79
- # re << pre + '(?:'
80
- re << pre + '('
81
- elsif scanner.matched == '\\)'
82
- re << pre + ')?'
45
+ private
46
+ def normalize_requirements!(requirements, separators)
47
+ requirements.each do |key, value|
48
+ if value.is_a?(Regexp)
49
+ if regexp_has_modifiers?(value)
50
+ requirements[key] = value
51
+ else
52
+ requirements[key] = value.source
53
+ end
54
+ else
55
+ requirements[key] = Regexp.escape(value)
56
+ end
83
57
  end
58
+ requirements.default ||= separators.any? ?
59
+ "[^#{separators.join}]+" : '.+'
60
+ requirements
84
61
  end
85
- re << scanner.rest
86
- str.replace(re)
87
- end
88
62
 
89
- def regexp_has_modifiers?(regexp)
90
- regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0
91
- end
63
+ def regexp_has_modifiers?(regexp)
64
+ regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0
65
+ end
66
+ end
92
67
  end
93
68
  end
@@ -1,5 +1,10 @@
1
- require 'rack/mount/regexp_with_named_groups'
2
- require 'strscan'
1
+ begin
2
+ require 'regin'
3
+ rescue LoadError
4
+ $: << File.expand_path(File.join(File.dirname(__FILE__), 'vendor/regin'))
5
+ require 'regin'
6
+ end
7
+
3
8
  require 'uri'
4
9
 
5
10
  module Rack::Mount
@@ -9,6 +14,19 @@ module Rack::Mount
9
14
  # more appropriate contexts.
10
15
  #++
11
16
  module Utils
17
+ def silence_debug
18
+ old_debug, $DEBUG = $DEBUG, nil
19
+ yield
20
+ ensure
21
+ $DEBUG = old_debug
22
+ end
23
+ module_function :silence_debug
24
+
25
+ def debug(msg)
26
+ warn "Rack::Mount #{msg}" if $DEBUG
27
+ end
28
+ module_function :debug
29
+
12
30
  # Normalizes URI path.
13
31
  #
14
32
  # Strips off trailing slash and ensures there is a leading slash.
@@ -19,25 +37,26 @@ module Rack::Mount
19
37
  # normalize_path("") # => "/"
20
38
  def normalize_path(path)
21
39
  path = "/#{path}"
22
- path.squeeze!(Const::SLASH)
23
- path.sub!(%r{/+\Z}, Const::EMPTY_STRING)
24
- path = Const::SLASH if path == Const::EMPTY_STRING
40
+ path.squeeze!('/')
41
+ path.sub!(%r{/+\Z}, '')
42
+ path = '/' if path == ''
25
43
  path
26
44
  end
27
45
  module_function :normalize_path
28
46
 
29
47
  # Removes trailing nils from array.
30
48
  #
31
- # pop_trailing_nils!([1, 2, 3]) # => [1, 2, 3]
32
- # pop_trailing_nils!([1, 2, 3, nil, nil]) # => [1, 2, 3]
33
- # pop_trailing_nils!([nil]) # => []
34
- def pop_trailing_nils!(ary)
35
- while ary.length > 0 && ary.last.nil?
49
+ # pop_trailing_blanks!([1, 2, 3]) # => [1, 2, 3]
50
+ # pop_trailing_blanks!([1, 2, 3, nil, ""]) # => [1, 2, 3]
51
+ # pop_trailing_blanks!([nil]) # => []
52
+ # pop_trailing_blanks!([""]) # => []
53
+ def pop_trailing_blanks!(ary)
54
+ while ary.length > 0 && (ary.last.nil? || ary.last == '')
36
55
  ary.pop
37
56
  end
38
57
  ary
39
58
  end
40
- module_function :pop_trailing_nils!
59
+ module_function :pop_trailing_blanks!
41
60
 
42
61
  RESERVED_PCHAR = ':@&=+$,;%'
43
62
  SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
@@ -47,14 +66,16 @@ module Rack::Mount
47
66
  UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
48
67
  end
49
68
 
69
+ Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
70
+
50
71
  def escape_uri(uri)
51
- URI.escape(uri.to_s, UNSAFE_PCHAR)
72
+ Parser.escape(uri.to_s, UNSAFE_PCHAR)
52
73
  end
53
74
  module_function :escape_uri
54
75
 
55
76
  if ''.respond_to?(:force_encoding)
56
77
  def unescape_uri(uri)
57
- URI.unescape(uri).force_encoding('utf-8')
78
+ Parser.unescape(uri).force_encoding('utf-8')
58
79
  end
59
80
  else
60
81
  def unescape_uri(uri)
@@ -72,33 +93,17 @@ module Rack::Mount
72
93
  }.join("&")
73
94
  when Hash
74
95
  value.map { |k, v|
75
- build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
96
+ build_nested_query(v, prefix ? "#{prefix}[#{Rack::Utils.escape(k)}]" : Rack::Utils.escape(k))
76
97
  }.join("&")
77
98
  when String
78
99
  raise ArgumentError, "value must be a Hash" if prefix.nil?
79
- "#{Rack::Utils.escape(prefix)}=#{Rack::Utils.escape(value)}"
80
- when NilClass
81
- Rack::Utils.escape(prefix)
100
+ "#{prefix}=#{Rack::Utils.escape(value)}"
82
101
  else
83
- if value.respond_to?(:to_param)
84
- build_nested_query(value.to_param.to_s, prefix)
85
- else
86
- Rack::Utils.escape(prefix)
87
- end
102
+ prefix
88
103
  end
89
104
  end
90
105
  module_function :build_nested_query
91
106
 
92
- def normalize_extended_expression(regexp)
93
- return regexp unless regexp.options & Regexp::EXTENDED != 0
94
- source = regexp.source
95
- source.gsub!(/#.+$/, '')
96
- source.gsub!(/\s+/, '')
97
- source.gsub!(/\\\//, '/')
98
- Regexp.compile(source)
99
- end
100
- module_function :normalize_extended_expression
101
-
102
107
  # Determines whether the regexp must match the entire string.
103
108
  #
104
109
  # regexp_anchored?(/^foo$/) # => true
@@ -106,166 +111,52 @@ module Rack::Mount
106
111
  # regexp_anchored?(/^foo/) # => false
107
112
  # regexp_anchored?(/foo$/) # => false
108
113
  def regexp_anchored?(regexp)
109
- regexp.source =~ /\A(\\A|\^).*(\\Z|\$)\Z/ ? true : false
114
+ regexp.source =~ /\A(\\A|\^).*(\\Z|\$)\Z/m ? true : false
110
115
  end
111
116
  module_function :regexp_anchored?
112
117
 
113
- # Returns static string source of Regexp if it only includes static
114
- # characters and no metacharacters. Otherwise the original Regexp is
115
- # returned.
116
- #
117
- # extract_static_regexp(/^foo$/) # => "foo"
118
- # extract_static_regexp(/^foo\.bar$/) # => "foo.bar"
119
- # extract_static_regexp(/^foo|bar$/) # => /^foo|bar$/
120
- def extract_static_regexp(regexp, options = nil)
121
- if regexp.is_a?(String)
122
- regexp = Regexp.compile("\\A#{regexp}\\Z", options)
123
- end
124
-
125
- # Just return if regexp is case-insensitive
126
- return regexp if regexp.casefold?
127
-
118
+ def normalize_extended_expression(regexp)
119
+ return regexp if (regexp.options & Regexp::EXTENDED) == 0
128
120
  source = regexp.source
129
- if regexp_anchored?(regexp)
130
- source.sub!(/^(\\A|\^)(.*)(\\Z|\$)$/, '\2')
131
- unescaped_source = source.gsub(/\\/, Const::EMPTY_STRING)
132
- if source == Regexp.escape(unescaped_source) &&
133
- Regexp.compile("\\A(#{source})\\Z") =~ unescaped_source
134
- return unescaped_source
135
- end
136
- end
137
- regexp
138
- end
139
- module_function :extract_static_regexp
140
-
141
- if Const::SUPPORTS_NAMED_CAPTURES
142
- NAMED_CAPTURE_REGEXP = /\?<([^>]+)>/
143
- else
144
- NAMED_CAPTURE_REGEXP = /\?:<([^>]+)>/
145
- end
146
-
147
- # Strips shim named capture syntax and returns a clean Regexp and
148
- # an ordered array of the named captures.
149
- #
150
- # extract_named_captures(/[a-z]+/) # => /[a-z]+/, []
151
- # extract_named_captures(/(?:<foo>[a-z]+)/) # => /([a-z]+)/, ['foo']
152
- # extract_named_captures(/([a-z]+)(?:<foo>[a-z]+)/)
153
- # # => /([a-z]+)([a-z]+)/, [nil, 'foo']
154
- def extract_named_captures(regexp)
155
- options = regexp.is_a?(Regexp) ? regexp.options : nil
156
- source = Regexp.compile(regexp).source
157
- names, scanner = [], StringScanner.new(source)
158
-
159
- while scanner.skip_until(/\(/)
160
- if scanner.scan(NAMED_CAPTURE_REGEXP)
161
- names << scanner[1]
162
- else
163
- names << nil
164
- end
165
- end
166
-
167
- names = [] unless names.any?
168
- source.gsub!(NAMED_CAPTURE_REGEXP, Const::EMPTY_STRING)
169
- return Regexp.compile(source, options), names
121
+ source.gsub!(/#.+$/, '')
122
+ source.gsub!(/\s+/, '')
123
+ source.gsub!(/\\\//, '/')
124
+ Regexp.compile(source)
170
125
  end
171
- module_function :extract_named_captures
172
-
173
- class Capture < Array #:nodoc:
174
- attr_reader :name, :optional
175
- alias_method :optional?, :optional
176
-
177
- def initialize(*args)
178
- options = args.last.is_a?(Hash) ? args.pop : {}
179
-
180
- @name = options.delete(:name)
181
- @name = @name.to_s if @name
182
-
183
- @optional = options.delete(:optional) || false
184
-
185
- super(args)
186
- end
187
-
188
- def ==(obj)
189
- obj.is_a?(Capture) && @name == obj.name && @optional == obj.optional && super
190
- end
191
-
192
- def optionalize!
193
- @optional = true
194
- self
195
- end
196
-
197
- def named?
198
- name && name != Const::EMPTY_STRING
199
- end
200
-
201
- def to_s
202
- source = "(#{join})"
203
- source << '?' if optional?
204
- source
205
- end
126
+ module_function :normalize_extended_expression
206
127
 
207
- def first_part
208
- first.is_a?(Capture) ? first.first_part : first
209
- end
128
+ def parse_regexp(regexp)
129
+ cache = @@_parse_regexp_cache ||= {}
210
130
 
211
- def last_part
212
- last.is_a?(Capture) ? last.last_part : last
131
+ if expression = cache[regexp]
132
+ return expression
213
133
  end
214
- end
215
134
 
216
- def extract_regexp_parts(regexp) #:nodoc:
217
135
  unless regexp.is_a?(RegexpWithNamedGroups)
218
136
  regexp = RegexpWithNamedGroups.new(regexp)
219
137
  end
220
138
 
221
- if regexp.source =~ /\?<([^>]+)>/
222
- regexp, names = extract_named_captures(regexp)
223
- else
224
- names = regexp.names
225
- end
226
- source = regexp.source
227
-
228
- source =~ /^(\\A|\^)/ ? source.gsub!(/^(\\A|\^)/, Const::EMPTY_STRING) :
229
- raise(ArgumentError, "#{source} needs to match the start of the string")
230
-
231
- scanner = StringScanner.new(source)
232
- stack = [[]]
233
-
234
- capture_index = 0
235
- until scanner.eos?
236
- char = scanner.getch
237
- cur = stack.last
238
-
239
- escaped = cur.last.is_a?(String) && cur.last[-1, 1] == '\\'
139
+ expression = Regin.parse(regexp)
240
140
 
241
- if char == '\\' && scanner.peek(1) == 'Z'
242
- scanner.pos += 1
243
- cur.push(Const::NULL)
244
- elsif escaped
245
- cur.push('') unless cur.last.is_a?(String)
246
- cur.last << char
247
- elsif char == '('
248
- name = names[capture_index]
249
- capture = Capture.new(:name => name)
250
- capture_index += 1
251
- cur.push(capture)
252
- stack.push(capture)
253
- elsif char == ')'
254
- capture = stack.pop
255
- if scanner.peek(1) == '?'
256
- scanner.pos += 1
257
- capture.optionalize!
141
+ unless Regin.regexp_supports_named_captures?
142
+ tag_captures = Proc.new do |group|
143
+ case group
144
+ when Regin::Group
145
+ # TODO: dup instead of mutating
146
+ group.instance_variable_set('@name', regexp.names[group.index]) if group.index
147
+ tag_captures.call(group.expression)
148
+ when Regin::Expression
149
+ group.each { |child| tag_captures.call(child) }
258
150
  end
259
- elsif char == '$'
260
- cur.push(Const::NULL)
261
- else
262
- cur.push('') unless cur.last.is_a?(String)
263
- cur.last << char
264
151
  end
152
+ tag_captures.call(expression)
265
153
  end
266
154
 
267
- stack.pop
155
+ cache[regexp] = expression.freeze
156
+ expression
157
+ rescue Racc::ParseError, Regin::Parser::ScanError
158
+ []
268
159
  end
269
- module_function :extract_regexp_parts
160
+ module_function :parse_regexp
270
161
  end
271
162
  end
@@ -0,0 +1,40 @@
1
+ module Regin
2
+ class Alternation < Collection
3
+ def initialize(*args)
4
+ args, options = extract_options(args)
5
+
6
+ if args.length == 1 && args.first.instance_of?(Array)
7
+ super(args.first)
8
+ else
9
+ super(args)
10
+ end
11
+
12
+ if options.key?(:ignorecase)
13
+ @array.map! { |e| e.dup(:ignorecase => options[:ignorecase]) }
14
+ end
15
+ end
16
+
17
+ # Returns true if expression could be treated as a literal string.
18
+ #
19
+ # Alternation groups are never literal.
20
+ def literal?
21
+ false
22
+ end
23
+
24
+ def flags
25
+ 0
26
+ end
27
+
28
+ def dup(options = {})
29
+ self.class.new(to_a, options)
30
+ end
31
+
32
+ def to_s(parent = false)
33
+ map { |e| e.to_s(parent) }.join('|')
34
+ end
35
+
36
+ def inspect #:nodoc:
37
+ to_s.inspect
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,4 @@
1
+ module Regin
2
+ class Anchor < Atom
3
+ end
4
+ end
@@ -0,0 +1,54 @@
1
+ module Regin
2
+ class Atom
3
+ attr_reader :value, :ignorecase
4
+
5
+ def initialize(value, options = {})
6
+ @value = value
7
+ @ignorecase = options[:ignorecase]
8
+ end
9
+
10
+ def option_names
11
+ %w( ignorecase )
12
+ end
13
+
14
+ # Returns true if expression could be treated as a literal string.
15
+ def literal?
16
+ false
17
+ end
18
+
19
+ def casefold?
20
+ ignorecase ? true : false
21
+ end
22
+
23
+ def dup(options = {})
24
+ original_options = option_names.inject({}) do |h, m|
25
+ h[m.to_sym] = send(m)
26
+ h
27
+ end
28
+ self.class.new(value, original_options.merge(options))
29
+ end
30
+
31
+ def to_s(parent = false)
32
+ "#{value}"
33
+ end
34
+
35
+ def inspect #:nodoc:
36
+ "#<#{self.class.to_s.sub('Regin::', '')} #{to_s.inspect}>"
37
+ end
38
+
39
+ def ==(other) #:nodoc:
40
+ case other
41
+ when String
42
+ other == to_s
43
+ else
44
+ eql?(other)
45
+ end
46
+ end
47
+
48
+ def eql?(other) #:nodoc:
49
+ other.instance_of?(self.class) &&
50
+ self.value.eql?(other.value) &&
51
+ (!!self.ignorecase).eql?(!!other.ignorecase)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,51 @@
1
+ module Regin
2
+ class Character < Atom
3
+ attr_reader :quantifier
4
+
5
+ def initialize(value, options = {})
6
+ @quantifier = options[:quantifier]
7
+ super
8
+ end
9
+
10
+ def option_names
11
+ %w( quantifier ) + super
12
+ end
13
+
14
+ # Returns true if expression could be treated as a literal string.
15
+ #
16
+ # A Character is literal is there is no quantifier attached to it.
17
+ def literal?
18
+ quantifier.nil? && !ignorecase
19
+ end
20
+
21
+ def to_s(parent = false)
22
+ if !parent && ignorecase
23
+ "(?i-mx:#{value})#{quantifier}"
24
+ else
25
+ "#{value}#{quantifier}"
26
+ end
27
+ end
28
+
29
+ def to_regexp(anchored = false)
30
+ re = to_s(true)
31
+ re = "\\A#{re}\\Z" if anchored
32
+ Regexp.compile(re, ignorecase)
33
+ end
34
+
35
+ def match(char)
36
+ to_regexp(true).match(char)
37
+ end
38
+
39
+ def include?(char)
40
+ if ignorecase
41
+ value.downcase == char.downcase
42
+ else
43
+ value == char
44
+ end
45
+ end
46
+
47
+ def eql?(other) #:nodoc:
48
+ super && quantifier.eql?(other.quantifier)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,50 @@
1
+ module Regin
2
+ class CharacterClass < Character
3
+ def initialize(value, options = {})
4
+ @negate = options[:negate]
5
+ super
6
+ end
7
+
8
+ def option_names
9
+ %w( negate ) + super
10
+ end
11
+
12
+ attr_reader :negate
13
+
14
+ def negated?
15
+ negate ? true : false
16
+ end
17
+
18
+ # Returns true if expression could be treated as a literal string.
19
+ #
20
+ # A CharacterClass is never literal.
21
+ def literal?
22
+ false
23
+ end
24
+
25
+ def bracketed?
26
+ value != '.' && value !~ /^\\[dDsSwW]$/
27
+ end
28
+
29
+ def to_s(parent = false)
30
+ if bracketed?
31
+ if !parent && ignorecase
32
+ "(?i-mx:[#{negate && '^'}#{value}])#{quantifier}"
33
+ else
34
+ "[#{negate && '^'}#{value}]#{quantifier}"
35
+ end
36
+ else
37
+ super
38
+ end
39
+ end
40
+
41
+ def include?(char)
42
+ re = quantifier ? to_s.sub(/#{Regexp.escape(quantifier)}$/, '') : to_s
43
+ Regexp.compile("\\A#{re}\\Z").match(char)
44
+ end
45
+
46
+ def eql?(other) #:nodoc:
47
+ super && negate == other.negate
48
+ end
49
+ end
50
+ end