sass 3.5.2 → 3.7.4

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 (163) hide show
  1. checksums.yaml +4 -4
  2. data/CODE_OF_CONDUCT.md +1 -1
  3. data/CONTRIBUTING.md +3 -3
  4. data/README.md +17 -9
  5. data/VERSION +1 -1
  6. data/VERSION_DATE +1 -1
  7. data/extra/sass-spec-ref.sh +9 -1
  8. data/lib/sass/engine.rb +1 -9
  9. data/lib/sass/exec/base.rb +0 -2
  10. data/lib/sass/exec/sass_scss.rb +1 -5
  11. data/lib/sass/importers/filesystem.rb +4 -2
  12. data/lib/sass/logger/base.rb +11 -0
  13. data/lib/sass/script/css_parser.rb +4 -1
  14. data/lib/sass/script/functions.rb +76 -41
  15. data/lib/sass/script/lexer.rb +62 -19
  16. data/lib/sass/script/parser.rb +260 -93
  17. data/lib/sass/script/tree/funcall.rb +0 -4
  18. data/lib/sass/script/tree/interpolation.rb +0 -3
  19. data/lib/sass/script/tree/operation.rb +1 -1
  20. data/lib/sass/script/value/color.rb +3 -2
  21. data/lib/sass/script/value/helpers.rb +8 -2
  22. data/lib/sass/script/value/number.rb +2 -1
  23. data/lib/sass/scss/css_parser.rb +6 -1
  24. data/lib/sass/scss/parser.rb +48 -18
  25. data/lib/sass/scss/rx.rb +1 -1
  26. data/lib/sass/scss/static_parser.rb +15 -18
  27. data/lib/sass/selector/comma_sequence.rb +2 -1
  28. data/lib/sass/selector/pseudo.rb +1 -1
  29. data/lib/sass/selector/sequence.rb +0 -4
  30. data/lib/sass/source/map.rb +0 -4
  31. data/lib/sass/tree/rule_node.rb +3 -6
  32. data/lib/sass/tree/visitors/perform.rb +2 -6
  33. data/lib/sass/tree/visitors/to_css.rb +4 -11
  34. data/lib/sass/util.rb +60 -20
  35. data/lib/sass/version.rb +0 -2
  36. metadata +38 -162
  37. data/Rakefile +0 -338
  38. data/lib/test.css +0 -4
  39. data/lib/test.css.map +0 -7
  40. data/test/sass-spec.yml +0 -3
  41. data/test/sass/cache_test.rb +0 -130
  42. data/test/sass/callbacks_test.rb +0 -60
  43. data/test/sass/compiler_test.rb +0 -225
  44. data/test/sass/conversion_test.rb +0 -2138
  45. data/test/sass/css2sass_test.rb +0 -523
  46. data/test/sass/css_variable_test.rb +0 -237
  47. data/test/sass/data/hsl-rgb.txt +0 -319
  48. data/test/sass/encoding_test.rb +0 -188
  49. data/test/sass/engine_test.rb +0 -3499
  50. data/test/sass/exec_test.rb +0 -95
  51. data/test/sass/extend_test.rb +0 -1678
  52. data/test/sass/fixtures/test_staleness_check_across_importers.css +0 -1
  53. data/test/sass/fixtures/test_staleness_check_across_importers.scss +0 -1
  54. data/test/sass/functions_test.rb +0 -2021
  55. data/test/sass/importer_test.rb +0 -420
  56. data/test/sass/logger_test.rb +0 -57
  57. data/test/sass/mock_importer.rb +0 -49
  58. data/test/sass/more_results/more1.css +0 -9
  59. data/test/sass/more_results/more1_with_line_comments.css +0 -26
  60. data/test/sass/more_results/more_import.css +0 -29
  61. data/test/sass/more_templates/_more_partial.sass +0 -2
  62. data/test/sass/more_templates/more1.sass +0 -23
  63. data/test/sass/more_templates/more_import.sass +0 -11
  64. data/test/sass/plugin_test.rb +0 -552
  65. data/test/sass/results/alt.css +0 -4
  66. data/test/sass/results/basic.css +0 -9
  67. data/test/sass/results/cached_import_option.css +0 -3
  68. data/test/sass/results/compact.css +0 -5
  69. data/test/sass/results/complex.css +0 -86
  70. data/test/sass/results/compressed.css +0 -1
  71. data/test/sass/results/expanded.css +0 -19
  72. data/test/sass/results/filename_fn.css +0 -3
  73. data/test/sass/results/if.css +0 -3
  74. data/test/sass/results/import.css +0 -31
  75. data/test/sass/results/import_charset.css +0 -5
  76. data/test/sass/results/import_charset_ibm866.css +0 -5
  77. data/test/sass/results/import_content.css +0 -1
  78. data/test/sass/results/line_numbers.css +0 -49
  79. data/test/sass/results/mixins.css +0 -95
  80. data/test/sass/results/multiline.css +0 -24
  81. data/test/sass/results/nested.css +0 -22
  82. data/test/sass/results/options.css +0 -1
  83. data/test/sass/results/parent_ref.css +0 -13
  84. data/test/sass/results/script.css +0 -16
  85. data/test/sass/results/scss_import.css +0 -31
  86. data/test/sass/results/scss_importee.css +0 -2
  87. data/test/sass/results/subdir/nested_subdir/nested_subdir.css +0 -1
  88. data/test/sass/results/subdir/subdir.css +0 -3
  89. data/test/sass/results/units.css +0 -11
  90. data/test/sass/results/warn.css +0 -0
  91. data/test/sass/results/warn_imported.css +0 -0
  92. data/test/sass/script_conversion_test.rb +0 -365
  93. data/test/sass/script_test.rb +0 -1429
  94. data/test/sass/scss/css_test.rb +0 -1266
  95. data/test/sass/scss/rx_test.rb +0 -159
  96. data/test/sass/scss/scss_test.rb +0 -4238
  97. data/test/sass/scss/test_helper.rb +0 -37
  98. data/test/sass/source_map_test.rb +0 -1052
  99. data/test/sass/superselector_test.rb +0 -209
  100. data/test/sass/templates/_cached_import_option_partial.scss +0 -1
  101. data/test/sass/templates/_double_import_loop2.sass +0 -1
  102. data/test/sass/templates/_filename_fn_import.scss +0 -11
  103. data/test/sass/templates/_imported_charset_ibm866.sass +0 -4
  104. data/test/sass/templates/_imported_charset_utf8.sass +0 -4
  105. data/test/sass/templates/_imported_content.sass +0 -3
  106. data/test/sass/templates/_partial.sass +0 -2
  107. data/test/sass/templates/_same_name_different_partiality.scss +0 -1
  108. data/test/sass/templates/alt.sass +0 -16
  109. data/test/sass/templates/basic.sass +0 -23
  110. data/test/sass/templates/bork1.sass +0 -2
  111. data/test/sass/templates/bork2.sass +0 -2
  112. data/test/sass/templates/bork3.sass +0 -2
  113. data/test/sass/templates/bork4.sass +0 -2
  114. data/test/sass/templates/bork5.sass +0 -3
  115. data/test/sass/templates/cached_import_option.scss +0 -3
  116. data/test/sass/templates/compact.sass +0 -17
  117. data/test/sass/templates/complex.sass +0 -305
  118. data/test/sass/templates/compressed.sass +0 -15
  119. data/test/sass/templates/double_import_loop1.sass +0 -1
  120. data/test/sass/templates/expanded.sass +0 -17
  121. data/test/sass/templates/filename_fn.scss +0 -18
  122. data/test/sass/templates/if.sass +0 -11
  123. data/test/sass/templates/import.sass +0 -12
  124. data/test/sass/templates/import_charset.sass +0 -9
  125. data/test/sass/templates/import_charset_ibm866.sass +0 -11
  126. data/test/sass/templates/import_content.sass +0 -4
  127. data/test/sass/templates/importee.less +0 -2
  128. data/test/sass/templates/importee.sass +0 -19
  129. data/test/sass/templates/line_numbers.sass +0 -13
  130. data/test/sass/templates/mixin_bork.sass +0 -5
  131. data/test/sass/templates/mixins.sass +0 -76
  132. data/test/sass/templates/multiline.sass +0 -20
  133. data/test/sass/templates/nested.sass +0 -25
  134. data/test/sass/templates/nested_bork1.sass +0 -2
  135. data/test/sass/templates/nested_bork2.sass +0 -2
  136. data/test/sass/templates/nested_bork3.sass +0 -2
  137. data/test/sass/templates/nested_bork4.sass +0 -2
  138. data/test/sass/templates/nested_import.sass +0 -2
  139. data/test/sass/templates/nested_mixin_bork.sass +0 -6
  140. data/test/sass/templates/options.sass +0 -2
  141. data/test/sass/templates/parent_ref.sass +0 -25
  142. data/test/sass/templates/same_name_different_ext.sass +0 -2
  143. data/test/sass/templates/same_name_different_ext.scss +0 -1
  144. data/test/sass/templates/same_name_different_partiality.scss +0 -1
  145. data/test/sass/templates/script.sass +0 -101
  146. data/test/sass/templates/scss_import.scss +0 -12
  147. data/test/sass/templates/scss_importee.scss +0 -1
  148. data/test/sass/templates/single_import_loop.sass +0 -1
  149. data/test/sass/templates/subdir/import_up1.scss +0 -1
  150. data/test/sass/templates/subdir/import_up2.scss +0 -1
  151. data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +0 -2
  152. data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +0 -3
  153. data/test/sass/templates/subdir/subdir.sass +0 -6
  154. data/test/sass/templates/units.sass +0 -11
  155. data/test/sass/templates/warn.sass +0 -3
  156. data/test/sass/templates/warn_imported.sass +0 -4
  157. data/test/sass/test_helper.rb +0 -8
  158. data/test/sass/util/multibyte_string_scanner_test.rb +0 -152
  159. data/test/sass/util/normalized_map_test.rb +0 -50
  160. data/test/sass/util/subset_map_test.rb +0 -90
  161. data/test/sass/util_test.rb +0 -403
  162. data/test/sass/value_helpers_test.rb +0 -178
  163. data/test/test_helper.rb +0 -149
