sass 3.3.0.alpha.231 → 3.3.0.alpha.243

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,3 +8,4 @@ require 'sass/script/value/bool'
8
8
  require 'sass/script/value/null'
9
9
  require 'sass/script/value/list'
10
10
  require 'sass/script/value/arg_list'
11
+ require 'sass/script/value/map'
@@ -158,6 +158,18 @@ MSG
158
158
  Sass::Script::Value::String.new("/#{self.to_s}")
159
159
  end
160
160
 
161
+ # Returns the hash code of this value. Two objects' hash codes should be
162
+ # equal if the objects are equal.
163
+ #
164
+ # @return [Fixnum] The hash code.
165
+ def hash
166
+ value.hash
167
+ end
168
+
169
+ def eql?(other)
170
+ self == other
171
+ end
172
+
161
173
  # @return [String] A readable representation of the value
162
174
  def inspect
163
175
  value.inspect
@@ -185,14 +197,30 @@ MSG
185
197
  # @raise [Sass::SyntaxError] if this value isn't an integer
186
198
  def assert_int!; to_i; end
187
199
 
200
+ # Returns the separator for this value. For non-list-like values or the
201
+ # empty list, this will be `nil`. For lists or maps, it will be `:space` or
202
+ # `:comma`.
203
+ #
204
+ # @return [Symbol]
205
+ def separator; nil; end
206
+
188
207
  # Returns the value of this value as a list.
189
208
  # Single values are considered the same as single-element lists.
190
209
  #
191
- # @return [Array<Value>] The of this value as a list
210
+ # @return [Array<Value>] This value as a list
192
211
  def to_a
193
212
  [self]
194
213
  end
195
214
 
215
+ # Returns the value of this value as a hash. Most values don't have hash
216
+ # representations, but [Map]s and empty [List]s do.
217
+ #
218
+ # @return [Hash<Value, Value>] This value as a hash
219
+ # @raise [Sass::SyntaxError] if this value doesn't have a hash representation
220
+ def to_h
221
+ raise Sass::SyntaxError.new("#{self.inspect} is not a map.")
222
+ end
223
+
196
224
  # Returns the string representation of this value
197
225
  # as it would be output to the CSS document.
198
226
  #
@@ -356,6 +356,10 @@ module Sass::Script::Value
356
356
  other.is_a?(Color) && rgb == other.rgb && alpha == other.alpha)
357
357
  end
358
358
 
359
+ def hash
360
+ [rgb, alpha].hash
361
+ end
362
+
359
363
  # Returns a copy of this color with one or more channels changed.
360
364
  # RGB or HSL colors may be changed, but not both at once.
361
365
  #
@@ -36,6 +36,10 @@ module Sass::Script::Value
36
36
  self.separator == other.separator)
37
37
  end
38
38
 
39
+ def hash
40
+ @hash ||= [value, separator].hash
41
+ end
42
+
39
43
  # @see Value#to_s
40
44
  def to_s(opts = {})
41
45
  raise Sass::SyntaxError.new("() isn't a valid CSS value.") if value.empty?
@@ -48,7 +52,8 @@ module Sass::Script::Value
48
52
  precedence = Sass::Script::Parser.precedence_of(separator)
49
53
  value.reject {|e| e.is_a?(Null)}.map do |v|
50
54
  if v.is_a?(List) && Sass::Script::Parser.precedence_of(v.separator) <= precedence ||
51
- separator == :space && v.is_a?(UnaryOperation) && (v.operator == :minus || v.operator == :plus)
55
+ separator == :space && v.is_a?(Sass::Script::Tree::UnaryOperation) &&
56
+ (v.operator == :minus || v.operator == :plus)
52
57
  "(#{v.to_sass(opts)})"
53
58
  else
54
59
  v.to_sass(opts)
@@ -56,6 +61,27 @@ module Sass::Script::Value
56
61
  end.join(sep_str(nil))
