From 88c4619bdd6c5afa47e8202d8759ba3bd431d5a4 Mon Sep 17 00:00:00 2001 From: Nathan Weizenbaum Date: Mon, 13 Oct 2008 09:49:35 -0700 Subject: [PATCH] Compile Sass to a full-fledged AST before evaluating and printing. --- lib/sass/css.rb | 6 -- lib/sass/engine.rb | 98 +++++++-------------------------- lib/sass/script.rb | 8 +-- lib/sass/tree/attr_node.rb | 29 ++++++---- lib/sass/tree/comment_node.rb | 15 ++++- lib/sass/tree/directive_node.rb | 9 ++- lib/sass/tree/for_node.rb | 26 +++++++++ lib/sass/tree/if_node.rb | 16 ++++++ lib/sass/tree/mixin_node.rb | 26 +++++++++ lib/sass/tree/node.rb | 48 +++++++++++++++- lib/sass/tree/rule_node.rb | 20 +++++-- lib/sass/tree/value_node.rb | 16 ------ lib/sass/tree/while_node.rb | 20 +++++++ test/sass/engine_test.rb | 5 +- 14 files changed, 212 insertions(+), 130 deletions(-) create mode 100644 lib/sass/tree/for_node.rb create mode 100644 lib/sass/tree/if_node.rb create mode 100644 lib/sass/tree/mixin_node.rb delete mode 100644 lib/sass/tree/value_node.rb create mode 100644 lib/sass/tree/while_node.rb diff --git a/lib/sass/css.rb b/lib/sass/css.rb index ab98899f..7dc86f86 100644 --- a/lib/sass/css.rb +++ b/lib/sass/css.rb @@ -17,12 +17,6 @@ module Sass end end - class ValueNode - def to_sass(tabs, opts = {}) - "#{value}\n" - end - end - class RuleNode def to_sass(tabs, opts = {}) str = "\n#{' ' * tabs}#{rule}#{children.any? { |c| c.is_a? AttrNode } ? "\n" : ''}" diff --git a/lib/sass/engine.rb b/lib/sass/engine.rb index 1d1997a4..14dc95b4 100644 --- a/lib/sass/engine.rb +++ b/lib/sass/engine.rb @@ -6,6 +6,10 @@ require 'sass/tree/rule_node' require 'sass/tree/comment_node' require 'sass/tree/attr_node' require 'sass/tree/directive_node' +require 'sass/tree/mixin_node' +require 'sass/tree/if_node' +require 'sass/tree/while_node' +require 'sass/tree/for_node' require 'sass/script' require 'sass/error' require 'haml/shared' @@ -21,7 +25,7 @@ module Sass # puts output class Engine Line = Struct.new(:text, :tabs, :index, :filename, :children) - Mixin = Struct.new(:args, :tree) + Mixin = Struct.new(:name, :args, :tree) # The character that begins a CSS attribute. ATTRIBUTE_CHAR = ?: @@ -115,7 +119,7 @@ module Sass def render_to_tree root = Tree::Node.new(@options) append_children(root, tree(tabulate(@template)).first, true) - root + root.perform(@environment) end private @@ -260,7 +264,7 @@ END if line.text =~ ATTRIBUTE_ALTERNATE_MATCHER parse_attribute(line.text, ATTRIBUTE_ALTERNATE) else - Tree::RuleNode.new(interpolate(line.text), @options) + Tree::RuleNode.new(line.text, @options) end end end @@ -280,11 +284,8 @@ END raise SyntaxError.new("Invalid attribute: \"#{line}\".", @line) end - if eq.strip[0] == SCRIPT_CHAR - value = Script.resolve(value, @environment, @line) - end - - Tree::AttrNode.new(interpolate(name), interpolate(value), @options) + expr = (eq.strip[0] == SCRIPT_CHAR) ? Script.parse(value, @line) : value + Tree::AttrNode.new(name, expr, @options) end def parse_variable(line) @@ -292,7 +293,7 @@ END raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath variable declarations.", @line + 1) unless line.children.empty? raise SyntaxError.new("Invalid variable: \"#{line.text}\".", @line) unless name && value - var = Script.parse(value, @environment, @line) + var = Script.parse(value, @line).perform(@environment) if op == '||=' @environment[name] ||= var else @@ -320,25 +321,17 @@ END if directive == "import" && value !~ /^(url\(|")/ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.", @line + 1) unless line.children.empty? import(value) - elsif directive == "if" - parse_if(line, root, value) elsif directive == "for" parse_for(line, root, value) elsif directive == "while" - parse_while(line, root, value) + Tree::WhileNode.new(Script.parse(value, line.index), @options) + elsif directive == "if" + Tree::IfNode.new(Script.parse(value, line.index), @options) else Tree::DirectiveNode.new(line.text, @options) end end - def parse_if(line, root, text) - if Script.parse(text, @environment, line.index).to_bool - append_children([], line.children, root) - else - [] - end - end - def parse_for(line, root, text) var, from_expr, to_name, to_expr = text.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first @@ -354,26 +347,8 @@ END end raise SyntaxError.new("Invalid variable \"#{var}\".", @line) unless var =~ Script::VALIDATE - from = Script.parse(from_expr, @environment, @line).to_i - to = Script.parse(to_expr, @environment, @line).to_i - range = Range.new(from, to, to_name == 'to') - - tree = [] - old_env = @environment.dup - for i in range - @environment[var[1..-1]] = Script::Number.new(i) - append_children(tree, line.children, root) - end - @environment = old_env - tree - end - - def parse_while(line, root, text) - tree = [] - while Script.parse(text, @environment, line.index).to_bool - append_children(tree, line.children, root) - end - tree + Tree::ForNode.new(var[1..-1], Script.parse(from_expr, @line), Script.parse(to_expr, @line), + to_name == 'to', @options) end # parses out the arguments between the commas and cleans up the mixin arguments @@ -402,10 +377,11 @@ END default_arg_found ||= default raise SyntaxError.new("Invalid variable \"#{arg}\".", @line) unless arg =~ Script::VALIDATE raise SyntaxError.new("Required arguments must not follow optional arguments \"#{arg}\".", @line) if default_arg_found && !default - default = Script.parse(default, @environment, @line) if default + default = Script.parse(default, @line).perform(@environment) if default { :name => arg[1..-1], :default_value => default } end - mixin = @mixins[name] = Mixin.new(args, line.children) + mixin = @mixins[name] = Mixin.new(name, args, []) + append_children(mixin.tree, line.children, false) :mixin end @@ -422,43 +398,7 @@ Mixin #{name} takes #{mixin.args.size} argument#{'s' if mixin.args.size != 1} but #{args.size} #{args.size == 1 ? 'was' : 'were'} passed. END - old_env = @environment.dup - mixin.args.zip(args).inject(@environment) do |env, (arg, value)| - env[arg[:name]] = if value - Script.parse(value, old_env, @line) - else - arg[:default_value] - end - raise SyntaxError.new("Mixin #{name} is missing parameter ##{mixin.args.index(arg)+1} (#{arg[:name]}).") unless env[arg[:name]] - env - end - - tree = append_children([], mixin.tree, root) - @environment = old_env - tree - end - - def interpolate(text) - scan = StringScanner.new(text) - str = '' - - while scan.scan(/(.*?)(\\*)\#\{/) - escapes = scan[2].size - str << scan.matched[0...-2 - escapes] - if escapes % 2 == 1 - str << '#{' - else - str << Script.resolve(balance(scan, ?{, ?}, 1)[0][0...-1], @environment, @line) - end - end - - str + scan.rest - end - - def balance(*args) - res = Haml::Shared.balance(*args) - return res if res - raise SyntaxError.new("Unbalanced brackets.", @line) + Tree::MixinNode.new(mixin, args.map {|s| Script.parse(s, @line)}, @options) end def import_paths diff --git a/lib/sass/script.rb b/lib/sass/script.rb index d625811f..68366873 100644 --- a/lib/sass/script.rb +++ b/lib/sass/script.rb @@ -18,12 +18,12 @@ module Sass # The regular expression used to validate variables without matching VALIDATE = /^!\w+$/ - def self.resolve(*args) - parse(*args).to_s + def self.resolve(value, line, environment) + parse(value, line).perform(environment).to_s end - def self.parse(value, environment, line) - Parser.parse(value).perform(environment) + def self.parse(value, line) + Parser.parse(value) rescue Sass::SyntaxError => e if e.message == "SassScript error" e.instance_eval do diff --git a/lib/sass/tree/attr_node.rb b/lib/sass/tree/attr_node.rb index 43e86be2..8d22b61c 100644 --- a/lib/sass/tree/attr_node.rb +++ b/lib/sass/tree/attr_node.rb @@ -1,25 +1,24 @@ -require 'sass/tree/node' - module Sass::Tree - class AttrNode < ValueNode - attr_accessor :name - + class AttrNode < Node + attr_accessor :name, :value + def initialize(name, value, options) @name = name - super(value, options) + @value = value + super(options) end - + def to_s(tabs, parent_name = nil) if value[-1] == ?; raise Sass::SyntaxError.new("Invalid attribute: #{declaration.dump} (This isn't CSS!).", @line) end real_name = name real_name = "#{parent_name}-#{real_name}" if parent_name - + if value.empty? && children.empty? raise Sass::SyntaxError.new("Invalid attribute: #{declaration.dump}.", @line) end - + join_string = case @style when :compact; ' ' when :compressed; '' @@ -30,14 +29,22 @@ module Sass::Tree if !value.empty? to_return << "#{spaces}#{real_name}:#{@style == :compressed ? '' : ' '}#{value};#{join_string}" end - + children.each do |kid| to_return << "#{kid.to_s(tabs, real_name)}" << join_string end - + (@style == :compressed && parent_name) ? to_return : to_return[0...-1] end + protected + + def perform!(environment) + @name = interpolate(@name, environment) + @value = @value.is_a?(String) ? interpolate(@value, environment) : @value.perform(environment).to_s + super + end + private def declaration diff --git a/lib/sass/tree/comment_node.rb b/lib/sass/tree/comment_node.rb index d7446a1c..4d568650 100644 --- a/lib/sass/tree/comment_node.rb +++ b/lib/sass/tree/comment_node.rb @@ -1,9 +1,12 @@ require 'sass/tree/node' module Sass::Tree - class CommentNode < ValueNode - def initialize(value, style) - super(value[2..-1].strip, style) + class CommentNode < Node + attr_accessor :value + + def initialize(value, options) + @value = value[2..-1].strip + super(options) end def to_s(tabs = 0, parent_name = nil) @@ -13,5 +16,11 @@ module Sass::Tree spaces + "/* " + ([value] + children.map {|c| c.text}). join(@style == :compact ? ' ' : "\n#{spaces} * ") + " */" end + + protected + + def _perform(environment) + self + end end end diff --git a/lib/sass/tree/directive_node.rb b/lib/sass/tree/directive_node.rb index d40c77ab..4d0d3aca 100644 --- a/lib/sass/tree/directive_node.rb +++ b/lib/sass/tree/directive_node.rb @@ -2,7 +2,14 @@ require 'sass/tree/node' require 'sass/tree/value_node' module Sass::Tree - class DirectiveNode < ValueNode + class DirectiveNode < Node + attr_accessor :value + + def initialize(value, options) + @value = value + super(options) + end + def to_s(tabs) if children.empty? value + ";" diff --git a/lib/sass/tree/for_node.rb b/lib/sass/tree/for_node.rb new file mode 100644 index 00000000..e9f2feb9 --- /dev/null +++ b/lib/sass/tree/for_node.rb @@ -0,0 +1,26 @@ +require 'sass/tree/node' + +module Sass::Tree + class ForNode < Node + def initialize(var, from, to, exclusive, options) + @var = var + @from = from + @to = to + @exclusive = exclusive + super(options) + end + + protected + + def _perform(environment) + from = @from.perform(environment).to_i + to = @to.perform(environment).to_i + range = Range.new(from, to, @exclusive) + + children = [] + sub_env = environment.dup + range.each {|i| children += perform_children(sub_env.merge(@var => Sass::Script::Number.new(i)))} + children + end + end +end diff --git a/lib/sass/tree/if_node.rb b/lib/sass/tree/if_node.rb new file mode 100644 index 00000000..1bdf4b49 --- /dev/null +++ b/lib/sass/tree/if_node.rb @@ -0,0 +1,16 @@ +require 'sass/tree/node' + +module Sass::Tree + class IfNode < Node + def initialize(expr, options) + @expr = expr + super(options) + end + + protected + + def _perform(environment) + @expr.perform(environment).to_bool ? perform_children(environment) : [] + end + end +end diff --git a/lib/sass/tree/mixin_node.rb b/lib/sass/tree/mixin_node.rb new file mode 100644 index 00000000..ec908341 --- /dev/null +++ b/lib/sass/tree/mixin_node.rb @@ -0,0 +1,26 @@ +require 'sass/tree/node' + +module Sass::Tree + class MixinNode < Node + def initialize(mixin, args, options) + @mixin = mixin + @args = args + super(options) + self.children = @mixin.tree + end + + protected + + def _perform(environment) + perform_children(@mixin.args.zip(@args).inject(environment.dup) do |env, (arg, value)| + env[arg[:name]] = if value + value.perform(environment) + else + arg[:default_value] + end + raise Sass::SyntaxError.new("Mixin #{@mixin.name} is missing parameter !#{arg[:name]}.") unless env[arg[:name]] + env + end) + end + end +end diff --git a/lib/sass/tree/node.rb b/lib/sass/tree/node.rb index 54259ab3..4082b750 100644 --- a/lib/sass/tree/node.rb +++ b/lib/sass/tree/node.rb @@ -22,7 +22,7 @@ module Sass result = String.new children.each do |child| if child.is_a? AttrNode - raise SyntaxError.new('Attributes aren\'t allowed at the root of a document.', child.line) + raise Sass::SyntaxError.new('Attributes aren\'t allowed at the root of a document.', child.line) else result << "#{child.to_s(1)}" + (@style == :compressed ? '' : "\n") end @@ -30,6 +30,52 @@ module Sass @style == :compressed ? result+"\n" : result[0...-1] end + def perform(environment) + _perform(environment) + rescue Sass::SyntaxError => e + e.sass_line ||= line + raise e + end + + protected + + def _perform(environment) + node = dup + node.perform!(environment) + node + end + + def perform!(environment) + self.children = perform_children(environment) + end + + def perform_children(environment) + children.map {|c| c.perform(environment)}.flatten + end + + def interpolate(text, environment) + scan = StringScanner.new(text) + str = '' + + while scan.scan(/(.*?)(\\*)\#\{/) + escapes = scan[2].size + str << scan.matched[0...-2 - escapes] + if escapes % 2 == 1 + str << '#{' + else + str << Sass::Script.resolve(balance(scan, ?{, ?}, 1)[0][0...-1], line, environment) + end + end + + str + scan.rest + end + + def balance(*args) + res = Haml::Shared.balance(*args) + return res if res + raise Sass::SyntaxError.new("Unbalanced brackets.", line) + end + private # This method should be overridden by subclasses to return an error message diff --git a/lib/sass/tree/rule_node.rb b/lib/sass/tree/rule_node.rb index ad900d60..6c1dcb64 100644 --- a/lib/sass/tree/rule_node.rb +++ b/lib/sass/tree/rule_node.rb @@ -1,13 +1,14 @@ -require 'sass/tree/node' -require 'sass/tree/attr_node' - module Sass::Tree - class RuleNode < ValueNode + class RuleNode < Node # The character used to include the parent selector PARENT = '&' - alias_method :rule, :value - alias_method :rule=, :value= + attr_accessor :rule + + def initialize(rule, options) + @rule = rule + super(options) + end def rules Array(rule) @@ -104,5 +105,12 @@ module Sass::Tree to_return end + + protected + + def perform!(environment) + self.rule = rules.map {|r| interpolate(r, environment)} + super + end end end diff --git a/lib/sass/tree/value_node.rb b/lib/sass/tree/value_node.rb deleted file mode 100644 index 4115de00..00000000 --- a/lib/sass/tree/value_node.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'sass/tree/node' - -module Sass::Tree - class ValueNode < Node - attr_accessor :value - - def initialize(value, options) - @value = value - super(options) - end - - def to_s(tabs = 0) - value - end - end -end diff --git a/lib/sass/tree/while_node.rb b/lib/sass/tree/while_node.rb new file mode 100644 index 00000000..ac6c229e --- /dev/null +++ b/lib/sass/tree/while_node.rb @@ -0,0 +1,20 @@ +require 'sass/tree/node' + +module Sass::Tree + class WhileNode < Node + def initialize(expr, options) + @expr = expr + super(options) + end + + private + + def _perform(environment) + children = [] + while @expr.perform(environment).to_bool + children += perform_children + end + children + end + end +end diff --git a/test/sass/engine_test.rb b/test/sass/engine_test.rb index 00e69993..979712a2 100755 --- a/test/sass/engine_test.rb +++ b/test/sass/engine_test.rb @@ -73,7 +73,7 @@ class SassEngineTest < Test::Unit::TestCase "a-\#{!b\n c: d" => ["Unbalanced brackets.", 1], "=a(!b = 1, !c)" => "Required arguments must not follow optional arguments \"!c\".", "=a(!b = 1)\n :a= !b\ndiv\n +a(1,2)" => "Mixin a takes 1 argument but 2 were passed.", - "=a(!b)\n :a= !b\ndiv\n +a" => "Mixin a is missing parameter #1 (b).", + "=a(!b)\n :a= !b\ndiv\n +a" => "Mixin a is missing parameter !b.", # Regression tests "a\n b:\n c\n d" => ["Illegal nesting: Only attributes may be nested beneath attributes.", 3], @@ -132,8 +132,7 @@ class SassEngineTest < Test::Unit::TestCase end def test_imported_exception - [1, 2].each do |i| - i = nil if i == 1 + [nil, 2].each do |i| begin Sass::Engine.new("@import bork#{i}", :load_paths => [File.dirname(__FILE__) + '/templates/']).render rescue Sass::SyntaxError => err