@@ -190,6 +190,14 @@ module Sass
190
190
  @scanner.string[pos, 1]
191
191
  end
192
192
 
193
+ # Consumes and returns single raw character from the input stream.
194
+ #
195
+ # @return [String]
196
+ def next_char
197
+ unpeek!
198
+ scan(/./)
199
+ end
200
+
193
201
  # Returns the next token without moving the lexer forward.
194
202
  #
195
203
  # @return [Token] The next token
@@ -200,6 +208,7 @@ module Sass
200
208
  # Rewinds the underlying StringScanner
201
209
  # to before the token returned by \{#peek}.
202
210
  def unpeek!
211
+ raise "[BUG] Can't unpeek before a queued token!" if @next_tok
203
212
  return unless @tok
204
213
  @scanner.pos = @tok.pos
205
214
  @line = @tok.source_range.start_pos.line
@@ -243,6 +252,30 @@ module Sass
243
252
  @scanner.string[old_pos...new_pos]
244
253
  end
245
254
 
255
+ # Runs a block, and rewinds the state of the lexer to the beginning of the
256
+ # block if it returns `nil` or `false`.
257
+ def try
258
+ old_pos = @scanner.pos
259
+ old_line = @line
260
+ old_offset = @offset
261
+ old_interpolation_stack = @interpolation_stack.dup
262
+ old_prev = @prev
263
+ old_tok = @tok
264
+ old_next_tok = @next_tok
265
+
266
+ result = yield
267
+ return result if result
268
+
269
+ @scanner.pos = old_pos
270
+ @line = old_line
271
+ @offset = old_offset
272
+ @interpolation_stack = old_interpolation_stack
273
+ @prev = old_prev
274
+ @tok = old_tok
275
+ @next_tok = old_next_tok
276
+ nil
277
+ end
278
+
246
279
  private