57
62
  end
58
63
 
64
+ # @see Value#to_h
65
+ def to_h
66
+ return Sass::Util.ordered_hash if value.empty?
67
+ return @map ||= Sass::Util.to_hash(value.map {|e| e.to_a}) if is_pseudo_map?
68
+ super
69
+ end
70
+
71
+ # Returns whether a warning still needs to be printed for this list being used as a map.
72
+ #
73
+ # @return [Boolean]
74
+ def needs_map_warning?
75
+ !@value.empty? && !@map
76
+ end
77
+
78
+ # Returns whether this is a list of pairs that can be used as a map.
79
+ #
80
+ # @return [Boolean]
81
+ def is_pseudo_map?
82
+ @is_pseudo_map ||= value.all? {|e| e.is_a?(Sass::Script::Value::List) && e.to_a.length == 2}
83
+ end
84
+
59
85
  # @see Value#inspect
60
86
  def inspect
61
87
  "(#{value.map {|e| e.inspect}.join(sep_str(nil))})"
@@ -0,0 +1,70 @@
1
+ module Sass::Script::Value
2
+ # A SassScript object representing a map from keys to values. Both keys and
3
+ # values can be any SassScript object.
4
+ class Map < Base
5
+ # The Ruby hash containing the contents of this map.
6
+ #
7
+ # @return [Hash<Node, Node>]
8
+ attr_reader :value
9
+ alias_method :to_h, :value
10
+
11
+ # Creates a new map.
12
+ #
13
+ # @param hash [Hash<Node, Node>]
14
+ def initialize(hash)
15
+ super(Sass::Util.ordered_hash(hash))
16
+ end
17
+
18
+ # @see Value#options=
19
+ def options=(options)
20
+ super
21
+ value.each do |k, v|
22
+ k.options = options
23
+ v.options = options
24
+ end
25
+ end
26
+
27
+ # @see Value#separator
28
+ def separator
29
+ :comma unless value.empty?
30
+ end
31
+
32
+ # @see Value#to_a
33
+ def to_a
34
+ value.map do |k, v|
35
+ list = List.new([k, v], :space)
36
+ list.options = options
37
+ list
38
+ end
39
+ end
40
+
41
+ # @see Value#eq
42
+ def eq(other)
43
+ Bool.new(other.is_a?(Map) && self.value == other.value)
44
+ end
45
+
46
+ def hash
47
+ @hash ||= value.hash
48
+ end
49
+
50
+ # @see Value#to_s
51
+ def to_s(opts = {})
52
+ raise Sass::SyntaxError.new("#{inspect} isn't a valid CSS value.")
53
+ end
54
+
55
+ def to_sass(opts = {})
56
+ return "()" if value.empty?
57
+
58
+ to_sass = lambda do |value|
59
+ if value.is_a?(Map) || (value.is_a?(List) && value.separator == :comma)
60
+ "(#{value.to_sass(opts)})"
61
+ else
62
+ value.to_sass(opts)
63
+ end
64
+ end
65
+
66
+ "(#{value.map {|(k, v)| "#{to_sass[k]}: #{to_sass[v]}"}.join(', ')})"
67
+ end
68
+ alias_method :inspect, :to_sass
69
+ end
70
+ end
@@ -206,6 +206,18 @@ module Sass::Script::Value
206
206
  Bool.new(this.value == other.value)
207
207
  end
208
208
 
209
+ def hash
210
+ [value, numerator_units, denominator_units].hash
211
+ end
212
+
213
+ # Hash-equality works differently than `==` equality for numbers.
214
+ # Hash-equality must be transitive, so it just compares the exact value,
215
+ # numerator units, and denominator units.
216
+ def eql?(other)
217
+ self.value == other.value && self.numerator_units == other.numerator_units &&
218
+ self.denominator_units == other.denominator_units
219
+ end
220
+
209
221
  # The SassScript `>` operation.
