sass 3.3.14 → 3.4.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +5 -5
  4. data/VERSION +1 -1
  5. data/VERSION_DATE +1 -1
  6. data/VERSION_NAME +1 -1
  7. data/bin/sass +1 -1
  8. data/bin/scss +1 -1
  9. data/lib/sass.rb +0 -5
  10. data/lib/sass/css.rb +1 -3
  11. data/lib/sass/engine.rb +28 -39
  12. data/lib/sass/environment.rb +13 -17
  13. data/lib/sass/error.rb +6 -9
  14. data/lib/sass/exec.rb +5 -771
  15. data/lib/sass/exec/base.rb +187 -0
  16. data/lib/sass/exec/sass_convert.rb +264 -0
  17. data/lib/sass/exec/sass_scss.rb +419 -0
  18. data/lib/sass/features.rb +6 -0
  19. data/lib/sass/importers.rb +0 -1
  20. data/lib/sass/importers/base.rb +5 -1
  21. data/lib/sass/importers/filesystem.rb +4 -21
  22. data/lib/sass/media.rb +1 -4
  23. data/lib/sass/plugin/compiler.rb +32 -136
  24. data/lib/sass/script/css_lexer.rb +1 -1
  25. data/lib/sass/script/functions.rb +363 -39
  26. data/lib/sass/script/lexer.rb +68 -50
  27. data/lib/sass/script/parser.rb +29 -14
  28. data/lib/sass/script/tree.rb +1 -0
  29. data/lib/sass/script/tree/funcall.rb +1 -1
  30. data/lib/sass/script/tree/interpolation.rb +19 -1
  31. data/lib/sass/script/tree/selector.rb +26 -0
  32. data/lib/sass/script/value.rb +0 -1
  33. data/lib/sass/script/value/bool.rb +0 -5
  34. data/lib/sass/script/value/color.rb +32 -12
  35. data/lib/sass/script/value/helpers.rb +107 -0
  36. data/lib/sass/script/value/list.rb +0 -15
  37. data/lib/sass/script/value/null.rb +0 -5
  38. data/lib/sass/script/value/number.rb +60 -14
  39. data/lib/sass/script/value/string.rb +53 -9
  40. data/lib/sass/scss/css_parser.rb +8 -2
  41. data/lib/sass/scss/parser.rb +175 -319
  42. data/lib/sass/scss/rx.rb +14 -5
  43. data/lib/sass/scss/static_parser.rb +298 -1
  44. data/lib/sass/selector.rb +56 -193
  45. data/lib/sass/selector/abstract_sequence.rb +28 -13
  46. data/lib/sass/selector/comma_sequence.rb +91 -12
  47. data/lib/sass/selector/pseudo.rb +256 -0
  48. data/lib/sass/selector/sequence.rb +99 -31
  49. data/lib/sass/selector/simple.rb +14 -25
  50. data/lib/sass/selector/simple_sequence.rb +101 -37
  51. data/lib/sass/shared.rb +1 -1
  52. data/lib/sass/source/map.rb +23 -9
  53. data/lib/sass/stack.rb +0 -6
  54. data/lib/sass/supports.rb +1 -1
  55. data/lib/sass/tree/at_root_node.rb +1 -0
  56. data/lib/sass/tree/directive_node.rb +7 -1
  57. data/lib/sass/tree/error_node.rb +18 -0
  58. data/lib/sass/tree/keyframe_rule_node.rb +15 -0
  59. data/lib/sass/tree/prop_node.rb +1 -1
  60. data/lib/sass/tree/rule_node.rb +11 -6
  61. data/lib/sass/tree/visitors/check_nesting.rb +3 -4
  62. data/lib/sass/tree/visitors/convert.rb +8 -17
  63. data/lib/sass/tree/visitors/cssize.rb +12 -24
  64. data/lib/sass/tree/visitors/deep_copy.rb +5 -0
  65. data/lib/sass/tree/visitors/perform.rb +43 -28
  66. data/lib/sass/tree/visitors/set_options.rb +5 -0
  67. data/lib/sass/tree/visitors/to_css.rb +14 -13
  68. data/lib/sass/util.rb +94 -90
  69. data/test/sass/cache_test.rb +1 -1
  70. data/test/sass/callbacks_test.rb +1 -1
  71. data/test/sass/compiler_test.rb +5 -14
  72. data/test/sass/conversion_test.rb +47 -1
  73. data/test/sass/css2sass_test.rb +3 -3
  74. data/test/sass/encoding_test.rb +219 -0
  75. data/test/sass/engine_test.rb +128 -191
  76. data/test/sass/exec_test.rb +2 -2
  77. data/test/sass/extend_test.rb +234 -17
  78. data/test/sass/functions_test.rb +268 -213
  79. data/test/sass/importer_test.rb +31 -21
  80. data/test/sass/logger_test.rb +1 -1
  81. data/test/sass/more_results/more_import.css +1 -1
  82. data/test/sass/plugin_test.rb +12 -11
  83. data/test/sass/results/compact.css +1 -1
  84. data/test/sass/results/complex.css +4 -4
  85. data/test/sass/results/expanded.css +1 -1
  86. data/test/sass/results/import.css +1 -1
  87. data/test/sass/results/import_charset_ibm866.css +2 -2
  88. data/test/sass/results/mixins.css +17 -17
  89. data/test/sass/results/nested.css +1 -1
  90. data/test/sass/results/parent_ref.css +2 -2
  91. data/test/sass/results/script.css +3 -3
  92. data/test/sass/results/scss_import.css +1 -1
  93. data/test/sass/script_conversion_test.rb +7 -4
  94. data/test/sass/script_test.rb +202 -79
  95. data/test/sass/scss/css_test.rb +95 -25
  96. data/test/sass/scss/rx_test.rb +4 -4
  97. data/test/sass/scss/scss_test.rb +363 -19
  98. data/test/sass/source_map_test.rb +48 -41
  99. data/test/sass/superselector_test.rb +191 -0
  100. data/test/sass/templates/scss_import.scss +2 -1
  101. data/test/sass/test_helper.rb +1 -1
  102. data/test/sass/util/multibyte_string_scanner_test.rb +1 -1
  103. data/test/sass/util/normalized_map_test.rb +1 -1
  104. data/test/sass/util/subset_map_test.rb +2 -2
  105. data/test/sass/util_test.rb +1 -1
  106. data/test/sass/value_helpers_test.rb +3 -3
  107. data/test/test_helper.rb +2 -2
  108. metadata +30 -7
  109. data/lib/sass/importers/deprecated_path.rb +0 -51
  110. data/lib/sass/script/value/deprecated_false.rb +0 -55