247
280
 
248
281
  def read_token
@@ -266,10 +299,14 @@ module Sass
266
299
  end
267
300
 
268
301
  def token
269
- if after_interpolation? && (interp = @interpolation_stack.pop)
270
- interp_type, interp_value = interp
302
+ if after_interpolation?
303
+ interp_type, interp_value = @interpolation_stack.pop
271
304
  if interp_type == :special_fun
272
305
  return special_fun_body(interp_value)
306
+ elsif interp_type.nil?
307
+ if @scanner.string[@scanner.pos - 1] == '}' && scan(REGULAR_EXPRESSIONS[:ident])
308
+ return [@scanner[2] ? :funcall : :ident, Sass::Util.normalize_ident_escapes(@scanner[1], start: false)]
309
+ end
273
310
  else
274
311
  raise "[BUG]: Unknown interp_type #{interp_type}" unless interp_type == :string
275
312
  return string(interp_value, true)
@@ -287,13 +324,12 @@ module Sass
287
324
 
288
325
  def _variable(rx)
289
326
  return unless scan(rx)
290
-
291
- [:const, @scanner[2]]
327
+ [:const, Sass::Util.normalize_ident_escapes(@scanner[2])]
292
328
  end
293
329
 
294
330
  def ident
295
331
  return unless scan(REGULAR_EXPRESSIONS[:ident])