210
222
  #
211
223
  # @param other [Number] The right-hand side of the operator
@@ -263,14 +263,20 @@ module Sass
263
263
 
264
264
  def each_directive(start_pos)
265
265
  tok!(/\$/)
266
- var = tok! IDENT
266
+ vars = [tok!(IDENT)]
267
267
  ss
268
+ while tok(/,/)
269
+ ss
270
+ tok!(/\$/)
271
+ vars << tok!(IDENT)
272
+ ss
273
+ end
268
274
 
269
275
  tok!(/in/)
270
276
  list = sass_script(:parse)
271
277
  ss
272
278
 
273
- block(node(Sass::Tree::EachNode.new(var, list), start_pos), :directive)
279
+ block(node(Sass::Tree::EachNode.new(vars, list), start_pos), :directive)
274
280
  end
275
281
 
276
282
  def while_directive(start_pos)
@@ -98,6 +98,15 @@ module Sass
98
98
  with_frame(filename, line, :mixin, name) {yield}
99
99
  end
100
100
 
101
+ def to_s
102
+ Sass::Util.enum_with_index(Sass::Util.enum_cons(frames.reverse + [nil], 2)).
103
+ map do |(frame, caller), i|
104
+ "#{i == 0 ? "on" : "from"} line #{frame.line}" +
105
+ " of #{frame.filename || "an unknown file"}" +
106
+ (caller && caller.name ? ", in `#{caller.name}'" : "")
107
+ end.join("\n")
108
+ end
109
+
101
110
  private
102
111
 
103
112
  def with_frame(filename, line, type, name = nil)
@@ -5,18 +5,18 @@ module Sass::Tree
5
5
  #
6
6
  # @see Sass::Tree
7
7
  class EachNode < Node
8
- # The name of the loop variable.
9
- # @return [String]
10
- attr_reader :var
8
+ # The names of the loop variables.
9
+ # @return [Array<String>]
10
+ attr_reader :vars
11
11
 
12
12
  # The parse tree for the list.
13
13
  # @return [Script::Tree::Node]
14
14
  attr_accessor :list
15
15
 
16
- # @param var [String] The name of the loop variable
16
+ # @param vars [Array<String>] The names of the loop variables
17
17
  # @param list [Script::Tree::Node] The parse tree for the list
18
- def initialize(var, list)
19
- @var = var
18
+ def initialize(vars, list)
19
+ @vars = vars
20
20
  @list = list
21
21
  super()
22
22
  end
@@ -58,7 +58,7 @@ module Sass::Tree
58
58
  # This is only readable in a CSS tree as it is written during the perform step
59
59
  # and only when the :trace_selectors option is set.
60
60
  #
61
- # @return [Array<String>]
61
+ # @return [String]
62
62
  attr_accessor :stack_trace
63
63
 
64
64
  # @param rule [Array<String, Sass::Script::Tree::Node>]
@@ -104,7 +104,8 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
104
104
  end
105
105
 
106
106
  def visit_each(node)
107
- "#{tab_str}@each $#{dasherize(node.var)} in #{node.list.to_sass(@options)}#{yield}"
107
+ vars = node.vars.map {|var| "$#{dasherize(var)}"}.join(", ")
108
+ "#{tab_str}@each #{vars} in #{node.list.to_sass(@options)}#{yield}"
108
109
  end
109
110
 
110
111
  def visit_extend(node)
@@ -91,12 +91,12 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
91
91
 
92
92
  def initialize(env)
93
93
  @environment = env
94
- @stack = Sass::Stack.new
95
94
  end
96
95
 
97
96
  # If an exception is raised, this adds proper metadata to the backtrace.
98
97
  def visit(node)
99
- @stack.with_base(node.filename, node.line) {super(node.dup)}
98
+ return super(node.dup) unless @environment
99
+ @environment.stack.with_base(node.filename, node.line) {super(node.dup)}
100
100
  rescue Sass::SyntaxError => e
