File: compiler.rb

package info (click to toggle)
ruby-mustermann19 0.4.3%2Bgit20160621-1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 756 kB
  • ctags: 445
  • sloc: ruby: 7,197; makefile: 3
file content (174 lines) | stat: -rw-r--r-- 6,796 bytes parent folder | download
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