296
- [@scanner[2] ? :funcall : :ident, @scanner[1]]
332
+ [@scanner[2] ? :funcall : :ident, Sass::Util.normalize_ident_escapes(@scanner[1])]
297
333
  end
298
334
 
299
335
  def string(re, open)
@@ -349,7 +385,9 @@ MESSAGE
349
385
 
350
386
  value = (@scanner[1] ? @scanner[1].to_f : @scanner[2].to_i) * (minus ? -1 : 1)
351
387
  value *= 10**@scanner[3].to_i if @scanner[3]
352
- script_number = Script::Value::Number.new(value, Array(@scanner[4]))
388
+ units = @scanner[4]
389
+ units = Sass::Util::normalize_ident_escapes(units) if units
390
+ script_number = Script::Value::Number.new(value, Array(units))
353
391
  [:number, script_number]
354
392
  end
355
393
 
@@ -368,24 +406,20 @@ MESSAGE
368
406
  # IDs in properties are used in the Basic User Interface Module
369
407
  # (http://www.w3.org/TR/css3-ui/).
370
408
  return unless scan(REGULAR_EXPRESSIONS[:id])
371
- if @scanner[0] =~ /^\#[0-9a-fA-F]+$/
372
- if @scanner[0].length == 4 || @scanner[0].length == 7
373
- return [:color, Script::Value::Color.from_hex(@scanner[0])]
374
- elsif @scanner[0].length == 5 || @scanner[0].length == 9
375
- filename = @options[:filename]
376
- Sass::Util.sass_warn <<MESSAGE
377
- DEPRECATION WARNING on line #{line}, column #{offset}#{" of #{filename}" if filename}:
378
- The value "#{@scanner[0]}" is currently parsed as a string, but it will be parsed as a color in
379
- future versions of Sass. Use "unquote('#{@scanner[0]}')" to continue parsing it as a string.
380
- MESSAGE
381
- end
409
+ if @scanner[0] =~ /^\#[0-9a-fA-F]+$/ &&
410
+ (@scanner[0].length == 4 || @scanner[0].length == 5 ||
411
+ @scanner[0].length == 7 || @scanner[0].length == 9)
412
+ return [:color, Script::Value::Color.from_hex(@scanner[0])]
382
413
  end
383
- [:ident, @scanner[0]]
414
+ [:ident, Sass::Util.normalize_ident_escapes(@scanner[0])]
384
415
  end
385
416
 
386
417
  def color
387
418
  return unless @scanner.match?(REGULAR_EXPRESSIONS[:color])
388
- return unless @scanner[0].length == 4 || @scanner[0].length == 7
419
+ unless @scanner[0].length == 4 || @scanner[0].length == 5 ||
420
+ @scanner[0].length == 7 || @scanner[0].length == 9
421
+ return
422
+ end
389
423
  script_color = Script::Value::Color.from_hex(scan(REGULAR_EXPRESSIONS[:color]))
390
424
  [:color, script_color]
391
425
  end
@@ -393,6 +427,15 @@ MESSAGE
393
427
  def selector
394
428
  start_pos = source_position
395
429
  return unless scan(REGULAR_EXPRESSIONS[:selector])
430
+
431
+ if @scanner.peek(1) == '&'
432
+ filename = @options[:filename]
433
+ Sass::Util.sass_warn <<MESSAGE
434
+ WARNING on line #{line}, column #{offset}#{" of #{filename}" if filename}:
435
+ In Sass, "&&" means two copies of the parent selector. You probably want to use "and" instead.
436
+ MESSAGE
437
+ end
438
+
396
439
  script_selector = Script::Tree::Selector.new
397
440
  script_selector.source_range = range(start_pos)
398
441
  [:selector, script_selector]
@@ -78,7 +78,7 @@ module Sass
78
78
  # Parses a SassScript expression,
79
79
  # ending it when it encounters one of the given identifier tokens.
80
80
  #
