sass 3.4.0 → 3.4.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +3 -1
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/CONTRIBUTING.md +148 -0
  5. data/MIT-LICENSE +1 -1
  6. data/README.md +26 -20
  7. data/Rakefile +103 -20
  8. data/VERSION +1 -1
  9. data/VERSION_DATE +1 -1
  10. data/extra/sass-spec-ref.sh +32 -0
  11. data/extra/update_watch.rb +1 -1
  12. data/lib/sass/cache_stores/filesystem.rb +7 -7
  13. data/lib/sass/cache_stores/memory.rb +4 -5
  14. data/lib/sass/callbacks.rb +2 -2
  15. data/lib/sass/css.rb +11 -10
  16. data/lib/sass/deprecation.rb +55 -0
  17. data/lib/sass/engine.rb +83 -38
  18. data/lib/sass/environment.rb +26 -2
  19. data/lib/sass/error.rb +12 -12
  20. data/lib/sass/exec/base.rb +15 -3
  21. data/lib/sass/exec/sass_convert.rb +34 -15
  22. data/lib/sass/exec/sass_scss.rb +23 -7
  23. data/lib/sass/features.rb +2 -2
  24. data/lib/sass/importers/base.rb +1 -1
  25. data/lib/sass/importers/deprecated_path.rb +51 -0
  26. data/lib/sass/importers/filesystem.rb +24 -16
  27. data/lib/sass/importers.rb +1 -0
  28. data/lib/sass/logger/base.rb +8 -2
  29. data/lib/sass/logger/delayed.rb +50 -0
  30. data/lib/sass/logger.rb +8 -3
  31. data/lib/sass/plugin/compiler.rb +42 -25
  32. data/lib/sass/plugin/configuration.rb +38 -22
  33. data/lib/sass/plugin/merb.rb +2 -2
  34. data/lib/sass/plugin/rack.rb +3 -3
  35. data/lib/sass/plugin/rails.rb +1 -1
  36. data/lib/sass/plugin/staleness_checker.rb +3 -3
  37. data/lib/sass/plugin.rb +3 -2
  38. data/lib/sass/script/css_parser.rb +2 -3
  39. data/lib/sass/script/css_variable_warning.rb +52 -0
  40. data/lib/sass/script/functions.rb +140 -73
  41. data/lib/sass/script/lexer.rb +37 -22
  42. data/lib/sass/script/parser.rb +235 -40
  43. data/lib/sass/script/tree/funcall.rb +12 -5
  44. data/lib/sass/script/tree/interpolation.rb +109 -4
  45. data/lib/sass/script/tree/list_literal.rb +31 -4
  46. data/lib/sass/script/tree/literal.rb +4 -0
  47. data/lib/sass/script/tree/node.rb +21 -3
  48. data/lib/sass/script/tree/operation.rb +54 -1
  49. data/lib/sass/script/tree/string_interpolation.rb +58 -37
  50. data/lib/sass/script/tree/variable.rb +1 -1
  51. data/lib/sass/script/value/base.rb +10 -9
  52. data/lib/sass/script/value/color.rb +42 -24
  53. data/lib/sass/script/value/helpers.rb +16 -6
  54. data/lib/sass/script/value/map.rb +1 -1
  55. data/lib/sass/script/value/number.rb +52 -19
  56. data/lib/sass/script/value/string.rb +46 -5
  57. data/lib/sass/script.rb +3 -3
  58. data/lib/sass/scss/css_parser.rb +16 -2
  59. data/lib/sass/scss/parser.rb +120 -75
  60. data/lib/sass/scss/rx.rb +9 -10
  61. data/lib/sass/scss/static_parser.rb +19 -14
  62. data/lib/sass/scss.rb +0 -2
  63. data/lib/sass/selector/abstract_sequence.rb +8 -6
  64. data/lib/sass/selector/comma_sequence.rb +25 -9
  65. data/lib/sass/selector/pseudo.rb +45 -35
  66. data/lib/sass/selector/sequence.rb +54 -18
  67. data/lib/sass/selector/simple.rb +11 -11
  68. data/lib/sass/selector/simple_sequence.rb +34 -15
  69. data/lib/sass/selector.rb +7 -10
  70. data/lib/sass/shared.rb +1 -1
  71. data/lib/sass/source/map.rb +7 -4
  72. data/lib/sass/source/position.rb +4 -4
  73. data/lib/sass/stack.rb +2 -2
  74. data/lib/sass/supports.rb +8 -10
  75. data/lib/sass/tree/comment_node.rb +1 -1
  76. data/lib/sass/tree/css_import_node.rb +9 -1
  77. data/lib/sass/tree/function_node.rb +8 -3
  78. data/lib/sass/tree/import_node.rb +6 -5
  79. data/lib/sass/tree/node.rb +5 -3
  80. data/lib/sass/tree/prop_node.rb +5 -6
  81. data/lib/sass/tree/rule_node.rb +14 -4
  82. data/lib/sass/tree/visitors/check_nesting.rb +18 -22
  83. data/lib/sass/tree/visitors/convert.rb +43 -26
  84. data/lib/sass/tree/visitors/cssize.rb +5 -1
  85. data/lib/sass/tree/visitors/deep_copy.rb +1 -1
  86. data/lib/sass/tree/visitors/extend.rb +15 -13
  87. data/lib/sass/tree/visitors/perform.rb +42 -17
  88. data/lib/sass/tree/visitors/set_options.rb +1 -1
  89. data/lib/sass/tree/visitors/to_css.rb +58 -30
  90. data/lib/sass/util/multibyte_string_scanner.rb +0 -2
  91. data/lib/sass/util/normalized_map.rb +0 -1
  92. data/lib/sass/util/subset_map.rb +1 -2
  93. data/lib/sass/util.rb +125 -68
  94. data/lib/sass/version.rb +2 -2
  95. data/lib/sass.rb +10 -3
  96. data/test/sass/compiler_test.rb +6 -2
  97. data/test/sass/conversion_test.rb +187 -53
  98. data/test/sass/css2sass_test.rb +50 -1
  99. data/test/sass/css_variable_test.rb +132 -0
  100. data/test/sass/engine_test.rb +207 -61
  101. data/test/sass/exec_test.rb +10 -0
  102. data/test/sass/extend_test.rb +101 -29
  103. data/test/sass/functions_test.rb +60 -9
  104. data/test/sass/importer_test.rb +9 -0
  105. data/test/sass/more_templates/more1.sass +10 -10
  106. data/test/sass/more_templates/more_import.sass +2 -2
  107. data/test/sass/plugin_test.rb +10 -8
  108. data/test/sass/results/script.css +3 -3
  109. data/test/sass/script_conversion_test.rb +58 -29
  110. data/test/sass/script_test.rb +430 -53
  111. data/test/sass/scss/css_test.rb +73 -7
  112. data/test/sass/scss/rx_test.rb +4 -0
  113. data/test/sass/scss/scss_test.rb +309 -4
  114. data/test/sass/source_map_test.rb +152 -74
  115. data/test/sass/superselector_test.rb +19 -0
  116. data/test/sass/templates/_partial.sass +1 -1
  117. data/test/sass/templates/basic.sass +10 -10
  118. data/test/sass/templates/bork1.sass +1 -1
  119. data/test/sass/templates/bork5.sass +1 -1
  120. data/test/sass/templates/compact.sass +10 -10
  121. data/test/sass/templates/complex.sass +187 -187
  122. data/test/sass/templates/compressed.sass +10 -10
  123. data/test/sass/templates/expanded.sass +10 -10
  124. data/test/sass/templates/import.sass +2 -2
  125. data/test/sass/templates/importee.sass +3 -3
  126. data/test/sass/templates/mixins.sass +22 -22
  127. data/test/sass/templates/multiline.sass +4 -4
  128. data/test/sass/templates/nested.sass +13 -13
  129. data/test/sass/templates/parent_ref.sass +12 -12
  130. data/test/sass/templates/script.sass +70 -70
  131. data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +1 -1
  132. data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +2 -2
  133. data/test/sass/templates/subdir/subdir.sass +3 -3
  134. data/test/sass/templates/units.sass +10 -10
  135. data/test/sass/util/multibyte_string_scanner_test.rb +10 -2
  136. data/test/sass/util_test.rb +15 -44
  137. data/test/sass-spec.yml +3 -0
  138. data/test/test_helper.rb +5 -4
  139. metadata +302 -295
  140. data/CONTRIBUTING +0 -3
  141. data/lib/sass/scss/script_lexer.rb +0 -15
  142. data/lib/sass/scss/script_parser.rb +0 -25
