sass4 4.0.0

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 (147) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +13 -0
  3. data/AGENTS.md +534 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/CONTRIBUTING.md +148 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.md +242 -0
  8. data/VERSION +1 -0
  9. data/VERSION_NAME +1 -0
  10. data/bin/sass +13 -0
  11. data/bin/sass-convert +12 -0
  12. data/bin/scss +13 -0
  13. data/extra/sass-spec-ref.sh +40 -0
  14. data/extra/update_watch.rb +13 -0
  15. data/init.rb +18 -0
  16. data/lib/sass/cache_stores/base.rb +88 -0
  17. data/lib/sass/cache_stores/chain.rb +34 -0
  18. data/lib/sass/cache_stores/filesystem.rb +60 -0
  19. data/lib/sass/cache_stores/memory.rb +46 -0
  20. data/lib/sass/cache_stores/null.rb +25 -0
  21. data/lib/sass/cache_stores.rb +15 -0
  22. data/lib/sass/callbacks.rb +67 -0
  23. data/lib/sass/css.rb +407 -0
  24. data/lib/sass/deprecation.rb +55 -0
  25. data/lib/sass/engine.rb +1236 -0
  26. data/lib/sass/environment.rb +236 -0
  27. data/lib/sass/error.rb +198 -0
  28. data/lib/sass/exec/base.rb +188 -0
  29. data/lib/sass/exec/sass_convert.rb +283 -0
  30. data/lib/sass/exec/sass_scss.rb +436 -0
  31. data/lib/sass/exec.rb +9 -0
  32. data/lib/sass/features.rb +48 -0
  33. data/lib/sass/importers/base.rb +182 -0
  34. data/lib/sass/importers/deprecated_path.rb +51 -0
  35. data/lib/sass/importers/filesystem.rb +221 -0
  36. data/lib/sass/importers.rb +23 -0
  37. data/lib/sass/logger/base.rb +47 -0
  38. data/lib/sass/logger/delayed.rb +50 -0
  39. data/lib/sass/logger/log_level.rb +45 -0
  40. data/lib/sass/logger.rb +17 -0
  41. data/lib/sass/media.rb +210 -0
  42. data/lib/sass/plugin/compiler.rb +552 -0
  43. data/lib/sass/plugin/configuration.rb +134 -0
  44. data/lib/sass/plugin/generic.rb +15 -0
  45. data/lib/sass/plugin/merb.rb +48 -0
  46. data/lib/sass/plugin/rack.rb +60 -0
  47. data/lib/sass/plugin/rails.rb +47 -0
  48. data/lib/sass/plugin/staleness_checker.rb +199 -0
  49. data/lib/sass/plugin.rb +134 -0
  50. data/lib/sass/railtie.rb +10 -0
  51. data/lib/sass/repl.rb +57 -0
  52. data/lib/sass/root.rb +7 -0
  53. data/lib/sass/script/css_lexer.rb +33 -0
  54. data/lib/sass/script/css_parser.rb +36 -0
  55. data/lib/sass/script/functions.rb +3103 -0
  56. data/lib/sass/script/lexer.rb +518 -0
  57. data/lib/sass/script/parser.rb +1164 -0
  58. data/lib/sass/script/tree/funcall.rb +314 -0
  59. data/lib/sass/script/tree/interpolation.rb +220 -0
  60. data/lib/sass/script/tree/list_literal.rb +119 -0
  61. data/lib/sass/script/tree/literal.rb +49 -0
  62. data/lib/sass/script/tree/map_literal.rb +64 -0
  63. data/lib/sass/script/tree/node.rb +119 -0
  64. data/lib/sass/script/tree/operation.rb +149 -0
  65. data/lib/sass/script/tree/selector.rb +26 -0
  66. data/lib/sass/script/tree/string_interpolation.rb +125 -0
  67. data/lib/sass/script/tree/unary_operation.rb +69 -0
  68. data/lib/sass/script/tree/variable.rb +57 -0
  69. data/lib/sass/script/tree.rb +16 -0
  70. data/lib/sass/script/value/arg_list.rb +36 -0
  71. data/lib/sass/script/value/base.rb +258 -0
  72. data/lib/sass/script/value/bool.rb +35 -0
  73. data/lib/sass/script/value/callable.rb +25 -0
  74. data/lib/sass/script/value/color.rb +704 -0
  75. data/lib/sass/script/value/function.rb +19 -0
  76. data/lib/sass/script/value/helpers.rb +298 -0
  77. data/lib/sass/script/value/list.rb +135 -0
  78. data/lib/sass/script/value/map.rb +70 -0
  79. data/lib/sass/script/value/null.rb +44 -0
  80. data/lib/sass/script/value/number.rb +564 -0
  81. data/lib/sass/script/value/string.rb +138 -0
  82. data/lib/sass/script/value.rb +13 -0
  83. data/lib/sass/script.rb +66 -0
  84. data/lib/sass/scss/css_parser.rb +61 -0
  85. data/lib/sass/scss/parser.rb +1343 -0
  86. data/lib/sass/scss/rx.rb +134 -0
  87. data/lib/sass/scss/static_parser.rb +351 -0
  88. data/lib/sass/scss.rb +14 -0
  89. data/lib/sass/selector/abstract_sequence.rb +112 -0
  90. data/lib/sass/selector/comma_sequence.rb +195 -0
  91. data/lib/sass/selector/pseudo.rb +291 -0
  92. data/lib/sass/selector/sequence.rb +661 -0
  93. data/lib/sass/selector/simple.rb +124 -0
  94. data/lib/sass/selector/simple_sequence.rb +348 -0
  95. data/lib/sass/selector.rb +327 -0
  96. data/lib/sass/shared.rb +76 -0
  97. data/lib/sass/source/map.rb +209 -0
  98. data/lib/sass/source/position.rb +39 -0
  99. data/lib/sass/source/range.rb +41 -0
  100. data/lib/sass/stack.rb +140 -0
  101. data/lib/sass/supports.rb +225 -0
  102. data/lib/sass/tree/at_root_node.rb +83 -0
  103. data/lib/sass/tree/charset_node.rb +22 -0
  104. data/lib/sass/tree/comment_node.rb +82 -0
  105. data/lib/sass/tree/content_node.rb +9 -0
  106. data/lib/sass/tree/css_import_node.rb +68 -0
  107. data/lib/sass/tree/debug_node.rb +18 -0
  108. data/lib/sass/tree/directive_node.rb +59 -0
  109. data/lib/sass/tree/each_node.rb +24 -0
  110. data/lib/sass/tree/error_node.rb +18 -0
  111. data/lib/sass/tree/extend_node.rb +43 -0
  112. data/lib/sass/tree/for_node.rb +36 -0
  113. data/lib/sass/tree/function_node.rb +44 -0
  114. data/lib/sass/tree/if_node.rb +52 -0
  115. data/lib/sass/tree/import_node.rb +75 -0
  116. data/lib/sass/tree/keyframe_rule_node.rb +15 -0
  117. data/lib/sass/tree/media_node.rb +48 -0
  118. data/lib/sass/tree/mixin_def_node.rb +38 -0
  119. data/lib/sass/tree/mixin_node.rb +52 -0
  120. data/lib/sass/tree/node.rb +240 -0
  121. data/lib/sass/tree/prop_node.rb +162 -0
  122. data/lib/sass/tree/return_node.rb +19 -0
  123. data/lib/sass/tree/root_node.rb +44 -0
  124. data/lib/sass/tree/rule_node.rb +153 -0
  125. data/lib/sass/tree/supports_node.rb +38 -0
  126. data/lib/sass/tree/trace_node.rb +33 -0
  127. data/lib/sass/tree/variable_node.rb +36 -0
  128. data/lib/sass/tree/visitors/base.rb +72 -0
  129. data/lib/sass/tree/visitors/check_nesting.rb +173 -0
  130. data/lib/sass/tree/visitors/convert.rb +350 -0
  131. data/lib/sass/tree/visitors/cssize.rb +362 -0
  132. data/lib/sass/tree/visitors/deep_copy.rb +107 -0
  133. data/lib/sass/tree/visitors/extend.rb +64 -0
  134. data/lib/sass/tree/visitors/perform.rb +572 -0
  135. data/lib/sass/tree/visitors/set_options.rb +139 -0
  136. data/lib/sass/tree/visitors/to_css.rb +440 -0
  137. data/lib/sass/tree/warn_node.rb +18 -0
  138. data/lib/sass/tree/while_node.rb +18 -0
  139. data/lib/sass/util/multibyte_string_scanner.rb +151 -0
  140. data/lib/sass/util/normalized_map.rb +122 -0
  141. data/lib/sass/util/subset_map.rb +109 -0
  142. data/lib/sass/util/test.rb +9 -0
  143. data/lib/sass/util.rb +1137 -0
  144. data/lib/sass/version.rb +120 -0
  145. data/lib/sass.rb +102 -0
  146. data/rails/init.rb +1 -0
  147. metadata +283 -0
