| require 'sass/script/lexer' |
| |
| module Sass |
| module Script |
| # The parser for SassScript. |
| # It parses a string of code into a tree of {Script::Tree::Node}s. |
| class Parser |
| # The line number of the parser's current position. |
| # |
| # @return [Integer] |
| def line |
| @lexer.line |
| end |
| |
| # The column number of the parser's current position. |
| # |
| # @return [Integer] |
| def offset |
| @lexer.offset |
| end |
| |
| # @param str [String, StringScanner] The source text to parse |
| # @param line [Integer] The line on which the SassScript appears. |
| # Used for error reporting and sourcemap building |
| # @param offset [Integer] The character (not byte) offset where the script starts in the line. |
| # Used for error reporting and sourcemap building |
| # @param options [{Symbol => Object}] An options hash; see |
| # {file:SASS_REFERENCE.md#Options the Sass options documentation}. |
| # This supports an additional `:allow_extra_text` option that controls |
| # whether the parser throws an error when extra text is encountered |
| # after the parsed construct. |
| def initialize(str, line, offset, options = {}) |
| @options = options |
| @allow_extra_text = options.delete(:allow_extra_text) |
| @lexer = lexer_class.new(str, line, offset, options) |
| @stop_at = nil |
| 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. |
| # |
| # @param warn_for_color [Boolean] Whether raw color values passed to |
| # interoplation should cause a warning. |
| # @return [Script::Tree::Node] The root node of the parse tree |
| # @raise [Sass::SyntaxError] if the expression isn't valid SassScript |
| def parse_interpolated(warn_for_color = false) |
| # Start two characters back to compensate for #{ |
| start_pos = Sass::Source::Position.new(line, offset - 2) |
| expr = assert_expr :expr |
| assert_tok :end_interpolation |
| expr = Sass::Script::Tree::Interpolation.new( |
| nil, expr, nil, false, false, :warn_for_color => warn_for_color) |
| check_for_interpolation expr |
| expr.options = @options |
| node(expr, start_pos) |
| rescue Sass::SyntaxError => e |
| e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] |
| raise e |
| end |
| |
| # Parses a SassScript expression. |
| # |
| # @return [Script::Tree::Node] The root node of the parse tree |
| # @raise [Sass::SyntaxError] if the expression isn't valid SassScript |
| def parse |
| expr = assert_expr :expr |
| assert_done |
| expr.options = @options |
| check_for_interpolation expr |
| 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 tokens [#include?(String)] A set of strings that delimit the expression. |
| # @return [Script::Tree::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 |
| check_for_interpolation expr |
| 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<Script::Tree::Node>, |
| # {String => Script::Tree::Node}, |
| # Script::Tree::Node, |
| # Script::Tree::Node)] |
| # The root nodes of the positional arguments, keyword arguments, and |
| # splat argument(s). 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, kwarg_splat = mixin_arglist |
| assert_tok(:rparen) |
| end |
| assert_done |
| |
| args.each do |a| |
| check_for_interpolation a |
| a.options = @options |
| end |
| |
| keywords.each do |_, v| |
| check_for_interpolation v |
| v.options = @options |
| end |
| |
| if splat |
| check_for_interpolation splat |
| splat.options = @options |
| end |
| |
| if kwarg_splat |
| check_for_interpolation kwarg_splat |
| kwarg_splat.options = @options |
| end |
| |
| return args, keywords, splat, kwarg_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::Tree::Node>, Script::Tree::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| |
| check_for_interpolation k |
| k.options = @options |
| |
| if v |
| check_for_interpolation v |
| v.options = @options |
| end |
| end |
| |
| if splat |
| check_for_interpolation splat |
| splat.options = @options |
| end |
| |
| 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::Tree::Node>, Script::Tree::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| |
| check_for_interpolation k |
| k.options = @options |
| |
| if v |
| check_for_interpolation v |
| v.options = @options |
| end |
| end |
| |
| if splat |
| check_for_interpolation splat |
| splat.options = @options |
| end |
| |
| 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::Tree::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 |
| check_for_interpolation expr |
| 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::Tree::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.inspect}" |
| 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 <<RUBY, __FILE__, __LINE__ + 1 |
| def #{name} |
| interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect}) |
| return interp if interp |
| return unless e = #{sub} |
| while tok = try_toks(#{ops.map {|o| o.inspect}.join(', ')}) |
| if interp = try_op_before_interp(tok, e) |
| other_interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect}, interp) |
| return interp unless other_interp |
| return other_interp |
| end |
| |
| e = node(Tree::Operation.new(e, assert_expr(#{sub.inspect}), tok.type), |
| e.source_range.start_pos) |
| end |
| e |
| end |
| RUBY |
| end |
| |
| def unary(op, sub) |
| class_eval <<RUBY, __FILE__, __LINE__ + 1 |
| def unary_#{op} |
| return #{sub} unless tok = try_tok(:#{op}) |
| interp = try_op_before_interp(tok) |
| return interp if interp |
| start_pos = source_position |
| node(Tree::UnaryOperation.new(assert_expr(:unary_#{op}), :#{op}), start_pos) |
| end |
| RUBY |
| end |
| end |
| |
| private |
| |
| def source_position |
| Sass::Source::Position.new(line, offset) |
| end |
| |
| def range(start_pos, end_pos = source_position) |
| Sass::Source::Range.new(start_pos, end_pos, @options[:filename], @options[:importer]) |
| end |
| |
| # @private |
| def lexer_class; Lexer; end |
| |
| def map |
| start_pos = source_position |
| e = interpolation |
| return unless e |
| return list e, start_pos unless @lexer.peek && @lexer.peek.type == :colon |
| |
| pair = map_pair(e) |
| map = node(Sass::Script::Tree::MapLiteral.new([pair]), start_pos) |
| while try_tok(:comma) |
| pair = map_pair |
| return map unless pair |
| map.pairs << pair |
| end |
| map |
| end |
| |
| def map_pair(key = nil) |
| return unless key ||= interpolation |
| assert_tok :colon |
| return key, assert_expr(:interpolation) |
| end |
| |
| def expr |
| start_pos = source_position |
| e = interpolation |
| return unless e |
| list e, start_pos |
| end |
| |
| def list(first, start_pos) |
| return first unless @lexer.peek && @lexer.peek.type == :comma |
| |
| list = node(Sass::Script::Tree::ListLiteral.new([first], separator: :comma), start_pos) |
| while (tok = try_tok(:comma)) |
| element_before_interp = list.elements.length == 1 ? list.elements.first : list |
| if (interp = try_op_before_interp(tok, element_before_interp)) |
| other_interp = try_ops_after_interp([:comma], :expr, interp) |
| return interp unless other_interp |
| return other_interp |
| end |
| return list unless (e = interpolation) |
| list.elements << e |
| list.source_range.end_pos = list.elements.last.source_range.end_pos |
| end |
| list |
| end |
| |
| production :equals, :interpolation, :single_eq |
| |
| def try_op_before_interp(op, prev = nil, after_interp = false) |
| return unless @lexer.peek && @lexer.peek.type == :begin_interpolation |
| unary = !prev && !after_interp |
| wb = @lexer.whitespace?(op) |
| str = literal_node(Script::Value::String.new(Lexer::OPERATORS_REVERSE[op.type]), |
| op.source_range) |
| |
| deprecation = |
| case op.type |
| when :comma; :potential |
| when :div, :single_eq; :none |
| when :plus; unary ? :none : :immediate |
| when :minus; @lexer.whitespace?(@lexer.peek) ? :immediate : :none |
| else; :immediate |
| end |
| |
| interp = node( |
| Script::Tree::Interpolation.new( |
| prev, str, nil, wb, false, :originally_text => true, :deprecation => deprecation), |
| (prev || str).source_range.start_pos) |
| interpolation(first: interp) |
| end |
| |
| def try_ops_after_interp(ops, name, prev = nil) |
| return unless @lexer.after_interpolation? |
| op = try_toks(*ops) |
| return unless op |
| interp = try_op_before_interp(op, prev, :after_interp) |
| return interp if interp |
| |
| wa = @lexer.whitespace? |
| str = literal_node(Script::Value::String.new(Lexer::OPERATORS_REVERSE[op.type]), |
| op.source_range) |
| str.line = @lexer.line |
| |
| deprecation = |
| case op.type |
| when :comma; :potential |
| when :div, :single_eq; :none |
| when :minus; @lexer.whitespace?(op) ? :immediate : :none |
| else; :immediate |
| end |
| interp = node( |
| Script::Tree::Interpolation.new( |
| prev, str, assert_expr(name), false, wa, |
| :originally_text => true, :deprecation => deprecation), |
| (prev || str).source_range.start_pos) |
| interp |
| end |
| |
| def interpolation(first: nil, inner: :space) |
| e = first || send(inner) |
| while (interp = try_tok(:begin_interpolation)) |
| wb = @lexer.whitespace?(interp) |
| char_before = @lexer.char(interp.pos - 1) |
| mid = assert_expr :expr |
| assert_tok :end_interpolation |
| wa = @lexer.whitespace? |
| char_after = @lexer.char |
| |
| after = send(inner) |
| before_deprecation = e.is_a?(Script::Tree::Interpolation) ? e.deprecation : :none |
| after_deprecation = after.is_a?(Script::Tree::Interpolation) ? after.deprecation : :none |
| |
| deprecation = |
| if before_deprecation == :immediate || after_deprecation == :immediate || |
| # Warn for #{foo}$var and #{foo}(1) but not #{$foo}1. |
| (after && !wa && char_after =~ /[$(]/) || |
| # Warn for $var#{foo} and (a)#{foo} but not a#{foo}. |
| (e && !wb && is_unsafe_before?(e, char_before)) |
| :immediate |
| else |
| :potential |
| end |
| |
| e = node( |
| Script::Tree::Interpolation.new(e, mid, after, wb, wa, :deprecation => deprecation), |
| (e || interp).source_range.start_pos) |
| end |
| e |
| end |
| |
| # Returns whether `expr` is unsafe to include before an interpolation. |
| # |
| # @param expr [Node] The expression to check. |
| # @param char_before [String] The character immediately before the |
| # interpolation being checked (and presumably the last character of |
| # `expr`). |
| # @return [Boolean] |
| def is_unsafe_before?(expr, char_before) |
| return char_before == ')' if is_safe_value?(expr) |
| |
| # Otherwise, it's only safe if it was another interpolation. |
| !expr.is_a?(Script::Tree::Interpolation) |
| end |
| |
| # Returns whether `expr` is safe as the value immediately before an |
| # interpolation. |
| # |
| # It's safe as long as the previous expression is an identifier or number, |
| # or a list whose last element is also safe. |
| def is_safe_value?(expr) |
| return is_safe_value?(expr.elements.last) if expr.is_a?(Script::Tree::ListLiteral) |
| return false unless expr.is_a?(Script::Tree::Literal) |
| expr.value.is_a?(Script::Value::Number) || |
| (expr.value.is_a?(Script::Value::String) && expr.value.type == :identifier) |
| end |
| |
| def space |
| start_pos = source_position |
| e = or_expr |
| return unless e |
| arr = [e] |
| while (e = or_expr) |
| arr << e |
| end |
| if arr.size == 1 |
| arr.first |
| else |
| node(Sass::Script::Tree::ListLiteral.new(arr, separator: :space), start_pos) |
| end |
| end |
| |
| production :or_expr, :and_expr, :or |
| production :and_expr, :eq_or_neq, :and |
| production :eq_or_neq, :relational, :eq, :neq |
| production :relational, :plus_or_minus, :gt, :gte, :lt, :lte |
| production :plus_or_minus, :times_div_or_mod, :plus, :minus |
| production :times_div_or_mod, :unary_plus, :times, :div, :mod |
| |
| unary :plus, :unary_minus |
| unary :minus, :unary_div |
| unary :div, :unary_not # For strings, so /foo/bar works |
| unary :not, :ident |
| |
| 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 = Sass::Script::Value::Color::COLOR_NAMES[name.value.downcase]) |
| literal_node(Sass::Script::Value::Color.new(color, name.value), name.source_range) |
| elsif name.value == "true" |
| literal_node(Sass::Script::Value::Bool.new(true), name.source_range) |
| elsif name.value == "false" |
| literal_node(Sass::Script::Value::Bool.new(false), name.source_range) |
| elsif name.value == "null" |
| literal_node(Sass::Script::Value::Null.new, name.source_range) |
| else |
| literal_node(Sass::Script::Value::String.new(name.value, :identifier), name.source_range) |
| end |
| end |
| |
| def funcall |
| tok = try_tok(:funcall) |
| return raw unless tok |
| args, keywords, splat, kwarg_splat = fn_arglist |
| assert_tok(:rparen) |
| node(Script::Tree::Funcall.new(tok.value, args, keywords, splat, kwarg_splat), |
| tok.source_range.start_pos, source_position) |
| end |
| |
| def defn_arglist!(must_have_parens) |
| if must_have_parens |
| assert_tok(:lparen) |
| else |
| return [], nil unless try_tok(:lparen) |
| end |
| |
| res = [] |
| splat = nil |
| must_have_default = false |
| loop do |
| break if peek_tok(:rparen) |
| c = assert_tok(:const) |
| var = node(Script::Tree::Variable.new(c.value), c.source_range) |
| if try_tok(:colon) |
| val = assert_expr(:space) |
| must_have_default = true |
| elsif try_tok(:splat) |
| splat = var |
| break |
| elsif must_have_default |
| raise SyntaxError.new( |
| "Required argument #{var.inspect} must come before any optional arguments.") |
| end |
| res << [var, val] |
| break unless try_tok(:comma) |
| end |
| assert_tok(:rparen) |
| return res, splat |
| end |
| |
| def fn_arglist |
| arglist(:equals, "function argument") |
| end |
| |
| def mixin_arglist |
| arglist(:interpolation, "mixin argument") |
| end |
| |
| def arglist(subexpr, description) |
| args = [] |
| keywords = Sass::Util::NormalizedMap.new |
| splat = nil |
| while (e = send(subexpr)) |
| if @lexer.peek && @lexer.peek.type == :colon |
| name = e |
| @lexer.expected!("comma") unless name.is_a?(Tree::Variable) |
| assert_tok(:colon) |
| value = assert_expr(subexpr, description) |
| |
| if keywords[name.name] |
| raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once") |
| end |
| |
| keywords[name.name] = value |
| else |
| if try_tok(:splat) |
| return args, keywords, splat, e if splat |
| splat, e = e, nil |
| elsif splat |
| raise SyntaxError.new("Only keyword arguments may follow variable arguments (...).") |
| elsif !keywords.empty? |
| raise SyntaxError.new("Positional arguments must come before keyword arguments.") |
| end |
| args << e if e |
| end |
| |
| return args, keywords, splat unless try_tok(:comma) |
| end |
| return args, keywords |
| end |
| |
| def raw |
| tok = try_tok(:raw) |
| return special_fun unless tok |
| literal_node(Script::Value::String.new(tok.value), tok.source_range) |
| end |
| |
| def special_fun |
| first = try_tok(:special_fun) |
| return square_list unless first |
| str = literal_node(first.value, first.source_range) |
| return str unless try_tok(:string_interpolation) |
| mid = assert_expr :expr |
| assert_tok :end_interpolation |
| last = assert_expr(:special_fun) |
| node( |
| Tree::Interpolation.new(str, mid, last, false, false), |
| first.source_range.start_pos) |
| end |
| |
| def square_list |
| start_pos = source_position |
| return paren unless try_tok(:lsquare) |
| |
| space_start_pos = source_position |
| e = interpolation(inner: :or_expr) |
| separator = nil |
| if e |
| elements = [e] |
| while (e = interpolation(inner: :or_expr)) |
| elements << e |
| end |
| |
| # If there's a comma after a space-separated list, it's actually a |
| # space-separated list nested in a comma-separated list. |
| if try_tok(:comma) |
| e = if elements.length == 1 |
| elements.first |
| else |
| node( |
| Sass::Script::Tree::ListLiteral.new(elements, separator: :space), |
| space_start_pos) |
| end |
| elements = [e] |
| |
| while (e = space) |
| elements << e |
| break unless try_tok(:comma) |
| end |
| separator = :comma |
| else |
| separator = :space if elements.length > 1 |
| end |
| else |
| elements = [] |
| end |
| |
| assert_tok(:rsquare) |
| end_pos = source_position |
| |
| node(Sass::Script::Tree::ListLiteral.new(elements, separator: separator, bracketed: true), |
| start_pos, end_pos) |
| end |
| |
| def paren |
| return variable unless try_tok(:lparen) |
| start_pos = source_position |
| e = map |
| e.force_division! if e |
| end_pos = source_position |
| assert_tok(:rparen) |
| e || node(Sass::Script::Tree::ListLiteral.new([]), start_pos, end_pos) |
| end |
| |
| def variable |
| start_pos = source_position |
| c = try_tok(:const) |
| return string unless c |
| node(Tree::Variable.new(*c.value), start_pos) |
| end |
| |
| def string |
| first = try_tok(:string) |
| return number unless first |
| str = literal_node(first.value, first.source_range) |
| return str unless try_tok(:string_interpolation) |
| mid = assert_expr :expr |
| assert_tok :end_interpolation |
| last = assert_expr(:string) |
| node(Tree::StringInterpolation.new(str, mid, last), first.source_range.start_pos) |
| end |
| |
| def number |
| tok = try_tok(:number) |
| return selector unless tok |
| num = tok.value |
| num.options = @options |
| num.original = num.to_s |
| literal_node(num, tok.source_range.start_pos) |
| end |
| |
| def selector |
| tok = try_tok(:selector) |
| return literal unless tok |
| node(tok.value, tok.source_range.start_pos) |
| end |
| |
| def literal |
| t = try_tok(:color) |
| return literal_node(t.value, t.source_range) if t |
| end |
| |
| # It would be possible to have unified #assert and #try methods, |
| # but detecting the method/token difference turns out to be quite expensive. |
| |
| EXPR_NAMES = { |
| :string => "string", |
| :default => "expression (e.g. 1px, bold)", |
| :mixin_arglist => "mixin argument", |
| :fn_arglist => "function argument", |
| :splat => "...", |
| :special_fun => '")"', |
| } |
| |
| def assert_expr(name, expected = nil) |
| e = send(name) |
| return e if e |
| @lexer.expected!(expected || EXPR_NAMES[name] || EXPR_NAMES[:default]) |
| end |
| |
| def assert_tok(name) |
| # Avoids an array allocation caused by argument globbing in assert_toks. |
| t = try_tok(name) |
| return t if t |
| @lexer.expected!(Lexer::TOKEN_NAMES[name] || name.to_s) |
| end |
| |
| def assert_toks(*names) |
| t = try_toks(*names) |
| return t if t |
| @lexer.expected!(names.map {|tok| Lexer::TOKEN_NAMES[tok] || tok}.join(" or ")) |
| end |
| |
| def peek_tok(name) |
| # Avoids an array allocation caused by argument globbing in the try_toks method. |
| peeked = @lexer.peek |
| peeked && name == peeked.type |
| end |
| |
| def try_tok(name) |
| peek_tok(name) && @lexer.next |
| end |
| |
| def try_toks(*names) |
| peeked = @lexer.peek |
| peeked && names.include?(peeked.type) && @lexer.next |
| end |
| |
| def assert_done |
| if @allow_extra_text |
| # If extra text is allowed, just rewind the lexer so that the |
| # StringScanner is pointing to the end of the parsed text. |
| @lexer.unpeek! |
| else |
| return if @lexer.done? |
| @lexer.expected!(EXPR_NAMES[:default]) |
| end |
| end |
| |
| # @overload node(value, source_range) |
| # @param value [Sass::Script::Value::Base] |
| # @param source_range [Sass::Source::Range] |
| # @overload node(value, start_pos, end_pos = source_position) |
| # @param value [Sass::Script::Value::Base] |
| # @param start_pos [Sass::Source::Position] |
| # @param end_pos [Sass::Source::Position] |
| def literal_node(value, source_range_or_start_pos, end_pos = source_position) |
| node(Sass::Script::Tree::Literal.new(value), source_range_or_start_pos, end_pos) |
| end |
| |
| # @overload node(node, source_range) |
| # @param node [Sass::Script::Tree::Node] |
| # @param source_range [Sass::Source::Range] |
| # @overload node(node, start_pos, end_pos = source_position) |
| # @param node [Sass::Script::Tree::Node] |
| # @param start_pos [Sass::Source::Position] |
| # @param end_pos [Sass::Source::Position] |
| def node(node, source_range_or_start_pos, end_pos = source_position) |
| source_range = |
| if source_range_or_start_pos.is_a?(Sass::Source::Range) |
| source_range_or_start_pos |
| else |
| range(source_range_or_start_pos, end_pos) |
| end |
| |
| node.line = source_range.start_pos.line |
| node.source_range = source_range |
| node.filename = @options[:filename] |
| node |
| end |
| |
| # Checks a script node for any immediately-deprecated interpolations, and |
| # emits warnings for them. |
| # |
| # @param node [Sass::Script::Tree::Node] |
| def check_for_interpolation(node) |
| nodes = [node] |
| until nodes.empty? |
| node = nodes.pop |
| unless node.is_a?(Sass::Script::Tree::Interpolation) && |
| node.deprecation == :immediate |
| nodes.concat node.children |
| next |
| end |
| |
| interpolation_deprecation(node) |
| end |
| end |
| |
| # Emits a deprecation warning for an interpolation node. |
| # |
| # @param node [Sass::Script::Tree::Node] |
| def interpolation_deprecation(interpolation) |
| return if @options[:_convert] |
| location = "on line #{interpolation.line}" |
| location << " of #{interpolation.filename}" if interpolation.filename |
| Sass::Util.sass_warn <<WARNING |
| DEPRECATION WARNING #{location}: |
| \#{} interpolation near operators will be simplified in a future version of Sass. |
| To preserve the current behavior, use quotes: |
| |
| #{interpolation.to_quoted_equivalent.to_sass} |
| |
| You can use the sass-convert command to automatically fix most cases. |
| WARNING |
| end |
| end |
| end |
| end |