@@ -19,13 +19,13 @@ module Sass
19
19
  # `source_range`: \[`Sass::Source::Range`\]
20
20
  # : The range in the source file in which the token appeared.
21
21
  #
22
- # `pos`: \[`Fixnum`\]
22
+ # `pos`: \[`Integer`\]
23
23
  # : The scanner position at which the SassScript token appeared.
24
24
  Token = Struct.new(:type, :value, :source_range, :pos)
25
25
 
26
26
  # The line number of the lexer's current position.
27
27
  #
28
- # @return [Fixnum]
28
+ # @return [Integer]
29
29
  def line
30
30
  return @line unless @tok
31
31
  @tok.source_range.start_pos.line
@@ -34,7 +34,7 @@ module Sass
34
34
  # The number of bytes into the current line
35
35
  # of the lexer's current position (1-based).
36
36
  #
37
- # @return [Fixnum]
37
+ # @return [Integer]
38
38
  def offset
39
39
  return @offset unless @tok
40
40
  @tok.source_range.start_pos.offset
@@ -71,8 +71,8 @@ module Sass
71
71
  OPERATORS_REVERSE = Sass::Util.map_hash(OPERATORS) {|k, v| [v, k]}
72
72
 
73
73
  TOKEN_NAMES = Sass::Util.map_hash(OPERATORS_REVERSE) {|k, v| [k, v.inspect]}.merge(
74
- :const => "variable (e.g. $foo)",
75
- :ident => "identifier (e.g. middle)")
74
+ :const => "variable (e.g. $foo)",
75
+ :ident => "identifier (e.g. middle)")
76
76
 
