1
0
Fork 0
mirror of https://github.com/haml/haml.git synced 2022-11-09 12:33:31 -05:00

Compile Sass to a full-fledged AST before evaluating and printing.

This commit is contained in:
Nathan Weizenbaum 2008-10-13 09:49:35 -07:00
parent 62d6cfdac6
commit 88c4619bdd
14 changed files with 212 additions and 130 deletions

View file

@ -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" : ''}"

View file

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

View file

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

View file

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

View file

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

View file

@ -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 + ";"

26
lib/sass/tree/for_node.rb Normal file
View file

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

16
lib/sass/tree/if_node.rb Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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