@@ -0,0 +1,15 @@
1
+ module Sass::Tree
2
+ class KeyframeRuleNode < Node
3
+ # The text of the directive after any interpolated SassScript has been resolved.
4
+ # Since this is only a static node, this is the only value property.
5
+ #
6
+ # @return [String]
7
+ attr_accessor :resolved_value
8
+
9
+ # @param resolved_value [String] See \{#resolved_value}
10
+ def initialize(resolved_value)
11
+ @resolved_value = resolved_value
12
+ super()
13
+ end
14
+ end
15
+ end
@@ -97,7 +97,7 @@ module Sass::Tree
97
97
  # @param opts [{Symbol => Object}] The options hash for the tree.
98
98
  # @param fmt [Symbol] `:scss` or `:sass`.
99
99
  def declaration(opts = {:old => @prop_syntax == :old}, fmt = :sass)
100
- name = self.name.map {|n| n.is_a?(String) ? n : "\#{#{n.to_sass(opts)}}"}.join
100
+ name = self.name.map {|n| n.is_a?(String) ? n : n.to_sass(opts)}.join
101
101
  if name[0] == ?:
102
102
  raise Sass::SyntaxError.new("The \"#{name}: #{self.class.val_to_sass(value, opts)}\"" +
103
103
  " hack is not allowed in the Sass indented syntax")
@@ -60,15 +60,20 @@ module Sass::Tree
60
60
  # @return [String]
61
61
  attr_accessor :stack_trace
62
62
 
63
- # @param rule [Array<String, Sass::Script::Tree::Node>]
63
+ # @param rule [Array<String, Sass::Script::Tree::Node>, Sass::Selector::CommaSequence]
64
+ # The CSS rule, either unparsed or parsed.
64
65
  # @param selector_source_range [Sass::Source::Range]
65
- # The CSS rule. See \{#rule}
66
66
  def initialize(rule, selector_source_range = nil)
67
- merged = Sass::Util.merge_adjacent_strings(rule)
68
- @rule = Sass::Util.strip_string_array(merged)
67
+ if rule.is_a?(Sass::Selector::CommaSequence)
68
+ @rule = [rule.to_s]
69
+ @parsed_rules = rule
70
+ else
71
+ merged = Sass::Util.merge_adjacent_strings(rule)
72
+ @rule = Sass::Util.strip_string_array(merged)
73
+ try_to_parse_non_interpolated_rules
74
+ end
69
75
  @selector_source_range = selector_source_range
70
76
  @tabs = 0
