| # 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 |