sass 3.3.0.rc.2 → 3.3.0.rc.3

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 (76) hide show
  1. checksums.yaml +15 -0
  2. data/CONTRIBUTING +1 -1
  3. data/README.md +7 -7
  4. data/Rakefile +4 -2
  5. data/VERSION +1 -1
  6. data/VERSION_DATE +1 -1
  7. data/bin/sass +5 -1
  8. data/bin/sass-convert +5 -1
  9. data/bin/scss +5 -1
  10. data/ext/mkrf_conf.rb +23 -0
  11. data/lib/sass/css.rb +1 -1
  12. data/lib/sass/engine.rb +19 -9
  13. data/lib/sass/environment.rb +8 -0
  14. data/lib/sass/exec.rb +4 -4
  15. data/lib/sass/features.rb +0 -1
  16. data/lib/sass/importers/base.rb +13 -6
  17. data/lib/sass/importers/deprecated_path.rb +8 -2
  18. data/lib/sass/importers/filesystem.rb +33 -7
  19. data/lib/sass/logger.rb +1 -4
  20. data/lib/sass/logger/base.rb +0 -2
  21. data/lib/sass/logger/log_level.rb +0 -2
  22. data/lib/sass/plugin.rb +2 -2
  23. data/lib/sass/plugin/compiler.rb +23 -11
  24. data/lib/sass/plugin/configuration.rb +0 -1
  25. data/lib/sass/railtie.rb +1 -0
  26. data/lib/sass/script/css_lexer.rb +0 -1
  27. data/lib/sass/script/css_parser.rb +0 -1
  28. data/lib/sass/script/functions.rb +158 -96
  29. data/lib/sass/script/lexer.rb +29 -35
  30. data/lib/sass/script/parser.rb +10 -3
  31. data/lib/sass/script/tree.rb +0 -1
  32. data/lib/sass/script/tree/funcall.rb +21 -5
  33. data/lib/sass/script/tree/list_literal.rb +0 -1
  34. data/lib/sass/script/value/arg_list.rb +7 -3
  35. data/lib/sass/script/value/bool.rb +0 -1
  36. data/lib/sass/script/value/null.rb +0 -1
  37. data/lib/sass/script/value/number.rb +2 -6
  38. data/lib/sass/scss/css_parser.rb +0 -1
  39. data/lib/sass/scss/parser.rb +5 -5
  40. data/lib/sass/scss/script_lexer.rb +0 -1
  41. data/lib/sass/scss/script_parser.rb +0 -1
  42. data/lib/sass/selector.rb +11 -1
  43. data/lib/sass/selector/comma_sequence.rb +3 -4
  44. data/lib/sass/selector/sequence.rb +11 -7
  45. data/lib/sass/selector/simple_sequence.rb +42 -11
  46. data/lib/sass/source/map.rb +6 -19
  47. data/lib/sass/tree/at_root_node.rb +1 -1
  48. data/lib/sass/tree/mixin_node.rb +2 -2
  49. data/lib/sass/tree/prop_node.rb +0 -1
  50. data/lib/sass/tree/variable_node.rb +0 -5
  51. data/lib/sass/tree/visitors/check_nesting.rb +0 -1
  52. data/lib/sass/tree/visitors/convert.rb +2 -2
  53. data/lib/sass/tree/visitors/cssize.rb +184 -84
  54. data/lib/sass/tree/visitors/deep_copy.rb +0 -1
  55. data/lib/sass/tree/visitors/perform.rb +14 -44
  56. data/lib/sass/util.rb +59 -12
  57. data/lib/sass/util/cross_platform_random.rb +19 -0
  58. data/lib/sass/util/normalized_map.rb +17 -1
  59. data/test/sass/compiler_test.rb +10 -0
  60. data/test/sass/conversion_test.rb +36 -0
  61. data/test/sass/css2sass_test.rb +19 -0
  62. data/test/sass/engine_test.rb +54 -105
  63. data/test/sass/functions_test.rb +233 -26
  64. data/test/sass/importer_test.rb +72 -10
  65. data/test/sass/plugin_test.rb +14 -0
  66. data/test/sass/script_conversion_test.rb +4 -4
  67. data/test/sass/script_test.rb +58 -21
  68. data/test/sass/scss/css_test.rb +8 -1
  69. data/test/sass/scss/scss_test.rb +376 -179
  70. data/test/sass/source_map_test.rb +8 -0
  71. data/test/sass/templates/subdir/import_up1.scss +1 -0
  72. data/test/sass/templates/subdir/import_up2.scss +1 -0
  73. data/test/sass/util_test.rb +16 -0
  74. data/test/test_helper.rb +12 -4
  75. metadata +269 -287
  76. data/lib/sass/script/tree/selector.rb +0 -30