81
- # @param tokens [#include?(String)] A set of strings that delimit the expression.
81
+ # @param tokens [#include?(String | Symbol)] A set of strings or symbols that delimit the expression.
82
82
  # @return [Script::Tree::Node] The root node of the parse tree
83
83
  # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
84
84
  def parse_until(tokens)
@@ -270,7 +270,11 @@ module Sass
270
270
  interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect})
271
271
  return interp if interp
272
272
  return unless e = #{sub}
273
- while tok = try_toks(#{ops.map {|o| o.inspect}.join(', ')})
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
+
274
278
  if interp = try_op_before_interp(tok, e)
275
279
  other_interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect}, interp)
276
280
  return interp unless other_interp
@@ -323,6 +327,7 @@ RUBY
323
327
  pair = map_pair
324
328
  return map unless pair
325
329
  map.pairs << pair
330
+ map.source_range.end_pos = map.pairs.last.last.source_range.end_pos
326
331
  end
327
332
  map
328
333
  end
@@ -385,8 +390,11 @@ RUBY
385
390
 
386
391
  def try_ops_after_interp(ops, name, prev = nil)
387
392
  return unless @lexer.after_interpolation?
388
- op = try_toks(*ops)
393
+ op = peek_toks(*ops)
389
394
  return unless op
395
+ return if @stop_at && @stop_at.include?(op.type)
396
+ @lexer.next
397
+
390
398
  interp = try_op_before_interp(op, prev, :after_interp)
391
399
  return interp if interp
392
400
 
@@ -415,7 +423,7 @@ RUBY
415
423
  while (interp = try_tok(:begin_interpolation))
416
424
  wb = @lexer.whitespace?(interp)
417
425
  char_before = @lexer.char(interp.pos - 1)
418
- mid = assert_expr :expr
426
+ mid = without_stop_at {assert_expr :expr}
419
427
  assert_tok :end_interpolation
420
428
  wa = @lexer.whitespace?
421
429
  char_after = @lexer.char
@@ -496,7 +504,7 @@ RUBY
496
504
  unary :not, :ident
497
505
 
498
506
  def ident
499
- return funcall unless @lexer.peek && @lexer.peek.type == :ident
507
+ return css_min_max unless @lexer.peek && @lexer.peek.type == :ident
500
508
  return if @stop_at && @stop_at.include?(@lexer.peek.value)
501
509
 
502
510
  name = @lexer.next
@@ -513,6 +521,122 @@ RUBY
513
521
  end
514
522
  end
515
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
+
516
640
  def funcall
517
641
  tok = try_tok(:funcall)
518
642
  return raw unless tok
@@ -529,28 +653,30 @@ RUBY
529
653
  return [], nil unless try_tok(:lparen)
530
654
  end
531
655
 
532
- res = []
533
- splat = nil
534
- must_have_default = false
535
- loop do
536
- break if peek_tok(:rparen)
537
- c = assert_tok(:const)
538
- var = node(Script::Tree::Variable.new(c.value), c.source_range)
539
- if try_tok(:colon)
540
- val = assert_expr(:space)
541
- must_have_default = true
542
- elsif try_tok(:splat)
543
- splat = var
544
- break
545
- elsif must_have_default
546
- raise SyntaxError.new(
547
- "Required argument #{var.inspect} must come before any optional arguments.")
656
+ without_stop_at do
657
+ res = []
658
+ splat = nil
659
+ must_have_default = false
660
+ loop do
661
+ break if peek_tok(:rparen)
662
+ c = assert_tok(:const)
663
+ var = node(Script::Tree::Variable.new(c.value), c.source_range)
664
+ if try_tok(:colon)
665
+ val = assert_expr(:space)
666
+ must_have_default = true
667
+ elsif try_tok(:splat)
668
+ splat = var
669
+ break
670
+ elsif must_have_default
671
+ raise SyntaxError.new(
672
+ "Required argument #{var.inspect} must come before any optional arguments.")
673
+ end
674
+ res << [var, val]
675
+ break unless try_tok(:comma)
548
676
  end
549
- res << [var, val]
550
- break unless try_tok(:comma)
677
+ assert_tok(:rparen)
678
+ return res, splat
551
679
  end
552
- assert_tok(:rparen)
553
- return res, splat
554
680
  end
555
681
 
556
682
  def fn_arglist
@@ -562,36 +688,38 @@ RUBY
562
688
  end
563
689
 
564
690
  def arglist(subexpr, description)