77
77
  # A list of operator strings ordered with longer names first
78
78
  # so that `>` and `<` don't clobber `>=` and `<=`.
@@ -80,7 +80,7 @@ module Sass
80
80
 
81
81
  # A sub-list of {OP_NAMES} that only includes operators
82
82
  # with identifier names.
83
- IDENT_OP_NAMES = OP_NAMES.select {|k, v| k =~ /^\w+/}
83
+ IDENT_OP_NAMES = OP_NAMES.select {|k, _v| k =~ /^\w+/}
84
84
 
85
85
  PARSEABLE_NUMBER = /(?:(\d*\.\d+)|(\d+))(?:[eE]([+-]?\d+))?(#{UNIT})?/
86
86
 
@@ -142,12 +142,12 @@ module Sass
142
142
  }
143
143
 
144
144
  # @param str [String, StringScanner] The source text to lex
145
- # @param line [Fixnum] The 1-based line on which the SassScript appears.
145
+ # @param line [Integer] The 1-based line on which the SassScript appears.
146
146
  # Used for error reporting and sourcemap building
147
- # @param offset [Fixnum] The 1-based character (not byte) offset in the line in the source.
147
+ # @param offset [Integer] The 1-based character (not byte) offset in the line in the source.
148
148
  # Used for error reporting and sourcemap building
149
149
  # @param options [{Symbol => Object}] An options hash;
150
- # see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
150
+ # see {file:SASS_REFERENCE.md#Options the Sass options documentation}
151
151
  def initialize(str, line, offset, options)
152
152
  @scanner = str.is_a?(StringScanner) ? str : Sass::Util::MultibyteStringScanner.new(str)
153
153
  @line = line
@@ -155,6 +155,8 @@ module Sass
155
155
  @options = options
156
156
  @interpolation_stack = []
157
157
  @prev = nil
158
+ @tok = nil
159
+ @next_tok = nil
158
160
  end
159
161
 
160
162
  # Moves the lexer forward one token.
@@ -179,6 +181,13 @@ module Sass
179
181
  end
180
182
  end
181
183
 
184
+ # Returns the given character.
185
+ #
186
+ # @return [String]
187
+ def char(pos = @scanner.pos)
188
+ @scanner.string[pos, 1]
189
+ end
190
+
182
191
  # Returns the next token without moving the lexer forward.
183
192
  #
184
193
  # @return [Token] The next token
@@ -189,16 +198,16 @@ module Sass
189
198
  # Rewinds the underlying StringScanner
190
199
  # to before the token returned by \{#peek}.
191
200
  def unpeek!
192
- if @tok
193
- @scanner.pos = @tok.pos
194
- @line = @tok.source_range.start_pos.line
195
- @offset = @tok.source_range.start_pos.offset
196
- end
201
+ return unless @tok
202
+ @scanner.pos = @tok.pos
203
+ @line = @tok.source_range.start_pos.line
204
+ @offset = @tok.source_range.start_pos.offset
197
205
  end
198
206
 
199
207
  # @return [Boolean] Whether or not there's more source text to lex.
200
208
  def done?