@@ -0,0 +1,1164 @@
1
+ require 'sass/script/lexer'
2
+
3
+ module Sass
4
+ module Script
5
+ # The parser for SassScript.
6
+ # It parses a string of code into a tree of {Script::Tree::Node}s.
7
+ class Parser
8
+ # The line number of the parser's current position.
9
+ #
10
+ # @return [Integer]
11
+ def line
12
+ @lexer.line
13
+ end
14
+
15
+ # The column number of the parser's current position.
16
+ #
17
+ # @return [Integer]
18
+ def offset
19
+ @lexer.offset
20
+ end
21
+
22
+ # @param str [String, StringScanner] The source text to parse
23
+ # @param line [Integer] The line on which the SassScript appears.
24
+ # Used for error reporting and sourcemap building
25
+ # @param offset [Integer] The character (not byte) offset where the script starts in the line.
26
+ # Used for error reporting and sourcemap building
27
+ # @param options [{Symbol => Object}] An options hash; see
28
+ # {file:SASS_REFERENCE.md#Options the Sass options documentation}.
29
+ # This supports an additional `:allow_extra_text` option that controls
30
+ # whether the parser throws an error when extra text is encountered
31
+ # after the parsed construct.
32
+ def initialize(str, line, offset, options = {})
33
+ @options = options
34
+ @allow_extra_text = options.delete(:allow_extra_text)
35
+ @lexer = lexer_class.new(str, line, offset, options)
36
+ @stop_at = nil
37
+ end
38
+
39
+ # Parses a SassScript expression within an interpolated segment (`#{}`).
40
+ # This means that it stops when it comes across an unmatched `}`,
41
+ # which signals the end of an interpolated segment,
42
+ # it returns rather than throwing an error.
43
+ #
44
+ # @param warn_for_color [Boolean] Whether raw color values passed to
45
+ # interoplation should cause a warning.
46
+ # @return [Script::Tree::Node] The root node of the parse tree
47
+ # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
48
+ def parse_interpolated(warn_for_color = false)
49
+ # Start two characters back to compensate for #{
50
+ start_pos = Sass::Source::Position.new(line, offset - 2)
51
+ expr = assert_expr :expr
52
+ assert_tok :end_interpolation
53
+ expr = Sass::Script::Tree::Interpolation.new(
54
+ nil, expr, nil, false, false, :warn_for_color => warn_for_color)
55
+ check_for_interpolation expr
56
+ expr.options = @options
57
+ node(expr, start_pos)
58
+ rescue Sass::SyntaxError => e
59
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
60
+ raise e
61
+ end
62
+
63
+ # Parses a SassScript expression.
64
+ #
65
+ # @return [Script::Tree::Node] The root node of the parse tree
66
+ # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
67
+ def parse
68
+ expr = assert_expr :expr
69
+ assert_done
70
+ expr.options = @options
71
+ check_for_interpolation expr
72
+ expr
73
+ rescue Sass::SyntaxError => e
74
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
75
+ raise e
76
+ end
77
+
78
+ # Parses a SassScript expression,
79
+ # ending it when it encounters one of the given identifier tokens.
80
+ #
81
+ # @param tokens [#include?(String | Symbol)] A set of strings or symbols that delimit the expression.
82
+ # @return [Script::Tree::Node] The root node of the parse tree
83
+ # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
84
+ def parse_until(tokens)
85
+ @stop_at = tokens
86
+ expr = assert_expr :expr
87
+ assert_done
88
+ expr.options = @options
89
+ check_for_interpolation expr
90
+ expr
91
+ rescue Sass::SyntaxError => e
92
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
93
+ raise e
94
+ end
95
+
96
+ # Parses the argument list for a mixin include.
97
+ #
98
+ # @return [(Array<Script::Tree::Node>,
99
+ # {String => Script::Tree::Node},
100
+ # Script::Tree::Node,
101
+ # Script::Tree::Node)]
102
+ # The root nodes of the positional arguments, keyword arguments, and
103
+ # splat argument(s). Keyword arguments are in a hash from names to values.
104
+ # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
105
+ def parse_mixin_include_arglist
106
+ args, keywords = [], {}
107
+ if try_tok(:lparen)
108
+ args, keywords, splat, kwarg_splat = mixin_arglist
109
+ assert_tok(:rparen)
110
+ end
111
+ assert_done
112
+
113
+ args.each do |a|
114
+ check_for_interpolation a
115
+ a.options = @options
116
+ end
117
+
118
+ keywords.each do |_, v|
119
+ check_for_interpolation v
120
+ v.options = @options
121
+ end
122
+
123
+ if splat
124
+ check_for_interpolation splat
125
+ splat.options = @options
126
+ end
127
+
128
+ if kwarg_splat
129
+ check_for_interpolation kwarg_splat
130
+ kwarg_splat.options = @options
131
+ end
132
+
133
+ return args, keywords, splat, kwarg_splat
134
+ rescue Sass::SyntaxError => e
135
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
136
+ raise e
137
+ end
138
+
139
+ # Parses the argument list for a mixin definition.
140
+ #
141
+ # @return [(Array<Script::Tree::Node>, Script::Tree::Node)]
142
+ # The root nodes of the arguments, and the splat argument.
143
+ # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
144
+ def parse_mixin_definition_arglist
145
+ args, splat = defn_arglist!(false)
146
+ assert_done
147
+
148
+ args.each do |k, v|
149
+ check_for_interpolation k
150
+ k.options = @options
151
+
152
+ if v
153
+ check_for_interpolation v
154
+ v.options = @options
155
+ end
156
+ end
157
+
158
+ if splat
159
+ check_for_interpolation splat
160
+ splat.options = @options
161
+ end
162
+
163
+ return args, splat
164
+ rescue Sass::SyntaxError => e
165
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
166
+ raise e
167
+ end
168
+
169
+ # Parses the argument list for a function definition.
170
+ #
171
+ # @return [(Array<Script::Tree::Node>, Script::Tree::Node)]
172
+ # The root nodes of the arguments, and the splat argument.
173
+ # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
174
+ def parse_function_definition_arglist
175
+ args, splat = defn_arglist!(true)
176
+ assert_done
177
+
178
+ args.each do |k, v|
179
+ check_for_interpolation k
180
+ k.options = @options
181
+
182
+ if v
183
+ check_for_interpolation v
184
+ v.options = @options
185
+ end
186
+ end
187
+
188
+ if splat
189
+ check_for_interpolation splat
190
+ splat.options = @options
191
+ end
192
+
193
+ return args, splat
194
+ rescue Sass::SyntaxError => e
195
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
196
+ raise e
197
+ end
198
+
199
+ # Parse a single string value, possibly containing interpolation.
200
+ # Doesn't assert that the scanner is finished after parsing.
201
+ #
202
+ # @return [Script::Tree::Node] The root node of the parse tree.
203
+ # @raise [Sass::SyntaxError] if the string isn't valid SassScript
204
+ def parse_string
205
+ unless (peek = @lexer.peek) &&
206
+ (peek.type == :string ||
207
+ (peek.type == :funcall && peek.value.downcase == 'url'))
208
+ lexer.expected!("string")
209
+ end
210
+
211
+ expr = assert_expr :funcall
212
+ check_for_interpolation expr
213
+ expr.options = @options
214
+ @lexer.unpeek!
215
+ expr
216
+ rescue Sass::SyntaxError => e
217
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
218
+ raise e
219
+ end
220
+
221
+ # Parses a SassScript expression.
222
+ #
223
+ # @overload parse(str, line, offset, filename = nil)
224
+ # @return [Script::Tree::Node] The root node of the parse tree
225
+ # @see Parser#initialize
226
+ # @see Parser#parse
227
+ def self.parse(*args)
228
+ new(*args).parse
229
+ end
230
+
231
+ PRECEDENCE = [
232
+ :comma, :single_eq, :space, :slash, :or, :and,
233
+ [:eq, :neq],
234
+ [:gt, :gte, :lt, :lte],
235
+ [:plus, :minus],
236
+ [:times, :div, :mod],
237
+ ]
238
+
239
+ ASSOCIATIVE = [:plus, :times]
240
+
241
+ class << self
242
+ # Returns an integer representing the precedence
243
+ # of the given operator.
244
+ # A lower integer indicates a looser binding.
245
+ #
246
+ # @private
247
+ def precedence_of(op)
248
+ PRECEDENCE.each_with_index do |e, i|
249
+ return i if Array(e).include?(op)
250
+ end
251
+ raise "[BUG] Unknown operator #{op.inspect}"
252
+ end
253
+
254
+ # Returns whether or not the given operation is associative.
255
+ #
256
+ # @private
257
+ def associative?(op)
258
+ ASSOCIATIVE.include?(op)
259
+ end
260
+
261
+ private
262
+
263
+ # Defines a simple left-associative production.
264
+ # name is the name of the production,
265
+ # sub is the name of the production beneath it,
266
+ # and ops is a list of operators for this precedence level
267
+ def production(name, sub, *ops)
268
+ class_eval <<RUBY, __FILE__, __LINE__ + 1
269
+ def #{name}
270
+ interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect})
271
+ return interp if interp
272
+ return unless e = #{sub}
273
+
274
+ while tok = peek_toks(#{ops.map {|o| o.inspect}.join(', ')})
275
+ return e if @stop_at && @stop_at.include?(tok.type)
276
+ @lexer.next
277
+
278
+ if interp = try_op_before_interp(tok, e)
279
+ other_interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect}, interp)
280
+ return interp unless other_interp
281
+ return other_interp
282
+ end
283
+
284
+ e = node(Tree::Operation.new(e, assert_expr(#{sub.inspect}), tok.type),
285
+ e.source_range.start_pos)
286
+ end
287
+ e
288
+ end
289
+ RUBY
290
+ end
291
+
292
+ def unary(op, sub)
293
+ class_eval <<RUBY, __FILE__, __LINE__ + 1
294
+ def unary_#{op}
295
+ return #{sub} unless tok = try_tok(:#{op})
296
+ interp = try_op_before_interp(tok)
297
+ return interp if interp
298
+ start_pos = source_position
299
+ node(Tree::UnaryOperation.new(assert_expr(:unary_#{op}), :#{op}), start_pos)
300
+ end
301
+ RUBY
302
+ end
303
+ end
304
+
305
+ private
306
+
307
+ def source_position
308
+ Sass::Source::Position.new(line, offset)
309
+ end
310
+
311
+ def range(start_pos, end_pos = source_position)
312
+ Sass::Source::Range.new(start_pos, end_pos, @options[:filename], @options[:importer])
313
+ end
314
+
315
+ # @private
316
+ def lexer_class; Lexer; end
317
+
318
+ def map
319
+ start_pos = source_position
320
+ e = interpolation
321
+ return unless e
322
+ return list e, start_pos unless @lexer.peek && @lexer.peek.type == :colon
323
+
324
+ pair = map_pair(e)
325
+ map = node(Sass::Script::Tree::MapLiteral.new([pair]), start_pos)
326
+ while try_tok(:comma)
327
+ pair = map_pair
328
+ return map unless pair
329
+ map.pairs << pair
330
+ map.source_range.end_pos = map.pairs.last.last.source_range.end_pos
331
+ end
332
+ map
333
+ end
334
+
335
+ def map_pair(key = nil)
336
+ return unless key ||= interpolation
337
+ assert_tok :colon
338
+ return key, assert_expr(:interpolation)
339
+ end
340
+
341
+ def expr
342
+ start_pos = source_position
343
+ e = interpolation
344
+ return unless e
345
+ list e, start_pos
346
+ end
347
+
348
+ def list(first, start_pos)
349
+ return first unless @lexer.peek && @lexer.peek.type == :comma
350
+
351
+ list = node(Sass::Script::Tree::ListLiteral.new([first], separator: :comma), start_pos)
352
+ while (tok = try_tok(:comma))
353
+ element_before_interp = list.elements.length == 1 ? list.elements.first : list
354
+ if (interp = try_op_before_interp(tok, element_before_interp))
355
+ other_interp = try_ops_after_interp([:comma], :expr, interp)
356
+ return interp unless other_interp
357
+ return other_interp
358
+ end
359
+ return list unless (e = interpolation)
360
+ list.elements << e
361
+ list.source_range.end_pos = list.elements.last.source_range.end_pos
362
+ end
363
+ list
364
+ end
365
+
366
+ production :equals, :interpolation, :single_eq
367
+
368
+ def try_op_before_interp(op, prev = nil, after_interp = false)
369
+ return unless @lexer.peek && @lexer.peek.type == :begin_interpolation
370
+ unary = !prev && !after_interp
371
+ wb = @lexer.whitespace?(op)
372
+ str = literal_node(Script::Value::String.new(Lexer::OPERATORS_REVERSE[op.type]),
373
+ op.source_range)
374
+
375
+ deprecation =
376
+ case op.type
377
+ when :comma; :potential
378
+ when :div, :single_eq; :none
379
+ when :plus; unary ? :none : :immediate
380
+ when :minus; @lexer.whitespace?(@lexer.peek) ? :immediate : :none
381
+ else; :immediate
382
+ end
383
+
384
+ interp = node(
385
+ Script::Tree::Interpolation.new(
386
+ prev, str, nil, wb, false, :originally_text => true, :deprecation => deprecation),
387
+ (prev || str).source_range.start_pos)
388
+ interpolation(first: interp)
389
+ end
390
+
391
+ def try_ops_after_interp(ops, name, prev = nil)
392
+ return unless @lexer.after_interpolation?
393
+ op = peek_toks(*ops)
394
+ return unless op
395
+ return if @stop_at && @stop_at.include?(op.type)
396
+ @lexer.next
397
+
398
+ interp = try_op_before_interp(op, prev, :after_interp)
399
+ return interp if interp
400
+
401
+ wa = @lexer.whitespace?
402
+ str = literal_node(Script::Value::String.new(Lexer::OPERATORS_REVERSE[op.type]),
403
+ op.source_range)
404
+ str.line = @lexer.line
405
+
406
+ deprecation =
407
+ case op.type
408
+ when :comma; :potential
409
+ when :div, :single_eq; :none
410
+ when :minus; @lexer.whitespace?(op) ? :immediate : :none
411
+ else; :immediate
412
+ end
413
+ interp = node(
414
+ Script::Tree::Interpolation.new(
415
+ prev, str, assert_expr(name), false, wa,
416
+ :originally_text => true, :deprecation => deprecation),
417
+ (prev || str).source_range.start_pos)
418
+ interp
419
+ end
420
+
421
+ def interpolation(first: nil, inner: :space)
422
+ e = first || send(inner)
423
+ while (interp = try_tok(:begin_interpolation))
424
+ wb = @lexer.whitespace?(interp)
425
+ char_before = @lexer.char(interp.pos - 1)
426
+ mid = without_stop_at {assert_expr :expr}
427
+ assert_tok :end_interpolation
428
+ wa = @lexer.whitespace?
429
+ char_after = @lexer.char
430
+
431
+ after = send(inner)
432
+ before_deprecation = e.is_a?(Script::Tree::Interpolation) ? e.deprecation : :none
433
+ after_deprecation = after.is_a?(Script::Tree::Interpolation) ? after.deprecation : :none
434
+
435
+ deprecation =
436
+ if before_deprecation == :immediate || after_deprecation == :immediate ||
437
+ # Warn for #{foo}$var and #{foo}(1) but not #{$foo}1.
438
+ (after && !wa && char_after =~ /[$(]/) ||
439
+ # Warn for $var#{foo} and (a)#{foo} but not a#{foo}.
440
+ (e && !wb && is_unsafe_before?(e, char_before))
441
+ :immediate
442
+ else
443
+ :potential
444
+ end
445
+
446
+ e = node(
447
+ Script::Tree::Interpolation.new(e, mid, after, wb, wa, :deprecation => deprecation),
448
+ (e || interp).source_range.start_pos)
449
+ end
450
+ e
451
+ end
452
+
453
+ # Returns whether `expr` is unsafe to include before an interpolation.
454
+ #
455
+ # @param expr [Node] The expression to check.
456
+ # @param char_before [String] The character immediately before the
457
+ # interpolation being checked (and presumably the last character of
458
+ # `expr`).
459
+ # @return [Boolean]
460
+ def is_unsafe_before?(expr, char_before)
461
+ return char_before == ')' if is_safe_value?(expr)
462
+
463
+ # Otherwise, it's only safe if it was another interpolation.
464
+ !expr.is_a?(Script::Tree::Interpolation)
465
+ end
466
+
467
+ # Returns whether `expr` is safe as the value immediately before an
468
+ # interpolation.
469
+ #
470
+ # It's safe as long as the previous expression is an identifier or number,
471
+ # or a list whose last element is also safe.
472
+ def is_safe_value?(expr)
473
+ return is_safe_value?(expr.elements.last) if expr.is_a?(Script::Tree::ListLiteral)
474
+ return false unless expr.is_a?(Script::Tree::Literal)
475
+ expr.value.is_a?(Script::Value::Number) ||
476
+ (expr.value.is_a?(Script::Value::String) && expr.value.type == :identifier)
477
+ end
478
+
479
+ def space
480
+ start_pos = source_position
481
+ e = or_expr
482
+ return unless e
483
+ arr = [e]
484
+ while (e = or_expr)
485
+ arr << e
486
+ end
487
+ if arr.size == 1
488
+ arr.first
489
+ else
490
+ node(Sass::Script::Tree::ListLiteral.new(arr, separator: :space), start_pos)
491
+ end
492
+ end
493
+
494
+ production :or_expr, :and_expr, :or
495
+ production :and_expr, :eq_or_neq, :and
496
+ production :eq_or_neq, :relational, :eq, :neq
497
+ production :relational, :plus_or_minus, :gt, :gte, :lt, :lte
498
+ production :plus_or_minus, :times_div_or_mod, :plus, :minus
499
+ production :times_div_or_mod, :unary_plus, :times, :div, :mod
500
+
501
+ unary :plus, :unary_minus
502
+ unary :minus, :unary_div
503
+ unary :div, :unary_not # For strings, so /foo/bar works
504
+ unary :not, :ident
505
+
506
+ def ident
507
+ return css_min_max unless @lexer.peek && @lexer.peek.type == :ident
508
+ return if @stop_at && @stop_at.include?(@lexer.peek.value)
509
+
510
+ name = @lexer.next
511
+ if (color = Sass::Script::Value::Color::COLOR_NAMES[name.value.downcase])
512
+ literal_node(Sass::Script::Value::Color.new(color, name.value), name.source_range)
513
+ elsif name.value == "true"
514
+ literal_node(Sass::Script::Value::Bool.new(true), name.source_range)
515
+ elsif name.value == "false"
516
+ literal_node(Sass::Script::Value::Bool.new(false), name.source_range)
517
+ elsif name.value == "null"
518
+ literal_node(Sass::Script::Value::Null.new, name.source_range)
519
+ else
520
+ literal_node(Sass::Script::Value::String.new(name.value, :identifier), name.source_range)
521
+ end
522
+ end
523
+
524
+ def css_min_max
525
+ @lexer.try do
526
+ next unless tok = try_tok(:funcall)
527
+ next unless %w[min max].include?(tok.value.downcase)
528
+ next unless contents = min_max_contents
529
+ node(array_to_interpolation(["#{tok.value}(", *contents]),
530
+ tok.source_range.start_pos, source_position)
531
+ end || funcall
532
+ end
533
+
534
+ def min_max_contents(allow_comma: true)
535
+ result = []
536
+ loop do
537
+ if tok = try_tok(:number)
538
+ result << tok.value.to_s
539
+ elsif value = min_max_interpolation
540
+ result << value
541
+ elsif value = min_max_calc
542
+ result << value.value
543
+ elsif value = min_max_function ||
544
+ min_max_parens ||
545
+ nested_min_max
546
+ result.concat value
547
+ else
548
+ return
549
+ end
550
+
551
+ if try_tok(:rparen)
552
+ result << ")"
553
+ return result
554
+ elsif tok = try_tok(:plus) || try_tok(:minus) || try_tok(:times) || try_tok(:div)
555
+ result << " #{Lexer::OPERATORS_REVERSE[tok.type]} "
556
+ elsif allow_comma && try_tok(:comma)
557
+ result << ", "
558
+ else
559
+ return
560
+ end
561
+ end
562
+ end
563
+
564
+ def min_max_interpolation
565
+ without_stop_at do
566
+ tok = try_tok(:begin_interpolation)
567
+ return unless tok
568
+ expr = without_stop_at {assert_expr :expr}
569
+ assert_tok :end_interpolation
570
+ expr
571
+ end
572
+ end
573
+
574
+ def min_max_function
575
+ return unless tok = peek_tok(:funcall)
576
+ return unless %w[calc env var].include?(tok.value.downcase)
577
+ @lexer.next
578
+ result = [tok.value, '(', *declaration_value, ')']
579
+ assert_tok :rparen
580
+ result
581
+ end
582
+
583
+ def min_max_calc
584
+ return unless tok = peek_tok(:special_fun)
585
+ return unless tok.value.value.downcase.start_with?("calc(")
586
+ @lexer.next.value
587
+ end
588
+
589
+ def min_max_parens
590
+ return unless try_tok :lparen
591
+ return unless contents = min_max_contents(allow_comma: false)
592
+ ['(', *contents]
593
+ end
594
+
595
+ def nested_min_max
596
+ return unless tok = peek_tok(:funcall)
597
+ return unless %w[min max].include?(tok.value.downcase)
598
+ @lexer.next
599
+ return unless contents = min_max_contents
600
+ [tok.value, '(', *contents]
601
+ end
602
+
603
+ def declaration_value
604
+ result = []
605
+ brackets = []
606
+ loop do
607
+ result << @lexer.str do
608
+ until @lexer.done? ||
609
+ peek_toks(:begin_interpolation,
610
+ :end_interpolation,
611
+ :lcurly,
612
+ :lparen,
613
+ :lsquare,
614
+ :rparen,
615
+ :rsquare)
616
+ @lexer.next || @lexer.next_char
617
+ end
618
+ end
619
+
620
+ if try_tok(:begin_interpolation)
621
+ result << assert_expr(:expr)
622
+ assert_tok :end_interpolation
623
+ elsif tok = try_toks(:lcurly, :lparen, :lsquare)
624
+ brackets << case tok.type
625
+ when :lcurly; :end_interpolation
626
+ when :lparen; :rparen
627
+ when :lsquare; :rsquare
628
+ end
629
+ result << Lexer::OPERATORS_REVERSE[tok.type]
630
+ elsif brackets.empty?
631
+ return result
632
+ else
633
+ bracket = brackets.pop
634
+ assert_tok bracket
635
+ result << Lexer::OPERATORS_REVERSE[bracket]
636
+ end
637
+ end
638
+ end
639
+
640
+ def funcall
641
+ tok = try_tok(:funcall)
642
+ return raw unless tok
643
+
644
+ # Special handling for CSS Color Level 4 functions
645
+ if %w[rgb rgba hsl hsla].include?(tok.value.downcase)
646
+ args, keywords, splat, kwarg_splat = color_fn_arglist
647
+ else
648
+ args, keywords, splat, kwarg_splat = fn_arglist
649
+ end
650
+
651
+ assert_tok(:rparen)
652
+ node(Script::Tree::Funcall.new(tok.value, args, keywords, splat, kwarg_splat),
653
+ tok.source_range.start_pos, source_position)
654
+ end
655
+
656
+ def defn_arglist!(must_have_parens)
657
+ if must_have_parens
658
+ assert_tok(:lparen)
659
+ else
660
+ return [], nil unless try_tok(:lparen)
661
+ end
662
+
663
+ without_stop_at do
664
+ res = []
665
+ splat = nil
666
+ must_have_default = false
667
+ loop do
668
+ break if peek_tok(:rparen)
669
+ c = assert_tok(:const)
670
+ var = node(Script::Tree::Variable.new(c.value), c.source_range)
671
+ if try_tok(:colon)
672
+ val = assert_expr(:space)
673
+ must_have_default = true
674
+ elsif try_tok(:splat)
675
+ splat = var
676
+ break
677
+ elsif must_have_default
678
+ raise SyntaxError.new(
679
+ "Required argument #{var.inspect} must come before any optional arguments.")
680
+ end
681
+ res << [var, val]
682
+ break unless try_tok(:comma)
683
+ end
684
+ assert_tok(:rparen)
685
+ return res, splat
686
+ end
687
+ end
688
+
689
+ def fn_arglist
690
+ arglist(:equals, "function argument")
691
+ end
692
+
693
+ # Special arglist parser for CSS Color Level 4 syntax
694
+ # Handles: rgb(255 0 0), rgb(255 0 0 / 0.5), rgb(255, 0, 0), rgb($r: 255, $g: 0, $b: 0)
695
+ def color_fn_arglist
696
+ without_stop_at do
697
+ args = []
698
+ keywords = Sass::Util::NormalizedMap.new
699
+ splat = nil
700
+
701
+ # Parse first argument using unary_plus to avoid division interpretation
702
+ e = unary_plus
703
+ return [args, keywords, splat] unless e
704
+
705
+ # Check what follows
706
+ peek = @lexer.peek
707
+
708
+ # Keyword argument: $red: 255
709
+ if peek && peek.type == :colon
710
+ name = e
711
+ @lexer.expected!("comma") unless name.is_a?(Tree::Variable)
712
+ assert_tok(:colon)
713
+ value = assert_expr(:equals, "function argument")
714
+ keywords[name.name] = value
715
+
716
+ # Continue parsing keyword arguments
717
+ while try_tok(:comma)
718
+ e = send(:equals)
719
+ break unless e
720
+
721
+ if @lexer.peek && @lexer.peek.type == :colon
722
+ name = e
723
+ @lexer.expected!("comma") unless name.is_a?(Tree::Variable)
724
+ assert_tok(:colon)
725
+ value = assert_expr(:equals, "function argument")
726
+
727
+ if keywords[name.name]
728
+ raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once")
729
+ end
730
+
731
+ keywords[name.name] = value
732
+ else
733
+ # Mixed positional and keyword - error will be raised by function
734
+ raise SyntaxError.new("Positional arguments must come before keyword arguments.")
735
+ end
736
+ end
737
+
738
+ return [args, keywords, splat]
739
+ end
740
+
741
+ # Comma-separated (legacy syntax): 255, 0, 0
742
+ if peek && peek.type == :comma
743
+ args << e
744
+
745
+ while try_tok(:comma)
746
+ e = send(:equals)
747
+ break unless e
748
+
749
+ # Check for keyword argument after positional
750
+ if @lexer.peek && @lexer.peek.type == :colon
751
+ name = e
752
+ @lexer.expected!("comma") unless name.is_a?(Tree::Variable)
753
+ assert_tok(:colon)
754
+ value = assert_expr(:equals, "function argument")
755
+ keywords[name.name] = value
756
+
757
+ # Continue parsing remaining keyword arguments
758
+ while try_tok(:comma)
759
+ e = send(:equals)
760
+ break unless e
761
+
762
+ if @lexer.peek && @lexer.peek.type == :colon
763
+ name = e
764
+ @lexer.expected!("comma") unless name.is_a?(Tree::Variable)
765
+ assert_tok(:colon)
766
+ value = assert_expr(:equals, "function argument")
767
+
768
+ if keywords[name.name]
769
+ raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once")
770
+ end
771
+
772
+ keywords[name.name] = value
773
+ else
774
+ raise SyntaxError.new("Positional arguments must come before keyword arguments.")
775
+ end
776
+ end
777
+ break
778
+ end
779
+
780
+ args << e
781
+ end
782
+
783
+ return [args, keywords, splat]
784
+ end
785
+
786
+ # Space-separated syntax (CSS Color Level 4): 255 0 0 / 0.5
787
+ start_pos = (e.source_range rescue nil)&.start_pos || source_position
788
+ values = [e]
789
+
790
+ # Collect space-separated values
791
+ loop do
792
+ peek = @lexer.peek
793
+ break if !peek || peek.type == :rparen
794
+
795
+ # Handle slash separator
796
+ if peek.type == :div
797
+ @lexer.next # consume /
798
+
799
+ # Parse alpha value
800
+ alpha = unary_plus
801
+ if alpha
802
+ # Build slash-separated list for alpha
803
+ alpha_list = node(
804
+ Sass::Script::Tree::ListLiteral.new([alpha], separator: :slash),
805
+ alpha.source_range.start_pos
806
+ )
807
+
808
+ # Build space-separated list for RGB/HSL values
809
+ rgb_list = if values.length == 1
810
+ values.first
811
+ else
812
+ node(
813
+ Sass::Script::Tree::ListLiteral.new(values, separator: :space),
814
+ start_pos
815
+ )
816
+ end
817
+
818
+ # Build outer space-separated list: (rgb alpha)
819
+ result = node(
820
+ Sass::Script::Tree::ListLiteral.new([rgb_list, alpha_list], separator: :space),
821
+ start_pos
822
+ )
823
+
824
+ return [[result], keywords, splat]
825
+ end
826
+ break
827
+ end
828
+
829
+ # Parse next value
830
+ val = unary_plus
831
+ break unless val
832
+ values << val
833
+ end
834
+
835
+ # Return space-separated values
836
+ result = if values.length == 1
837
+ values.first
838
+ else
839
+ node(
840
+ Sass::Script::Tree::ListLiteral.new(values, separator: :space),
841
+ start_pos
842
+ )
843
+ end
844
+
845
+ [[result], keywords, splat]
846
+ end
847
+ end
848
+
849
+ def mixin_arglist
850
+ arglist(:interpolation, "mixin argument")
851
+ end
852
+
853
+ def arglist(subexpr, description)
854
+ without_stop_at do
855
+ args = []
856
+ keywords = Sass::Util::NormalizedMap.new
857
+ splat = nil
858
+ while (e = send(subexpr))
859
+ if @lexer.peek && @lexer.peek.type == :colon
860
+ name = e
861
+ @lexer.expected!("comma") unless name.is_a?(Tree::Variable)
862
+ assert_tok(:colon)
863
+ value = assert_expr(subexpr, description)
864
+
865
+ if keywords[name.name]
866
+ raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once")
867
+ end
868
+
869
+ keywords[name.name] = value
870
+ else
871
+ if try_tok(:splat)
872
+ return args, keywords, splat, e if splat
873
+ splat, e = e, nil
874
+ elsif splat
875
+ raise SyntaxError.new("Only keyword arguments may follow variable arguments (...).")
876
+ elsif !keywords.empty?
877
+ raise SyntaxError.new("Positional arguments must come before keyword arguments.")
878
+ end
879
+ args << e if e
880
+ end
881
+
882
+ return args, keywords, splat unless try_tok(:comma)
883
+ end
884
+ return args, keywords
885
+ end
886
+ end
887
+
888
+ def raw
889
+ tok = try_tok(:raw)
890
+ return special_fun unless tok
891
+ literal_node(Script::Value::String.new(tok.value), tok.source_range)
892
+ end
893
+
894
+ def special_fun
895
+ first = try_tok(:special_fun)
896
+ return square_list unless first
897
+ str = literal_node(first.value, first.source_range)
898
+ return str unless try_tok(:string_interpolation)
899
+ mid = without_stop_at {assert_expr :expr}
900
+ assert_tok :end_interpolation
901
+ last = assert_expr(:special_fun)
902
+ node(
903
+ Tree::Interpolation.new(str, mid, last, false, false),
904
+ first.source_range.start_pos)
905
+ end
906
+
907
+ def square_list
908
+ start_pos = source_position
909
+ return paren unless try_tok(:lsquare)
910
+
911
+ without_stop_at do
912
+ space_start_pos = source_position
913
+ e = interpolation(inner: :or_expr)
914
+ separator = nil
915
+ if e
916
+ elements = [e]
917
+ while (e = interpolation(inner: :or_expr))
918
+ elements << e
919
+ end
920
+
921
+ # If there's a comma after a space-separated list, it's actually a
922
+ # space-separated list nested in a comma-separated list.
923
+ if try_tok(:comma)
924
+ e = if elements.length == 1
925
+ elements.first
926
+ else
927
+ node(
928
+ Sass::Script::Tree::ListLiteral.new(elements, separator: :space),
929
+ space_start_pos)
930
+ end
931
+ elements = [e]
932
+
933
+ while (e = space)
934
+ elements << e
935
+ break unless try_tok(:comma)
936
+ end
937
+ separator = :comma
938
+ else
939
+ separator = :space if elements.length > 1
940
+ end
941
+ else
942
+ elements = []
943
+ end
944
+
945
+ assert_tok(:rsquare)
946
+ end_pos = source_position
947
+
948
+ node(Sass::Script::Tree::ListLiteral.new(elements, separator: separator, bracketed: true),
949
+ start_pos, end_pos)
950
+ end
951
+ end
952
+
953
+ def paren
954
+ return variable unless try_tok(:lparen)
955
+ without_stop_at do
956
+ start_pos = source_position
957
+ e = map
958
+ e.force_division! if e
959
+ end_pos = source_position
960
+ assert_tok(:rparen)
961
+ e || node(Sass::Script::Tree::ListLiteral.new([]), start_pos, end_pos)
962
+ end
963
+ end
964
+
965
+ def variable
966
+ start_pos = source_position
967
+ c = try_tok(:const)
968
+ return string unless c
969
+ node(Tree::Variable.new(*c.value), start_pos)
970
+ end
971
+
972
+ def string
973
+ first = try_tok(:string)
974
+ return number unless first
975
+ str = literal_node(first.value, first.source_range)
976
+ return str unless try_tok(:string_interpolation)
977
+ mid = assert_expr :expr
978
+ assert_tok :end_interpolation
979
+ last = without_stop_at {assert_expr(:string)}
980
+ node(Tree::StringInterpolation.new(str, mid, last), first.source_range.start_pos)
981
+ end
982
+
983
+ def number
984
+ tok = try_tok(:number)
985
+ return selector unless tok
986
+ num = tok.value
987
+ num.options = @options
988
+ num.original = num.to_s
989
+ literal_node(num, tok.source_range.start_pos)
990
+ end
991
+
992
+ def selector
993
+ tok = try_tok(:selector)
994
+ return literal unless tok
995
+ node(tok.value, tok.source_range.start_pos)
996
+ end
997
+
998
+ def literal
999
+ t = try_tok(:color)
1000
+ return literal_node(t.value, t.source_range) if t
1001
+ end
1002
+
1003
+ # It would be possible to have unified #assert and #try methods,
1004
+ # but detecting the method/token difference turns out to be quite expensive.
1005
+
1006
+ EXPR_NAMES = {
1007
+ :string => "string",
1008
+ :default => "expression (e.g. 1px, bold)",
1009
+ :mixin_arglist => "mixin argument",
1010
+ :fn_arglist => "function argument",
1011
+ :splat => "...",
1012
+ :special_fun => '")"',
1013
+ }
1014
+
1015
+ def assert_expr(name, expected = nil)
1016
+ e = send(name)
1017
+ return e if e
1018
+ @lexer.expected!(expected || EXPR_NAMES[name] || EXPR_NAMES[:default])
1019
+ end
1020
+
1021
+ def assert_tok(name)
1022
+ # Avoids an array allocation caused by argument globbing in assert_toks.
1023
+ t = try_tok(name)
1024
+ return t if t
1025
+ @lexer.expected!(Lexer::TOKEN_NAMES[name] || name.to_s)
1026
+ end
1027
+
1028
+ def assert_toks(*names)
1029
+ t = try_toks(*names)
1030
+ return t if t
1031
+ @lexer.expected!(names.map {|tok| Lexer::TOKEN_NAMES[tok] || tok}.join(" or "))
1032
+ end
1033
+
1034
+ def peek_tok(name)
1035
+ # Avoids an array allocation caused by argument globbing in the try_toks method.
1036
+ peeked = @lexer.peek
1037
+ peeked && name == peeked.type && peeked
1038
+ end
1039
+
1040
+ def peek_toks(*names)
1041
+ peeked = @lexer.peek
1042
+ peeked && names.include?(peeked.type) && peeked
1043
+ end
1044
+
1045
+ def try_tok(name)
1046
+ peek_tok(name) && @lexer.next
1047
+ end
1048
+
1049
+ def try_toks(*names)
1050
+ peek_toks(*names) && @lexer.next
1051
+ end
1052
+
1053
+ def assert_done
1054
+ if @allow_extra_text
1055
+ # If extra text is allowed, just rewind the lexer so that the
1056
+ # StringScanner is pointing to the end of the parsed text.
1057
+ @lexer.unpeek!
1058
+ else
1059
+ return if @lexer.done?
1060
+ @lexer.expected!(EXPR_NAMES[:default])
1061
+ end
1062
+ end
1063
+
1064
+ def without_stop_at
1065
+ old_stop_at = @stop_at
1066
+ @stop_at = nil
1067
+ yield
1068
+ ensure
1069
+ @stop_at = old_stop_at
1070
+ end
1071
+
1072
+ # @overload node(value, source_range)
1073
+ # @param value [Sass::Script::Value::Base]
1074
+ # @param source_range [Sass::Source::Range]
1075
+ # @overload node(value, start_pos, end_pos = source_position)
1076
+ # @param value [Sass::Script::Value::Base]
1077
+ # @param start_pos [Sass::Source::Position]
1078
+ # @param end_pos [Sass::Source::Position]
1079
+ def literal_node(value, source_range_or_start_pos, end_pos = source_position)
1080
+ node(Sass::Script::Tree::Literal.new(value), source_range_or_start_pos, end_pos)
1081
+ end
1082
+
1083
+ # @overload node(node, source_range)
1084
+ # @param node [Sass::Script::Tree::Node]
1085
+ # @param source_range [Sass::Source::Range]
1086
+ # @overload node(node, start_pos, end_pos = source_position)
1087
+ # @param node [Sass::Script::Tree::Node]
1088
+ # @param start_pos [Sass::Source::Position]
1089
+ # @param end_pos [Sass::Source::Position]
1090
+ def node(node, source_range_or_start_pos, end_pos = source_position)
1091
+ source_range =
1092
+ if source_range_or_start_pos.is_a?(Sass::Source::Range)
1093
+ source_range_or_start_pos
1094
+ else
1095
+ range(source_range_or_start_pos, end_pos)
1096
+ end
1097
+
1098
+ node.line = source_range.start_pos.line
1099
+ node.source_range = source_range
1100
+ node.filename = @options[:filename]
1101
+ node
1102
+ end
1103
+
1104
+ # Converts an array of strings and expressions to a string interoplation
1105
+ # object.
1106
+ #
1107
+ # @param array [Array<Script::Tree:Node | String>]
1108
+ # @return [Script::Tree::StringInterpolation]
1109
+ def array_to_interpolation(array)
1110
+ Sass::Util.merge_adjacent_strings(array).reverse.inject(nil) do |after, value|
1111
+ if value.is_a?(::String)
1112
+ literal = Sass::Script::Tree::Literal.new(
1113
+ Sass::Script::Value::String.new(value))
1114
+ next literal unless after
1115
+ Sass::Script::Tree::StringInterpolation.new(literal, after.mid, after.after)
1116
+ else
1117
+ Sass::Script::Tree::StringInterpolation.new(
1118
+ Sass::Script::Tree::Literal.new(
1119
+ Sass::Script::Value::String.new('')),
1120
+ value,
1121
+ after || Sass::Script::Tree::Literal.new(
1122
+ Sass::Script::Value::String.new('')))
1123
+ end
1124
+ end
1125
+ end
1126
+
1127
+ # Checks a script node for any immediately-deprecated interpolations, and
1128
+ # emits warnings for them.
1129
+ #
1130
+ # @param node [Sass::Script::Tree::Node]
1131
+ def check_for_interpolation(node)
1132
+ nodes = [node]
1133
+ until nodes.empty?
1134
+ node = nodes.pop
1135
+ unless node.is_a?(Sass::Script::Tree::Interpolation) &&
1136
+ node.deprecation == :immediate
1137
+ nodes.concat node.children
1138
+ next
1139
+ end
1140
+
1141
+ interpolation_deprecation(node)
1142
+ end
1143
+ end
1144
+
1145
+ # Emits a deprecation warning for an interpolation node.
1146
+ #
1147
+ # @param node [Sass::Script::Tree::Node]
1148
+ def interpolation_deprecation(interpolation)
1149
+ return if @options[:_convert]
1150
+ location = "on line #{interpolation.line}"
1151
+ location << " of #{interpolation.filename}" if interpolation.filename
1152
+ Sass::Util.sass_warn <<WARNING
1153
+ DEPRECATION WARNING #{location}:
1154
+ \#{} interpolation near operators will be simplified in a future version of Sass.
1155
+ To preserve the current behavior, use quotes:
1156
+
1157
+ #{interpolation.to_quoted_equivalent.to_sass}
1158
+
1159
+ You can use the sass-convert command to automatically fix most cases.
1160
+ WARNING
1161
+ end
1162
+ end
1163
+ end
1164
+ end