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

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.
@@ -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 {