71
- try_to_parse_non_interpolated_rules
72
77
  super()
73
78
  end
74
79
 
@@ -130,7 +135,7 @@ module Sass::Tree
130
135
  if @rule.all? {|t| t.kind_of?(String)}
131
136
  # We don't use real filename/line info because we don't have it yet.
132
137
  # When we get it, we'll set it on the parsed rules if possible.
133
- parser = Sass::SCSS::StaticParser.new(@rule.join.strip, '', nil, 1)
138
+ parser = Sass::SCSS::StaticParser.new(@rule.join.strip, nil, nil, 1)
134
139
  # rubocop:disable RescueModifier
135
140
  @parsed_rules = parser.parse_selector rescue nil
136
141
  # rubocop:enable RescueModifier
@@ -123,7 +123,7 @@ class Sass::Tree::Visitors::CheckNesting < Sass::Tree::Visitors::Base
123
123
 
124
124
  VALID_FUNCTION_CHILDREN = [
125
125
  Sass::Tree::CommentNode, Sass::Tree::DebugNode, Sass::Tree::ReturnNode,
126
- Sass::Tree::VariableNode, Sass::Tree::WarnNode
126
+ Sass::Tree::VariableNode, Sass::Tree::WarnNode, Sass::Tree::ErrorNode
127
127
  ] + CONTROL_NODES
128
128
  def invalid_function_child?(parent, child)
129
129
  unless is_any_of?(child, VALID_FUNCTION_CHILDREN)
@@ -140,9 +140,8 @@ class Sass::Tree::Visitors::CheckNesting < Sass::Tree::Visitors::Base
140
140
  end
141
141
  end
142
142
 
143
- VALID_PROP_PARENTS = [Sass::Tree::RuleNode, Sass::Tree::PropNode,
144
- Sass::Tree::MixinDefNode, Sass::Tree::DirectiveNode,
145
- Sass::Tree::MixinNode]
143
+ VALID_PROP_PARENTS = [Sass::Tree::RuleNode, Sass::Tree::KeyframeRuleNode, Sass::Tree::PropNode,
144
+ Sass::Tree::MixinDefNode, Sass::Tree::DirectiveNode, Sass::Tree::MixinNode]
146
145
  def invalid_prop_parent?(parent, child)
147
146
  unless is_any_of?(parent, VALID_PROP_PARENTS)
148
147
  "Properties are only allowed within rules, directives, mixin includes, or other properties." +
@@ -96,6 +96,10 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
96
96
  "#{tab_str}@debug #{node.expr.to_sass(@options)}#{semi}\n"
97
97
  end
98
98
 
99
+ def visit_error(node)
100
+ "#{tab_str}@error #{node.expr.to_sass(@options)}#{semi}\n"
101
+ end
102
+
99
103
  def visit_directive(node)
100
104
  res = "#{tab_str}#{interp_to_src(node.value)}"