201
- whitespace unless after_interpolation? && @interpolation_stack.last
209
+ return if @next_tok
210
+ whitespace unless after_interpolation? && !@interpolation_stack.empty?
202
211
  @scanner.eos? && @tok.nil?
203
212
  end
204
213
 
@@ -235,6 +244,11 @@ module Sass
235
244
  private
236
245
 
237
246
  def read_token
247
+ if (tok = @next_tok)
248
+ @next_tok = nil
249
+ return tok
250
+ end
251
+
238
252
  return if done?
239
253
  start_pos = source_position
240
254
  value = token
@@ -293,9 +307,9 @@ MESSAGE
293
307
  end
294
308
 
295
309
  if @scanner[2] == '#{' # '
296
- @scanner.pos -= 2 # Don't actually consume the #{
297
- @offset -= 2
298
310
  @interpolation_stack << [:string, re]
311
+ start_pos = Sass::Source::Position.new(@line, @offset - 2)
312
+ @next_tok = Token.new(:string_interpolation, range(start_pos), @scanner.pos - 2)
299
313
  end
300
314
  str =
301
315
  if re == :uri
@@ -392,9 +406,9 @@ MESSAGE
392
406
  else
393
407
  raise "[BUG] Unreachable" unless @scanner[1] == '#{' # '
394
408
  str.slice!(-2..-1)
395
- @scanner.pos -= 2 # Don't actually consume the #{
396
- @offset -= 2
397
409
  @interpolation_stack << [:special_fun, parens]
410
+ start_pos = Sass::Source::Position.new(@line, @offset - 2)
411
+ @next_tok = Token.new(:string_interpolation, range(start_pos), @scanner.pos - 2)
398
412
  end
399
413
 
400
414
  return [:special_fun, Sass::Script::Value::String.new(str)]
@@ -405,7 +419,7 @@ MESSAGE
405
419
  end
406
420
 
407
421
  def special_val
408
- return unless scan(/!important/i)
422
+ return unless scan(/!#{W}important/i)
409
423
  [:string, Script::Value::String.new("!important")]
410
424
  end
411
425
 
@@ -418,8 +432,9 @@ MESSAGE
418
432
  def op
419
433
  op = scan(REGULAR_EXPRESSIONS[:op])
420
434
  return unless op
421
- @interpolation_stack << nil if op == :begin_interpolation
422
- [OPERATORS[op]]
435
+ name = OPERATORS[op]
436
+ @interpolation_stack << nil if name == :begin_interpolation
437
+ [name]
423
438
  end
424
439
 
425
440
  def raw(rx)
@@ -1,4 +1,5 @@
1
1
  require 'sass/script/lexer'
2
+ require 'sass/script/css_variable_warning'
2
3
 
3
4
  module Sass
4
5
  module Script
@@ -7,28 +8,34 @@ module Sass
7
8
  class Parser
8
9
  # The line number of the parser's current position.
9
10
  #
10
- # @return [Fixnum]
11
+ # @return [Integer]
11
12
  def line
12
13
  @lexer.line
13
14
  end
14
15
 
15
16
  # The column number of the parser's current position.
16
17
  #
17
- # @return [Fixnum]
18
+ # @return [Integer]
18
19
  def offset
19
20
  @lexer.offset
20
21
  end
21
22
 
22
23
  # @param str [String, StringScanner] The source text to parse
23
- # @param line [Fixnum] The line on which the SassScript appears.
24
+ # @param line [Integer] The line on which the SassScript appears.
24
25
  # Used for error reporting and sourcemap building
25
- # @param offset [Fixnum] The character (not byte) offset where the script starts in the line.
26
+ # @param offset [Integer] The character (not byte) offset where the script starts in the line.
26
27
  # Used for error reporting and sourcemap building
27
- # @param options [{Symbol => Object}] An options hash;
28
- # see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
28
+ # @param options [{Symbol => Object}] An options hash; see
29
+ # {file:SASS_REFERENCE.md#Options the Sass options documentation}.
30
+ # This supports an additional `:allow_extra_text` option that controls
31
+ # whether the parser throws an error when extra text is encountered
32
+ # after the parsed construct.
29
33
  def initialize(str, line, offset, options = {})
30
34
  @options = options
35
+ @allow_extra_text = options.delete(:allow_extra_text)
31
36
  @lexer = lexer_class.new(str, line, offset, options)
37
+ @stop_at = nil
38
+ @css_variable_warning = nil
32
39
  end
33
40
 
34
41
  # Parses a SassScript expression within an interpolated segment (`#{}`).
@@ -46,7 +53,8 @@ module Sass
46
53
  expr = assert_expr :expr
47
54
  assert_tok :end_interpolation
48
55
  expr = Sass::Script::Tree::Interpolation.new(
49
- nil, expr, nil, !:wb, !:wa, !:originally_text, warn_for_color)
56
+ nil, expr, nil, false, false, :warn_for_color => warn_for_color)
57
+ check_for_interpolation expr
50
58
  expr.options = @options
