2007-04-01 10:03:12 +00:00
|
|
|
require File.dirname(__FILE__) + '/../sass'
|
|
|
|
require 'sass/tree/node'
|
|
|
|
require 'strscan'
|
|
|
|
|
|
|
|
module Sass
|
|
|
|
# :stopdoc:
|
|
|
|
module Tree
|
|
|
|
class Node
|
2008-04-29 13:39:00 -07:00
|
|
|
def to_sass(opts = {})
|
2007-04-01 10:03:12 +00:00
|
|
|
result = ''
|
|
|
|
|
|
|
|
children.each do |child|
|
2008-04-29 13:14:43 -07:00
|
|
|
result << "#{child.to_sass(0, opts)}\n"
|
2007-04-01 10:03:12 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
result
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class RuleNode
|
2008-04-29 13:14:43 -07:00
|
|
|
def to_sass(tabs, opts = {})
|
2009-04-25 13:56:30 -07:00
|
|
|
str = "\n#{' ' * tabs}#{rules.first}#{children.any? { |c| c.is_a? AttrNode } ? "\n" : ''}"
|
2007-04-01 10:03:12 +00:00
|
|
|
|
|
|
|
children.each do |child|
|
2008-04-29 13:14:43 -07:00
|
|
|
str << "#{child.to_sass(tabs + 1, opts)}"
|
2007-04-01 10:03:12 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
str
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class AttrNode
|
2008-04-29 13:14:43 -07:00
|
|
|
def to_sass(tabs, opts = {})
|
|
|
|
"#{' ' * tabs}#{opts[:alternate] ? '' : ':'}#{name}#{opts[:alternate] ? ':' : ''} #{value}\n"
|
2007-04-01 10:03:12 +00:00
|
|
|
end
|
|
|
|
end
|
2008-09-02 16:59:28 -07:00
|
|
|
|
|
|
|
class DirectiveNode
|
|
|
|
def to_sass(tabs, opts = {})
|
|
|
|
"#{' ' * tabs}#{value}#{children.map {|c| c.to_sass(tabs + 1, opts)}}\n"
|
|
|
|
end
|
|
|
|
end
|
2007-04-01 10:03:12 +00:00
|
|
|
end
|
2007-12-16 21:29:25 +00:00
|
|
|
|
|
|
|
# This class is based on the Ruby 1.9 ordered hashes.
|
|
|
|
# It keeps the semantics and most of the efficiency of normal hashes
|
|
|
|
# while also keeping track of the order in which elements were set.
|
|
|
|
class OrderedHash
|
2008-05-14 21:24:56 -07:00
|
|
|
Node = Struct.new(:key, :value, :next, :prev)
|
2007-12-16 21:29:25 +00:00
|
|
|
include Enumerable
|
|
|
|
|
|
|
|
def initialize
|
|
|
|
@hash = {}
|
|
|
|
end
|
|
|
|
|
2008-05-14 21:24:56 -07:00
|
|
|
def initialize_copy(other)
|
|
|
|
@hash = other.instance_variable_get('@hash').clone
|
|
|
|
end
|
|
|
|
|
2007-12-16 21:29:25 +00:00
|
|
|
def [](key)
|
|
|
|
@hash[key] && @hash[key].value
|
|
|
|
end
|
|
|
|
|
|
|
|
def []=(key, value)
|
2008-05-14 21:24:56 -07:00
|
|
|
node = Node.new(key, value)
|
|
|
|
|
|
|
|
if old = @hash[key]
|
|
|
|
if old.prev
|
|
|
|
old.prev.next = old.next
|
|
|
|
else # old is @first and @last
|
|
|
|
@first = @last = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2007-12-16 21:29:25 +00:00
|
|
|
if @first.nil?
|
|
|
|
@first = @last = node
|
|
|
|
else
|
2008-05-14 21:24:56 -07:00
|
|
|
node.prev = @last
|
2007-12-16 21:29:25 +00:00
|
|
|
@last.next = node
|
|
|
|
@last = node
|
|
|
|
end
|
2008-05-14 21:24:56 -07:00
|
|
|
|
2007-12-16 21:29:25 +00:00
|
|
|
@hash[key] = node
|
|
|
|
value
|
|
|
|
end
|
|
|
|
|
|
|
|
def each
|
|
|
|
return unless @first
|
|
|
|
yield [@first.key, @first.value]
|
|
|
|
node = @first
|
|
|
|
yield [node.key, node.value] while node = node.next
|
|
|
|
self
|
|
|
|
end
|
|
|
|
|
|
|
|
def values
|
|
|
|
self.map { |k, v| v }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2007-04-01 10:03:12 +00:00
|
|
|
# :startdoc:
|
|
|
|
|
|
|
|
# This class contains the functionality used in the +css2sass+ utility,
|
|
|
|
# namely converting CSS documents to Sass templates.
|
|
|
|
class CSS
|
|
|
|
|
|
|
|
# Creates a new instance of Sass::CSS that will compile the given document
|
|
|
|
# to a Sass string when +render+ is called.
|
2008-04-29 13:14:43 -07:00
|
|
|
def initialize(template, options = {})
|
2007-04-01 10:03:12 +00:00
|
|
|
if template.is_a? IO
|
|
|
|
template = template.read
|
|
|
|
end
|
|
|
|
|
2008-04-29 19:12:04 -07:00
|
|
|
@options = options
|
2007-04-01 10:03:12 +00:00
|
|
|
@template = StringScanner.new(template)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Processes the document and returns the result as a string
|
|
|
|
# containing the CSS template.
|
|
|
|
def render
|
2007-04-01 22:39:44 +00:00
|
|
|
begin
|
2008-09-05 12:36:50 -07:00
|
|
|
build_tree.to_sass(@options).strip + "\n"
|
2007-04-01 22:39:44 +00:00
|
|
|
rescue Exception => err
|
|
|
|
line = @template.string[0...@template.pos].split("\n").size
|
2008-04-07 23:09:17 -07:00
|
|
|
|
2007-04-01 22:39:44 +00:00
|
|
|
err.backtrace.unshift "(css):#{line}"
|
|
|
|
raise err
|
|
|
|
end
|
2007-04-01 10:03:12 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def build_tree
|
2008-08-12 21:35:16 -04:00
|
|
|
root = Tree::Node.new({})
|
2007-04-01 22:33:08 +00:00
|
|
|
whitespace
|
2008-04-29 12:41:53 -07:00
|
|
|
rules root
|
|
|
|
expand_commas root
|
|
|
|
parent_ref_rules root
|
|
|
|
remove_parent_refs root
|
|
|
|
flatten_rules root
|
|
|
|
fold_commas root
|
2007-04-01 22:33:08 +00:00
|
|
|
root
|
|
|
|
end
|
2007-04-01 10:03:12 +00:00
|
|
|
|
2008-09-02 16:59:28 -07:00
|
|
|
def rules(root)
|
|
|
|
while r = rule
|
|
|
|
root << r
|
2007-04-03 08:12:39 +00:00
|
|
|
whitespace
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2008-09-02 16:59:28 -07:00
|
|
|
def rule
|
|
|
|
return unless rule = @template.scan(/[^\{\};]+/)
|
|
|
|
rule.strip!
|
|
|
|
directive = rule[0] == ?@
|
2007-04-01 10:03:12 +00:00
|
|
|
|
2008-09-02 16:59:28 -07:00
|
|
|
if directive
|
2008-09-02 17:02:11 -07:00
|
|
|
node = Tree::DirectiveNode.new(rule, {})
|
2008-09-02 16:59:28 -07:00
|
|
|
return node if @template.scan(/;/)
|
2007-04-01 22:33:08 +00:00
|
|
|
|
2008-09-02 16:59:28 -07:00
|
|
|
assert_match /\{/
|
|
|
|
whitespace
|
|
|
|
|
|
|
|
rules(node)
|
|
|
|
return node
|
2007-04-01 22:33:08 +00:00
|
|
|
end
|
2008-09-02 16:59:28 -07:00
|
|
|
|
|
|
|
assert_match /\{/
|
2008-09-02 17:02:11 -07:00
|
|
|
node = Tree::RuleNode.new(rule, {})
|
2008-09-02 16:59:28 -07:00
|
|
|
attributes(node)
|
|
|
|
return node
|
2007-04-01 22:33:08 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def attributes(rule)
|
|
|
|
while @template.scan(/[^:\}\s]+/)
|
|
|
|
name = @template[0]
|
|
|
|
whitespace
|
2007-04-01 10:03:12 +00:00
|
|
|
|
2007-04-01 22:33:08 +00:00
|
|
|
assert_match /:/
|
2008-04-07 23:09:17 -07:00
|
|
|
|
2007-04-01 22:33:08 +00:00
|
|
|
value = ''
|
2007-12-16 21:50:50 +00:00
|
|
|
while @template.scan(/[^;\s\}]+/)
|
2007-04-01 22:33:08 +00:00
|
|
|
value << @template[0] << whitespace
|
2007-04-01 10:03:12 +00:00
|
|
|
end
|
2008-04-07 23:09:17 -07:00
|
|
|
|
2007-12-16 21:50:50 +00:00
|
|
|
assert_match /(;|(?=\}))/
|
2008-08-12 21:35:16 -04:00
|
|
|
rule << Tree::AttrNode.new(name, value, {})
|
2007-04-01 10:03:12 +00:00
|
|
|
end
|
|
|
|
|
2007-04-01 22:33:08 +00:00
|
|
|
assert_match /\}/
|
|
|
|
end
|
|
|
|
|
|
|
|
def whitespace
|
|
|
|
space = @template.scan(/\s*/) || ''
|
|
|
|
|
|
|
|
# If we've hit a comment,
|
|
|
|
# go past it and look for more whitespace
|
|
|
|
if @template.scan(/\/\*/)
|
|
|
|
@template.scan_until(/\*\//)
|
|
|
|
return space + whitespace
|
|
|
|
end
|
|
|
|
return space
|
|
|
|
end
|
|
|
|
|
|
|
|
def assert_match(re)
|
|
|
|
if !@template.scan(re)
|
2008-03-09 12:57:38 -07:00
|
|
|
line = @template.string[0..@template.pos].count "\n"
|
|
|
|
# Display basic regexps as plain old strings
|
|
|
|
expected = re.source == Regexp.escape(re.source) ? "\"#{re.source}\"" : re.inspect
|
|
|
|
raise Exception.new("Invalid CSS on line #{line}: expected #{expected}")
|
2007-04-01 22:33:08 +00:00
|
|
|
end
|
|
|
|
whitespace
|
2007-04-01 10:03:12 +00:00
|
|
|
end
|
2007-04-03 05:14:03 +00:00
|
|
|
|
2007-12-16 22:22:56 +00:00
|
|
|
# Transform
|
|
|
|
#
|
|
|
|
# foo, bar, baz
|
|
|
|
# color: blue
|
|
|
|
#
|
|
|
|
# into
|
|
|
|
#
|
|
|
|
# foo
|
|
|
|
# color: blue
|
|
|
|
# bar
|
|
|
|
# color: blue
|
|
|
|
# baz
|
|
|
|
# color: blue
|
|
|
|
#
|
|
|
|
# Yes, this expands the amount of code,
|
|
|
|
# but it's necessary to get nesting to work properly.
|
|
|
|
def expand_commas(root)
|
|
|
|
root.children.map! do |child|
|
2009-04-25 13:56:30 -07:00
|
|
|
next child unless Tree::RuleNode === child && child.rules.first.include?(',')
|
|
|
|
child.rules.first.split(',').map do |rule|
|
2008-11-22 09:30:19 -08:00
|
|
|
node = Tree::RuleNode.new(rule.strip, {})
|
2007-12-16 22:22:56 +00:00
|
|
|
node.children = child.children
|
|
|
|
node
|
|
|
|
end
|
|
|
|
end
|
|
|
|
root.children.flatten!
|
|
|
|
end
|
|
|
|
|
2008-04-29 12:41:53 -07:00
|
|
|
# Make rules use parent refs so that
|
|
|
|
#
|
|
|
|
# foo
|
|
|
|
# color: green
|
|
|
|
# foo.bar
|
|
|
|
# color: blue
|
|
|
|
#
|
|
|
|
# becomes
|
|
|
|
#
|
|
|
|
# foo
|
|
|
|
# color: green
|
|
|
|
# &.bar
|
|
|
|
# color: blue
|
|
|
|
#
|
|
|
|
# This has the side effect of nesting rules,
|
|
|
|
# so that
|
2007-12-16 21:29:25 +00:00
|
|
|
#
|
|
|
|
# foo
|
|
|
|
# color: green
|
|
|
|
# foo bar
|
|
|
|
# color: red
|
|
|
|
# foo baz
|
|
|
|
# color: blue
|
|
|
|
#
|
|
|
|
# becomes
|
|
|
|
#
|
|
|
|
# foo
|
|
|
|
# color: green
|
2008-04-29 12:41:53 -07:00
|
|
|
# & bar
|
2007-12-16 21:29:25 +00:00
|
|
|
# color: red
|
2008-04-29 12:41:53 -07:00
|
|
|
# & baz
|
2007-12-16 21:29:25 +00:00
|
|
|
# color: blue
|
2008-04-07 23:09:17 -07:00
|
|
|
#
|
2008-04-29 12:41:53 -07:00
|
|
|
def parent_ref_rules(root)
|
2007-12-16 21:29:25 +00:00
|
|
|
rules = OrderedHash.new
|
2007-12-16 22:22:56 +00:00
|
|
|
root.children.select { |c| Tree::RuleNode === c }.each do |child|
|
2007-12-16 22:05:54 +00:00
|
|
|
root.children.delete child
|
2009-04-25 13:56:30 -07:00
|
|
|
first, rest = child.rules.first.scan(/^(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?$/).first
|
2008-08-12 21:35:16 -04:00
|
|
|
rules[first] ||= Tree::RuleNode.new(first, {})
|
2007-12-16 21:29:25 +00:00
|
|
|
if rest
|
2009-04-25 13:56:30 -07:00
|
|
|
child.rules = ["&" + rest]
|
2007-12-16 21:29:25 +00:00
|
|
|
rules[first] << child
|
2007-04-03 08:12:39 +00:00
|
|
|
else
|
2007-12-16 21:29:25 +00:00
|
|
|
rules[first].children += child.children
|
2007-04-03 08:12:39 +00:00
|
|
|
end
|
2007-04-03 05:14:03 +00:00
|
|
|
end
|
|
|
|
|
2008-04-29 12:41:53 -07:00
|
|
|
rules.values.each { |v| parent_ref_rules(v) }
|
2007-12-16 21:29:25 +00:00
|
|
|
root.children += rules.values
|
|
|
|
end
|
|
|
|
|
2008-04-29 12:41:53 -07:00
|
|
|
# Remove useless parent refs so that
|
|
|
|
#
|
|
|
|
# foo
|
|
|
|
# & bar
|
|
|
|
# color: blue
|
|
|
|
#
|
|
|
|
# becomes
|
|
|
|
#
|
|
|
|
# foo
|
|
|
|
# bar
|
|
|
|
# color: blue
|
|
|
|
#
|
|
|
|
def remove_parent_refs(root)
|
|
|
|
root.children.each do |child|
|
|
|
|
if child.is_a?(Tree::RuleNode)
|
2009-04-25 13:56:30 -07:00
|
|
|
child.rules.first.gsub! /^& +/, ''
|
2008-04-29 12:41:53 -07:00
|
|
|
remove_parent_refs child
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2007-12-16 21:29:25 +00:00
|
|
|
# Flatten rules so that
|
|
|
|
#
|
|
|
|
# foo
|
|
|
|
# bar
|
|
|
|
# baz
|
|
|
|
# color: red
|
|
|
|
#
|
|
|
|
# becomes
|
|
|
|
#
|
|
|
|
# foo bar baz
|
|
|
|
# color: red
|
2008-04-07 23:09:17 -07:00
|
|
|
#
|
2008-04-29 15:09:07 -07:00
|
|
|
# and
|
|
|
|
#
|
|
|
|
# foo
|
|
|
|
# &.bar
|
|
|
|
# color: blue
|
|
|
|
#
|
|
|
|
# becomes
|
|
|
|
#
|
|
|
|
# foo.bar
|
|
|
|
# color: blue
|
|
|
|
#
|
2007-12-16 21:29:25 +00:00
|
|
|
def flatten_rules(root)
|
2007-12-16 22:20:47 +00:00
|
|
|
root.children.each { |child| flatten_rule(child) if child.is_a?(Tree::RuleNode) }
|
2007-12-16 22:05:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def flatten_rule(rule)
|
|
|
|
while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode)
|
|
|
|
child = rule.children.first
|
2008-04-29 15:09:07 -07:00
|
|
|
|
2009-04-25 13:56:30 -07:00
|
|
|
if child.rules.first[0] == ?&
|
|
|
|
rule.rules = [child.rules.first.gsub(/^&/, rule.rules.first)]
|
2008-04-29 15:09:07 -07:00
|
|
|
else
|
2009-04-25 13:56:30 -07:00
|
|
|
rule.rules = ["#{rule.rules.first} #{child.rules.first}"]
|
2008-04-29 15:09:07 -07:00
|
|
|
end
|
|
|
|
|
2007-12-16 22:05:54 +00:00
|
|
|
rule.children = child.children
|
2007-04-03 05:14:03 +00:00
|
|
|
end
|
2007-12-16 21:29:25 +00:00
|
|
|
|
2007-12-16 22:05:54 +00:00
|
|
|
flatten_rules(rule)
|
2007-04-03 05:14:03 +00:00
|
|
|
end
|
2007-12-16 22:50:59 +00:00
|
|
|
|
|
|
|
# Transform
|
|
|
|
#
|
|
|
|
# foo
|
|
|
|
# bar
|
|
|
|
# color: blue
|
|
|
|
# baz
|
|
|
|
# color: blue
|
|
|
|
#
|
|
|
|
# into
|
|
|
|
#
|
|
|
|
# foo
|
|
|
|
# bar, baz
|
|
|
|
# color: blue
|
|
|
|
#
|
|
|
|
def fold_commas(root)
|
|
|
|
prev_rule = nil
|
|
|
|
root.children.map! do |child|
|
2009-02-19 20:05:11 -08:00
|
|
|
next child unless child.is_a?(Tree::RuleNode)
|
2007-12-16 22:50:59 +00:00
|
|
|
|
|
|
|
if prev_rule && prev_rule.children == child.children
|
2009-04-25 13:56:30 -07:00
|
|
|
prev_rule.rules.first << ", #{child.rules.first}"
|
2007-12-16 22:50:59 +00:00
|
|
|
next nil
|
|
|
|
end
|
|
|
|
|
|
|
|
fold_commas(child)
|
|
|
|
prev_rule = child
|
|
|
|
child
|
|
|
|
end
|
|
|
|
root.children.compact!
|
|
|
|
end
|
2007-04-01 10:03:12 +00:00
|
|
|
end
|
|
|
|
end
|