101
105
  res.gsub!(/^@import \#\{(.*)\}([^}]*)$/, '@import \1\2')
@@ -236,7 +240,7 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
236
240
  end
237
241
 
238
242
  def visit_rule(node)
239
- rule = node.parsed_rules ? node.parsed_rules.to_a : node.rule
243
+ rule = node.parsed_rules ? [node.parsed_rules.to_s] : node.rule
240
244
  if @format == :sass
241
245
  name = selector_to_sass(rule)
242
246
  name = "\\" + name if name[0] == ?:
@@ -279,10 +283,7 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
279
283
  private
280
284
 
281
285
  def interp_to_src(interp)
282
- interp.map do |r|
283
- next r if r.is_a?(String)
284
- "\#{#{r.to_sass(@options)}}"
285
- end.join
286
+ interp.map {|r| r.is_a?(String) ? r : r.to_sass(@options)}.join
286
287
  end
287
288
 
288
289
  # Like interp_to_src, but removes the unnecessary `#{}` around the keys and
@@ -294,17 +295,7 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
294
295
  e.value.value
295
296
  end
296
297
 
297
- Sass::Util.enum_with_index(interp).map do |r, i|
298
- next r if r.is_a?(String)
299
- before, after = interp[i - 1], interp[i + 1]
300
- if before.is_a?(String) && after.is_a?(String) &&
301
- ((before[-1] == ?( && after[0] == ?:) ||
302
- (before =~ /:\s*/ && after[0] == ?)))
303
- r.to_sass(@options)
304
- else
305
- "\#{#{r.to_sass(@options)}}"
306
- end
307
- end.join
298
+ interp_to_src(interp)
308
299
  end
309
300
 
310
301
  def selector_to_src(sel)
@@ -316,7 +307,7 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
316
307
  if r.is_a?(String)
317
308
  r.gsub(/(,)?([ \t]*)\n\s*/) {$1 ? "#{$1}#{$2}\n" : " "}
318
309
  else
319
- "\#{#{r.to_sass(@options)}}"
310
+ r.to_sass(@options)
320
311
  end
321
312
  end.join
322
313
  end
@@ -123,29 +123,8 @@ class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
123
123
 
124
124
  # Registers an extension in the `@extends` subset map.
125
125
  def visit_extend(node)
126
- node.resolved_selector.members.each do |seq|
127
- if seq.members.size > 1
128
- raise Sass::SyntaxError.new("Can't extend #{seq.to_a.join}: can't extend nested selectors")
129
- end
130
-
131
- sseq = seq.members.first
132
- if !sseq.is_a?(Sass::Selector::SimpleSequence)
133
- raise Sass::SyntaxError.new("Can't extend #{seq.to_a.join}: invalid selector")
134
- elsif sseq.members.any? {|ss| ss.is_a?(Sass::Selector::Parent)}
135
- raise Sass::SyntaxError.new("Can't extend #{seq.to_a.join}: can't extend parent selectors")
136
- end
137
-
138
- sel = sseq.members
139
- parent.resolved_rules.members.each do |member|
140
- unless member.members.last.is_a?(Sass::Selector::SimpleSequence)
141
- raise Sass::SyntaxError.new("#{member} can't extend: invalid selector")
142
- end
143
-
144
- parent_directives = @parents.select {|p| p.is_a?(Sass::Tree::DirectiveNode)}
145
- @extends[sel] = Extend.new(member, sel, node, parent_directives, :not_found)
146
- end
147
- end
148
-
126
+ parent.resolved_rules.populate_extends(@extends, node.resolved_selector, node,
127
+ @parents.select {|p| p.is_a?(Sass::Tree::DirectiveNode)})
149
128
  []
150
129
  end
151
130
 
@@ -232,6 +211,14 @@ class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
232
211
  rules
233
212
  end
234
213
 
214
+ def visit_keyframerule(node)
215
+ return node unless node.has_children
216
+
217
+ yield
218
+
219
+ debubble(node.children, node)
220
+ end
221
+
235
222
  # Bubbles a directive up through RuleNodes.
236
223
  def visit_directive(node)
237
224
  return node unless node.has_children
@@ -249,7 +236,8 @@ class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
249
236
  child.node.resolved_value == node.resolved_value
250
237
  end
251
238
 
252
- if directive_exists
239
+ # We know empty @keyframes directives do nothing.
240
+ if directive_exists || node.name == '@keyframes'
253
241
  []
254
242
  else
255
243
  empty_node = node.dup
@@ -16,6 +16,11 @@ class Sass::Tree::Visitors::DeepCopy < Sass::Tree::Visitors::Base
16
16
  yield
17
17
  end
18
18
 
19
+ def visit_error(node)
20
+ node.expr = node.expr.deep_copy
21
+ yield
22
+ end
23
+
19
24
  def visit_each(node)
20
25
  node.list = node.list.deep_copy
21
26
  yield
@@ -207,6 +207,17 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
207
207
  []
208
208
  end
209
209
 
210
+ # Throws the expression as an error.
211
+ def visit_error(node)
212
+ res = node.expr.perform(@environment)
213
+ if res.is_a?(Sass::Script::Value::String)
214
+ res = res.value
215
+ else
216
+ res = res.to_sass
217
+ end
218
+ raise Sass::SyntaxError.new(res)
219
+ end
220
+
210
221
  # Runs the child nodes once for each value in the list.
211
222
  def visit_each(node)
212
223
  list = node.list.perform(@environment)
@@ -377,18 +388,31 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
377
388
  # Runs SassScript interpolation in the selector,
378
389
  # and then parses the result into a {Sass::Selector::CommaSequence}.
379
390
  def visit_rule(node)
380
- old_at_root_without_rule, @at_root_without_rule = @at_root_without_rule, false
391
+ old_at_root_without_rule = @at_root_without_rule
381
392
  parser = Sass::SCSS::StaticParser.new(run_interp(node.rule),
382
393
  node.filename, node.options[:importer], node.line)
383
- node.parsed_rules ||= parser.parse_selector
384
- node.resolved_rules = node.parsed_rules.resolve_parent_refs(
385
- @environment.selector, !old_at_root_without_rule)
386
- node.stack_trace = @environment.stack.to_s if node.options[:trace_selectors]
387
- with_environment Sass::Environment.new(@environment, node.options) do
388
- @environment.selector = node.resolved_rules
389
- node.children = node.children.map {|c| visit(c)}.flatten
394
+ if @in_keyframes
395
+ keyframe_rule_node = Sass::Tree::KeyframeRuleNode.new(parser.parse_keyframes_selector)
396
+ keyframe_rule_node.options = node.options
397
+ keyframe_rule_node.line = node.line
398
+ keyframe_rule_node.filename = node.filename
399
+ keyframe_rule_node.source_range = node.source_range
400
+ with_environment Sass::Environment.new(@environment, node.options) do
401
+ keyframe_rule_node.children = node.children.map {|c| visit(c)}.flatten
402
+ end
403
+ keyframe_rule_node
404
+ else
405
+ @at_root_without_rule = false
406
+ node.parsed_rules ||= parser.parse_selector
407
+ node.resolved_rules = node.parsed_rules.resolve_parent_refs(
408
+ @environment.selector, !old_at_root_without_rule)
409
+ node.stack_trace = @environment.stack.to_s if node.options[:trace_selectors]
410
+ with_environment Sass::Environment.new(@environment, node.options) do
411
+ @environment.selector = node.resolved_rules
412
+ node.children = node.children.map {|c| visit(c)}.flatten
413
+ end
414
+ node
390
415
  end
391
- node
392
416
  ensure
393
417
  @at_root_without_rule = old_at_root_without_rule
394
418
  end
@@ -405,36 +429,24 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
405
429
  end
406
430
 
407
431
  old_at_root_without_rule = @at_root_without_rule
432
+ old_in_keyframes = @in_keyframes
408
433
  @at_root_without_rule = true if node.exclude?('rule')
434
+ @in_keyframes = false if node.exclude?('keyframes')
409
435
  yield
410
436
  ensure
437
+ @in_keyframes = old_in_keyframes
411
438
  @at_root_without_rule = old_at_root_without_rule
412
439
  end
413
440
 
414
441
  # Loads the new variable value into the environment.
415
442
  def visit_variable(node)
416
443
  env = @environment
417
- identifier = [node.name, node.filename, node.line]
418
- if node.global
419
- env = env.global_env
420
- elsif env.parent && env.is_var_global?(node.name) &&
421
- !env.global_env.global_warning_given.include?(identifier)
422
- env.global_env.global_warning_given.add(identifier)
423
- var_expr = "$#{node.name}: #{node.expr.to_sass(env.options)} !global"
424
- var_expr << " !default" if node.guarded
425
- location = "on line #{node.line}"
426
- location << " of #{node.filename}" if node.filename
427
- Sass::Util.sass_warn <<WARNING
428
- DEPRECATION WARNING #{location}:
429
- Assigning to global variable "$#{node.name}" by default is deprecated.
430
- In future versions of Sass, this will create a new local variable.
431
- If you want to assign to the global variable, use "#{var_expr}" instead.
432
- Note that this will be incompatible with Sass 3.2.
433
- WARNING
444
+ env = env.global_env if node.global
445
+ if node.guarded
446
+ var = env.var(node.name)
447
+ return [] if var && !var.null?
434
448
  end
435
449
 
436
- var = env.var(node.name)
437
- return [] if node.guarded && var && !var.null?
438
450
  val = node.expr.perform(@environment)
439
451
  if node.expr.source_range
440
452
  val.source_range = node.expr.source_range
@@ -466,10 +478,13 @@ WARNING
466
478
 
467
479
  def visit_directive(node)
468
480
  node.resolved_value = run_interp(node.value)
481
+ old_in_keyframes, @in_keyframes = @in_keyframes, node.normalized_name == "@keyframes"
469
482
  with_environment Sass::Environment.new(@environment) do
470
483
  node.children = node.children.map {|c| visit(c)}.flatten
471
484
  node
472
485
  end
486
+ ensure
487
+ @in_keyframes = old_in_keyframes
473
488
  end
474
489
 
475
490
  def visit_media(node)
@@ -25,6 +25,11 @@ class Sass::Tree::Visitors::SetOptions < Sass::Tree::Visitors::Base
25
25
  yield
26
26
  end
27
27
 
28
+ def visit_error(node)
29
+ node.expr.options = @options
30
+ yield
31
+ end
32
+
28
33
  def visit_each(node)
29
34
  node.list.options = @options
30
35
  yield
@@ -127,21 +127,18 @@ class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
127
127
  return "" if @result.empty?
128
128
 
129
129
  output "\n"
130
- return @result if Sass::Util.ruby1_8? || @result.ascii_only?
131
-
132
- if node.children.first.is_a?(Sass::Tree::CharsetNode)
133
- begin
134
- encoding = node.children.first.name
135
- # Default to big-endian encoding, because we have to decide somehow
136
- encoding << 'BE' if encoding =~ /\Autf-(16|32)\Z/i
137
- @result = @result.encode(Encoding.find(encoding))
138
- rescue EncodingError
130
+
131
+ unless Sass::Util.ruby1_8? || @result.ascii_only?
132
+ if node.style == :compressed
133
+ # A byte order mark is sufficient to tell browsers that this
134
+ # file is UTF-8 encoded, and will override any other detection
135
+ # methods as per http://encoding.spec.whatwg.org/#decode-and-encode.
136
+ prepend! "\uFEFF"
137
+ else
138
+ prepend! "@charset \"UTF-8\";\n"
139
139
  end
140
140
  end
141
141
 
142
- prepend! "@charset \"#{@result.encoding.name}\";#{
143
- node.style == :compressed ? '' : "\n"
144
- }".encode(@result.encoding)
145
142
  @result
