lib/sass/tree/node.rb in haml-2.2.24 vs lib/sass/tree/node.rb in haml-3.0.0.beta.1

- old
+ new

@@ -1,30 +1,47 @@ module Sass # A namespace for nodes in the Sass parse tree. # - # The Sass parse tree has two states. - # When it's first parsed, it has nodes for mixin definitions - # and for loops and so forth, + # The Sass parse tree has three states: dynamic, static Sass, and static CSS. + # + # When it's first parsed, a Sass document is in the dynamic state. + # It has nodes for mixin definitions and `@for` loops and so forth, # in addition to nodes for CSS rules and properties. + # Nodes that only appear in this state are called **dynamic nodes**. # - # However, {Tree::Node#perform} returns a different sort of tree. - # This tree maps more closely to the resulting CSS document - # than it does to the original Sass document. - # It still has nodes for CSS rules and properties, + # {Tree::Node#perform} returns a static Sass tree, which is different. + # It still has nodes for CSS rules and properties # but it doesn't have any dynamic-generation-related nodes. + # The nodes in this state are in the same structure as the Sass document: + # rules and properties are nested beneath one another. + # Nodes that can be in this state or in the dynamic state + # are called **static nodes**. # - # Nodes that only appear in the pre-perform state are called **dynamic nodes**; - # those that appear in both states are called **static nodes**. + # {Tree::Node#cssize} then returns a static CSS tree. + # This is like a static Sass tree, + # but the structure exactly mirrors that of the generated CSS. + # Rules and properties can't be nested beneath one another in this state. + # + # Finally, {Tree::Node#to_s} can be called on a static CSS tree + # to get the actual CSS code as a string. module Tree - # This class doubles as the root node of the parse tree - # and the superclass of all other parse-tree nodes. + # The abstract superclass of all parse-tree nodes. class Node + include Enumerable + # The child nodes of this node. # # @return [Array<Tree::Node>] attr_accessor :children + # Whether or not this node has child nodes. + # This may be true even when \{#children} is empty, + # in which case this node has an empty block (e.g. `{}`). + # + # @return [Boolean] + attr_accessor :has_children + # The line of the document on which this node appeared. # # @return [Fixnum] attr_accessor :line @@ -50,10 +67,16 @@ def options=(options) children.each {|c| c.options = options} @options = options end + # @private + def children=(children) + self.has_children ||= !children.empty? + @children = children + end + # The name of the document on which this node appeared. # # @return [String] def filename @filename || (@options && @options[:filename]) @@ -63,25 +86,18 @@ # # @param child [Tree::Node] The child node # @raise [Sass::SyntaxError] if `child` is invalid # @see #invalid_child? def <<(child) + return if child.nil? if msg = invalid_child?(child) - raise Sass::SyntaxError.new(msg, child.line) + raise Sass::SyntaxError.new(msg, :line => child.line) end + self.has_children = true @children << child end - # Return the last child node. - # - # We need this because {Tree::Node} duck types as an Array for {Sass::Engine}. - # - # @return [Tree::Node] The last child node - def last - children.last - end - # Compares this node and another object (only other {Tree::Node}s will be equal). # This does a structural comparison; # if the contents of the nodes and all the child nodes are equivalent, # then the nodes are as well. # @@ -98,78 +114,154 @@ # Runs the dynamic Sass code *and* computes the CSS for the tree. # # @see #perform # @see #to_s def render - perform(Environment.new).to_s + perform(Environment.new).cssize.to_s end # True if \{#to\_s} will return `nil`; # that is, if the node shouldn't be rendered. # Should only be called in a static tree. # # @return [Boolean] def invisible?; false; end - # Computes the CSS corresponding to this Sass tree. + # The output style. See {file:SASS_REFERENCE.md#sass_options the Sass options documentation}. # + # @return [Symbol] + def style + @options[:style] + end + + # Computes the CSS corresponding to this static CSS tree. + # + # \{#to_s} shouldn't be overridden directly; instead, override \{#\_to\_s}. # Only static-node subclasses need to implement \{#to\_s}. # # This may return `nil`, but it will only do so if \{#invisible?} is true. # + # @param args [Array] Passed on to \{#\_to\_s} # @return [String, nil] The resulting CSS + # @see Sass::Tree + def to_s(*args) + _to_s(*args) + rescue Sass::SyntaxError => e + e.modify_backtrace(:filename => filename, :line => line) + raise e + end + + # Converts a static Sass tree (e.g. the output of \{#perform}) + # into a static CSS tree. + # + # \{#cssize} shouldn't be overridden directly; + # instead, override \{#\_cssize} or \{#cssize!}. + # + # @param parent [Node, nil] The parent node of this node. + # This should only be non-nil if the parent is the same class as this node + # @return [Tree::Node] The resulting tree of static nodes # @raise [Sass::SyntaxError] if some element of the tree is invalid # @see Sass::Tree - def to_s - result = String.new - children.each do |child| - if child.is_a? PropNode - message = "Properties aren't allowed at the root of a document." + - child.pseudo_class_selector_message - raise Sass::SyntaxError.new(message, child.line) - else - next if child.invisible? - child_str = child.to_s(1) - result << child_str + (style == :compressed ? '' : "\n") - end - end - result.rstrip! - return "" if result.empty? - return result + "\n" - rescue Sass::SyntaxError => e; e.add_metadata(filename, line) + def cssize(parent = nil) + _cssize((parent if parent.class == self.class)) + rescue Sass::SyntaxError => e + e.modify_backtrace(:filename => filename, :line => line) + raise e end - # Runs the dynamic Sass code: + # Converts a dynamic tree into a static Sass tree. + # That is, runs the dynamic Sass code: # mixins, variables, control directives, and so forth. # This doesn't modify this node or any of its children. # # \{#perform} shouldn't be overridden directly; - # if you want to return a new node (or list of nodes), - # override \{#\_perform}; - # if you want to destructively modify this node, - # override \{#perform!}. + # instead, override \{#\_perform} or \{#perform!}. # # @param environment [Sass::Environment] The lexical environment containing # variable and mixin values # @return [Tree::Node] The resulting tree of static nodes # @raise [Sass::SyntaxError] if some element of the tree is invalid # @see Sass::Tree def perform(environment) - environment.options = @options if self.class == Tree::Node _perform(environment) - rescue Sass::SyntaxError => e; e.add_metadata(filename, line) + rescue Sass::SyntaxError => e + e.modify_backtrace(:filename => filename, :line => line) + raise e end - # The output style. See {file:SASS_REFERENCE.md#sass_options the Sass options documentation}. + # Iterates through each node in the tree rooted at this node + # in a pre-order walk. # - # @return [Symbol] - def style - @options[:style] + # @yield node + # @yieldparam node [Node] a node in the tree + def each(&block) + yield self + children.each {|c| c.each(&block)} end + # Converts a node to Sass code that will generate it. + # + # @param tabs [Fixnum] The amount of tabulation to use for the Sass code + # @param opts [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}) + # @return [String] The Sass code corresponding to the node + def to_sass(tabs = 0, opts = {}) + to_src(tabs, opts, :sass) + end + + # Converts a node to SCSS code that will generate it. + # + # @param tabs [Fixnum] The amount of tabulation to use for the SCSS code + # @param opts [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}) + # @return [String] The Sass code corresponding to the node + def to_scss(tabs = 0, opts = {}) + to_src(tabs, opts, :scss) + end + protected + # Computes the CSS corresponding to this particular Sass node. + # + # This method should never raise {Sass::SyntaxError}s. + # Such errors will not be properly annotated with Sass backtrace information. + # All error conditions should be checked in earlier transformations, + # such as \{#cssize} and \{#perform}. + # + # @param args [Array] ignored + # @return [String, nil] The resulting CSS + # @see #to_s + # @see Sass::Tree + def _to_s + raise NotImplementedError.new("All static-node subclasses of Sass::Tree::Node must override #_to_s or #to_s.") + end + + # Converts this static Sass node into a static CSS node, + # returning the new node. + # This doesn't modify this node or any of its children. + # + # @param parent [Node, nil] The parent node of this node. + # This should only be non-nil if the parent is the same class as this node + # @return [Tree::Node, Array<Tree::Node>] The resulting static CSS nodes + # @raise [Sass::SyntaxError] if some element of the tree is invalid + # @see #cssize + # @see Sass::Tree + def _cssize(parent) + node = dup + node.cssize!(parent) + node + end + + # Destructively converts this static Sass node into a static CSS node. + # This *does* modify this node, + # but will be run non-destructively by \{#\_cssize\}. + # + # @param parent [Node, nil] The parent node of this node. + # This should only be non-nil if the parent is the same class as this node + # @see #cssize + def cssize!(parent) + self.children = children.map {|c| c.cssize(self)}.flatten + end + # Runs any dynamic Sass code in this particular node. # This doesn't modify this node or any of its children. # # @param environment [Sass::Environment] The lexical environment containing # variable and mixin values @@ -200,52 +292,87 @@ # @return [Array<Tree::Node>] The resulting static nodes def perform_children(environment) children.map {|c| c.perform(environment)}.flatten end - # Replaces SassScript in a chunk of text (via `#{}`) + # Replaces SassScript in a chunk of text # with the resulting value. # - # @param text [String] The text to interpolate + # @param text [Array<String, Sass::Script::Node>] The text to interpolate # @param environment [Sass::Environment] The lexical environment containing # variable and mixin values # @return [String] The interpolated text - def interpolate(text, environment) - res = '' - rest = Haml::Shared.handle_interpolation text do |scan| - escapes = scan[2].size - res << scan.matched[0...-2 - escapes] - if escapes % 2 == 1 - res << "\\" * (escapes - 1) << '#{' - else - res << "\\" * [0, escapes - 1].max - res << Script::Parser.new(scan, line, scan.pos - scan.matched_size, filename). - parse_interpolated.perform(environment).to_s - end - end - res + rest + def run_interp(text, environment) + text.map do |r| + next r if r.is_a?(String) + val = r.perform(environment) + # Interpolated strings should never render with quotes + next val.value if val.is_a?(Sass::Script::String) + val.to_s + end.join.strip end # @see Haml::Shared.balance # @raise [Sass::SyntaxError] if the brackets aren't balanced def balance(*args) res = Haml::Shared.balance(*args) return res if res - raise Sass::SyntaxError.new("Unbalanced brackets.", line) + raise Sass::SyntaxError.new("Unbalanced brackets.", :line => line) end # Returns an error message if the given child node is invalid, # and false otherwise. # - # By default, all child nodes are valid. + # By default, all child nodes except those only allowed at root level + # ({Tree::MixinDefNode}, {Tree::ImportNode}) are valid. # This is expected to be overriden by subclasses # for which some children are invalid. # # @param child [Tree::Node] A potential child node # @return [Boolean, String] Whether or not the child node is valid, # as well as the error message to display if it is invalid def invalid_child?(child) - false + case child + when Tree::MixinDefNode + "Mixins may only be defined at the root of a document." + when Tree::ImportNode + "Import directives may only be used at the root of a document." + end + end + + # Converts a node to Sass or SCSS code that will generate it. + # + # This method is called by the default \{#to\_sass} and \{#to\_scss} methods, + # so that the same code can be used for both with minor variations. + # + # @param tabs [Fixnum] The amount of tabulation to use for the SCSS code + # @param opts [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}) + # @param fmt [Symbol] `:sass` or `:scss` + # @return [String] The Sass or SCSS code corresponding to the node + def to_src(tabs, opts, fmt) + raise NotImplementedError.new("All static-node subclasses of Sass::Tree::Node must override #to_#{fmt}.") + end + + # Converts the children of this node to a Sass or SCSS string. + # This will return the trailing newline for the previous line, + # including brackets if this is SCSS. + # + # @param tabs [Fixnum] The amount of tabulation to use for the Sass code + # @param opts [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}) + # @param fmt [Symbol] `:sass` or `:scss` + # @return [String] The Sass or CSS code corresponding to the children + def children_to_src(tabs, opts, fmt) + (fmt == :sass ? "\n" : " {\n") + + children.map {|c| c.send("to_#{fmt}", tabs + 1, opts)}.join.rstrip + + (fmt == :sass ? "\n" : " }\n") + end + + # Returns a semicolon if this is SCSS, or an empty string if this is Sass. + # + # @param fmt [Symbol] `:sass` or `:scss` + # @return [String] A semicolon or the empty string + def semi(fmt) + fmt == :sass ? "" : ";" end end end end