2008-10-12 22:03:06 -04:00
|
|
|
require 'sass/script/lexer'
|
2008-10-11 07:00:55 -04:00
|
|
|
|
|
|
|
module Sass
|
2008-10-12 22:03:06 -04:00
|
|
|
module Script
|
2009-04-24 23:11:00 -04:00
|
|
|
# The parser for SassScript.
|
2009-04-25 05:00:36 -04:00
|
|
|
# It parses a string of code into a tree of {Script::Node}s.
|
2008-10-12 23:26:56 -04:00
|
|
|
class Parser
|
2009-04-24 23:11:00 -04:00
|
|
|
# @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
|
2009-12-01 21:00:35 -05:00
|
|
|
# @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.new(str, line, offset, options)
|
2008-10-11 07:00:55 -04:00
|
|
|
end
|
|
|
|
|
2009-04-24 23:11:00 -04:00
|
|
|
# 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.
|
|
|
|
#
|
2009-04-25 05:00:36 -04:00
|
|
|
# @return [Script::Node] The root node of the parse tree
|
2009-04-24 23:11:00 -04:00
|
|
|
# @raise [Sass::SyntaxError] if the expression isn't valid SassScript
|
2009-01-09 17:54:17 -05:00
|
|
|
def parse_interpolated
|
|
|
|
expr = assert_expr :expr
|
2009-02-26 05:30:37 -05:00
|
|
|
assert_tok :end_interpolation
|
2009-01-09 17:54:17 -05:00
|
|
|
expr
|
|
|
|
end
|
|
|
|
|
2009-04-24 23:11:00 -04:00
|
|
|
# Parses a SassScript expression.
|
|
|
|
#
|
2009-04-25 05:00:36 -04:00
|
|
|
# @return [Script::Node] The root node of the parse tree
|
2009-04-24 23:11:00 -04:00
|
|
|
# @raise [Sass::SyntaxError] if the expression isn't valid SassScript
|
2008-10-11 07:00:55 -04:00
|
|
|
def parse
|
2009-01-09 17:54:17 -05:00
|
|
|
expr = assert_expr :expr
|
2009-07-19 17:20:55 -04:00
|
|
|
assert_done
|
2009-01-09 17:54:17 -05:00
|
|
|
expr
|
2008-10-11 07:00:55 -04:00
|
|
|
end
|
|
|
|
|
2009-07-19 17:20:55 -04:00
|
|
|
# Parses the argument list for a mixin include.
|
|
|
|
#
|
|
|
|
# @return [Array<Script::Node>] The root nodes of the arguments.
|
|
|
|
# @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
|
|
|
|
def parse_mixin_include_arglist
|
|
|
|
args = []
|
|
|
|
|
|
|
|
if try_tok(:lparen)
|
|
|
|
args = arglist || args
|
|
|
|
assert_tok(:rparen)
|
|
|
|
end
|
|
|
|
assert_done
|
|
|
|
|
|
|
|
args
|
|
|
|
end
|
|
|
|
|
|
|
|
# Parses the argument list for a mixin definition.
|
|
|
|
#
|
|
|
|
# @return [Array<Script::Node>] The root nodes of the arguments.
|
|
|
|
# @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
|
|
|
|
def parse_mixin_definition_arglist
|
|
|
|
args = []
|
|
|
|
|
|
|
|
if try_tok(:lparen)
|
|
|
|
args = defn_arglist(false) || args
|
|
|
|
assert_tok(:rparen)
|
|
|
|
end
|
|
|
|
assert_done
|
|
|
|
|
|
|
|
args
|
|
|
|
end
|
|
|
|
|
2009-04-24 23:11:00 -04:00
|
|
|
# Parses a SassScript expression.
|
|
|
|
#
|
2009-05-13 23:29:49 -04:00
|
|
|
# @overload parse(str, line, offset, filename = nil)
|
2009-04-25 05:00:36 -04:00
|
|
|
# @return [Script::Node] The root node of the parse tree
|
2009-04-24 23:11:00 -04:00
|
|
|
# @see Parser#initialize
|
|
|
|
# @see Parser#parse
|
2008-10-11 07:00:55 -04:00
|
|
|
def self.parse(*args)
|
|
|
|
new(*args).parse
|
|
|
|
end
|
|
|
|
|
2009-04-24 23:11:00 -04:00
|
|
|
class << self
|
|
|
|
private
|
2008-10-11 07:00:55 -04:00
|
|
|
|
2009-04-24 23:11:00 -04:00
|
|
|
# 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 <<RUBY
|
|
|
|
def #{name}
|
|
|
|
return unless e = #{sub}
|
|
|
|
while tok = try_tok(#{ops.map {|o| o.inspect}.join(', ')})
|
|
|
|
e = Operation.new(e, assert_expr(#{sub.inspect}), tok.type)
|
|
|
|
end
|
|
|
|
e
|
2008-10-11 07:00:55 -04:00
|
|
|
end
|
|
|
|
RUBY
|
2009-04-24 23:11:00 -04:00
|
|
|
end
|
2008-10-11 07:00:55 -04:00
|
|
|
|
2009-04-24 23:11:00 -04:00
|
|
|
def unary(op, sub)
|
|
|
|
class_eval <<RUBY
|
|
|
|
def unary_#{op}
|
|
|
|
return #{sub} unless try_tok(:#{op})
|
|
|
|
UnaryOperation.new(assert_expr(:unary_#{op}), :#{op})
|
|
|
|
end
|
2008-10-11 07:00:55 -04:00
|
|
|
RUBY
|
2009-04-24 23:11:00 -04:00
|
|
|
end
|
2008-10-11 07:00:55 -04:00
|
|
|
end
|
|
|
|
|
2009-04-24 23:11:00 -04:00
|
|
|
private
|
|
|
|
|
2008-10-11 07:00:55 -04:00
|
|
|
production :expr, :concat, :comma
|
|
|
|
|
|
|
|
def concat
|
2008-10-11 17:13:39 -04:00
|
|
|
return unless e = or_expr
|
|
|
|
while sub = or_expr
|
2008-10-11 07:00:55 -04:00
|
|
|
e = Operation.new(e, sub, :concat)
|
|
|
|
end
|
|
|
|
e
|
|
|
|
end
|
|
|
|
|
2008-10-11 17:13:39 -04:00
|
|
|
production :or_expr, :and_expr, :or
|
|
|
|
production :and_expr, :eq_or_neq, :and
|
2008-10-11 17:39:28 -04:00
|
|
|
production :eq_or_neq, :relational, :eq, :neq
|
|
|
|
production :relational, :plus_or_minus, :gt, :gte, :lt, :lte
|
2008-10-11 07:00:55 -04:00
|
|
|
production :plus_or_minus, :times_div_or_mod, :plus, :minus
|
2008-10-11 17:13:39 -04:00
|
|
|
production :times_div_or_mod, :unary_minus, :times, :div, :mod
|
2008-10-11 07:00:55 -04:00
|
|
|
|
|
|
|
unary :minus, :unary_div
|
|
|
|
unary :div, :unary_not # For strings, so /foo/bar works
|
|
|
|
unary :not, :funcall
|
|
|
|
|
|
|
|
def funcall
|
|
|
|
return paren unless name = try_tok(:ident)
|
|
|
|
# An identifier without arguments is just a string
|
2008-12-09 11:58:58 -05:00
|
|
|
unless try_tok(:lparen)
|
2009-12-01 21:00:35 -05:00
|
|
|
filename = @options[:filename]
|
2008-12-10 12:47:47 -05:00
|
|
|
warn(<<END)
|
|
|
|
DEPRECATION WARNING:
|
2009-12-01 21:00:35 -05:00
|
|
|
On line #{name.line}, character #{name.offset}#{" of '#{filename}'" if filename}
|
2008-12-10 12:47:47 -05:00
|
|
|
Implicit strings have been deprecated and will be removed in version 2.4.
|
|
|
|
'#{name.value}' was not quoted. Please add double quotes (e.g. "#{name.value}").
|
|
|
|
END
|
2008-12-09 15:16:31 -05:00
|
|
|
Script::String.new(name.value)
|
2008-12-09 11:58:58 -05:00
|
|
|
else
|
|
|
|
args = arglist || []
|
|
|
|
assert_tok(:rparen)
|
2008-12-09 15:16:31 -05:00
|
|
|
Script::Funcall.new(name.value, args)
|
2008-12-09 11:58:58 -05:00
|
|
|
end
|
2008-10-11 07:00:55 -04:00
|
|
|
end
|
|
|
|
|
2009-07-19 17:20:55 -04:00
|
|
|
def defn_arglist(must_have_default)
|
|
|
|
return unless c = try_tok(:const)
|
|
|
|
var = Script::Variable.new(c.value)
|
|
|
|
if try_tok(:single_eq)
|
|
|
|
val = assert_expr(:concat)
|
|
|
|
elsif must_have_default
|
2009-09-13 17:24:02 -04:00
|
|
|
raise SyntaxError.new("Required argument #{var.inspect} must come before any optional arguments.")
|
2009-07-19 17:20:55 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
return [[var, val]] unless try_tok(:comma)
|
|
|
|
[[var, val], *defn_arglist(val)]
|
|
|
|
end
|
|
|
|
|
2008-10-11 07:00:55 -04:00
|
|
|
def arglist
|
|
|
|
return unless e = concat
|
|
|
|
return [e] unless try_tok(:comma)
|
|
|
|
[e, *arglist]
|
|
|
|
end
|
|
|
|
|
|
|
|
def paren
|
2008-10-12 22:03:06 -04:00
|
|
|
return variable unless try_tok(:lparen)
|
2008-10-11 07:00:55 -04:00
|
|
|
e = assert_expr(:expr)
|
|
|
|
assert_tok(:rparen)
|
|
|
|
return e
|
|
|
|
end
|
|
|
|
|
2008-10-12 22:03:06 -04:00
|
|
|
def variable
|
2009-01-09 17:54:17 -05:00
|
|
|
return string unless c = try_tok(:const)
|
2008-12-09 15:16:31 -05:00
|
|
|
Variable.new(c.value)
|
2008-10-11 07:00:55 -04:00
|
|
|
end
|
|
|
|
|
2009-01-09 17:54:17 -05:00
|
|
|
def string
|
|
|
|
return literal unless first = try_tok(:string)
|
|
|
|
return first.value unless try_tok(:begin_interpolation)
|
|
|
|
mid = parse_interpolated
|
2009-02-26 05:30:37 -05:00
|
|
|
last = assert_expr(:string)
|
|
|
|
Operation.new(first.value, Operation.new(mid, last, :plus), :plus)
|
2009-01-09 17:54:17 -05:00
|
|
|
end
|
|
|
|
|
2008-10-11 07:00:55 -04:00
|
|
|
def literal
|
2009-01-09 17:54:17 -05:00
|
|
|
(t = try_tok(:number, :color, :bool)) && (return t.value)
|
2008-10-11 07:00:55 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# It would be possible to have unified #assert and #try methods,
|
|
|
|
# but detecting the method/token difference turns out to be quite expensive.
|
|
|
|
|
|
|
|
def assert_expr(name)
|
|
|
|
(e = send(name)) && (return e)
|
2008-12-09 15:16:31 -05:00
|
|
|
raise Sass::SyntaxError.new("Expected expression, was #{@lexer.done? ? 'end of text' : "#{@lexer.peek.type} token"}.")
|
2008-10-11 07:00:55 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def assert_tok(*names)
|
|
|
|
(t = try_tok(*names)) && (return t)
|
2008-12-09 15:16:31 -05:00
|
|
|
raise Sass::SyntaxError.new("Expected #{names.join(' or ')} token, was #{@lexer.done? ? 'end of text' : "#{@lexer.peek.type} token"}.")
|
2008-10-11 07:00:55 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def try_tok(*names)
|
2008-10-14 12:34:43 -04:00
|
|
|
peeked = @lexer.peek
|
2009-02-26 05:30:37 -05:00
|
|
|
peeked && names.include?(peeked.type) && @lexer.next
|
2008-10-11 07:00:55 -04:00
|
|
|
end
|
2009-07-19 17:20:55 -04:00
|
|
|
|
|
|
|
def assert_done
|
|
|
|
return if @lexer.done?
|
|
|
|
raise Sass::SyntaxError.new("Unexpected #{@lexer.peek.type} token.")
|
|
|
|
end
|
2008-10-11 07:00:55 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|