File: cssize.rb

package info (click to toggle)
ruby-sass 3.7.4-6
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,396 kB
  • sloc: ruby: 32,443; sh: 26; makefile: 25
file content (362 lines) | stat: -rw-r--r-- 11,476 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# A visitor for converting a static Sass tree into a static CSS tree.
class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base
  # @param root [Tree::Node] The root node of the tree to visit.
  # @return [(Tree::Node, Sass::Util::SubsetMap)] The resulting tree of static nodes
  #   *and* the extensions defined for this tree
  def self.visit(root); super; end

  protected

  # Returns the immediate parent of the current node.
  # @return [Tree::Node]
  def parent
    @parents.last
  end

  def initialize
    @parents = []
    @extends = Sass::Util::SubsetMap.new
  end

  # If an exception is raised, this adds proper metadata to the backtrace.
  def visit(node)
    super(node)
  rescue Sass::SyntaxError => e
    e.modify_backtrace(:filename => node.filename, :line => node.line)
    raise e
  end

  # Keeps track of the current parent node.
  def visit_children(parent)
    with_parent parent do
      parent.children = visit_children_without_parent(parent)
      parent
    end
  end

  # Like {#visit\_children}, but doesn't set {#parent}.
  #
  # @param node [Sass::Tree::Node]
  # @return [Array<Sass::Tree::Node>] the flattened results of
  #   visiting all the children of `node`
  def visit_children_without_parent(node)
    node.children.map {|c| visit(c)}.flatten
  end

  # Runs a block of code with the current parent node
  # replaced with the given node.
  #
  # @param parent [Tree::Node] The new parent for the duration of the block.
  # @yield A block in which the parent is set to `parent`.
  # @return [Object] The return value of the block.
  def with_parent(parent)
    @parents.push parent
    yield
  ensure
    @parents.pop
  end

  # Converts the entire document to CSS.
  #
  # @return [(Tree::Node, Sass::Util::SubsetMap)] The resulting tree of static nodes
  #   *and* the extensions defined for this tree
  def visit_root(node)
    yield

    if parent.nil?
      imports_to_move = []
      import_limit = nil
      i = -1
      node.children.reject! do |n|
        i += 1
        if import_limit
          next false unless n.is_a?(Sass::Tree::CssImportNode)
          imports_to_move << n
          next true
        end

        if !n.is_a?(Sass::Tree::CommentNode) &&
            !n.is_a?(Sass::Tree::CharsetNode) &&
            !n.is_a?(Sass::Tree::CssImportNode)
          import_limit = i
        end

        false
      end

      if import_limit
        node.children = node.children[0...import_limit] + imports_to_move +
          node.children[import_limit..-1]
      end
    end

    return node, @extends
  rescue Sass::SyntaxError => e
    e.sass_template ||= node.template
    raise e
  end

  # A simple struct wrapping up information about a single `@extend` instance. A
  # single {ExtendNode} can have multiple Extends if either the parent node or
  # the extended selector is a comma sequence.
  #
  # @attr extender [Sass::Selector::Sequence]
  #   The selector of the CSS rule containing the `@extend`.
  # @attr target [Array<Sass::Selector::Simple>] The selector being `@extend`ed.
  # @attr node [Sass::Tree::ExtendNode] The node that produced this extend.
  # @attr directives [Array<Sass::Tree::DirectiveNode>]
  #   The directives containing the `@extend`.
  # @attr success [Boolean]
  #   Whether this extend successfully matched a selector.
  Extend = Struct.new(:extender, :target, :node, :directives, :success)

  # Registers an extension in the `@extends` subset map.
  def visit_extend(node)
    parent.resolved_rules.populate_extends(@extends, node.resolved_selector, node,
      @parents.select {|p| p.is_a?(Sass::Tree::DirectiveNode)})
    []
  end

  # Modifies exception backtraces to include the imported file.
  def visit_import(node)
    visit_children_without_parent(node)
  rescue Sass::SyntaxError => e
    e.modify_backtrace(:filename => node.children.first.filename)
    e.add_backtrace(:filename => node.filename, :line => node.line)
    raise e
  end

  # Asserts that all the traced children are valid in their new location.
  def visit_trace(node)
    visit_children_without_parent(node)
  rescue Sass::SyntaxError => e
    e.modify_backtrace(:mixin => node.name, :filename => node.filename, :line => node.line)
    e.add_backtrace(:filename => node.filename, :line => node.line)
    raise e
  end

  # Converts nested properties into flat properties
  # and updates the indentation of the prop node based on the nesting level.
  def visit_prop(node)
    if parent.is_a?(Sass::Tree::PropNode)
      node.resolved_name = "#{parent.resolved_name}-#{node.resolved_name}"
      node.tabs = parent.tabs + (parent.resolved_value.empty? ? 0 : 1) if node.style == :nested
    end

    yield

    result = node.children.dup
    if !node.resolved_value.empty? || node.children.empty?
      node.send(:check!)
      result.unshift(node)
    end

    result
  end

  def visit_atroot(node)
    # If there aren't any more directives or rules that this @at-root needs to
    # exclude, we can get rid of it and just evaluate the children.
    if @parents.none? {|n| node.exclude_node?(n)}
      results = visit_children_without_parent(node)
      results.each {|c| c.tabs += node.tabs if bubblable?(c)}
      if !results.empty? && bubblable?(results.last)
        results.last.group_end = node.group_end
      end
      return results
    end

    # If this @at-root excludes the immediate parent, return it as-is so that it
    # can be bubbled up by the parent node.
    return Bubble.new(node) if node.exclude_node?(parent)

    # Otherwise, duplicate the current parent and move it into the @at-root
    # node. As above, returning an @at-root node signals to the parent directive
    # that it should be bubbled upwards.
    bubble(node)
  end

  # The following directives are visible and have children. This means they need
  # to be able to handle bubbling up nodes such as @at-root and @media.

  # Updates the indentation of the rule node based on the nesting
  # level. The selectors were resolved in {Perform}.
  def visit_rule(node)
    yield

    rules = node.children.select {|c| bubblable?(c)}
    props = node.children.reject {|c| bubblable?(c) || c.invisible?}

    unless props.empty?
      node.children = props
      rules.each {|r| r.tabs += 1} if node.style == :nested
      rules.unshift(node)
    end

    rules = debubble(rules)
    unless parent.is_a?(Sass::Tree::RuleNode) || rules.empty? || !bubblable?(rules.last)
      rules.last.group_end = true
    end
    rules
  end

  def visit_keyframerule(node)
    return node unless node.has_children

    yield

    debubble(node.children, node)
  end

  # Bubbles a directive up through RuleNodes.
  def visit_directive(node)
    return node unless node.has_children
    if parent.is_a?(Sass::Tree::RuleNode)
      # @keyframes shouldn't include the rule nodes, so we manually create a
      # bubble that doesn't have the parent's contents for them.
      return node.normalized_name == '@keyframes' ? Bubble.new(node) : bubble(node)
    end

    yield

    # Since we don't know if the mere presence of an unknown directive may be
    # important, we should keep an empty version around even if all the contents
    # are removed via @at-root. However, if the contents are just bubbled out,
    # we don't need to do so.
    directive_exists = node.children.any? do |child|
      next true unless child.is_a?(Bubble)
      next false unless child.node.is_a?(Sass::Tree::DirectiveNode)
      child.node.resolved_value == node.resolved_value
    end

    # We know empty @keyframes directives do nothing.
    if directive_exists || node.name == '@keyframes'
      []
    else
      empty_node = node.dup
      empty_node.children = []
      [empty_node]
    end + debubble(node.children, node)
  end

  # Bubbles the `@media` directive up through RuleNodes
  # and merges it with other `@media` directives.
  def visit_media(node)
    return bubble(node) if parent.is_a?(Sass::Tree::RuleNode)
    return Bubble.new(node) if parent.is_a?(Sass::Tree::MediaNode)

    yield

    debubble(node.children, node) do |child|
      next child unless child.is_a?(Sass::Tree::MediaNode)
      # Copies of `node` can be bubbled, and we don't want to merge it with its
      # own query.
      next child if child.resolved_query == node.resolved_query
      next child if child.resolved_query = child.resolved_query.merge(node.resolved_query)
    end
  end

  # Bubbles the `@supports` directive up through RuleNodes.
  def visit_supports(node)
    return node unless node.has_children
    return bubble(node) if parent.is_a?(Sass::Tree::RuleNode)

    yield

    debubble(node.children, node)
  end

  private

  # "Bubbles" `node` one level by copying the parent and wrapping `node`'s
  # children with it.
  #
  # @param node [Sass::Tree::Node].
  # @return [Bubble]
  def bubble(node)
    new_rule = parent.dup
    new_rule.children = node.children
    node.children = [new_rule]
    Bubble.new(node)
  end

  # Pops all bubbles in `children` and intersperses the results with the other
  # values.
  #
  # If `parent` is passed, it's copied and used as the parent node for the
  # nested portions of `children`.
  #
  # @param children [List<Sass::Tree::Node, Bubble>]
  # @param parent [Sass::Tree::Node]
  # @yield [node] An optional block for processing bubbled nodes. Each bubbled
  #   node will be passed to this block.
  # @yieldparam node [Sass::Tree::Node] A bubbled node.
  # @yieldreturn [Sass::Tree::Node?] A node to use in place of the bubbled node.
  #   This can be the node itself, or `nil` to indicate that the node should be
  #   omitted.
  # @return [List<Sass::Tree::Node, Bubble>]
  def debubble(children, parent = nil)
    # Keep track of the previous parent so that we don't divide `parent`
    # unnecessarily if the `@at-root` doesn't produce any new nodes (e.g.
    # `@at-root {@extend %foo}`).
    previous_parent = nil

    Sass::Util.slice_by(children) {|c| c.is_a?(Bubble)}.map do |(is_bubble, slice)|
      unless is_bubble
        next slice unless parent
        if previous_parent
          previous_parent.children.push(*slice)
          next []
        else
          previous_parent = new_parent = parent.dup
          new_parent.children = slice
          next new_parent
        end
      end

      slice.map do |bubble|
        next unless (node = block_given? ? yield(bubble.node) : bubble.node)
        node.tabs += bubble.tabs
        node.group_end = bubble.group_end
        results = [visit(node)].flatten
        previous_parent = nil unless results.empty?
        results
      end.compact
    end.flatten
  end

  # Returns whether or not a node can be bubbled up through the syntax tree.
  #
  # @param node [Sass::Tree::Node]
  # @return [Boolean]
  def bubblable?(node)
    node.is_a?(Sass::Tree::RuleNode) || node.bubbles?
  end

  # A wrapper class for a node that indicates to the parent that it should
  # treat the wrapped node as a sibling rather than a child.
  #
  # Nodes should be wrapped before they're passed to \{Cssize.visit}. They will
  # be automatically visited upon calling \{#pop}.
  #
  # This duck types as a [Sass::Tree::Node] for the purposes of
  # tree-manipulation operations.
  class Bubble
    attr_accessor :node
    attr_accessor :tabs
    attr_accessor :group_end

    def initialize(node)
      @node = node
      @tabs = 0
    end

    def bubbles?
      true
    end

    def inspect
      "(Bubble #{node.inspect})"
    end
  end
end