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

376 lines
8.9 KiB
Ruby
Raw Normal View History

require File.dirname(__FILE__) + '/../sass'
require 'sass/tree/node'
require 'strscan'
module Sass
module Tree
class Node
2009-04-11 03:50:24 -07:00
# 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 [Hash<Symbol, Object>] An options hash (see {Sass::CSS#initialize})
2009-04-11 03:50:24 -07:00
# @return [String] The Sass code corresponding to the node
def to_sass(tabs = 0, opts = {})
result = ''
children.each do |child|
2009-04-11 03:50:24 -07:00
result << "#{' ' * tabs}#{child.to_sass(0, opts)}\n"
end
result
end
end
class RuleNode
2009-04-11 03:50:24 -07:00
# @see Node#to_sass
def to_sass(tabs, opts = {})
str = "\n#{' ' * tabs}#{rules.first}#{children.any? { |c| c.is_a? AttrNode } ? "\n" : ''}"
children.each do |child|
str << "#{child.to_sass(tabs + 1, opts)}"
end
str
end
end
class AttrNode
2009-04-11 03:50:24 -07:00
# @see Node#to_sass
def to_sass(tabs, opts = {})
"#{' ' * tabs}#{opts[:alternate] ? '' : ':'}#{name}#{opts[:alternate] ? ':' : ''} #{value}\n"
end
end
class DirectiveNode
2009-04-11 03:50:24 -07:00
# @see Node#to_sass
def to_sass(tabs, opts = {})
"#{' ' * tabs}#{value}#{children.map {|c| c.to_sass(tabs + 1, opts)}}\n"
end
end
end
2009-04-11 03:50:24 -07:00
# This class converts CSS documents into Sass templates.
# It works by parsing the CSS document into a {Sass::Tree} structure,
# and then applying various transformations to the structure
# to produce more concise and idiomatic Sass.
#
# Example usage:
#
# Sass::CSS.new("p { color: blue }").render #=> "p\n :color blue"
class CSS
2009-04-11 03:50:24 -07:00
# @param [String] template The CSS code
# @param [Hash<Symbol, Object>] options An options hash.
2009-04-11 03:50:24 -07:00
# `:alternate`: Whether or not to output alternate attribute syntax
# (`color: blue` as opposed to `:color blue`).
def initialize(template, options = {})
if template.is_a? IO
template = template.read
end
@options = options
@template = StringScanner.new(template)
end
2009-04-11 03:50:24 -07:00
# Converts the CSS template into Sass code.
#
# @return [String] The resulting Sass code
def render
begin
2009-04-11 03:50:24 -07:00
build_tree.to_sass(0, @options).strip + "\n"
rescue Exception => err
line = @template.string[0...@template.pos].split("\n").size
2008-04-07 23:09:17 -07:00
err.backtrace.unshift "(css):#{line}"
raise err
end
end
private
2009-04-11 03:50:24 -07:00
# Parses the CSS template and applies various transformations
#
# @return [Tree::Node] The root node of the parsed tree
def build_tree
root = Tree::Node.new
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
root
end
2009-04-11 03:50:24 -07:00
# Parses a set of CSS rules.
#
# @param [Tree::Node] The parent node of the rules
def rules(root)
while r = rule
root << r
whitespace
end
end
2009-04-11 03:50:24 -07:00
# Parses a single CSS rule.
#
# @return [Tree::Node] The parsed rule
def rule
return unless rule = @template.scan(/[^\{\};]+/)
rule.strip!
directive = rule[0] == ?@
if directive
node = Tree::DirectiveNode.new(rule)
return node if @template.scan(/;/)
assert_match /\{/
whitespace
rules(node)
return node
end
assert_match /\{/
node = Tree::RuleNode.new(rule)
attributes(node)
return node
end
2009-04-11 03:50:24 -07:00
# Parses a set of CSS attributes within a rule.
#
# @param [Tree::RuleNode] rule The parent node of the attributes
def attributes(rule)
while @template.scan(/[^:\}\s]+/)
name = @template[0]
whitespace
assert_match /:/
2008-04-07 23:09:17 -07:00
value = ''
while @template.scan(/[^;\s\}]+/)
value << @template[0] << whitespace
end
2008-04-07 23:09:17 -07:00
assert_match /(;|(?=\}))/
rule << Tree::AttrNode.new(name, value, nil)
end
assert_match /\}/
end
2009-04-11 03:50:24 -07:00
# Moves the scanner over a section of whitespace or comments.
#
# @return [String] The ignored whitespace
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
2009-04-11 03:50:24 -07:00
# Moves the scanner over a regular expression,
# raising an exception if it doesn't match.
#
# @param [Regexp] The regular expression to assert
def assert_match(re)
if !@template.scan(re)
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}")
end
whitespace
end
# Transform
#
2009-04-11 03:50:24 -07:00
# foo, bar, baz
# color: blue
#
# into
#
2009-04-11 03:50:24 -07:00
# foo
# color: blue
# bar
# color: blue
# baz
# color: blue
#
2009-04-11 03:50:24 -07:00
# @param [Tree::Node] root The parent node
def expand_commas(root)
root.children.map! do |child|
next child unless Tree::RuleNode === child && child.rules.first.include?(',')
child.rules.first.split(',').map do |rule|
node = Tree::RuleNode.new(rule.strip)
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
#
2009-04-11 03:50:24 -07:00
# foo
# color: green
# foo.bar
# color: blue
2008-04-29 12:41:53 -07:00
#
# becomes
#
2009-04-11 03:50:24 -07:00
# foo
# color: green
# &.bar
# color: blue
2008-04-29 12:41:53 -07:00
#
# This has the side effect of nesting rules,
# so that
#
2009-04-11 03:50:24 -07:00
# foo
# color: green
# foo bar
# color: red
# foo baz
# color: blue
#
# becomes
#
2009-04-11 03:50:24 -07:00
# foo
# color: green
# & bar
# color: red
# & baz
# color: blue
2008-04-07 23:09:17 -07:00
#
2009-04-11 03:50:24 -07:00
# @param [Tree::Node] root The parent node
2008-04-29 12:41:53 -07:00
def parent_ref_rules(root)
current_rule = nil
root.children.select { |c| Tree::RuleNode === c }.each do |child|
root.children.delete child
first, rest = child.rules.first.scan(/^(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?$/).first
if current_rule.nil? || current_rule.rules.first != first
current_rule = Tree::RuleNode.new(first)
root << current_rule
end
if rest
child.rules = ["&" + rest]
current_rule << child
else
current_rule.children += child.children
end
end
root.children.each { |v| parent_ref_rules(v) }
end
2008-04-29 12:41:53 -07:00
# Remove useless parent refs so that
#
2009-04-11 03:50:24 -07:00
# foo
# & bar
# color: blue
2008-04-29 12:41:53 -07:00
#
# becomes
#
2009-04-11 03:50:24 -07:00
# foo
# bar
# color: blue
2008-04-29 12:41:53 -07:00
#
2009-04-11 03:50:24 -07:00
# @param [Tree::Node] root The parent node
2008-04-29 12:41:53 -07:00
def remove_parent_refs(root)
root.children.each do |child|
if child.is_a?(Tree::RuleNode)
child.rules.first.gsub! /^& +/, ''
2008-04-29 12:41:53 -07:00
remove_parent_refs child
end
end
end
# Flatten rules so that
#
2009-04-11 03:50:24 -07:00
# foo
# bar
# :color red
#
# becomes
#
2009-04-11 03:50:24 -07:00
# foo bar
# :color red
2008-04-07 23:09:17 -07:00
#
# and
#
2009-04-11 03:50:24 -07:00
# foo
# &.bar
# color: blue
#
# becomes
#
2009-04-11 03:50:24 -07:00
# foo.bar
# color: blue
#
2009-04-11 03:50:24 -07:00
# @param [Tree::Node] root The parent node
def flatten_rules(root)
root.children.each { |child| flatten_rule(child) if child.is_a?(Tree::RuleNode) }
end
2009-04-11 03:50:24 -07:00
# Flattens a single rule
#
# @param [Tree::RuleNode] rule The candidate for flattening
# @see #flatten_rules
def flatten_rule(rule)
while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode)
child = rule.children.first
if child.rules.first[0] == ?&
rule.rules = [child.rules.first.gsub(/^&/, rule.rules.first)]
else
rule.rules = ["#{rule.rules.first} #{child.rules.first}"]
end
rule.children = child.children
end
flatten_rules(rule)
end
# Transform
#
2009-04-11 03:50:24 -07:00
# foo
# bar
# color: blue
# baz
# color: blue
#
# into
#
2009-04-11 03:50:24 -07:00
# foo
# bar, baz
# color: blue
#
2009-04-11 03:50:24 -07:00
# @param [Tree::RuleNode] rule The candidate for flattening
def fold_commas(root)
prev_rule = nil
root.children.map! do |child|
next child unless child.is_a?(Tree::RuleNode)
if prev_rule && prev_rule.children == child.children
prev_rule.rules.first << ", #{child.rules.first}"
next nil
end
fold_commas(child)
prev_rule = child
child
end
root.children.compact!
end
end
end