File: parser.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 (254 lines) | stat: -rw-r--r-- 8,711 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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
require 'mustermann/ast/node'
require 'forwardable'
require 'strscan'

module Mustermann
  # @see Mustermann::AST::Pattern
  module AST
    # Simple, StringScanner based parser.
    # @!visibility private
    class Parser
      # @param [String] string to be parsed
      # @return [Mustermann::AST::Node] parse tree for string
      # @!visibility private
      def self.parse(string, options = {})
        new(options).parse(string)
      end

      # Defines another grammar rule for first character.
      #
      # @see Mustermann::Rails
      # @see Mustermann::Sinatra
      # @see Mustermann::Template
      # @!visibility private
      def self.on(*chars, &block)
        chars.each do |char|
          define_method("read %p" % char, &block)
        end
      end

      # Defines another grammar rule for a suffix.
      #
      # @see Mustermann::Sinatra
      # @!visibility private
      def self.suffix(pattern = /./, options = {}, &block)
        after = options[:after] || :node
        @suffix ||= []
        @suffix << [pattern, after, block] if block
        @suffix
      end

      # @!visibility private
      attr_reader :buffer, :string, :pattern

      extend Forwardable
      def_delegators :buffer, :eos?, :getch, :pos

      # @!visibility private
      def initialize(options = {})
        pattern = options.delete(:pattern)
        @pattern = pattern
      end

      # @param [String] string to be parsed
      # @return [Mustermann::AST::Node] parse tree for string
      # @!visibility private
      def parse(string)
        @string = string
        @buffer = ::StringScanner.new(string)
        node(:root, string) { read unless eos? }
      end

      # @example
      #   node(:char, 'x').compile =~ 'x' # => true
      #
      # @param [Symbol] type node type
      # @return [Mustermann::AST::Node]
      # @!visibility private
      def node(type, *args, &block)
        type  = Node[type] unless type.respond_to? :new
        start = pos
        node  = block ? type.parse(*args, &block) : type.new(*args)
        min_size(start, pos, node)
      end

      # Create a node for a character we don't have an explicit rule for.
      #
      # @param [String] char the character
      # @return [Mustermann::AST::Node] the node
      # @!visibility private
      def default_node(char)
        char == ?/ ? node(:separator, char) : node(:char, char)
      end

      # Reads the next element from the buffer.
      # @return [Mustermann::AST::Node] next element
      # @!visibility private
      def read
        start  = pos
        char   = getch
        method = "read %p" % char
        element= respond_to?(method) ? send(method, char) : default_node(char)
        min_size(start, pos, element)
        read_suffix(element)
      end

      # sets start on node to start if it's not set to a lower value.
      # sets stop on node to stop if it's not set to a higher value.
      # @return [Mustermann::AST::Node] the node passed as third argument
      # @!visibility private
      def min_size(start, stop, node)
        stop  ||= start
        start ||= stop
        node.start = start unless node.start and node.start < start
        node.stop  = stop  unless node.stop  and node.stop  > stop
        node
      end

      # Checks for a potential suffix on the buffer.
      # @param [Mustermann::AST::Node] element node without suffix
      # @return [Mustermann::AST::Node] node with suffix
      # @!visibility private
      def read_suffix(element)
        self.class.suffix.inject(element) do |ele, (regexp, after, callback)|
          next ele unless ele.is_a?(after) and payload = scan(regexp)
          content = instance_exec(payload, ele, &callback)
          min_size(element.start, pos, content)
        end
      end

      # Wrapper around {StringScanner#scan} that turns strings into escaped
      # regular expressions and returns a MatchData if the regexp has any
      # named captures.
      #
      # @param [Regexp, String] regexp
      # @see StringScanner#scan
      # @return [String, MatchData, nil]
      # @!visibility private
      def scan(regexp)
        regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp
        string = buffer.scan(regexp)
        regexp.names.any? ? regexp.match(string) : string
      end

      # Asserts a regular expression matches what's next on the buffer.
      # Will return corresponding MatchData if regexp includes named captures.
      #
      # @param [Regexp] regexp expected to match
      # @return [String, MatchData] the match
      # @raise [Mustermann::ParseError] if expectation wasn't met
      # @!visibility private
      def expect(regexp, options = {})
        char = options.delete(:char) || nil
        scan(regexp) || unexpected(char, options)
      end

      # Allows to read a string inside brackets. It does not expect the string
      # to start with an opening bracket.
      #
      # @example
      #   buffer.string = "fo<o>>ba<r>"
      #   read_brackets(?<, ?>) # => "fo<o>"
      #   buffer.rest # => "ba<r>"
      #
      # @!visibility private
      def read_brackets(open, close, options = {})
        char = options.delete(:char) || nil
        escape = options.delete(:escape) || '\\'
        quote = options.delete(:quote) || false
        result = ""
        escape = false if escape.nil?
        while current = getch
          case current
          when close  then return result
          when open   then result << open   << read_brackets(open, close) << close
          when escape then result << escape << getch
          else result << current
          end
        end
        unexpected(char, options)
      end


      # Reads an argument string of the format arg1,args2,key:value
      #
      # @!visibility private
      def read_args(key_separator, close, options = {})
        separator = options.delete(:separator) || ?,
        symbol_keys = options.fetch(:symbol_keys, true)
        options.delete(:symbol_keys)
        list, map = [], {}
        while buffer.peek(1) != close
          scan(separator)
          entries = read_list(close, separator, options.merge(separator: key_separator))
          case entries.size
          when 1 then list += entries
          when 2 then map[symbol_keys ? entries.first.to_sym : entries.first] = entries.last
          else        unexpected(key_separator)
          end
          buffer.pos -= 1
        end
        expect(close)
        [list, map]
      end

      # Reads a separated list with the ability to quote, escape and add spaces.
      #
      # @!visibility private
      #def read_list(*close, separator: ?,, escape: ?\\, quotes: [?", ?'], ignore: " ", options)
      def read_list(*close)
        options = close.last.kind_of?(Hash) ? close.pop : {}
        separator = options.delete(:separator) || ?,
        escape    = options.delete(:escape) || '\\'
        quotes    = options.delete(:quotes) || [?", ?']
        ignore    = options.delete(:ignore) || " "
        result = []
        while current = getch
          element = result.empty? ? result : result.last
          case current
          when *close    then return result
          when ignore    then nil # do nothing
          when separator then result  << ""
          when escape    then element << getch
          when *quotes   then element << read_escaped(current, escape: escape)
          else element << current
          end
        end
        unexpected(current, options)
      end

      # Read a string until a terminating character, ignoring escaped versions of said character.
      #
      # @!visibility private
      #def read_escaped(close, escape: ?\\, **options)
      def read_escaped(close, options = {})
        escape = options.delete(:escape) || '\\'
        result = ""
        while current = getch
          case current
          when close  then return result
          when escape then result << getch
          else result << current
          end
        end
        unexpected(current, options)
      end

      # Helper for raising an exception for an unexpected character.
      # Will read character from buffer if buffer is passed in.
      #
      # @param [String, nil] char the unexpected character
      # @raise [Mustermann::ParseError, Exception]
      # @!visibility private
      def unexpected(char = nil, options = {})
        options, char = char, nil if char.is_a?(Hash)
        char ||= getch
        exception = options.fetch(:exception, ParseError)
        char = "space" if char == " "
        raise exception, "unexpected #{char || "end of string"} while parsing #{string.inspect}"
      end
    end

    #private_constant :Parser
  end
end