101
101
  e.modify_backtrace(:filename => node.filename, :line => node.line)
102
102
  raise e
@@ -155,8 +155,14 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
155
155
  list = node.list.perform(@environment)
156
156
 
157
157
  with_environment Sass::Environment.new(@environment) do
158
- list.to_a.map do |v|
159
- @environment.set_local_var(node.var, v)
158
+ list.to_a.map do |value|
159
+ if node.vars.length == 1
160
+ @environment.set_local_var(node.vars.first, value)
161
+ else
162
+ node.vars.zip(value.to_a) do |(var, sub_value)|
163
+ @environment.set_local_var(var, sub_value || Sass::Script::Value::Null.new)
164
+ end
165
+ end
160
166
  node.children.map {|c| visit(c)}
161
167
  end.flatten
162
168
  end
@@ -220,12 +226,12 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
220
226
  return resolved_node
221
227
  end
222
228
  file = node.imported_file
223
- if @stack.frames.any? {|f| f.is_import? && f.filename == file.options[:filename]}
229
+ if @environment.stack.frames.any? {|f| f.is_import? && f.filename == file.options[:filename]}
224
230
  handle_import_loop!(node)
225
231
  end
226
232
 
227
233
  begin
228
- @stack.with_import(node.filename, node.line) do
234
+ @environment.stack.with_import(node.filename, node.line) do
229
235
  root = file.to_tree
230
236
  Sass::Tree::Visitors::CheckNesting.visit(root)
231
237
  node.children = root.children.map {|c| visit(c)}.flatten
@@ -249,10 +255,10 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
249
255
  # Runs a mixin.
250
256
  def visit_mixin(node)
251
257
  include_loop = true
252
- handle_include_loop!(node) if @stack.frames.any? {|f| f.is_mixin? && f.name == node.name}
258
+ handle_include_loop!(node) if @environment.stack.frames.any? {|f| f.is_mixin? && f.name == node.name}
253
259
  include_loop = false
254
260
 
255
- @stack.with_mixin(node.filename, node.line, node.name) do
261
+ @environment.stack.with_mixin(node.filename, node.line, node.name) do
256
262
  raise Sass::SyntaxError.new("Undefined mixin '#{node.name}'.") unless mixin = @environment.mixin(node.name)
257
263
 
258
264
  if node.children.any? && !mixin.has_content
@@ -282,7 +288,7 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
282
288
 
283
289
  def visit_content(node)
284
290
  return [] unless content = @environment.content
285
- @stack.with_mixin(node.filename, node.line, '@content') do
291
+ @environment.stack.with_mixin(node.filename, node.line, '@content') do
286
292
  trace_node = Sass::Tree::TraceNode.from_node('@content', node)
287
293
  with_environment(@environment.caller) {trace_node.children = content.map {|c| visit(c.dup)}.flatten}
288
294
  trace_node
@@ -315,7 +321,7 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
315
321
  parser = Sass::SCSS::StaticParser.new(run_interp(node.rule),
316
322
  node.filename, node.options[:importer], node.line)
317
323
  node.parsed_rules ||= parser.parse_selector
318
- node.stack_trace = stack_trace if node.options[:trace_selectors]
324
+ node.stack_trace = @environment.stack.to_s if node.options[:trace_selectors]
319
325
  yield
320
326
  end
321
327
 
@@ -338,7 +344,7 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
338
344
  res = node.expr.perform(@environment)
339
345
  res = res.value if res.is_a?(Sass::Script::Value::String)
340
346
  msg = "WARNING: #{res}\n "
341
- msg << stack_trace.join("\n ") << "\n"
347
+ msg << @environment.stack.to_s.gsub("\n", "\n ") << "\n"
342
348
  Sass::Util.sass_warn msg
343
349
  []
344
350
  end
@@ -382,15 +388,6 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
382
388
 