146
143
  rescue Sass::SyntaxError => e
147
144
  e.sass_template ||= node.template
@@ -281,7 +278,7 @@ class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
281
278
 
282
279
  joined_rules = node.resolved_rules.members.map do |seq|
283
280
  next if seq.has_placeholder?
284
- rule_part = seq.to_a.join
281
+ rule_part = seq.to_s
285
282
  if node.style == :compressed
286
283
  rule_part.gsub!(/([^,])\s*\n\s*/m, '\1 ')
287
284
  rule_part.gsub!(/\s*([,+>])\s*/m, '\1')
@@ -353,6 +350,10 @@ class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
353
350
  # @comment
354
351
  # rubocop:enable MethodLength
355
352
 
353
+ def visit_keyframerule(node)
354
+ visit_directive(node)
355
+ end
356
+
356
357
  private
357
358
 
358
359
  def debug_info_rule(debug_info, options)
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  require 'erb'
2
3
  require 'set'
3
4
  require 'enumerator'
@@ -337,6 +338,18 @@ module Sass
337
338
  minuend.select {|e| set.include?(e)}
338
339
  end
339
340
 
341
+ # Returns the maximum of `val1` and `val2`. We use this over \{Array.max} to
342
+ # avoid unnecessary garbage collection.
343
+ def max(val1, val2)
344
+ val1 > val2 ? val1 : val2
345
+ end
346
+
347
+ # Returns the minimum of `val1` and `val2`. We use this over \{Array.min} to
348
+ # avoid unnecessary garbage collection.
349
+ def min(val1, val2)
350
+ val1 <= val2 ? val1 : val2
351
+ end
352
+
340
353
  # Returns a string description of the character that caused an