565
- args = []
566
- keywords = Sass::Util::NormalizedMap.new
567
- splat = nil
568
- while (e = send(subexpr))
569
- if @lexer.peek && @lexer.peek.type == :colon
570
- name = e
571
- @lexer.expected!("comma") unless name.is_a?(Tree::Variable)
572
- assert_tok(:colon)
573
- value = assert_expr(subexpr, description)
574
-
575
- if keywords[name.name]
576
- raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once")
577
- end
691
+ without_stop_at do
692
+ args = []
693
+ keywords = Sass::Util::NormalizedMap.new
694
+ splat = nil
695
+ while (e = send(subexpr))
696
+ if @lexer.peek && @lexer.peek.type == :colon
697
+ name = e
698
+ @lexer.expected!("comma") unless name.is_a?(Tree::Variable)
699
+ assert_tok(:colon)
700
+ value = assert_expr(subexpr, description)
701
+
702
+ if keywords[name.name]
703
+ raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once")
704
+ end
578
705
 
579
- keywords[name.name] = value
580
- else
581
- if try_tok(:splat)
582
- return args, keywords, splat, e if splat
583
- splat, e = e, nil
584
- elsif splat
585
- raise SyntaxError.new("Only keyword arguments may follow variable arguments (...).")
586
- elsif !keywords.empty?
587
- raise SyntaxError.new("Positional arguments must come before keyword arguments.")
706
+ keywords[name.name] = value
707
+ else
708
+ if try_tok(:splat)
709
+ return args, keywords, splat, e if splat
710
+ splat, e = e, nil
711
+ elsif splat
712
+ raise SyntaxError.new("Only keyword arguments may follow variable arguments (...).")
713
+ elsif !keywords.empty?
714
+ raise SyntaxError.new("Positional arguments must come before keyword arguments.")
715
+ end
716
+ args << e if e
588
717
  end
589
- args << e if e
590
- end
591
718
 
592
- return args, keywords, splat unless try_tok(:comma)
719
+ return args, keywords, splat unless try_tok(:comma)
720
+ end
721
+ return args, keywords
593
722
  end
594
- return args, keywords
595
723
  end
596
724
 
597
725
  def raw
@@ -605,7 +733,7 @@ RUBY
605
733
  return square_list unless first
606
734
  str = literal_node(first.value, first.source_range)
607
735
  return str unless try_tok(:string_interpolation)
608
- mid = assert_expr :expr
736
+ mid = without_stop_at {assert_expr :expr}
609
737
  assert_tok :end_interpolation
610
738
  last = assert_expr(:special_fun)