51
59
  node(expr, start_pos)
52
60
  rescue Sass::SyntaxError => e
@@ -56,12 +64,23 @@ module Sass
56
64
 
57
65
  # Parses a SassScript expression.
58
66
  #
67
+ # @param css_variable [Boolean] Whether this is the value of a CSS variable.
59
68
  # @return [Script::Tree::Node] The root node of the parse tree
60
69
  # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
61
- def parse
70
+ def parse(css_variable = false)
71
+ if css_variable
72
+ @css_variable_warning = CssVariableWarning.new
73
+ end
74
+
62
75
  expr = assert_expr :expr
63
76
  assert_done
64
77
  expr.options = @options
78
+ check_for_interpolation expr
79
+
80
+ if css_variable
81
+ @css_variable_warning.value = expr
82
+ end
83
+
65
84
  expr
66
85
  rescue Sass::SyntaxError => e
67
86
  e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
@@ -79,6 +98,7 @@ module Sass
79
98
  expr = assert_expr :expr
80
99
  assert_done
81
100
  expr.options = @options
101
+ check_for_interpolation expr
82
102
  expr
83
103
  rescue Sass::SyntaxError => e
84
104
  e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
@@ -102,10 +122,26 @@ module Sass
102
122
  end
103
123
  assert_done
104
124
 
105
- args.each {|a| a.options = @options}
106
- keywords.each {|k, v| v.options = @options}
107
- splat.options = @options if splat
108
- kwarg_splat.options = @options if kwarg_splat
125
+ args.each do |a|
126
+ check_for_interpolation a
127
+ a.options = @options
128
+ end
129
+
130
+ keywords.each do |_k, v|
131
+ check_for_interpolation v
132
+ v.options = @options
133
+ end
134
+
135
+ if splat
136
+ check_for_interpolation splat
137
+ splat.options = @options
138
+ end
139
+
140
+ if kwarg_splat
141
+ check_for_interpolation kwarg_splat
142
+ kwarg_splat.options = @options
143
+ end
144
+
109
145
  return args, keywords, splat, kwarg_splat
110
146
  rescue Sass::SyntaxError => e
111
147
  e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
@@ -122,10 +158,20 @@ module Sass
122
158
  assert_done
123
159
 
124
160
  args.each do |k, v|
161
+ check_for_interpolation k
125
162
  k.options = @options
126
- v.options = @options if v
163
+
164
+ if v
165
+ check_for_interpolation v
166
+ v.options = @options
167
+ end
127
168
  end
128
- splat.options = @options if splat
169
+
170
+ if splat
171
+ check_for_interpolation splat
172
+ splat.options = @options
173
+ end
174
+
129
175
  return args, splat
130
176
  rescue Sass::SyntaxError => e
131
177
  e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
@@ -142,10 +188,20 @@ module Sass
142
188
  assert_done
143
189
 
144
190
  args.each do |k, v|
191
+ check_for_interpolation k
145
192
  k.options = @options
146
- v.options = @options if v
193
+
194
+ if v
195
+ check_for_interpolation v
196
+ v.options = @options
197
+ end
147
198
  end
148
- splat.options = @options if splat
199
+
200
+ if splat
201
+ check_for_interpolation splat
202
+ splat.options = @options
203
+ end
204
+
149
205
  return args, splat
150
206
  rescue Sass::SyntaxError => e
151
207
  e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
@@ -165,6 +221,7 @@ module Sass
165
221
  end
166
222
 
167
223
  expr = assert_expr :funcall
224
+ check_for_interpolation expr
168
225
  expr.options = @options
169
226
  @lexer.unpeek!
170
227
  expr
@@ -175,12 +232,12 @@ module Sass
175
232
 
176
233
  # Parses a SassScript expression.
177
234
  #
178
- # @overload parse(str, line, offset, filename = nil)
179
235
  # @return [Script::Tree::Node] The root node of the parse tree
180
236
  # @see Parser#initialize
181
237
  # @see Parser#parse