341
354
  # `Encoding::UndefinedConversionError`.
342
355
  #
@@ -707,6 +720,14 @@ module Sass
707
720
  @ruby1_8_6 = ruby1_8? && RUBY_VERSION_COMPONENTS[2] < 7
708
721
  end
709
722
 
723
+ # Whether or not this is running under Ruby 1.9.2 exactly.
724
+ #
725
+ # @return [Boolean]
726
+ def ruby1_9_2?
727
+ return @ruby1_9_2 if defined?(@ruby1_9_2)
728
+ @ruby1_9_2 = RUBY_VERSION_COMPONENTS == [1, 9, 2]
729
+ end
730
+
710
731
  # Wehter or not this is running under JRuby 1.6 or lower.
711
732
  def jruby1_6?
712
733
  return @jruby1_6 if defined?(@jruby1_6)
@@ -751,114 +772,76 @@ module Sass
751
772
  (pairs_or_hash.is_a?(NormalizedMap) ? NormalizedMap : OrderedHash)[*flatten(pairs_or_hash, 1)]
752
773
  end
753
774
 
754
- # Checks that the encoding of a string is valid in Ruby 1.9
755
- # and cleans up potential encoding gotchas like the UTF-8 BOM.
756
- # If it's not, yields an error string describing the invalid character
757
- # and the line on which it occurrs.
758
- #
759
- # @param str [String] The string of which to check the encoding
760
- # @yield [msg] A block in which an encoding error can be raised.
761
- # Only yields if there is an encoding error
762
- # @yieldparam msg [String] The error message to be raised
763
- # @return [String] `str`, potentially with encoding gotchas like BOMs removed
764
- def check_encoding(str)
765
- if ruby1_8?
766
- return str.gsub(/\A\xEF\xBB\xBF/, '') # Get rid of the UTF-8 BOM
767
- elsif str.valid_encoding?
768
- # Get rid of the Unicode BOM if possible
769
- if str.encoding.name =~ /^UTF-(8|16|32)(BE|LE)?$/
770
- return str.gsub(Regexp.new("\\A\uFEFF".encode(str.encoding.name)), '')
771
- else
772
- return str
773
- end
774
- end
775
-
776
- encoding = str.encoding
777
- newlines = Regexp.new("\r\n|\r|\n".encode(encoding).force_encoding("binary"))
778
- str.force_encoding("binary").split(newlines).each_with_index do |line, i|
779
- begin
780
- line.encode(encoding)
781
- rescue Encoding::UndefinedConversionError => e
782
- yield <<MSG.rstrip, i + 1
783
- Invalid #{encoding.name} character #{undefined_conversion_error_char(e)}
784
- MSG
785
- end
786
- end
787
- str
775
+ unless ruby1_8?
776
+ CHARSET_REGEXP = /\A@charset "([^"]+)"/
777
+ UTF_8_BOM = "\xEF\xBB\xBF".force_encoding('BINARY')
778
+ UTF_16BE_BOM = "\xFE\xFF".force_encoding('BINARY')
779
+ UTF_16LE_BOM = "\xFF\xFE".force_encoding('BINARY')
788
780
  end
