# 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] 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 # In Ruby 1.8, ensures that there's only one `@charset` directive # and that it's at the top of the document. # # @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? # In Ruby 1.9 we can make all @charset nodes invisible # and infer the final @charset from the encoding of the final string. if Sass::Util.ruby1_8? charset = node.children.find {|c| c.is_a?(Sass::Tree::CharsetNode)} node.children.reject! {|c| c.is_a?(Sass::Tree::CharsetNode)} node.children.unshift charset if charset end 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] The selector being `@extend`ed. # @attr node [Sass::Tree::ExtendNode] The node that produced this extend. # @attr directives [Array] # The directives containing the `@extend`. # @attr result [Symbol] # The result of this extend. One of `:not_found` (the target doesn't exist # in the document), `:failed_to_unify` (the target exists but cannot be # unified with the extender), or `:succeeded`. Extend = Struct.new(:extender, :target, :node, :directives, :result) # Registers an extension in the `@extends` subset map. def visit_extend(node) node.resolved_selector.members.each do |seq| if seq.members.size > 1 raise Sass::SyntaxError.new("Can't extend #{seq.to_a.join}: can't extend nested selectors") end sseq = seq.members.first if !sseq.is_a?(Sass::Selector::SimpleSequence) raise Sass::SyntaxError.new("Can't extend #{seq.to_a.join}: invalid selector") elsif sseq.members.any? {|ss| ss.is_a?(Sass::Selector::Parent)} raise Sass::SyntaxError.new("Can't extend #{seq.to_a.join}: can't extend parent selectors") end sel = sseq.members parent.resolved_rules.members.each do |member| unless member.members.last.is_a?(Sass::Selector::SimpleSequence) raise Sass::SyntaxError.new("#{member} can't extend: invalid selector") end parent_directives = @parents.select {|p| p.is_a?(Sass::Tree::DirectiveNode)} @extends[sel] = Extend.new(member, sel, node, parent_directives, :not_found) end end [] 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 # Bubbles a directive up through RuleNodes. def visit_directive(node) return node unless node.has_children return bubble(node) if parent.is_a?(Sass::Tree::RuleNode) 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 if directive_exists [] 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] # @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] 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