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:
parent
62d6cfdac6
commit
88c4619bdd
14 changed files with 212 additions and 130 deletions
|
@ -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" : ''}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
26
lib/sass/tree/for_node.rb
Normal 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
16
lib/sass/tree/if_node.rb
Normal 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
|
26
lib/sass/tree/mixin_node.rb
Normal file
26
lib/sass/tree/mixin_node.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
20
lib/sass/tree/while_node.rb
Normal file
20
lib/sass/tree/while_node.rb
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue