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 |greedy: true, **options| t(payload, allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved, parametric: operator.parametric, separator: operator.separator, **options) end translate :with_look_ahead do |**options| lookahead = each_leaf.inject("") do |ahead, element| ahead + t(element, skip_optional: true, lookahead: ahead, greedy: false, no_captures: true, **options).to_s end lookahead << (at_end ? '$' : '/') t(head, lookahead: lookahead, **options) + 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(no_captures: true, **options)})" end # @return [String] regexp without the named capture # @!visibility private def pattern(capture: nil, **options) 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, greedy: true, **options) "#{string}#{qualifier || "+#{?? unless greedy}"}" end def with_lookahead(string, lookahead: nil, **options) lookahead ? "(?:(?!#{lookahead})#{string})" : string end def from_hash(hash, **options) pattern(capture: hash[name.to_sym], **options) end def from_array(array, **options) Regexp.union(*array.map { |e| pattern(capture: e, **options) }) 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] # Remove this line after fixing broken compatibility between 2.1 and 2.2 options.delete(:parametric) if options.has_key?(:parametric) parametric super(parametric: false, **options) end # @!visibility private def pattern(parametric: false, separator: nil, **options) register_param(parametric: parametric, separator: separator, **options) 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(allow_reserved: false, **options) allow_reserved ? '[\w\-\.~%\:/\?#\[\]@\!\$\&\'\(\)\*\+,;=]' : '[\w\-\.~%]' end # @!visibility private def register_param(parametric: false, split_params: nil, separator: nil, **options) 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, uri_decode: true, space_matches_plus: true, **options) 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, except: nil, **options) except &&= "(?!#{translate(except, no_captures: true, **options)}\\Z)" Regexp.new("#{except}#{translate(ast, **options)}") end end private_constant :Compiler end end