PageRenderTime 60ms CodeModel.GetById 11ms app.highlight 44ms RepoModel.GetById 1ms app.codeStats 0ms

/scalate-jruby/src/main/resources/haml-3.0.25/lib/sass/script/parser.rb

http://github.com/scalate/scalate
Ruby | 401 lines | 294 code | 49 blank | 58 comment | 29 complexity | 6d780790bfea25960cf994d4b458b0a9 MD5 | raw file
  1require 'sass/script/lexer'
  2
  3module Sass
  4  module Script
  5    # The parser for SassScript.
  6    # It parses a string of code into a tree of {Script::Node}s.
  7    class Parser
  8      # The line number of the parser's current position.
  9      #
 10      # @return [Fixnum]
 11      def line
 12        @lexer.line
 13      end
 14
 15      # @param str [String, StringScanner] The source text to parse
 16      # @param line [Fixnum] The line on which the SassScript appears.
 17      #   Used for error reporting
 18      # @param offset [Fixnum] The number of characters in on which the SassScript appears.
 19      #   Used for error reporting
 20      # @param options [{Symbol => Object}] An options hash;
 21      #   see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
 22      def initialize(str, line, offset, options = {})
 23        @options = options
 24        @lexer = lexer_class.new(str, line, offset, options)
 25      end
 26
 27      # Parses a SassScript expression within an interpolated segment (`#{}`).
 28      # This means that it stops when it comes across an unmatched `}`,
 29      # which signals the end of an interpolated segment,
 30      # it returns rather than throwing an error.
 31      #
 32      # @return [Script::Node] The root node of the parse tree
 33      # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
 34      def parse_interpolated
 35        expr = assert_expr :expr
 36        assert_tok :end_interpolation
 37        expr.options = @options
 38        expr
 39      rescue Sass::SyntaxError => e
 40        e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
 41        raise e
 42      end
 43
 44      # Parses a SassScript expression.
 45      #
 46      # @return [Script::Node] The root node of the parse tree
 47      # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
 48      def parse
 49        expr = assert_expr :expr
 50        assert_done
 51        expr.options = @options
 52        expr
 53      rescue Sass::SyntaxError => e
 54        e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
 55        raise e
 56      end
 57
 58      # Parses a SassScript expression,
 59      # ending it when it encounters one of the given identifier tokens.
 60      #
 61      # @param [#include?(String)] A set of strings that delimit the expression.
 62      # @return [Script::Node] The root node of the parse tree
 63      # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
 64      def parse_until(tokens)
 65        @stop_at = tokens
 66        expr = assert_expr :expr
 67        assert_done
 68        expr.options = @options
 69        expr
 70      rescue Sass::SyntaxError => e
 71        e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
 72        raise e
 73      end
 74
 75      # Parses the argument list for a mixin include.
 76      #
 77      # @return [Array<Script::Node>] The root nodes of the arguments.
 78      # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
 79      def parse_mixin_include_arglist
 80        args = []
 81
 82        if try_tok(:lparen)
 83          args = arglist || args
 84          assert_tok(:rparen)
 85        end
 86        assert_done
 87
 88        args.each {|a| a.options = @options}
 89        args
 90      rescue Sass::SyntaxError => e
 91        e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
 92        raise e
 93      end
 94
 95      # Parses the argument list for a mixin definition.
 96      #
 97      # @return [Array<Script::Node>] The root nodes of the arguments.
 98      # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
 99      def parse_mixin_definition_arglist
