1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
|
require 'mustermann/ast/translator'
module Mustermann
# @see Mustermann::AST::Pattern
module AST
# Regexp compilation logic.
# @!visibility private
class Compiler < Translator
raises CompileError
# Trivial compilations
translate(Array) { |o = {}| map { |e| t(e, o) }.join }
translate(:node) { |o = {}| t(payload, o) }
translate(:separator) { |o = {}| Regexp.escape(payload) }
translate(:optional) { |o = {}| "(?:%s)?" % t(payload, o) }
translate(:char) { |o = {}| t.encoded(payload, o) }
translate :union do |options = {}|
"(?:%s)" % payload.map { |e| "(?:%s)" % t(e, options) }.join(?|)
end
translate :expression do |options = {}|
greedy = options.fetch(:greedy, true)
t(payload, options.merge(allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved,
parametric: operator.parametric, separator: operator.separator))
end
translate :with_look_ahead do |options = {}|
lookahead = each_leaf.inject("") do |ahead, element|
ahead + t(element, options.merge(skip_optional: true, lookahead: ahead, greedy: false, no_captures: true)).to_s
end
lookahead << (at_end ? '$' : '/')
t(head, options.merge(lookahead: lookahead)) + t(payload, options)
end
# Capture compilation is complex. :(
# @!visibility private
class Capture < NodeTranslator
register :capture
# @!visibility private
def translate(options = {})
return pattern(options) if options[:no_captures]
"(?<#{name}>#{translate(options.merge(no_captures: true))})"
end
# @return [String] regexp without the named capture
# @!visibility private
def pattern(options = {})
capture = options.delete(:capture)
case capture
when Symbol then from_symbol(capture, options)
when Array then from_array(capture, options)
when Hash then from_hash(capture, options)
when String then from_string(capture, options)
when nil then from_nil(options)
else capture
end
end
private
def qualified(string, options = {})
greedy = options.fetch(:greedy, true)
"#{string}#{qualifier || "+#{?? unless greedy}"}"
end
def with_lookahead(string, options = {})
lookahead = options.delete(:lookahead)
lookahead ? "(?:(?!#{lookahead})#{string})" : string
end
def from_hash(hash, options = {}) pattern(options.merge(capture: hash[name.to_sym])) end
def from_array(array, options = {}) Regexp.union(*array.map { |e| pattern(options.merge(capture: e)) }) end
def from_symbol(symbol, options = {}) qualified(with_lookahead("[[:#{symbol}:]]", options), options) end
def from_string(string, options = {}) Regexp.new(string.chars.map { |c| t.encoded(c, options) }.join) end
def from_nil(options = {}) qualified(with_lookahead(default(options), options), options) end
def default(options = {}) constraint || "[^/\\?#]" end
end
# @!visibility private
class Splat < Capture
register :splat, :named_splat
# splats are always non-greedy
# @!visibility private
def pattern(options = {})
constraint || ".*?"
end
end
# @!visibility private
class Variable < Capture
register :variable
# @!visibility private
def translate(options = {})
return super(options) if explode or not options[:parametric]
parametric super(options.merge(parametric: false))
end
# @!visibility private
def pattern(options = {})
parametric = options.delete(:parametric) || false
separator = options.delete(:separator)
register_param(options.merge(parametric: parametric, separator: separator))
pattern = super(options)
pattern = parametric(pattern) if parametric
pattern = "#{pattern}(?:#{Regexp.escape(separator)}#{pattern})*" if explode and separator
pattern
end
# @!visibility private
def parametric(string)
"#{Regexp.escape(name)}(?:=#{string})?"
end
# @!visibility private
def qualified(string, options = {})
prefix ? "#{string}{1,#{prefix}}" : super(string, options)
end
# @!visibility private
def default(options = {})
allow_reserved = options.delete(:allow_reserved) || false
allow_reserved ? '[\w\-\.~%\:/\?#\[\]@\!\$\&\'\(\)\*\+,;=]' : '[\w\-\.~%]'
end
# @!visibility private
def register_param(options = {})
parametric = options.has_key?(:parametric) ? options.delete(:parametric) : false
split_params = options.delete(:split_params)
separator = options.delete(:separator)
return unless explode and split_params
split_params[name] = { separator: separator, parametric: parametric }
end
end
# @return [String] Regular expression for matching the given character in all representations
# @!visibility private
def encoded(char, options ={})
uri_decode = options.fetch(:uri_decode, true)
space_matches_plus = options.fetch(:space_matches_plus, true)
return Regexp.escape(char) unless uri_decode
encoded = escape(char, escape: /./)
list = [escape(char), encoded.downcase, encoded.upcase].uniq.map { |c| Regexp.escape(c) }
if char == " "
list << encoded('+') if space_matches_plus
list << " "
end
"(?:%s)" % list.join("|")
end
# Compiles an AST to a regular expression.
# @param [Mustermann::AST::Node] ast the tree
# @return [Regexp] corresponding regular expression.
#
# @!visibility private
def self.compile(ast, options = {})
new.compile(ast, options)
end
# Compiles an AST to a regular expression.
# @param [Mustermann::AST::Node] ast the tree
# @return [Regexp] corresponding regular expression.
#
# @!visibility private
def compile(ast, options = {})
except = options.delete(:except)
except &&= "(?!#{translate(except, options.merge(no_captures: true))}\\Z)"
Regexp.new("#{except}#{translate(ast, options)}")
end
end
#private_constant :Compiler
end
end
|