182
- def self.parse(*args)
183
- new(*args).parse
238
+ def self.parse(value, line, offset, options = {})
239
+ css_variable = options.delete :css_variable
240
+ new(value, line, offset, options).parse(css_variable)
184
241
  end
185
242
 
186
243
  PRECEDENCE = [
@@ -193,6 +250,8 @@ module Sass
193
250
 
194
251
  ASSOCIATIVE = [:plus, :times]
195
252
 
253
+ VALID_CSS_OPS = [:comma, :single_eq, :space, :div]
254
+
196
255
  class << self
197
256
  # Returns an integer representing the precedence
198
257
  # of the given operator.
@@ -232,6 +291,10 @@ module Sass
232
291
  return other_interp
233
292
  end
234
293
 
294
+ if @css_variable_warning && !VALID_CSS_OPS.include?(tok.type)
295
+ @css_variable_warning.warn!
296
+ end
297
+
235
298
  e = node(Tree::Operation.new(e, assert_expr(#{sub.inspect}), tok.type),
236
299
  e.source_range.start_pos)
237
300
  end
@@ -247,6 +310,8 @@ RUBY
247
310
  interp = try_op_before_interp(tok)
248
311
  return interp if interp
249
312
  start_pos = source_position
313
+
314
+ @css_variable_warning.warn! if @css_variable_warning
250
315
  node(Tree::UnaryOperation.new(assert_expr(:unary_#{op}), :#{op}), start_pos)
251
316
  end
252
317
  RUBY
@@ -273,6 +338,7 @@ RUBY
273
338
  return list e, start_pos unless @lexer.peek && @lexer.peek.type == :colon
274
339
 
275
340
  pair = map_pair(e)
341
+ @css_variable_warning.warn! if @css_variable_warning
276
342
  map = node(Sass::Script::Tree::MapLiteral.new([pair]), start_pos)
277
343
  while try_tok(:comma)
278
344
  pair = map_pair
@@ -308,19 +374,32 @@ RUBY
308
374
  end
309
375
  return list unless (e = interpolation)
310
376
  list.elements << e
377
+ list.source_range.end_pos = list.elements.last.source_range.end_pos
311
378
  end
312
379
  list
313
380
  end
314
381
 
315
382
  production :equals, :interpolation, :single_eq
316
383
 
317
- def try_op_before_interp(op, prev = nil)
384
+ def try_op_before_interp(op, prev = nil, after_interp = false)
318
385
  return unless @lexer.peek && @lexer.peek.type == :begin_interpolation
386
+ unary = !prev && !after_interp
319
387
  wb = @lexer.whitespace?(op)
320
388
  str = literal_node(Script::Value::String.new(Lexer::OPERATORS_REVERSE[op.type]),
321
389
  op.source_range)
390
+
391
+ deprecation =
392
+ case op.type
393
+ when :comma; :potential
394
+ when :div, :single_eq; :none
395
+ when :plus; unary ? :none : :immediate
396
+ when :minus; @lexer.whitespace?(@lexer.peek) ? :immediate : :none
397
+ else; :immediate
398
+ end
399
+
322
400
  interp = node(
323
- Script::Tree::Interpolation.new(prev, str, nil, wb, !:wa, :originally_text),
401
+ Script::Tree::Interpolation.new(
402
+ prev, str, nil, wb, false, :originally_text => true, :deprecation => deprecation),
324
403
  (prev || str).source_range.start_pos)
325
404
  interpolation(interp)
326
405
  end
@@ -329,15 +408,25 @@ RUBY
329
408
  return unless @lexer.after_interpolation?
330
409
  op = try_toks(*ops)
331
410
  return unless op
332
- interp = try_op_before_interp(op, prev)
411
+ interp = try_op_before_interp(op, prev, :after_interp)
333
412
  return interp if interp
334
413
 
335
414
  wa = @lexer.whitespace?
336
415
  str = literal_node(Script::Value::String.new(Lexer::OPERATORS_REVERSE[op.type]),
337
416
  op.source_range)
338
417
  str.line = @lexer.line
418
+
419
+ deprecation =
420
+ case op.type
421
+ when :comma; :potential
422
+ when :div, :single_eq; :none
423
+ when :minus; @lexer.whitespace?(op) ? :immediate : :none
424
+ else; :immediate
425
+ end
339
426
  interp = node(
340
- Script::Tree::Interpolation.new(prev, str, assert_expr(name), !:wb, wa, :originally_text),
427
+ Script::Tree::Interpolation.new(
428
+ prev, str, assert_expr(name), false, wa,
429
+ :originally_text => true, :deprecation => deprecation),
341
430
  (prev || str).source_range.start_pos)
342
431
  interp
343
432
  end
@@ -346,16 +435,60 @@ RUBY
346
435
  e = first
347
436
  while (interp = try_tok(:begin_interpolation))
348
437
  wb = @lexer.whitespace?(interp)
349
- mid = assert_expr :expr
438
+ char_before = @lexer.char(interp.pos - 1)
439
+ mid = without_css_variable_warning {assert_expr :expr}
350
440
  assert_tok :end_interpolation
351
441
  wa = @lexer.whitespace?
442
+ char_after = @lexer.char
443
+
444
+ after = space
445
+ before_deprecation = e.is_a?(Script::Tree::Interpolation) ? e.deprecation : :none
446
+ after_deprecation = after.is_a?(Script::Tree::Interpolation) ? after.deprecation : :none
447
+
448
+ deprecation =
449
+ if before_deprecation == :immediate || after_deprecation == :immediate ||
450
+ # Warn for #{foo}$var and #{foo}(1) but not #{$foo}1.
451
+ (after && !wa && char_after =~ /[$(]/) ||
452
+ # Warn for $var#{foo} and (a)#{foo} but not a#{foo}.
453
+ (e && !wb && is_unsafe_before?(e, char_before))
454
+ :immediate
455
+ else
456
+ :potential
457
+ end
458
+
352
459
  e = node(
353
- Script::Tree::Interpolation.new(e, mid, space, wb, wa),
354
- (e || mid).source_range.start_pos)
460
+ Script::Tree::Interpolation.new(e, mid, after, wb, wa, :deprecation => deprecation),
461
+ (e || interp).source_range.start_pos)
355
462
  end
356
463
  e
357
464
  end
358
465
 
466
+ # Returns whether `expr` is unsafe to include before an interpolation.
467
+ #
468
+ # @param expr [Node] The expression to check.
469
+ # @param char_before [String] The character immediately before the
470
+ # interpolation being checked (and presumably the last character of
471
+ # `expr`).
472
+ # @return [Boolean]
473
+ def is_unsafe_before?(expr, char_before)
474
+ return char_before == ')' if is_safe_value?(expr)
475
+
476
+ # Otherwise, it's only safe if it was another interpolation.
477
+ !expr.is_a?(Script::Tree::Interpolation)
478
+ end
479
+
480
+ # Returns whether `expr` is safe as the value immediately before an
481
+ # interpolation.
482
+ #
483
+ # It's safe as long as the previous expression is an identifier or number,
484
+ # or a list whose last element is also safe.
485
+ def is_safe_value?(expr)
486
+ return is_safe_value?(expr.elements.last) if expr.is_a?(Script::Tree::ListLiteral)
487
+ return false unless expr.is_a?(Script::Tree::Literal)
488
+ expr.value.is_a?(Script::Value::Number) ||
489
+ (expr.value.is_a?(Script::Value::String) && expr.value.type == :identifier)
490
+ end
491
+
359
492
  def space
360
493
  start_pos = source_position
361
494
  e = or_expr
@@ -497,30 +630,33 @@ RUBY
497
630
  first = try_tok(:special_fun)
498
631
  return paren unless first
499
632
  str = literal_node(first.value, first.source_range)
500
- return str unless try_tok(:begin_interpolation)
501
- mid = parse_interpolated
633
+ return str unless try_tok(:string_interpolation)
634
+ mid = without_css_variable_warning {assert_expr :expr}
635
+ assert_tok :end_interpolation
502
636
  last = assert_expr(:special_fun)
503
- node(Tree::Interpolation.new(str, mid, last, false, false),
504
- first.source_range.start_pos)
637
+ node(
638
+ Tree::Interpolation.new(str, mid, last, false, false),
639
+ first.source_range.start_pos)
505
640
  end
506
641
 
507
642
  def paren
508
643
  return variable unless try_tok(:lparen)
509
- was_in_parens = @in_parens
510
- @in_parens = true
511
644
  start_pos = source_position
512
645
  e = map
646
+ e.force_division! if e
513
647
  end_pos = source_position
514
648
  assert_tok(:rparen)
515
- return e || node(Sass::Script::Tree::ListLiteral.new([], nil), start_pos, end_pos)
516
- ensure
517
- @in_parens = was_in_parens
649
+
650
+ @css_variable_warning.warn! if @css_variable_warning
651
+ e || node(Sass::Script::Tree::ListLiteral.new([], nil), start_pos, end_pos)
518
652
  end
519
653
 
520
654
  def variable
521
655
  start_pos = source_position
522
656
  c = try_tok(:const)
523
657
  return string unless c
658
+
659
+ @css_variable_warning.warn! if @css_variable_warning
524
660
  node(Tree::Variable.new(*c.value), start_pos)
525
661
  end
526
662
 
@@ -528,8 +664,8 @@ RUBY
528
664
  first = try_tok(:string)
529
665
  return number unless first
530
666
  str = literal_node(first.value, first.source_range)
531
- return str unless try_tok(:begin_interpolation)
532
- mid = assert_expr :expr
667
+ return str unless try_tok(:string_interpolation)
668
+ mid = without_css_variable_warning {assert_expr :expr}
533
669
  assert_tok :end_interpolation
534
670
  last = assert_expr(:string)
535
671
  node(Tree::StringInterpolation.new(str, mid, last), first.source_range.start_pos)
@@ -539,13 +675,15 @@ RUBY
539
675
  tok = try_tok(:number)
540
676
  return selector unless tok
541
677
  num = tok.value
542
- num.original = num.to_s unless @in_parens
678
+ num.options = @options
679
+ num.original = num.to_s
543
680
  literal_node(num, tok.source_range.start_pos)
544
681
  end
545
682
 
546
683
  def selector
547
684
  tok = try_tok(:selector)
548
685
  return literal unless tok
686
+ @css_variable_warning.warn! if @css_variable_warning
549
687
  node(tok.value, tok.source_range.start_pos)
550
688
  end
551
689
 
@@ -597,8 +735,14 @@ RUBY
597
735
  end
598
736
 
599
737
  def assert_done
600
- return if @lexer.done?
601
- @lexer.expected!(EXPR_NAMES[:default])
738
+ if @allow_extra_text
739
+ # If extra text is allowed, just rewind the lexer so that the
740
+ # StringScanner is pointing to the end of the parsed text.
741
+ @lexer.unpeek!
742
+ else
743
+ return if @lexer.done?
744
+ @lexer.expected!(EXPR_NAMES[:default])
745
+ end
602
746
  end
603
747
 
604
748
  # @overload node(value, source_range)
@@ -627,11 +771,62 @@ RUBY
627
771
  range(source_range_or_start_pos, end_pos)
628
772
  end
629
773
 
774
+ node.css_variable_warning = @css_variable_warning
630
775
  node.line = source_range.start_pos.line
631
776
  node.source_range = source_range
632
777
  node.filename = @options[:filename]
633
778
  node
634
779
  end
780
+
781
+ # Runs the given block without CSS variable warnings enabled.
782
+ #
783
+ # CSS warnings don't apply within interpolation, so this is used to
784
+ # disable them.
785
+ #
786
+ # @yield []
787
+ def without_css_variable_warning
788
+ old_css_variable_warning = @css_variable_warning
789
+ @css_variable_warning = nil
790
+ yield
791
+ ensure
792
+ @css_variable_warning = old_css_variable_warning
793
+ end
794
+
795
+ # Checks a script node for any immediately-deprecated interpolations, and
796
+ # emits warnings for them.
797
+ #
798
+ # @param node [Sass::Script::Tree::Node]
799
+ def check_for_interpolation(node)
800
+ nodes = [node]
801
+ until nodes.empty?
802
+ node = nodes.pop
803
+ unless node.is_a?(Sass::Script::Tree::Interpolation) &&
804
+ node.deprecation == :immediate
805
+ nodes.concat node.children
806
+ next
807
+ end
808
+
809
+ interpolation_deprecation(node)
810
+ end
811
+ end
812
+
813
+ # Emits a deprecation warning for an interpolation node.
814
+ #
815
+ # @param node [Sass::Script::Tree::Node]
816
+ def interpolation_deprecation(interpolation)
817
+ return if @options[:_convert]
818
+ location = "on line #{interpolation.line}"
819
+ location << " of #{interpolation.filename}" if interpolation.filename
820
+ Sass::Util.sass_warn <<WARNING
821
+ DEPRECATION WARNING #{location}:
822
+ \#{} interpolation near operators will be simplified in a future version of Sass.
823
+ To preserve the current behavior, use quotes:
824
+
825
+ #{interpolation.to_quoted_equivalent.to_sass}
826
+
827
+ You can use the sass-convert command to automatically fix most cases.
828
+ WARNING
829
+ end
635
830
  end
636
831
  end
637
832
  end