require 'sass/script/lexer' module Sass module Script # The parser for SassScript. # It parses a string of code into a tree of {Script::Node}s. class Parser # The line number of the parser's current position. # # @return [Fixnum] def line @lexer.line end # The column number of the parser's current position. # # @return [Fixnum] def offset @lexer.offset end # @param str [String, StringScanner] The source text to parse # @param line [Fixnum] The line on which the SassScript appears. # Used for error reporting and sourcemap building # @param offset [Fixnum] The character (not byte) offset in the line on which the SassScript appears. # Used for error reporting and sourcemap building # @param options [{Symbol => Object}] An options hash; # see {file:SASS_REFERENCE.md#sass_options the Sass options documentation} def initialize(str, line, offset, options = {}) @options = options @lexer = lexer_class.new(str, line, offset, options) end # Parses a SassScript expression within an interpolated segment (`#{}`). # This means that it stops when it comes across an unmatched `}`, # which signals the end of an interpolated segment, # it returns rather than throwing an error. # # @return [Script::Node] The root node of the parse tree # @raise [Sass::SyntaxError] if the expression isn't valid SassScript def parse_interpolated start_pos = source_position expr = assert_expr :expr assert_tok :end_interpolation expr.options = @options expr.source_range = range(start_pos) expr rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parses a SassScript expression. # # @return [Script::Node] The root node of the parse tree # @raise [Sass::SyntaxError] if the expression isn't valid SassScript def parse start_pos = source_position expr = assert_expr :expr assert_done expr.options = @options expr.source_range = range(start_pos) expr rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parses a SassScript expression, # ending it when it encounters one of the given identifier tokens. # # @param [#include?(String)] A set of strings that delimit the expression. # @return [Script::Node] The root node of the parse tree # @raise [Sass::SyntaxError] if the expression isn't valid SassScript def parse_until(tokens) @stop_at = tokens expr = assert_expr :expr assert_done expr.options = @options expr rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parses the argument list for a mixin include. # # @return [(Array, {String => Script::Node}, Script::Node)] # The root nodes of the positional arguments, keyword arguments, and # splat argument. Keyword arguments are in a hash from names to values. # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript def parse_mixin_include_arglist args, keywords = [], {} if try_tok(:lparen) args, keywords, splat = mixin_arglist || [[], {}] assert_tok(:rparen) end assert_done args.each {|a| a.options = @options} keywords.each {|k, v| v.options = @options} splat.options = @options if splat return args, keywords, splat rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parses the argument list for a mixin definition. # # @return [(Array, Script::Node)] # The root nodes of the arguments, and the splat argument. # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript def parse_mixin_definition_arglist args, splat = defn_arglist!(false) assert_done args.each do |k, v| k.options = @options v.options = @options if v end splat.options = @options if splat return args, splat rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parses the argument list for a function definition. # # @return [(Array, Script::Node)] # The root nodes of the arguments, and the splat argument. # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript def parse_function_definition_arglist args, splat = defn_arglist!(true) assert_done args.each do |k, v| k.options = @options v.options = @options if v end splat.options = @options if splat return args, splat rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parse a single string value, possibly containing interpolation. # Doesn't assert that the scanner is finished after parsing. # # @return [Script::Node] The root node of the parse tree. # @raise [Sass::SyntaxError] if the string isn't valid SassScript def parse_string unless (peek = @lexer.peek) && (peek.type == :string || (peek.type == :funcall && peek.value.downcase == 'url')) lexer.expected!("string") end expr = assert_expr :funcall expr.options = @options @lexer.unpeek! expr rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parses a SassScript expression. # # @overload parse(str, line, offset, filename = nil) # @return [Script::Node] The root node of the parse tree # @see Parser#initialize # @see Parser#parse def self.parse(*args) new(*args).parse end PRECEDENCE = [ :comma, :single_eq, :space, :or, :and, [:eq, :neq], [:gt, :gte, :lt, :lte], [:plus, :minus], [:times, :div, :mod], ] ASSOCIATIVE = [:plus, :times] class << self # Returns an integer representing the precedence # of the given operator. # A lower integer indicates a looser binding. # # @private def precedence_of(op) PRECEDENCE.each_with_index do |e, i| return i if Array(e).include?(op) end raise "[BUG] Unknown operator #{op}" end # Returns whether or not the given operation is associative. # # @private def associative?(op) ASSOCIATIVE.include?(op) end private # Defines a simple left-associative production. # name is the name of the production, # sub is the name of the production beneath it, # and ops is a list of operators for this precedence level def production(name, sub, *ops) class_eval < "string", :default => "expression (e.g. 1px, bold)", :mixin_arglist => "mixin argument", :fn_arglist => "function argument", } def assert_expr(name, expected = nil) (e = send(name)) && (return e) @lexer.expected!(expected || EXPR_NAMES[name] || EXPR_NAMES[:default]) end def assert_tok(*names) (t = try_tok(*names)) && (return t) @lexer.expected!(names.map {|tok| Lexer::TOKEN_NAMES[tok] || tok}.join(" or ")) end def try_tok(*names) peeked = @lexer.peek peeked && names.include?(peeked.type) && @lexer.next end def assert_done return if @lexer.done? @lexer.expected!(EXPR_NAMES[:default]) end def node(node, start_pos = source_position, end_pos = nil) node.line = start_pos.line node.filename = @options[:filename] node.source_range = range(start_pos, end_pos) if end_pos node end end end end