The parser for SassScript. It parses a string of code into a tree of {Script::Node}s.
It would be possible to have unified assert and try methods, but detecting the method/token difference turns out to be quite expensive.
Returns whether or not the given operation is associative.
@private
# File lib/sass/script/parser.rb, line 195 def associative?(op) ASSOCIATIVE.include?(op) end
@param str [String, StringScanner] The source text to parse @param line [Fixnum] The line on which the SassScript appears.
Used for error reporting
@param offset [Fixnum] The number of characters in on which the SassScript appears.
Used for error reporting
@param options [{Symbol => Object}] An options hash;
see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
# File lib/sass/script/parser.rb, line 22 def initialize(str, line, offset, options = {}) @options = options @lexer = lexer_class.new(str, line, offset, options) 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 #parse
# File lib/sass/script/parser.rb, line 165 def self.parse(*args) new(*args).parse end
Returns an integer representing the precedence of the given operator. A lower integer indicates a looser binding.
@private
# File lib/sass/script/parser.rb, line 185 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
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
# File lib/sass/script/parser.rb, line 205 def production(name, sub, *ops) class_eval " def #{name} interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect}) and return interp return unless e = #{sub} while tok = try_tok(#{ops.map {|o| o.inspect}.join(', ')}) if interp = try_op_before_interp(tok, e) return interp unless other_interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect}, interp) return other_interp end line = @lexer.line e = Operation.new(e, assert_expr(#{sub.inspect}), tok.type) e.line = line end e end ", __FILE__, __LINE__ + 1 end
# File lib/sass/script/parser.rb, line 225 def unary(op, sub) class_eval " def unary_#{op} return #{sub} unless tok = try_tok(:#{op}) interp = try_op_before_interp(tok) and return interp line = @lexer.line op = UnaryOperation.new(assert_expr(:unary_#{op}), :#{op}) op.line = line op end ", __FILE__, __LINE__ + 1 end
The line number of the parser's current position.
@return [Fixnum]
# File lib/sass/script/parser.rb, line 11 def line @lexer.line 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
# File lib/sass/script/parser.rb, line 48 def parse 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 function definition.
@return [(Array<Script::Node>, Script::Node)]
The root nodes of the arguments, and the splat argument.
@raise [Sass::SyntaxError] if the argument list isn't valid SassScript
# File lib/sass/script/parser.rb, line 123 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
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
# File lib/sass/script/parser.rb, line 34 def parse_interpolated expr = assert_expr :expr assert_tok :end_interpolation 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 definition.
@return [(Array<Script::Node>, Script::Node)]
The root nodes of the arguments, and the splat argument.
@raise [Sass::SyntaxError] if the argument list isn't valid SassScript
# File lib/sass/script/parser.rb, line 103 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 mixin include.
@return [(Array<Script::Node>, {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
# File lib/sass/script/parser.rb, line 81 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
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
# File lib/sass/script/parser.rb, line 143 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, 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
# File lib/sass/script/parser.rb, line 64 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
# File lib/sass/script/parser.rb, line 381 def arglist(subexpr, description) return unless e = send(subexpr) args = [] keywords = {} loop do if @lexer.peek && @lexer.peek.type == :colon name = e @lexer.expected!("comma") unless name.is_a?(Variable) assert_tok(:colon) value = assert_expr(subexpr, description) if keywords[name.underscored_name] raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once") end keywords[name.underscored_name] = value else if !keywords.empty? raise SyntaxError.new("Positional arguments must come before keyword arguments.") end return args, keywords, e if try_tok(:splat) args << e end return args, keywords unless try_tok(:comma) e = assert_expr(subexpr, description) end end
# File lib/sass/script/parser.rb, line 491 def assert_done return if @lexer.done? @lexer.expected!(EXPR_NAMES[:default]) end
# File lib/sass/script/parser.rb, line 476 def assert_expr(name, expected = nil) (e = send(name)) && (return e) @lexer.expected!(expected || EXPR_NAMES[name] || EXPR_NAMES[:default]) end
# File lib/sass/script/parser.rb, line 481 def assert_tok(*names) (t = try_tok(*names)) && (return t) @lexer.expected!(names.map {|tok| Lexer::TOKEN_NAMES[tok] || tok}.join(" or ")) end
# File lib/sass/script/parser.rb, line 343 def defn_arglist!(must_have_parens) if must_have_parens assert_tok(:lparen) else return [], nil unless try_tok(:lparen) end return [], nil if try_tok(:rparen) res = [] splat = nil must_have_default = false loop do c = assert_tok(:const) var = Script::Variable.new(c.value) if try_tok(:colon) val = assert_expr(:space) must_have_default = true elsif must_have_default raise SyntaxError.new("Required argument #{var.inspect} must come before any optional arguments.") elsif try_tok(:splat) splat = var break end res << [var, val] break unless try_tok(:comma) end assert_tok(:rparen) return res, splat end
# File lib/sass/script/parser.rb, line 244 def expr line = @lexer.line return unless e = interpolation list = node(List.new([e], :comma), line) while tok = try_tok(:comma) if interp = try_op_before_interp(tok, list) return interp unless other_interp = try_ops_after_interp([:comma], :expr, interp) return other_interp end list.value << assert_expr(:interpolation) end list.value.size == 1 ? list.value.first : list end
# File lib/sass/script/parser.rb, line 373 def fn_arglist arglist(:equals, "function argument") end
# File lib/sass/script/parser.rb, line 336 def funcall return raw unless tok = try_tok(:funcall) args, keywords, splat = fn_arglist || [[], {}] assert_tok(:rparen) node(Script::Funcall.new(tok.value, args, keywords, splat)) end
# File lib/sass/script/parser.rb, line 318 def ident return funcall unless @lexer.peek && @lexer.peek.type == :ident return if @stop_at && @stop_at.include?(@lexer.peek.value) name = @lexer.next if color = Color::COLOR_NAMES[name.value.downcase] node(Color.new(color)) elsif name.value == "true" node(Script::Bool.new(true)) elsif name.value == "false" node(Script::Bool.new(false)) elsif name.value == "null" node(Script::Null.new) else node(Script::String.new(name.value, :identifier)) end end
# File lib/sass/script/parser.rb, line 283 def interpolation(first = space) e = first while interp = try_tok(:begin_interpolation) wb = @lexer.whitespace?(interp) line = @lexer.line mid = parse_interpolated wa = @lexer.whitespace? e = Script::Interpolation.new(e, mid, space, wb, wa) e.line = line end e end
@private
# File lib/sass/script/parser.rb, line 242 def lexer_class; Lexer; end
# File lib/sass/script/parser.rb, line 462 def literal (t = try_tok(:color)) && (return t.value) end
# File lib/sass/script/parser.rb, line 377 def mixin_arglist arglist(:interpolation, "mixin argument") end
# File lib/sass/script/parser.rb, line 496 def node(node, line = @lexer.line) node.line = line node end
# File lib/sass/script/parser.rb, line 455 def number return literal unless tok = try_tok(:number) num = tok.value num.original = num.to_s unless @in_parens num end
# File lib/sass/script/parser.rb, line 427 def paren return variable unless try_tok(:lparen) was_in_parens = @in_parens @in_parens = true line = @lexer.line e = expr assert_tok(:rparen) return e || node(List.new([], :space), line) ensure @in_parens = was_in_parens end
# File lib/sass/script/parser.rb, line 412 def raw return special_fun unless tok = try_tok(:raw) node(Script::String.new(tok.value)) end
# File lib/sass/script/parser.rb, line 296 def space line = @lexer.line return unless e = or_expr arr = [e] while e = or_expr arr << e end arr.size == 1 ? arr.first : node(List.new(arr, :space), line) end
# File lib/sass/script/parser.rb, line 417 def special_fun return paren unless tok = try_tok(:special_fun) first = node(Script::String.new(tok.value.first)) Sass::Util.enum_slice(tok.value[1..-1], 2).inject(first) do |l, (i, r)| Script::Interpolation.new( l, i, r && node(Script::String.new(r)), false, false) end end
# File lib/sass/script/parser.rb, line 444 def string return number unless first = try_tok(:string) return first.value unless try_tok(:begin_interpolation) line = @lexer.line mid = parse_interpolated last = assert_expr(:string) interp = StringInterpolation.new(first.value, mid, last) interp.line = line interp end
# File lib/sass/script/parser.rb, line 260 def try_op_before_interp(op, prev = nil) return unless @lexer.peek && @lexer.peek.type == :begin_interpolation wb = @lexer.whitespace?(op) str = Script::String.new(Lexer::OPERATORS_REVERSE[op.type]) str.line = @lexer.line interp = Script::Interpolation.new(prev, str, nil, wb, !:wa, :originally_text) interp.line = @lexer.line interpolation(interp) end
# File lib/sass/script/parser.rb, line 270 def try_ops_after_interp(ops, name, prev = nil) return unless @lexer.after_interpolation? return unless op = try_tok(*ops) interp = try_op_before_interp(op, prev) and return interp wa = @lexer.whitespace? str = Script::String.new(Lexer::OPERATORS_REVERSE[op.type]) str.line = @lexer.line interp = Script::Interpolation.new(prev, str, assert_expr(name), !:wb, wa, :originally_text) interp.line = @lexer.line return interp end
# File lib/sass/script/parser.rb, line 486 def try_tok(*names) peeked = @lexer.peek peeked && names.include?(peeked.type) && @lexer.next end
# File lib/sass/script/parser.rb, line 439 def variable return string unless c = try_tok(:const) node(Variable.new(*c.value)) end