789
781
 
790
782
  # Like {\#check\_encoding}, but also checks for a `@charset` declaration
791
783
  # at the beginning of the file and uses that encoding if it exists.
792
784
  #
793
- # The Sass encoding rules are simple.
794
- # If a `@charset` declaration exists,
795
- # we assume that that's the original encoding of the document.
796
- # Otherwise, we use whatever encoding Ruby has.
797
- # Then we convert that to UTF-8 to process internally.
798
- # The UTF-8 end result is what's returned by this method.
785
+ # Sass follows CSS's decoding rules.
799
786
  #
800
787
  # @param str [String] The string of which to check the encoding
801
- # @yield [msg] A block in which an encoding error can be raised.
802
- # Only yields if there is an encoding error
803
- # @yieldparam msg [String] The error message to be raised
804
788
  # @return [(String, Encoding)] The original string encoded as UTF-8,
805
789
  # and the source encoding of the string (or `nil` under Ruby 1.8)
806
790
  # @raise [Encoding::UndefinedConversionError] if the source encoding
807
791
  # cannot be converted to UTF-8
808
792
  # @raise [ArgumentError] if the document uses an unknown encoding with `@charset`
809
- def check_sass_encoding(str, &block)
810
- return check_encoding(str, &block), nil if ruby1_8?
811
- # We allow any printable ASCII characters but double quotes in the charset decl
812
- bin = str.dup.force_encoding("BINARY")
813
- encoding = Sass::Util::ENCODINGS_TO_CHECK.find do |enc|
814
- re = Sass::Util::CHARSET_REGEXPS[enc]
815
- re && bin =~ re
793
+ # @raise [Sass::SyntaxError] If the document declares an encoding that
794
+ # doesn't match its contents, or it doesn't declare an encoding and its
795
+ # contents are invalid in the native encoding.
796
+ def check_sass_encoding(str)
797
+ # On Ruby 1.8 we can't do anything complicated with encodings.
798
+ # Instead, we just strip out a UTF-8 BOM if it exists and
799
+ # sanitize according to Section 3.3 of CSS Syntax Level 3. We
800
+ # don't sanitize null characters since they might be components
801
+ # of other characters.
802
+ if ruby1_8?
803
+ return str.gsub(/\A\xEF\xBB\xBF/, '').gsub(/\r\n?|\f/, "\n"), nil
816
804
  end
817
- charset, bom = $1, $2
818
- if charset
819
- charset = charset.force_encoding(encoding).encode("UTF-8")
820
- if (endianness = encoding[/[BL]E$/])
821
- begin
822
- Encoding.find(charset + endianness)
823
- charset << endianness
824
- rescue ArgumentError # Encoding charset + endianness doesn't exist
805
+
806
+ # Determine the fallback encoding following section 3.2 of CSS Syntax Level 3 and Encodings:
807
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#determine-the-fallback-encoding
808
+ # http://encoding.spec.whatwg.org/#decode
809
+ binary = str.dup.force_encoding("BINARY")
810
+ if binary.start_with?(UTF_8_BOM)
811
+ binary.slice! 0, UTF_8_BOM.length
812
+ str = binary.force_encoding('UTF-8')
813
+ elsif binary.start_with?(UTF_16BE_BOM)
814
+ binary.slice! 0, UTF_16BE_BOM.length
815
+ str = binary.force_encoding('UTF-16BE')
816
+ elsif binary.start_with?(UTF_16LE_BOM)
817
+ binary.slice! 0, UTF_16LE_BOM.length
818
+ str = binary.force_encoding('UTF-16LE')
819
+ elsif binary =~ CHARSET_REGEXP
820
+ charset = $1.force_encoding('US-ASCII')
821
+ # Ruby 1.9.2 doesn't recognize a UTF-16 encoding without an endian marker.
822
+ if ruby1_9_2? && charset.downcase == 'utf-16'
823
+ encoding = Encoding.find('UTF-8')
824
+ else
825
+ encoding = Encoding.find(charset)
826
+ if encoding.name == 'UTF-16' || encoding.name == 'UTF-16BE'
827
+ encoding = Encoding.find('UTF-8')
825
828
  end