611
739
  node(
@@ -617,54 +745,58 @@ RUBY
617
745
  start_pos = source_position
618
746
  return paren unless try_tok(:lsquare)
619
747
 
620
- space_start_pos = source_position
621
- e = interpolation(inner: :or_expr)
622
- separator = nil
623
- if e
624
- elements = [e]
625
- while (e = interpolation(inner: :or_expr))
626
- elements << e
627
- end
628
-
629
- # If there's a comma after a space-separated list, it's actually a
630
- # space-separated list nested in a comma-separated list.
631
- if try_tok(:comma)
632
- e = if elements.length == 1
633
- elements.first
634
- else
635
- node(
636
- Sass::Script::Tree::ListLiteral.new(elements, separator: :space),
637
- space_start_pos)
638
- end
748
+ without_stop_at do
749
+ space_start_pos = source_position
750
+ e = interpolation(inner: :or_expr)
751
+ separator = nil
752
+ if e
639
753
  elements = [e]
640
-
641
- while (e = space)
754
+ while (e = interpolation(inner: :or_expr))
642
755
  elements << e
643
- break unless try_tok(:comma)
644
756
  end
645
- separator = :comma
757
+
758
+ # If there's a comma after a space-separated list, it's actually a
759
+ # space-separated list nested in a comma-separated list.
760
+ if try_tok(:comma)
761
+ e = if elements.length == 1
762
+ elements.first
763
+ else
764
+ node(
765
+ Sass::Script::Tree::ListLiteral.new(elements, separator: :space),
766
+ space_start_pos)
767
+ end
768
+ elements = [e]
769
+
770
+ while (e = space)
771
+ elements << e
772
+ break unless try_tok(:comma)
773
+ end
774
+ separator = :comma
775
+ else
776
+ separator = :space if elements.length > 1
777
+ end
646
778
  else
647
- separator = :space if elements.length > 1
779
+ elements = []
648
780
  end
649
- else
650
- elements = []
651
- end
652
781
 
653
- assert_tok(:rsquare)
654
- end_pos = source_position
782
+ assert_tok(:rsquare)
783
+ end_pos = source_position
655
784
 
656
- node(Sass::Script::Tree::ListLiteral.new(elements, separator: separator, bracketed: true),
657
- start_pos, end_pos)
785
+ node(Sass::Script::Tree::ListLiteral.new(elements, separator: separator, bracketed: true),
786
+ start_pos, end_pos)
787
+ end
658
788
  end
659
789
 
660
790
  def paren
661
791
  return variable unless try_tok(:lparen)
662
- start_pos = source_position
663
- e = map
664
- e.force_division! if e
665
- end_pos = source_position
666
- assert_tok(:rparen)
667
- e || node(Sass::Script::Tree::ListLiteral.new([]), start_pos, end_pos)
792
+ without_stop_at do
793
+ start_pos = source_position
794
+ e = map
795
+ e.force_division! if e
796
+ end_pos = source_position
797
+ assert_tok(:rparen)
798
+ e || node(Sass::Script::Tree::ListLiteral.new([]), start_pos, end_pos)
799
+ end
668
800
  end
669
801
 
670
802
  def variable
@@ -681,7 +813,7 @@ RUBY
681
813
  return str unless try_tok(:string_interpolation)
682
814
  mid = assert_expr :expr
683
815
  assert_tok :end_interpolation
684
- last = assert_expr(:string)
816
+ last = without_stop_at {assert_expr(:string)}
685
817
  node(Tree::StringInterpolation.new(str, mid, last), first.source_range.start_pos)
686
818
  end
687
819
 
@@ -739,7 +871,12 @@ RUBY
739
871
  def peek_tok(name)
740
872
  # Avoids an array allocation caused by argument globbing in the try_toks method.
741
873
  peeked = @lexer.peek
742
- peeked && name == peeked.type
874
+ peeked && name == peeked.type && peeked
875
+ end
876
+
877
+ def peek_toks(*names)
878
+ peeked = @lexer.peek
879
+ peeked && names.include?(peeked.type) && peeked
743
880
  end
744
881
 
745
882
  def try_tok(name)
@@ -747,8 +884,7 @@ RUBY
747
884
  end
748
885
 
749
886
  def try_toks(*names)
750
- peeked = @lexer.peek
751
- peeked && names.include?(peeked.type) && @lexer.next
887
+ peek_toks(*names) && @lexer.next
752
888
  end
753
889
 
754
890
  def assert_done
@@ -762,6 +898,14 @@ RUBY
762
898
  end
763
899
  end
764
900
 
901
+ def without_stop_at
902
+ old_stop_at = @stop_at
903
+ @stop_at = nil
904
+ yield
905
+ ensure
906
+ @stop_at = old_stop_at
907
+ end
908
+
765
909
  # @overload node(value, source_range)
766
910
  # @param value [Sass::Script::Value::Base]
767
911
  # @param source_range [Sass::Source::Range]
@@ -794,6 +938,29 @@ RUBY
794
938
  node
795
939
  end
796
940
 
941
+ # Converts an array of strings and expressions to a string interoplation
942
+ # object.
943
+ #
944
+ # @param array [Array<Script::Tree:Node | String>]
945
+ # @return [Script::Tree::StringInterpolation]
946
+ def array_to_interpolation(array)
947
+ Sass::Util.merge_adjacent_strings(array).reverse.inject(nil) do |after, value|
948
+ if value.is_a?(::String)
949
+ literal = Sass::Script::Tree::Literal.new(
950
+ Sass::Script::Value::String.new(value))
951
+ next literal unless after
952
+ Sass::Script::Tree::StringInterpolation.new(literal, after.mid, after.after)
953
+ else
954
+ Sass::Script::Tree::StringInterpolation.new(
955
+ Sass::Script::Tree::Literal.new(
956
+ Sass::Script::Value::String.new('')),
957
+ value,
958
+ after || Sass::Script::Tree::Literal.new(
959
+ Sass::Script::Value::String.new('')))
960
+ end
961
+ end
962
+ end
963
+
797
964
  # Checks a script node for any immediately-deprecated interpolations, and
798
965
  # emits warnings for them.
799
966
  #