383
389
  private
384
390
 
385
- def stack_trace
386
- Sass::Util.enum_with_index(Sass::Util.enum_cons(@stack.frames.reverse + [nil], 2)).
387
- map do |(frame, caller), i|
388
- "#{i == 0 ? "on" : "from"} line #{frame.line}" +
389
- " of #{frame.filename || "an unknown file"}" +
390
- (caller && caller.name ? ", in `#{caller.name}'" : "")
391
- end
392
- end
393
-
394
391
  def run_interp_no_strip(text)
395
392
  text.map do |r|
396
393
  next r if r.is_a?(String)
@@ -408,7 +405,7 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
408
405
  def handle_include_loop!(node)
409
406
  msg = "An @include loop has been found:"
410
407
  content_count = 0
411
- mixins = @stack.frames.select {|f| f.is_mixin?}.reverse.map {|f| f.name}.select do |name|
408
+ mixins = @environment.stack.frames.select {|f| f.is_mixin?}.reverse.map {|f| f.name}.select do |name|
412
409
  if name == '@content'
413
410
  content_count += 1
414
411
  false
@@ -431,7 +428,7 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
431
428
 
432
429
  def handle_import_loop!(node)
433
430
  msg = "An @import loop has been found:"
434
- files = @stack.frames.select {|f| f.is_import?}.map {|f| f.filename}.compact
431
+ files = @environment.stack.frames.select {|f| f.is_import?}.map {|f| f.filename}.compact
435
432
  if node.filename == node.imported_file.options[:filename]
436
433
  raise Sass::SyntaxError.new("#{msg} #{node.filename} imports itself")
437
434
  end
@@ -280,7 +280,7 @@ class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
280
280
  output "\n"
281
281
  elsif node.options[:trace_selectors]
282
282
  output("#{old_spaces}/* ")
283
- output(node.stack_trace.join("\n #{old_spaces}"))
283
+ output(node.stack_trace.gsub("\n", "\n #{old_spaces}"))
284
284
  output(" */\n")
285
285
  elsif node.options[:line_comments]
286
286
  output("#{old_spaces}/* line #{node.line}")
@@ -747,6 +747,10 @@ a
747
747
  c
748
748
  @each $str in foo, bar, baz, bang
749
749
  d: $str
750
+
751
+ c
752
+ @each $key, $value in (foo: 1, bar: 2, baz: 3)
753
+ \#{$key}: $value
750
754
  SASS
751
755
  a {
752
756
  @each $number in 1px 2px 3px 4px {
@@ -759,6 +763,12 @@ c {
759
763
  d: $str;
760
764
  }
761
765
  }
766
+
767
+ c {
768
+ @each $key, $value in (foo: 1, bar: 2, baz: 3) {
769
+ \#{$key}: $value;
770
+ }
771
+ }
762
772
  SCSS
763
773
  end
764
774
 
@@ -1646,6 +1646,27 @@ a
1646
1646
  SASS
1647
1647
  end
1648
1648
 
1649
+ def test_destructuring_each
1650
+ assert_equal <<CSS, render(<<SCSS)
1651
+ a {
1652
+ foo: 1px;
1653
+ bar: 2px;
1654
+ baz: 3px; }
1655
+
1656
+ c {
1657
+ foo: "Value is bar";
1658
+ bar: "Value is baz";
1659
+ bang: "Value is "; }
1660
+ CSS
1661
+ a
1662
+ @each $name, $number in (foo: 1px, bar: 2px, baz: 3px)
1663
+ \#{$name}: $number
1664
+ c
1665
+ @each $key, $value in (foo bar) (bar, baz) bang
1666
+ \#{$key}: "Value is \#{$value}"
1667
+ SCSS
1668
+ end
1669
+
1649
1670
  def test_variable_reassignment
1650
1671
  assert_equal(<<CSS, render(<<SASS))
1651
1672
  a {