826
829
  end
827
- str.force_encoding(charset)
828
- elsif bom
829
- str.force_encoding(encoding)
830
+ str = binary.force_encoding(encoding)
831
+ elsif str.encoding.name == "ASCII-8BIT"
832
+ # Normally we want to fall back on believing the Ruby string
833
+ # encoding, but if that's just binary we want to make sure
834
+ # it's valid UTF-8.
835
+ str = str.force_encoding('utf-8')
830
836
  end
831
837
 
832
- str = check_encoding(str, &block)
833
- return str.encode("UTF-8"), str.encoding
834
- end
838
+ find_encoding_error(str) unless str.valid_encoding?
835
839
 
836
- unless ruby1_8?
837
- # @private
838
- def _enc(string, encoding)
839
- string.encode(encoding).force_encoding("BINARY")
840
- end
841
-
842
- # We could automatically add in any non-ASCII-compatible encodings here,
843
- # but there's not really a good way to do that
844
- # without manually checking that each encoding
845
- # encodes all ASCII characters properly,
846
- # which takes long enough to affect the startup time of the CLI.
847
- ENCODINGS_TO_CHECK = %w[UTF-8 UTF-16BE UTF-16LE UTF-32BE UTF-32LE]
848
-
849
- CHARSET_REGEXPS = Hash.new do |h, e|
850
- h[e] =
851
- begin
852
- # /\A(?:\uFEFF)?@charset "(.*?)"|\A(\uFEFF)/
853
- Regexp.new(/\A(?:#{_enc("\uFEFF", e)})?#{
854
- _enc('@charset "', e)}(.*?)#{_enc('"', e)}|\A(#{
855
- _enc("\uFEFF", e)})/)
856
- rescue Encoding::ConverterNotFoundError => _
857
- nil # JRuby on Java 5 doesn't support UTF-32
858
- rescue
859
- # /\A@charset "(.*?)"/
860
- Regexp.new(/\A#{_enc('@charset "', e)}(.*?)#{_enc('"', e)}/)
861
- end
840
+ begin
841
+ # If the string is valid, preprocess it according to section 3.3 of CSS Syntax Level 3.
842
+ return str.encode("UTF-8").gsub(/\r\n?|\f/, "\n").tr("\u0000", "�"), str.encoding
843
+ rescue EncodingError
844
+ find_encoding_error(str)
862
845
  end
863
846
  end
864
847
 
@@ -1268,6 +1251,27 @@ MSG
1268
1251
 
1269
1252
  private
1270
1253
 
1254
+ def find_encoding_error(str)
1255
+ encoding = str.encoding
1256
+ cr = Regexp.quote("\r".encode(encoding).force_encoding('BINARY'))
1257
+ lf = Regexp.quote("\n".encode(encoding).force_encoding('BINARY'))
1258
+ ff = Regexp.quote("\f".encode(encoding).force_encoding('BINARY'))
1259
+ line_break = /#{cr}#{lf}?|#{ff}|#{lf}/
1260
+
1261
+ str.force_encoding("binary").split(line_break).each_with_index do |line, i|
1262
+ begin
1263
+ line.encode(encoding)
1264
+ rescue Encoding::UndefinedConversionError => e
1265
+ raise Sass::SyntaxError.new(
1266
+ "Invalid #{encoding.name} character #{undefined_conversion_error_char(e)}",
1267
+ :line => i + 1)
1268
+ end
1269
+ end
1270
+
1271
+ # We shouldn't get here, but it's possible some weird encoding stuff causes it.
1272
+ return str, str.encoding
1273
+ end
1274
+
1271
1275
  # rubocop:disable LineLength
1272
1276
 
1273
1277
  # Calculates the memoization table for the Least Common Subsequence algorithm.