rack-mount 0.0.1
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/LICENSE +20 -0
- data/README.rdoc +28 -0
- data/lib/rack/mount/analysis/frequency.rb +51 -0
- data/lib/rack/mount/analysis/histogram.rb +25 -0
- data/lib/rack/mount/analysis/splitting.rb +145 -0
- data/lib/rack/mount/const.rb +45 -0
- data/lib/rack/mount/exceptions.rb +3 -0
- data/lib/rack/mount/generatable_regexp.rb +163 -0
- data/lib/rack/mount/generation/route.rb +57 -0
- data/lib/rack/mount/generation/route_set.rb +163 -0
- data/lib/rack/mount/meta_method.rb +104 -0
- data/lib/rack/mount/mixover.rb +47 -0
- data/lib/rack/mount/multimap.rb +94 -0
- data/lib/rack/mount/prefix.rb +31 -0
- data/lib/rack/mount/recognition/code_generation.rb +99 -0
- data/lib/rack/mount/recognition/route.rb +59 -0
- data/lib/rack/mount/recognition/route_set.rb +88 -0
- data/lib/rack/mount/regexp_with_named_groups.rb +49 -0
- data/lib/rack/mount/route.rb +69 -0
- data/lib/rack/mount/route_set.rb +109 -0
- data/lib/rack/mount/strexp.rb +93 -0
- data/lib/rack/mount/utils.rb +271 -0
- data/lib/rack/mount/vendor/multimap/multimap.rb +466 -0
- data/lib/rack/mount/vendor/multimap/multiset.rb +153 -0
- data/lib/rack/mount/vendor/multimap/nested_multimap.rb +156 -0
- data/lib/rack/mount.rb +35 -0
- metadata +100 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rack/mount/utils'
|
2
|
+
|
3
|
+
module Rack::Mount
|
4
|
+
unless Const::SUPPORTS_NAMED_CAPTURES
|
5
|
+
# A wrapper that adds shim named capture support to older
|
6
|
+
# versions of Ruby.
|
7
|
+
#
|
8
|
+
# Because the named capture syntax causes a parse error, an
|
9
|
+
# alternate syntax is used to indicate named captures.
|
10
|
+
#
|
11
|
+
# Ruby 1.9+ named capture syntax:
|
12
|
+
#
|
13
|
+
# /(?<foo>[a-z]+)/
|
14
|
+
#
|
15
|
+
# Ruby 1.8 shim syntax:
|
16
|
+
#
|
17
|
+
# /(?:<foo>[a-z]+)/
|
18
|
+
class RegexpWithNamedGroups < Regexp
|
19
|
+
def self.new(regexp) #:nodoc:
|
20
|
+
if regexp.is_a?(RegexpWithNamedGroups)
|
21
|
+
regexp
|
22
|
+
else
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Wraps Regexp with named capture support.
|
28
|
+
def initialize(regexp)
|
29
|
+
regexp, @names = Utils.extract_named_captures(regexp)
|
30
|
+
@names.freeze
|
31
|
+
super(regexp)
|
32
|
+
end
|
33
|
+
|
34
|
+
def names
|
35
|
+
@names.dup
|
36
|
+
end
|
37
|
+
|
38
|
+
def named_captures
|
39
|
+
named_captures = {}
|
40
|
+
names.each_with_index { |n, i|
|
41
|
+
named_captures[n] = [i+1] if n
|
42
|
+
}
|
43
|
+
named_captures
|
44
|
+
end
|
45
|
+
end
|
46
|
+
else
|
47
|
+
RegexpWithNamedGroups = Regexp
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'rack/mount/generatable_regexp'
|
2
|
+
require 'rack/mount/regexp_with_named_groups'
|
3
|
+
require 'rack/mount/utils'
|
4
|
+
|
5
|
+
module Rack::Mount
|
6
|
+
# Route is an internal class used to wrap a single route attributes.
|
7
|
+
#
|
8
|
+
# Plugins should not depend on any method on this class or instantiate
|
9
|
+
# new Route objects. Instead use the factory method, RouteSet#add_route
|
10
|
+
# to create new routes and add them to the set.
|
11
|
+
class Route
|
12
|
+
extend Mixover
|
13
|
+
|
14
|
+
# Include generation and recognition concerns
|
15
|
+
include Generation::Route, Recognition::Route
|
16
|
+
|
17
|
+
# Valid rack application to call if conditions are met
|
18
|
+
attr_reader :app
|
19
|
+
|
20
|
+
# A hash of conditions to match against. Conditions may be expressed
|
21
|
+
# as strings or regexps to match against.
|
22
|
+
attr_reader :conditions
|
23
|
+
|
24
|
+
# A hash of values that always gets merged into the parameters hash
|
25
|
+
attr_reader :defaults
|
26
|
+
|
27
|
+
# Symbol identifier for the route used with named route generations
|
28
|
+
attr_reader :name
|
29
|
+
|
30
|
+
def initialize(set, app, conditions, defaults, name)
|
31
|
+
@set = set
|
32
|
+
|
33
|
+
unless app.respond_to?(:call)
|
34
|
+
raise ArgumentError, 'app must be a valid rack application' \
|
35
|
+
' and respond to call'
|
36
|
+
end
|
37
|
+
@app = app
|
38
|
+
|
39
|
+
@name = name.to_sym if name
|
40
|
+
@defaults = (defaults || {}).freeze
|
41
|
+
|
42
|
+
unless conditions.is_a?(Hash)
|
43
|
+
raise ArgumentError, 'conditions must be a Hash'
|
44
|
+
end
|
45
|
+
@conditions = {}
|
46
|
+
|
47
|
+
conditions.each do |method, pattern|
|
48
|
+
next unless method && pattern
|
49
|
+
|
50
|
+
unless @set.valid_conditions.include?(method)
|
51
|
+
raise ArgumentError, 'conditions may only include ' +
|
52
|
+
@set.valid_conditions.inspect
|
53
|
+
end
|
54
|
+
|
55
|
+
pattern = Regexp.compile("\\A#{Regexp.escape(pattern)}\\Z") if pattern.is_a?(String)
|
56
|
+
pattern = Utils.normalize_extended_expression(pattern)
|
57
|
+
pattern = RegexpWithNamedGroups.new(pattern)
|
58
|
+
pattern.extend(GeneratableRegexp::InstanceMethods)
|
59
|
+
@conditions[method] = pattern.freeze
|
60
|
+
end
|
61
|
+
|
62
|
+
@conditions.freeze
|
63
|
+
end
|
64
|
+
|
65
|
+
def inspect #:nodoc:
|
66
|
+
"#<#{self.class.name} @app=#{@app.inspect} @conditions=#{@conditions.inspect} @defaults=#{@defaults.inspect} @name=#{@name.inspect}>"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'rack/mount/multimap'
|
2
|
+
require 'rack/mount/route'
|
3
|
+
require 'rack/mount/utils'
|
4
|
+
|
5
|
+
module Rack::Mount
|
6
|
+
class RouteSet
|
7
|
+
extend Mixover
|
8
|
+
|
9
|
+
# Include generation and recognition concerns
|
10
|
+
include Generation::RouteSet, Recognition::RouteSet
|
11
|
+
include Recognition::CodeGeneration
|
12
|
+
|
13
|
+
# Initialize a new RouteSet without optimizations
|
14
|
+
def self.new_without_optimizations(*args, &block)
|
15
|
+
new_without_module(Recognition::CodeGeneration, *args, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Basic RouteSet initializer.
|
19
|
+
#
|
20
|
+
# If a block is given, the set is yielded and finalized.
|
21
|
+
#
|
22
|
+
# See other aspects for other valid options:
|
23
|
+
# - <tt>Generation::RouteSet.new</tt>
|
24
|
+
# - <tt>Recognition::RouteSet.new</tt>
|
25
|
+
def initialize(options = {}, &block)
|
26
|
+
@request_class = options.delete(:request_class) || Rack::Request
|
27
|
+
@routes = []
|
28
|
+
expire!
|
29
|
+
|
30
|
+
if block_given?
|
31
|
+
yield self
|
32
|
+
rehash
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Builder method to add a route to the set
|
37
|
+
#
|
38
|
+
# <tt>app</tt>:: A valid Rack app to call if the conditions are met.
|
39
|
+
# <tt>conditions</tt>:: A hash of conditions to match against.
|
40
|
+
# Conditions may be expressed as strings or
|
41
|
+
# regexps to match against.
|
42
|
+
# <tt>defaults</tt>:: A hash of values that always gets merged in
|
43
|
+
# <tt>name</tt>:: Symbol identifier for the route used with named
|
44
|
+
# route generations
|
45
|
+
def add_route(app, conditions = {}, defaults = {}, name = nil)
|
46
|
+
route = Route.new(self, app, conditions, defaults, name)
|
47
|
+
@routes << route
|
48
|
+
expire!
|
49
|
+
route
|
50
|
+
end
|
51
|
+
|
52
|
+
# See <tt>Recognition::RouteSet#call</tt>
|
53
|
+
def call(env)
|
54
|
+
raise NotImplementedError
|
55
|
+
end
|
56
|
+
|
57
|
+
# See <tt>Generation::RouteSet#url</tt>
|
58
|
+
def url(*args)
|
59
|
+
raise NotImplementedError
|
60
|
+
end
|
61
|
+
|
62
|
+
# See <tt>Generation::RouteSet#generate</tt>
|
63
|
+
def generate(*args)
|
64
|
+
raise NotImplementedError
|
65
|
+
end
|
66
|
+
|
67
|
+
# Number of routes in the set
|
68
|
+
def length
|
69
|
+
@routes.length
|
70
|
+
end
|
71
|
+
|
72
|
+
def rehash #:nodoc:
|
73
|
+
end
|
74
|
+
|
75
|
+
# Finalizes the set and builds optimized data structures. You *must*
|
76
|
+
# freeze the set before you can use <tt>call</tt> and <tt>url</tt>.
|
77
|
+
# So remember to call freeze after you are done adding routes.
|
78
|
+
def freeze
|
79
|
+
unless frozen?
|
80
|
+
rehash
|
81
|
+
@routes.each { |route| route.freeze }
|
82
|
+
@routes.freeze
|
83
|
+
end
|
84
|
+
|
85
|
+
super
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
def expire! #:nodoc:
|
90
|
+
end
|
91
|
+
|
92
|
+
# An internal helper method for constructing a nested set from
|
93
|
+
# the linear route set.
|
94
|
+
#
|
95
|
+
# build_nested_route_set([:request_method, :path_info]) { |route, method|
|
96
|
+
# route.send(method)
|
97
|
+
# }
|
98
|
+
def build_nested_route_set(keys, &block)
|
99
|
+
graph = Multimap.new
|
100
|
+
@routes.each_with_index do |route, index|
|
101
|
+
k = keys.map { |key| block.call(key, index) }
|
102
|
+
Utils.pop_trailing_nils!(k)
|
103
|
+
k.map! { |key| key || /.+/ }
|
104
|
+
graph[*k] = route
|
105
|
+
end
|
106
|
+
graph
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
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)
|
26
|
+
|
27
|
+
re = Regexp.escape(str)
|
28
|
+
requirements = requirements ? requirements.dup : {}
|
29
|
+
|
30
|
+
normalize_requirements!(requirements, separators)
|
31
|
+
parse_dynamic_segments!(re, requirements)
|
32
|
+
parse_optional_segments!(re)
|
33
|
+
|
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
|
49
|
+
end
|
50
|
+
requirements.default ||= separators.any? ?
|
51
|
+
"[^#{separators.join}]+" : '.+'
|
52
|
+
requirements
|
53
|
+
end
|
54
|
+
|
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)
|
70
|
+
end
|
71
|
+
|
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 + ')?'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
re << scanner.rest
|
86
|
+
str.replace(re)
|
87
|
+
end
|
88
|
+
|
89
|
+
def regexp_has_modifiers?(regexp)
|
90
|
+
regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
require 'rack/mount/regexp_with_named_groups'
|
2
|
+
require 'strscan'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Rack::Mount
|
6
|
+
# Private utility methods used throughout Rack::Mount.
|
7
|
+
#--
|
8
|
+
# This module is a trash can. Try to move these functions into
|
9
|
+
# more appropriate contexts.
|
10
|
+
#++
|
11
|
+
module Utils
|
12
|
+
# Normalizes URI path.
|
13
|
+
#
|
14
|
+
# Strips off trailing slash and ensures there is a leading slash.
|
15
|
+
#
|
16
|
+
# normalize_path("/foo") # => "/foo"
|
17
|
+
# normalize_path("/foo/") # => "/foo"
|
18
|
+
# normalize_path("foo") # => "/foo"
|
19
|
+
# normalize_path("") # => "/"
|
20
|
+
def normalize_path(path)
|
21
|
+
path = "/#{path}"
|
22
|
+
path.squeeze!(Const::SLASH)
|
23
|
+
path.sub!(%r{/+\Z}, Const::EMPTY_STRING)
|
24
|
+
path = Const::SLASH if path == Const::EMPTY_STRING
|
25
|
+
path
|
26
|
+
end
|
27
|
+
module_function :normalize_path
|
28
|
+
|
29
|
+
# Removes trailing nils from array.
|
30
|
+
#
|
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?
|
36
|
+
ary.pop
|
37
|
+
end
|
38
|
+
ary
|
39
|
+
end
|
40
|
+
module_function :pop_trailing_nils!
|
41
|
+
|
42
|
+
RESERVED_PCHAR = ':@&=+$,;%'
|
43
|
+
SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
|
44
|
+
if RUBY_VERSION >= '1.9'
|
45
|
+
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze
|
46
|
+
else
|
47
|
+
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
|
48
|
+
end
|
49
|
+
|
50
|
+
def escape_uri(uri)
|
51
|
+
URI.escape(uri.to_s, UNSAFE_PCHAR)
|
52
|
+
end
|
53
|
+
module_function :escape_uri
|
54
|
+
|
55
|
+
if ''.respond_to?(:force_encoding)
|
56
|
+
def unescape_uri(uri)
|
57
|
+
URI.unescape(uri).force_encoding('utf-8')
|
58
|
+
end
|
59
|
+
else
|
60
|
+
def unescape_uri(uri)
|
61
|
+
URI.unescape(uri)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
module_function :unescape_uri
|
65
|
+
|
66
|
+
# Taken from Rack 1.1.x to build nested query strings
|
67
|
+
def build_nested_query(value, prefix = nil) #:nodoc:
|
68
|
+
case value
|
69
|
+
when Array
|
70
|
+
value.map { |v|
|
71
|
+
build_nested_query(v, "#{prefix}[]")
|
72
|
+
}.join("&")
|
73
|
+
when Hash
|
74
|
+
value.map { |k, v|
|
75
|
+
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
|
76
|
+
}.join("&")
|
77
|
+
when String
|
78
|
+
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)
|
82
|
+
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
|
88
|
+
end
|
89
|
+
end
|
90
|
+
module_function :build_nested_query
|
91
|
+
|
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
|
+
# Determines whether the regexp must match the entire string.
|
103
|
+
#
|
104
|
+
# regexp_anchored?(/^foo$/) # => true
|
105
|
+
# regexp_anchored?(/foo/) # => false
|
106
|
+
# regexp_anchored?(/^foo/) # => false
|
107
|
+
# regexp_anchored?(/foo$/) # => false
|
108
|
+
def regexp_anchored?(regexp)
|
109
|
+
regexp.source =~ /\A(\\A|\^).*(\\Z|\$)\Z/ ? true : false
|
110
|
+
end
|
111
|
+
module_function :regexp_anchored?
|
112
|
+
|
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
|
+
|
128
|
+
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
|
170
|
+
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
|
206
|
+
|
207
|
+
def first_part
|
208
|
+
first.is_a?(Capture) ? first.first_part : first
|
209
|
+
end
|
210
|
+
|
211
|
+
def last_part
|
212
|
+
last.is_a?(Capture) ? last.last_part : last
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def extract_regexp_parts(regexp) #:nodoc:
|
217
|
+
unless regexp.is_a?(RegexpWithNamedGroups)
|
218
|
+
regexp = RegexpWithNamedGroups.new(regexp)
|
219
|
+
end
|
220
|
+
|
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] == '\\'
|
240
|
+
|
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!
|
258
|
+
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
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
stack.pop
|
268
|
+
end
|
269
|
+
module_function :extract_regexp_parts
|
270
|
+
end
|
271
|
+
end
|