100        args = defn_arglist!(false)
101        assert_done
102
103        args.each do |k, v|
104          k.options = @options
105          v.options = @options if v
106        end
107        args
108      rescue Sass::SyntaxError => e
109        e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
110        raise e
111      end
112
113      # Parses a SassScript expression.
114      #
115      # @overload parse(str, line, offset, filename = nil)
116      # @return [Script::Node] The root node of the parse tree
117      # @see Parser#initialize
118      # @see Parser#parse
119      def self.parse(*args)
120        new(*args).parse
121      end
122
123      PRECEDENCE = [
124        :comma, :single_eq, :concat, :or, :and,
125        [:eq, :neq],
126        [:gt, :gte, :lt, :lte],
127        [:plus, :minus],
128        [:times, :div, :mod],
129      ]
130
131      ASSOCIATIVE = [:comma, :concat, :plus, :times]
132
133      class << self
134        # Returns an integer representing the precedence
135        # of the given operator.
136        # A lower integer indicates a looser binding.
137        #
138        # @private
139        def precedence_of(op)
140          PRECEDENCE.each_with_index do |e, i|
141            return i if Array(e).include?(op)
142          end
143          raise "[BUG] Unknown operator #{op}"
144        end
145
146        # Returns whether or not the given operation is associative.
147        #
148        # @private
149        def associative?(op)
150          ASSOCIATIVE.include?(op)
151        end
152
153        private
154
155        # Defines a simple left-associative production.
156        # name is the name of the production,
157        # sub is the name of the production beneath it,
158        # and ops is a list of operators for this precedence level
159        def production(name, sub, *ops)
160          class_eval <<RUBY
161            def #{name}
162              interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect}) and return interp
163              return unless e = #{sub}
164              while tok = try_tok(#{ops.map {|o| o.inspect}.join(', ')})
165                interp = try_op_before_interp(tok, e) and return interp
166                line = @lexer.line
167                e = Operation.new(e, assert_expr(#{sub.inspect}), tok.type)
168                e.line = line
169              end
170              e
171            end
172RUBY
173        end
174
175        def unary(op, sub)
176          class_eval <<RUBY
177            def unary_#{op}
178              return #{sub} unless tok = try_tok(:#{op})
179              interp = try_op_before_interp(tok) and return interp
180              line = @lexer.line 
181              op = UnaryOperation.new(assert_expr(:unary_#{op}), :#{op})
182              op.line = line
183              op
184            end
185RUBY
186        end
187      end
188
189      private
190
191      # @private
192      def lexer_class; Lexer; end
193
194      production :expr, :interpolation, :comma
195      production :equals, :interpolation, :single_eq
196
197      def try_op_before_interp(op, prev = nil)
198        return unless @lexer.peek && @lexer.peek.type == :begin_interpolation
199        wb = @lexer.whitespace?(op)
200        str = Script::String.new(Lexer::OPERATORS_REVERSE[op.type])
201        str.line = @lexer.line
202        interp = Script::Interpolation.new(prev, str, nil, wb, !:wa, :originally_text)
203        interp.line = @lexer.line
204        interpolation(interp)
205      end
206
207      def try_ops_after_interp(ops, name)
208        return unless @lexer.after_interpolation?
209        return unless op = try_tok(*ops)
210        interp = try_op_before_interp(op) and return interp
211
212        wa = @lexer.whitespace?
213        str = Script::String.new(Lexer::OPERATORS_REVERSE[op.type])
214        str.line = @lexer.line
215        interp = Script::Interpolation.new(nil, str, assert_expr(name), !:wb, wa, :originally_text)
216        interp.line = @lexer.line
217        return interp
218      end
219
220      def interpolation(first = concat)
221        e = first
222        while interp = try_tok(:begin_interpolation)
223          wb = @lexer.whitespace?(interp)
224          line = @lexer.line
225          mid = parse_interpolated
226          wa = @lexer.whitespace?
227          e = Script::Interpolation.new(e, mid, concat, wb, wa)
228          e.line = line
229        end
230        e
231      end
232
233      def concat
234        return unless e = or_expr
235        while sub = or_expr
236          e = node(Operation.new(e, sub, :concat))
237        end
238        e
239      end
240
241      production :or_expr, :and_expr, :or
242      production :and_expr, :eq_or_neq, :and
243      production :eq_or_neq, :relational, :eq, :neq
244      production :relational, :plus_or_minus, :gt, :gte, :lt, :lte
245      production :plus_or_minus, :times_div_or_mod, :plus, :minus
246      production :times_div_or_mod, :unary_plus, :times, :div, :mod
247
248      unary :plus, :unary_minus
249      unary :minus, :unary_div
250      unary :div, :unary_not # For strings, so /foo/bar works
251      unary :not, :ident
252
253      def ident
254        return funcall unless @lexer.peek && @lexer.peek.type == :ident
255        return if @stop_at && @stop_at.include?(@lexer.peek.value)
256
257        name = @lexer.next
258        if color = Color::HTML4_COLORS[name.value.downcase]
259          return node(Color.new(color))
260        end
261        node(Script::String.new(name.value, :identifier))
262      end
263
264      def funcall
265        return raw unless tok = try_tok(:funcall)
266        args = fn_arglist || []
267        assert_tok(:rparen)
268        node(Script::Funcall.new(tok.value, args))
269      end
270
271      def defn_arglist!(must_have_default)
272        return [] unless try_tok(:lparen)
273        return [] if try_tok(:rparen)
274        res = []
275        loop do
276          line = @lexer.line
277          offset = @lexer.offset + 1
278          c = assert_tok(:const)
279          var = Script::Variable.new(c.value)
280          if tok = (try_tok(:colon) || try_tok(:single_eq))
281            val = assert_expr(:concat)
282
283            if tok.type == :single_eq
284              val.context = :equals
285              val.options = @options
286              Script.equals_warning("mixin argument defaults", "$#{c.value}",
287                val.to_sass, false, line, offset, @options[:filename])
288            end
289            must_have_default = true
290          elsif must_have_default
291            raise SyntaxError.new("Required argument #{var.inspect} must come before any optional arguments.")
292          end
293          res << [var, val]
294          break unless try_tok(:comma)
295        end
296        assert_tok(:rparen)
297        res
298      end
299
300      def fn_arglist
301        return unless e = equals
302        return [e] unless try_tok(:comma)
303        [e, *assert_expr(:fn_arglist)]
304      end
305
306      def arglist
307        return unless e = interpolation
308        return [e] unless try_tok(:comma)
309        [e, *assert_expr(:arglist)]
310      end
311
312      def raw
313        return special_fun unless tok = try_tok(:raw)
314        node(Script::String.new(tok.value))
315      end
316
317      def special_fun
318        return paren unless tok = try_tok(:special_fun)
319        first = node(Script::String.new(tok.value.first))
320        Haml::Util.enum_slice(tok.value[1..-1], 2).inject(first) do |l, (i, r)|
321          Script::Interpolation.new(
322            l, i, r && node(Script::String.new(r)),
323            false, false)
324        end
325      end
326
327      def paren
328        return variable unless try_tok(:lparen)
329        was_in_parens = @in_parens
330        @in_parens = true
331        e = assert_expr(:expr)
332        assert_tok(:rparen)
333        return e
334      ensure
335        @in_parens = was_in_parens
336      end
337
338      def variable
339        return string unless c = try_tok(:const)
340        node(Variable.new(*c.value))
341      end
342
343      def string
344        return number unless first = try_tok(:string)
345        return first.value unless try_tok(:begin_interpolation)
346        line = @lexer.line
347        mid = parse_interpolated
348        last = assert_expr(:string)
349        interp = StringInterpolation.new(first.value, mid, last)
350        interp.line = line
351        interp
352      end
353
354      def number
355        return literal unless tok = try_tok(:number)
356        num = tok.value
357        num.original = num.to_s unless @in_parens
358        num
359      end
360
361      def literal
362        (t = try_tok(:color, :bool)) && (return t.value)
363      end
364
365      # It would be possible to have unified #assert and #try methods,
366      # but detecting the method/token difference turns out to be quite expensive.
367
368      EXPR_NAMES = {
369        :string => "string",
370        :default => "expression (e.g. 1px, bold)",
371        :arglist => "mixin argument",
372        :fn_arglist => "function argument",
373      }
374
375      def assert_expr(name)
376        (e = send(name)) && (return e)
377        @lexer.expected!(EXPR_NAMES[name] || EXPR_NAMES[:default])
378      end
379
380      def assert_tok(*names)
381        (t = try_tok(*names)) && (return t)
382        @lexer.expected!(names.map {|tok| Lexer::TOKEN_NAMES[tok] || tok}.join(" or "))
383      end
384
385      def try_tok(*names)
386        peeked =  @lexer.peek
387        peeked && names.include?(peeked.type) && @lexer.next
388      end
389
390      def assert_done
391        return if @lexer.done?
392        @lexer.expected!(EXPR_NAMES[:default])
393      end
394
395      def node(node)
396        node.line = @lexer.line
397        node
398      end
399    end
400  end
401end