@@ -94,9 +94,9 @@ module Sass::Source
94
94
  # rubocop:disable MethodLength
95
95
  def to_json(options)
96
96
  css_uri, css_path, sourcemap_path =
97
- [:css_uri, :css_path, :sourcemap_path].map {|o| options[o]}
97
+ options[:css_uri], options[:css_path], options[:sourcemap_path]
98
98
  unless css_uri || (css_path && sourcemap_path)
99
- raise ArgumentError.new("Sass::Source::Map#to_json requires either " +
99
+ raise ArgumentError.new("Sass::Source::Map#to_json requires either " \
100
100
  "the :css_uri option or both the :css_path and :soucemap_path options.")
101
101
  end
102
102
  css_path &&= Pathname.pwd.join(Pathname.new(css_path)).cleanpath
@@ -111,7 +111,6 @@ module Sass::Source
111
111
  next_source_id = 0
112
112
  line_data = []
113
113
  segment_data_for_line = []
114
- no_public_url = Set.new
115
114
 
116
115
  # These track data necessary for the delta coding.
117
116
  previous_target_line = nil
@@ -122,22 +121,9 @@ module Sass::Source
122
121
 
123
122
  @data.each do |m|
124
123
  file, importer = m.input.file, m.input.importer
125
- unless (source_uri = importer && importer.public_url(file))
126
- if importer.is_a?(Sass::Importers::Filesystem) && sourcemap_path
127
- file_path = Pathname.new(importer.root).join(file)
128
- source_uri = file_path.relative_path_from(sourcemap_path.dirname).to_s
129
- elsif no_public_url.include?(file)
130
- next
131
- else
132
- no_public_url << file
133
- Sass::Util.sass_warn <<WARNING
134
- WARNING: Couldn't determine public URL for "#{file}" while generating sourcemap.
135
- Without a public URL, there's nothing for the source map to link to.
136
- Custom importers should define the #public_url method.
137
- WARNING
138
- next
139
- end
140
- end
124
+ source_uri = importer &&
125
+ importer.public_url(file, sourcemap_path && sourcemap_path.dirname.to_s)
126
+ next unless source_uri
141
127
 
142
128
  current_source_id = source_uri_to_id[source_uri]
143
129
  unless current_source_id
@@ -190,6 +176,7 @@ WARNING
190
176
  source_names = []
191
177
  (0...next_source_id).each {|id| source_names.push(id_to_source_uri[id].to_s)}
192
178
  write_json_field(result, "sources", source_names)
179
+ write_json_field(result, "names", [])
193
180
  write_json_field(result, "file", css_uri)
194
181
 
195
182
  result << "\n}"
@@ -70,7 +70,7 @@ module Sass
70
70
  # @return [Boolean]
71
71
  def exclude_node?(node)
72
72
  return exclude?(node.name.gsub(/^@/, '')) if node.is_a?(Sass::Tree::DirectiveNode)
73
- exclude?(node.class.to_s.gsub(/.*::(.*)Node$/, '\1').downcase)
73
+ exclude?('rule') && node.is_a?(Sass::Tree::RuleNode)
74
74
  end
75
75
 
76
76
  # @see Node#bubbles?
@@ -16,7 +16,7 @@ module Sass::Tree
16
16
  attr_accessor :args
17
17
 
18
18
  # A hash from keyword argument names to values.
19
- # @return [{String => Script::Tree::Node}]
19
+ # @return [Sass::Util::NormalizedMap<Script::Tree::Node>]
20
20
  attr_accessor :keywords
21
21
 
22
22
  # The first splat argument for this mixin, if one exists.
@@ -39,7 +39,7 @@ module Sass::Tree
39
39
  # @param args [Array<Script::Tree::Node>] See \{#args}
40
40
  # @param splat [Script::Tree::Node] See \{#splat}
41
41
  # @param kwarg_splat [Script::Tree::Node] See \{#kwarg_splat}
42
- # @param keywords [{String => Script::Tree::Node}] See \{#keywords}
42
+ # @param keywords [Sass::Util::NormalizedMap<Script::Tree::Node>] See \{#keywords}
43
43
  def initialize(name, args, keywords, splat, kwarg_splat)
44
44
  @name = name
45
45
  @args = args
@@ -166,7 +166,6 @@ module Sass::Tree
166
166
 
167
167
  Sass::Script::Value::String.new("(#{node.to_sass(opts)})")
168
168
  end
169
-
170
169
  end
171
170
  end
172
171
  end
@@ -20,11 +20,6 @@ module Sass
20
20
  # @return [Boolean]
21
21
  attr_reader :global
22
22
 
23
- # Whether we've warned about deprecated global variable
24
- # assignment yet for this node.
25
- # @return [Boolean]
26
- attr_accessor :global_warning_given
27
-
28
23
  # @param name [String] The name of the variable
29
24
  # @param expr [Script::Tree::Node] See \{#expr}
30
25
  # @param guarded [Boolean] See \{#guarded}
@@ -1,6 +1,5 @@
1
1
  # A visitor for checking that all nodes are properly nested.
2
2
  class Sass::Tree::Visitors::CheckNesting < Sass::Tree::Visitors::Base
3
-
4
3
  protected
5
4
 
6
5
  def initialize
@@ -68,7 +68,7 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
68
68
  end
69
69
 
70
70
  if content.include?("\n")
71
- content.gsub!(%r{\n( \*|//)}, "\n ")
71
+ content.gsub!(/\n \*/, "\n ")
72
72
  spaces = content.scan(/\n( *)/).map {|s| s.first.size}.min
73
73
  sep = node.type == :silent ? "\n//" : "\n *"
74
74
  if spaces >= 2
@@ -207,7 +207,7 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
207
207
 
208
208
  unless node.args.empty? && node.keywords.empty? && node.splat.nil?
209
209
  args = node.args.map(&arg_to_sass)
210
- keywords = Sass::Util.hash_to_a(node.keywords).
210
+ keywords = Sass::Util.hash_to_a(node.keywords.as_stored).
211
211
  map {|k, v| "$#{dasherize(k)}: #{arg_to_sass[v]}"}
212
212
 
213
213
  if node.splat
@@ -9,10 +9,12 @@ class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
9
9
 
10
10
  # Returns the immediate parent of the current node.
11
11
  # @return [Tree::Node]
12
- attr_reader :parent
12
+ def parent
13
+ @parents.last
14
+ end
13
15
 
14
16
  def initialize
15
- @parent_directives = []
17
+ @parents = []
16
18
  @extends = Sass::Util::SubsetMap.new
17
19
  end
18
20
 
@@ -41,8 +43,6 @@ class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
41
43
  node.children.map {|c| visit(c)}.flatten
42
44
  end
43
45
 
44
- MERGEABLE_DIRECTIVES = [Sass::Tree::MediaNode]
45
-
46
46
  # Runs a block of code with the current parent node
47
47
  # replaced with the given node.
48
48
  #
@@ -50,19 +50,10 @@ class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
50
50
  # @yield A block in which the parent is set to `parent`.
51
51
  # @return [Object] The return value of the block.
52
52
  def with_parent(parent)
53
- if parent.is_a?(Sass::Tree::DirectiveNode)
54
- if MERGEABLE_DIRECTIVES.any? {|klass| parent.is_a?(klass)}
55
- old_parent_directive = @parent_directives.pop
56
- end
57
- @parent_directives.push parent
58
- end
59
-
60
- old_parent, @parent = @parent, parent
53
+ @parents.push parent
61
54
  yield
62
55
  ensure
63
- @parent_directives.pop if parent.is_a?(Sass::Tree::DirectiveNode)
64
- @parent_directives.push old_parent_directive if old_parent_directive
65
- @parent = old_parent
56
+ @parents.pop
66
57
  end
67
58
 
68
59
  # In Ruby 1.8, ensures that there's only one `@charset` directive
@@ -82,17 +73,29 @@ class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
82
73
  node.children.unshift charset if charset
83
74
  end
84
75
 
85
- imports = Sass::Util.extract!(node.children) do |c|
86
- c.is_a?(Sass::Tree::DirectiveNode) && !c.is_a?(Sass::Tree::MediaNode) &&
87
- c.resolved_value =~ /^@import /i
76
+ imports_to_move = []
77
+ import_limit = nil
78
+ i = -1
79
+ node.children.reject! do |n|
80
+ i += 1
81
+ if import_limit
82
+ next false unless n.is_a?(Sass::Tree::CssImportNode)
83
+ imports_to_move << n
84
+ next true
85
+ end
86
+
87
+ if !n.is_a?(Sass::Tree::CommentNode) &&
88
+ !n.is_a?(Sass::Tree::CharsetNode) &&
89
+ !n.is_a?(Sass::Tree::CssImportNode)
90
+ import_limit = i
91
+ end
92
+
93
+ false
88
94
  end
89
- charset_and_index = Sass::Util.ruby1_8? &&
90
- node.children.each_with_index.find {|c, _| c.is_a?(Sass::Tree::CharsetNode)}
91
- if charset_and_index
92
- index = charset_and_index.last
93
- node.children = node.children[0..index] + imports + node.children[index + 1..-1]
94
- else
95
- node.children = imports + node.children
95
+
96
+ if import_limit
97
+ node.children = node.children[0...import_limit] + imports_to_move +
98
+ node.children[import_limit..-1]
96
99
  end
97
100
  end
98
101
 
@@ -138,7 +141,8 @@ class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
138
141
  raise Sass::SyntaxError.new("#{member} can't extend: invalid selector")
139
142
  end
140
143
 
141
- @extends[sel] = Extend.new(member, sel, node, @parent_directives.dup, :not_found)
144
+ parent_directives = @parents.select {|p| p.is_a?(Sass::Tree::DirectiveNode)}
145
+ @extends[sel] = Extend.new(member, sel, node, parent_directives, :not_found)
142
146
  end
143
147
  end
144
148
 
@@ -154,31 +158,6 @@ class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
154
158
  raise e
155
159
  end
156
160
 
157
- # Bubbles the `@media` directive up through RuleNodes
158
- # and merges it with other `@media` directives.
159
- def visit_media(node)
160
- yield unless bubble(node)
161
-
162
- bubbled = node.children.select do |n|
163
- n.is_a?(Sass::Tree::AtRootNode) || n.is_a?(Sass::Tree::MediaNode)
164
- end
165
- node.children -= bubbled
166
-
167
- bubbled = bubbled.map do |n|
168
- next visit(n) if n.is_a?(Sass::Tree::AtRootNode)
169
- # Otherwise, n should be a MediaNode.
170
- next [] unless n.resolved_query = n.resolved_query.merge(node.resolved_query)
171
- n
172
- end.flatten
173
-
174
- (node.children.empty? ? [] : [node]) + bubbled
175
- end
176
-
177
- # Bubbles the `@supports` directive up through RuleNodes.
178
- def visit_supports(node)
179
- visit_directive(node) {yield}
180
- end
181
-
182
161
  # Asserts that all the traced children are valid in their new location.
183
162
  def visit_trace(node)
184
163
  visit_children_without_parent(node)
@@ -188,16 +167,6 @@ class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
188
167
  raise e
189
168
  end
190
169
 
191
- # Bubbles a directive up through RuleNodes.
192
- def visit_directive(node)
193
- return yield unless node.has_children
194
- yield unless (bubbled = bubble(node))
195
- at_roots = node.children.select {|n| n.is_a?(Sass::Tree::AtRootNode)}
196
- node.children -= at_roots
197
- at_roots.map! {|n| visit(n)}.flatten
198
- (bubbled && node.children.empty? ? [] : [node]) + at_roots
199
- end
200
-
201
170
  # Converts nested properties into flat properties
202
171
  # and updates the indentation of the prop node based on the nesting level.
203
172
  def visit_prop(node)
@@ -217,15 +186,38 @@ class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
217
186
  result
218
187
  end
219
188
 
189
+ def visit_atroot(node)
190
+ # If there aren't any more directives or rules that this @at-root needs to
191
+ # exclude, we can get rid of it and just evaluate the children.
192
+ if @parents.none? {|n| node.exclude_node?(n)}
193
+ results = visit_children_without_parent(node)
194
+ results.each {|c| c.tabs += node.tabs if bubblable?(c)}
195
+ if !results.empty? && bubblable?(results.last)
196
+ results.last.group_end = node.group_end
197
+ end
198
+ return results
199
+ end
200
+
201
+ # If this @at-root excludes the immediate parent, return it as-is so that it
202
+ # can be bubbled up by the parent node.
203
+ return Bubble.new(node) if node.exclude_node?(parent)
204
+
205
+ # Otherwise, duplicate the current parent and move it into the @at-root
206
+ # node. As above, returning an @at-root node signals to the parent directive
207
+ # that it should be bubbled upwards.
208
+ bubble(node)
209
+ end
210
+
211
+ # The following directives are visible and have children. This means they need
212
+ # to be able to handle bubbling up nodes such as @at-root and @media.
213
+
220
214
  # Updates the indentation of the rule node based on the nesting
221
215
  # level. The selectors were resolved in {Perform}.
222
216
  def visit_rule(node)
223
217
  yield
224
218
 
225
- rules = node.children.select {|c| c.is_a?(Sass::Tree::RuleNode) || c.bubbles?}
226
- props = node.children.reject {|c| c.is_a?(Sass::Tree::RuleNode) || c.bubbles? || c.invisible?}
227
-
228
- rules.map {|c| c.is_a?(Sass::Tree::AtRootNode) ? visit(c) : c}.flatten
219
+ rules = node.children.select {|c| bubblable?(c)}
220
+ props = node.children.reject {|c| bubblable?(c) || c.invisible?}
229
221
 
230
222
  unless props.empty?
231
223
  node.children = props
@@ -233,37 +225,145 @@ class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
233
225
  rules.unshift(node)
234
226
  end
235
227
 
236
- rules.last.group_end = true unless parent.is_a?(Sass::Tree::RuleNode) || rules.empty?
237
-
228
+ rules = debubble(rules)
229
+ unless parent.is_a?(Sass::Tree::RuleNode) || rules.empty? || !bubblable?(rules.last)
230
+ rules.last.group_end = true
231
+ end
238
232
  rules
239
233
  end
240
234
 
241
- def visit_atroot(node)
242
- if @parent_directives.any? {|n| node.exclude_node?(n)}
243
- return node if node.exclude_node?(parent)
235
+ # Bubbles a directive up through RuleNodes.
236
+ def visit_directive(node)
237
+ return node unless node.has_children
238
+ return bubble(node) if parent.is_a?(Sass::Tree::RuleNode)
239
+
240
+ yield
241
+
242
+ # Since we don't know if the mere presence of an unknown directive may be
243
+ # important, we should keep an empty version around even if all the contents
244
+ # are removed via @at-root. However, if the contents are just bubbled out,
245
+ # we don't need to do so.
246
+ directive_exists = node.children.any? do |child|
247
+ next true unless child.is_a?(Bubble)
248
+ next false unless child.node.is_a?(Sass::Tree::DirectiveNode)
249
+ child.node.resolved_value == node.resolved_value
250
+ end
251
+
252
+ if directive_exists
253
+ []
254
+ else
255
+ empty_node = node.dup
256
+ empty_node.children = []
257
+ [empty_node]
258
+ end + debubble(node.children, node)
259
+ end
260
+
261
+ # Bubbles the `@media` directive up through RuleNodes
262
+ # and merges it with other `@media` directives.
263
+ def visit_media(node)
264
+ return bubble(node) if parent.is_a?(Sass::Tree::RuleNode)
265
+ return Bubble.new(node) if parent.is_a?(Sass::Tree::MediaNode)
266
+
267
+ yield
244
268
 
245
- new_rule = parent.dup
246
- new_rule.children = node.children
247
- node.children = [new_rule]
248
- return node
269
+ debubble(node.children, node) do |child|
270
+ next child unless child.is_a?(Sass::Tree::MediaNode)
271
+ # Copies of `node` can be bubbled, and we don't want to merge it with its
272
+ # own query.
273
+ next child if child.resolved_query == node.resolved_query
274
+ next child if child.resolved_query = child.resolved_query.merge(node.resolved_query)
249
275
  end
276
+ end
277
+
278
+ # Bubbles the `@supports` directive up through RuleNodes.
279
+ def visit_supports(node)
280
+ return node unless node.has_children
281
+ return bubble(node) if parent.is_a?(Sass::Tree::RuleNode)
250
282
 
251
- results = visit_children_without_parent(node)
252
- results.each {|n| n.tabs += node.tabs}
253
- results.last.group_end = node.group_end unless results.empty?
254
- results
283
+ yield
284
+
285
+ debubble(node.children, node)
255
286
  end
256
287
 
257
288
  private
258
289
 
290
+ # "Bubbles" `node` one level by copying the parent and wrapping `node`'s
291
+ # children with it.
292
+ #
293
+ # @param node [Sass::Tree::Node].
294
+ # @return [Bubble]
259
295
  def bubble(node)
260
- return unless parent.is_a?(Sass::Tree::RuleNode)
261
296
  new_rule = parent.dup
262
297
  new_rule.children = node.children
263
- node.children = with_parent(node) {Array(visit(new_rule))}
264
- # If the last child is actually the end of the group,
265
- # the parent's cssize will set it properly
266
- node.children.last.group_end = false unless node.children.empty?
267
- true
298
+ node.children = [new_rule]
299
+ Bubble.new(node)
300
+ end
301
+
302
+ # Pops all bubbles in `children` and intersperses the results with the other
303
+ # values.
304
+ #
305
+ # If `parent` is passed, it's copied and used as the parent node for the
306
+ # nested portions of `children`.
307
+ #
308
+ # @param children [List<Sass::Tree::Node, Bubble>]
309
+ # @param parent [Sass::Tree::Node]
310
+ # @yield [node] An optional block for processing bubbled nodes. Each bubbled
311
+ # node will be passed to this block.
312
+ # @yieldparam node [Sass::Tree::Node] A bubbled node.
313
+ # @yieldreturn [Sass::Tree::Node?] A node to use in place of the bubbled node.
314
+ # This can be the node itself, or `nil` to indicate that the node should be
315
+ # omitted.
316
+ # @return [List<Sass::Tree::Node, Bubble>]
317
+ def debubble(children, parent = nil)
318
+ Sass::Util.slice_by(children) {|c| c.is_a?(Bubble)}.map do |(is_bubble, slice)|
319
+ unless is_bubble
320
+ next slice unless parent
321
+ new_parent = parent.dup
322
+ new_parent.children = slice
323
+ next new_parent
324
+ end
325
+
326
+ next slice.map do |bubble|
327
+ next unless (node = block_given? ? yield(bubble.node) : bubble.node)
328
+ node.tabs += bubble.tabs
329
+ node.group_end = bubble.group_end
330
+ [visit(node)].flatten
331
+ end.compact
332
+ end.flatten
333
+ end
334
+
335
+ # Returns whether or not a node can be bubbled up through the syntax tree.
336
+ #
337
+ # @param node [Sass::Tree::Node]
338
+ # @return [Boolean]
339
+ def bubblable?(node)
340
+ node.is_a?(Sass::Tree::RuleNode) || node.bubbles?
341
+ end
342
+
343
+ # A wrapper class for a node that indicates to the parent that it should
344
+ # treat the wrapped node as a sibling rather than a child.
345
+ #
346
+ # Nodes should be wrapped before they're passed to \{Cssize.visit}. They will
347
+ # be automatically visited upon calling \{#pop}.
348
+ #
349
+ # This duck types as a [Sass::Tree::Node] for the purposes of
350
+ # tree-manipulation operations.
351
+ class Bubble
352
+ attr_accessor :node
353
+ attr_accessor :tabs
354
+ attr_accessor :group_end
355
+
356
+ def initialize(node)
357
+ @node = node
358
+ @tabs = 0
359
+ end
360
+
361
+ def bubbles?
362
+ true
363
+ end
364
+
365
+ def inspect
366
+ "(Bubble #{node.inspect})"
367
+ end
268
368
  end
269
369
  end