Refactor the cross browser support and list functions from gradient module.

This commit is contained in:
Chris Eppstein 2010-12-31 12:55:14 -08:00
parent 15f760e002
commit e11f1035bf
7 changed files with 262 additions and 443 deletions

View File

@ -1,7 +1,7 @@
module Compass
end
%w(dependencies util sass_extensions core_ext version errors quick_cache).each do |lib|
%w(dependencies util browser_support sass_extensions core_ext version errors quick_cache).each do |lib|
require "compass/#{lib}"
end

View File

@ -0,0 +1,62 @@
module Compass
module BrowserSupport
extend self
ASPECTS = %w(webkit moz o ms svg pie css2)
SIMPLE_FUNCTIONS = {
"image" => %w(), # No browsers implement this yet.
"cross-fade" => %w() # No browsers implement this yet.
}
# Adds support for one or more aspects for the given simple function
# Example:
#
# Compass::BrowserSupport.add_support("image", "moz", "webkit")
# # => Adds support for moz and webkit to the image() function.
#
# This function can be called one or more times in a compass configuration
# file in order to add support for new, simple browser functions without
# waiting for a new compass release.
def add_support(function, *aspects)
aspects.each do |aspect|
unless ASPECTS.include?(aspect)
Compass::Util.compass_warn "Unknown support aspect: #{aspect}"
next
end
unless supports?(function, aspect)
SIMPLE_FUNCTIONS[function.to_s] ||= []
SIMPLE_FUNCTIONS[function.to_s] << aspect.to_s
end
end
end
# Removes support for one or more aspects for the given simple function
# Example:
#
# Compass::BrowserSupport.remove_support("image", "o", "ms")
# # => Adds support for moz and webkit to the image() function.
#
# This function can be called one or more times in a compass configuration
# file in order to remove support for simple functions that no longer need to
# a prefix without waiting for a new compass release.
def remove_support(function, *aspects)
aspects.each do |aspect|
unless ASPECTS.include?(aspect)
Compass::Util.compass_warn "Unknown support aspect: #{aspect}"
next
end
SIMPLE_FUNCTIONS[function.to_s].reject!{|a| a == aspect.to_s}
end
end
def supports?(function, aspect)
SIMPLE_FUNCTIONS.has_key?(function.to_s) && SIMPLE_FUNCTIONS[function.to_s].include?(aspect.to_s)
end
def has_aspect?(function)
SIMPLE_FUNCTIONS.has_key?(function.to_s) && SIMPLE_FUNCTIONS[function.to_s].size > 0
end
end
end

View File

@ -4,7 +4,7 @@ end
%w(
selectors enumerate urls display
inline_image image_size constants gradient_support
font_files lists colors trig sprites
font_files lists colors trig sprites cross_browser_support
).each do |func|
require "compass/sass_extensions/functions/#{func}"
end
@ -23,6 +23,7 @@ module Sass::Script::Functions
include Compass::SassExtensions::Functions::Colors
include Compass::SassExtensions::Functions::Trig
include Compass::SassExtensions::Functions::Sprites
include Compass::SassExtensions::Functions::CrossBrowserSupport
end
# Wierd that this has to be re-included to pick up sub-modules. Ruby bug?

View File

@ -0,0 +1,39 @@
module Compass::SassExtensions::Functions::CrossBrowserSupport
# Check if any of the arguments passed require a vendor prefix.
def prefixed(prefix, *args)
aspect = prefix.value.sub(/^-/,"")
needed = args.any?{|a| a.respond_to?(:supports?) && a.supports?(aspect)}
Sass::Script::Bool.new(needed)
end
%w(webkit moz o ms svg pie css2).each do |prefix|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
# Syntactic sugar to apply the given prefix
# -moz($arg) is the same as calling prefix(-moz, $arg)
def _#{prefix}(*args)
prefix("#{prefix}", *args)
end
RUBY
end
def prefix(prefix, *objects)
prefix = prefix.value if prefix.is_a?(Sass::Script::String)
prefix = prefix[1..-1] if prefix[0] == ?-
if objects.size > 1
self.prefix(prefix, Sass::Script::List.new(objects, :comma))
else
object = objects.first
if object.is_a?(Sass::Script::List)
Sass::Script::List.new(object.value.map{|e|
self.prefix(prefix, e)
}, object.separator)
elsif object.respond_to?(:supports?) && object.supports?(prefix) && object.respond_to?(:"to_#{prefix}")
object.options = options
object.send(:"to_#{prefix}")
else
object
end
end
end
end

View File

@ -1,319 +1,6 @@
module Compass::SassExtensions::Functions::GradientSupport
module ListFreeSassSupport
class List < Sass::Script::Literal
attr_accessor :values
def children
values
end
def value
# duck type to a Sass List
values
end
def initialize(*values)
self.values = values
end
def join_with
", "
end
def inspect
to_s
end
def to_s(options = self.options)
values.map {|v| v.to_s }.join(join_with)
end
def size
values.size
end
end
class SpaceList < List
def join_with
" "
end
end
# given a position list, return a corresponding position in percents
def grad_point(position)
position = position.is_a?(Sass::Script::String) ? position.value : position.to_s
position = if position[" "]
if position =~ /(top|bottom|center) (left|right|center)/
"#{$2} #{$1}"
else
position
end
else
case position
when /top|bottom/
"center #{position}"
when /left|right/
"#{position} center"
when /center/
"center center"
else
"#{position} center"
end
end
position = position.
gsub(/top/, "0%").
gsub(/bottom/, "100%").
gsub(/left/,"0%").
gsub(/right/,"100%").
gsub(/center/, "50%")
SpaceList.new(*position.split(/ /).map{|s| Sass::Script::String.new(s)})
end
def color_stops(*args)
List.new(*args.map do |arg|
case arg
when ColorStop
arg
when Sass::Script::Color
ColorStop.new(arg)
when Sass::Script::String
# We get a string as the result of concatenation
# So we have to reparse the expression
parse_color_stop(arg)
else
raise Sass::SyntaxError, "Not a valid color stop: #{arg.class.name}: #{arg}"
end
end)
end
# Returns a comma-delimited list after removing any non-true values
def compact(*args)
List.new(*args.reject{|a| !a.to_bool})
end
# Returns a list object from a value that was passed.
# This can be used to unpack a space separated list that got turned
# into a string by sass before it was passed to a mixin.
def _compass_list(arg)
return arg if arg.is_a?(List)
values = case arg
when Sass::Script::String
expr = Sass::Script::Parser.parse(arg.value, 0, 0)
if expr.is_a?(Sass::Script::Operation)
extract_list_values(expr)
elsif expr.is_a?(Sass::Script::Funcall)
expr.perform(Sass::Environment.new) #we already evaluated the args in context so no harm in using a fake env
else
[arg]
end
else
[arg]
end
SpaceList.new(*values)
end
def _compass_space_list(list)
if list.is_a?(List) && !list.is_a?(SpaceList)
SpaceList.new(*list.values)
elsif list.is_a?(SpaceList)
list
else
SpaceList.new(list)
end
end
def _compass_list_size(list)
Sass::Script::Number.new(list.size)
end
# slice a sublist from a list
def _compass_slice(list, start_index, end_index = nil)
end_index ||= Sass::Script::Number.new(-1)
start_index = start_index.value
end_index = end_index.value
start_index -= 1 unless start_index < 0
end_index -= 1 unless end_index < 0
list.class.new *list.values[start_index..end_index]
end
# Check if any of the arguments passed have a tendency towards vendor prefixing.
def prefixed(prefix, *args)
method = prefix.value.sub(/^-/,"")
args.map!{|a| a.is_a?(List) ? a.values : a}.flatten!
Sass::Script::Bool.new(args.any?{|a| a.supports?(method)})
end
%w(webkit moz o ms svg pie css2).each do |prefix|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def _#{prefix}(*args)
List.new(*args.map! {|a| add_prefix("#{prefix}", a)})
end
RUBY
end
protected
def add_prefix(prefix, object)
if object.is_a?(List)
object.class.new(object.value.map{|e|
add_prefix(prefix, e)
})
elsif object.respond_to?(:supports?) && object.supports?(prefix)
object.options = options
object.send(:"to_#{prefix_method}")
else
object
end
end
def color_stop?(arg)
parse_color_stop(arg)
rescue
nil
end
def assert_list(value)
unless value.is_a?(List)
raise ArgumentError.new("#{value.inspect} is not a list")
end
end
end
module ListBasedSassSupport
# given a position list, return a corresponding position in percents
def grad_point(position)
position = unless position.is_a?(Sass::Script::List)
Sass::Script::List.new([position], :space)
else
Sass::Script::List.new(position.value.dup, position.separator)
end
position.value.reject!{|p| p.is_a?(Sass::Script::Number) && p.numerator_units.include?("deg")}
if (position.value.first.value =~ /top|bottom/) or (position.value.last.value =~ /left|right/)
# browsers are pretty forgiving of reversed positions so we are too.
position.value.reverse!
end
if position.value.size == 1
if position.value.first.value =~ /top|bottom/
position.value.unshift Sass::Script::String.new("center")
elsif position.value.first.value =~ /left|right/
position.value.push Sass::Script::String.new("center")
end
end
position.value.map! do |p|
case p.value
when /top|left/
Sass::Script::Number.new(0, ["%"])
when /bottom|right/
Sass::Script::Number.new(100, ["%"])
when /center/
Sass::Script::Number.new(50, ["%"])
else
p
end
end
position
end
def color_stops(*args)
Sass::Script::List.new(args.map do |arg|
case arg
when ColorStop
arg
when Sass::Script::Color
ColorStop.new(arg)
when Sass::Script::List
ColorStop.new(*arg.value)
else
raise Sass::SyntaxError, "Not a valid color stop: #{arg.class.name}: #{arg}"
end
end, :comma)
end
# Returns a comma-delimited list after removing any non-true values
def compact(*args)
sep = :comma
if args.size == 1 && args.first.is_a?(Sass::Script::List)
args = args.first.value
sep = args.first.separator
end
Sass::Script::List.new(args.reject{|a| !a.to_bool}, sep)
end
# Returns a list object from a value that was passed.
# This can be used to unpack a space separated list that got turned
# into a string by sass before it was passed to a mixin.
def _compass_list(arg)
if arg.is_a?(Sass::Script::List)
Sass::Script::List.new(arg.value.dup, arg.separator)
else
Sass::Script::List.new([arg], :space)
end
end
def _compass_space_list(list)
if list.is_a?(Sass::Script::List)
Sass::Script::List.new(list.value.dup, :space)
else
Sass::Script::List.new([list], :space)
end
end
def _compass_list_size(list)
assert_list list
Sass::Script::Number.new(list.value.size)
end
# slice a sublist from a list
def _compass_slice(list, start_index, end_index = nil)
end_index ||= Sass::Script::Number.new(-1)
start_index = start_index.value
end_index = end_index.value
start_index -= 1 unless start_index < 0
end_index -= 1 unless end_index < 0
Sass::Script::List.new list.values[start_index..end_index], list.separator
end
# Check if any of the arguments passed have require the vendor prefix.
def prefixed(prefix, *args)
aspect = prefix.value.sub(/^-/,"")
needed = args.any?{|a| a.respond_to?(:supports?) && a.supports?(aspect)}
Sass::Script::Bool.new(needed)
end
%w(webkit moz o ms svg pie css2).each do |prefix|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def _#{prefix}(*args)
Sass::Script::List.new(args.map! {|a| add_prefix("#{prefix}", a)}, :comma)
end
RUBY
end
protected
def add_prefix(prefix, object)
if object.is_a?(Sass::Script::List)
Sass::Script::List.new(object.value.map{|e|
add_prefix(prefix, e)
}, object.separator)
elsif object.respond_to?(:supports?) && object.supports?(prefix)
object.options = options
object.send(:"to_#{prefix}")
else
object
end
end
def color_stop?(arg)
arg.is_a?(ColorStop) ||
(arg.is_a?(Sass::Script::List) && ColorStop.new(*arg.value)) ||
ColorStop.new(arg)
rescue
nil
end
def assert_list(value)
unless value.is_a?(Sass::Script::List)
raise ArgumentError.new("#{value.inspect} is not a list")
end
end
end
GRADIENT_ASPECTS = %w(webkit moz svg pie css2).freeze
class ColorStop < Sass::Script::Literal
attr_accessor :color, :stop
@ -346,8 +33,6 @@ module Compass::SassExtensions::Functions::GradientSupport
end
end
GRADIENT_ASPECTS = %w(webkit moz svg pie css2).freeze
class RadialGradient < Sass::Script::Literal
attr_accessor :position_and_angle, :shape_and_size, :color_stops
def children
@ -458,11 +143,53 @@ module Compass::SassExtensions::Functions::GradientSupport
module Functions
# While supporting sass 3.1 and older versions, we need two different implementations.
if defined?(Sass::Script::List)
include ListBasedSassSupport
else
include ListFreeSassSupport
# given a position list, return a corresponding position in percents
def grad_point(position)
position = unless position.is_a?(Sass::Script::List)
Sass::Script::List.new([position], :space)
else
Sass::Script::List.new(position.value.dup, position.separator)
end
position.value.reject!{|p| p.is_a?(Sass::Script::Number) && p.numerator_units.include?("deg")}
if (position.value.first.value =~ /top|bottom/) or (position.value.last.value =~ /left|right/)
# browsers are pretty forgiving of reversed positions so we are too.
position.value.reverse!
end
if position.value.size == 1
if position.value.first.value =~ /top|bottom/
position.value.unshift Sass::Script::String.new("center")
elsif position.value.first.value =~ /left|right/
position.value.push Sass::Script::String.new("center")
end
end
position.value.map! do |p|
case p.value
when /top|left/
Sass::Script::Number.new(0, ["%"])
when /bottom|right/
Sass::Script::Number.new(100, ["%"])
when /center/
Sass::Script::Number.new(50, ["%"])
else
p
end
end
position
end
def color_stops(*args)
Sass::Script::List.new(args.map do |arg|
case arg
when ColorStop
arg
when Sass::Script::Color
ColorStop.new(arg)
when Sass::Script::List
ColorStop.new(*arg.value)
else
raise Sass::SyntaxError, "Not a valid color stop: #{arg.class.name}: #{arg}"
end
end, :comma)
end
def radial_gradient(position_and_angle, shape_and_size, *color_stops)
@ -519,7 +246,7 @@ module Compass::SassExtensions::Functions::GradientSupport
end
def color_stops_in_percentages(color_list)
assert_list(color_list)
assert_type color_list, :List
color_list = normalize_stops(color_list)
max = color_list.value.last.stop
last_value = nil
@ -538,13 +265,13 @@ module Compass::SassExtensions::Functions::GradientSupport
# returns the end position of the gradient from the color stop
def grad_end_position(color_list, radial = Sass::Script::Bool.new(false))
assert_list(color_list)
assert_type color_list, :List
default = Sass::Script::Number.new(100)
grad_position(color_list, Sass::Script::Number.new(color_list.value.size), default, radial)
end
def grad_position(color_list, index, default, radial = Sass::Script::Bool.new(false))
assert_list(color_list)
assert_type color_list, :List
stop = color_list.value[index.value - 1].stop
if stop && radial.to_bool
orig_stop = stop
@ -587,55 +314,16 @@ module Compass::SassExtensions::Functions::GradientSupport
inline_image_string(svg.gsub(/\s+/, ' '), 'image/svg+xml')
end
# Get the nth value from a list
def _compass_nth(list, place)
assert_list list
if place.value == "first"
list.value.first
elsif place.value == "last"
list.value.last
else
list.value[place.value - 1]
end
end
def blank(obj)
case obj
when Sass::Script::Bool
Sass::Script::Bool.new !obj.to_bool
when Sass::Script::String
Sass::Script::Bool.new obj.value.strip.size == 0
when Sass::Script::List
Sass::Script::Bool.new obj.value.size == 0 || obj.value.all?{|el| blank(el).to_bool}
else
Sass::Script::Bool.new false
end
end
private
# After using the sass script parser to parse a string, this reconstructs
# a list from operands to the space/concat operation
def extract_list_values(operation)
left = operation.instance_variable_get("@operand1")
right = operation.instance_variable_get("@operand2")
left = extract_list_values(left) if left.is_a?(Sass::Script::Operation)
right = extract_list_values(right) if right.is_a?(Sass::Script::Operation)
left = literalize(left) unless left.is_a?(Array)
right = literalize(right) unless right.is_a?(Array)
Array(left) + Array(right)
end
# Makes a literal from other various script nodes.
def literalize(node)
case node
when Sass::Script::Literal
node
when Sass::Script::Funcall
node.perform(Sass::Environment.new)
else
Sass::Script::String.new(node.to_s)
end
def color_stop?(arg)
arg.is_a?(ColorStop) ||
(arg.is_a?(Sass::Script::List) && ColorStop.new(*arg.value)) ||
ColorStop.new(arg)
rescue
nil
end
def normalize_stops(color_list)
positions = color_list.value.map{|obj| obj.dup}
# fill in the start and end positions, if unspecified
@ -726,24 +414,32 @@ EOS
end
def _center_position
if defined?(Sass::Script::List)
Sass::Script::List.new([
Sass::Script::String.new("center"),
Sass::Script::String.new("center")
],:space)
else
Sass::Script::String.new("center center")
end
Sass::Script::List.new([
Sass::Script::String.new("center"),
Sass::Script::String.new("center")
],:space)
end
end
module Assertions
def assert_type(value, type, name = nil)
return if value.is_a?(Sass::Script.const_get(type))
err = "#{value.inspect} is not a #{type.to_s.downcase}"
err = "$#{name}: " + err if name
raise ArgumentError.new(err)
end
end
class LinearGradient < Sass::Script::Literal
include Assertions
include Functions
include Compass::SassExtensions::Functions::Constants
include Compass::SassExtensions::Functions::InlineImage
end
class RadialGradient < Sass::Script::Literal
include Assertions
include Functions
include Compass::SassExtensions::Functions::Constants
include Compass::SassExtensions::Functions::InlineImage

View File

@ -1,4 +1,79 @@
module Compass::SassExtensions::Functions::Lists
# Returns true when the object is false, an empty string, or an empty list
def blank(obj)
case obj
when Sass::Script::Bool
Sass::Script::Bool.new !obj.to_bool
when Sass::Script::String
Sass::Script::Bool.new obj.value.strip.size == 0
when Sass::Script::List
Sass::Script::Bool.new obj.value.size == 0 || obj.value.all?{|el| blank(el).to_bool}
else
Sass::Script::Bool.new false
end
end
# Returns a new list after removing any non-true values
def compact(*args)
sep = :comma
if args.size == 1 && args.first.is_a?(Sass::Script::List)
args = args.first.value
sep = args.first.separator
end
Sass::Script::List.new(args.reject{|a| !a.to_bool}, sep)
end
# Get the nth value from a list
def _compass_nth(list, place)
assert_type list, :List
if place.value == "first"
list.value.first
elsif place.value == "last"
list.value.last
else
list.value[place.value - 1]
end
end
# Returns a list object from a value that was passed.
# This can be used to unpack a space separated list that got turned
# into a string by sass before it was passed to a mixin.
def _compass_list(arg)
if arg.is_a?(Sass::Script::List)
Sass::Script::List.new(arg.value.dup, arg.separator)
else
Sass::Script::List.new([arg], :space)
end
end
# If the argument is a list, it will return a new list that is space delimited
# Otherwise it returns a new, single element, space-delimited list.
def _compass_space_list(list)
if list.is_a?(Sass::Script::List)
Sass::Script::List.new(list.value.dup, :space)
else
Sass::Script::List.new([list], :space)
end
end
# Returns the size of the list.
def _compass_list_size(list)
assert_list list
Sass::Script::Number.new(list.value.size)
end
# slice a sublist from a list
def _compass_slice(list, start_index, end_index = nil)
end_index ||= Sass::Script::Number.new(-1)
start_index = start_index.value
end_index = end_index.value
start_index -= 1 unless start_index < 0
end_index -= 1 unless end_index < 0
Sass::Script::List.new list.values[start_index..end_index], list.separator
end
# returns the first value of a space delimited list.
def first_value_of(list)
if list.is_a?(Sass::Script::String)
Sass::Script::String.new(list.value.split(/\s+/).first)
@ -8,4 +83,13 @@ module Compass::SassExtensions::Functions::Lists
list
end
end
protected
def assert_list(value)
unless value.is_a?(Sass::Script::List)
raise ArgumentError.new("#{value.inspect} is not a list")
end
end
end

View File

@ -2,69 +2,6 @@ require 'sass/script/node'
require 'sass/script/literal'
require 'sass/script/funcall'
module Compass
module BrowserSupport
extend self
ASPECTS = %w(webkit moz o ms svg pie css2)
SIMPLE_FUNCTIONS = {
"image" => %w(), # No browsers implement this yet.
"cross-fade" => %w() # No browsers implement this yet.
}
# Adds support for one or more aspects for the given simple function
# Example:
#
# Compass::BrowserSupport.add_support("image", "moz", "webkit")
# # => Adds support for moz and webkit to the image() function.
#
# This function can be called one or more times in a compass configuration
# file in order to add support for new, simple browser functions without
# waiting for a new compass release.
def add_support(function, *aspects)
aspects.each do |aspect|
unless ASPECTS.include?(aspect)
Compass::Util.compass_warn "Unknown support aspect: #{aspect}"
next
end
unless supports?(function, aspect)
SIMPLE_FUNCTIONS[function.to_s] ||= []
SIMPLE_FUNCTIONS[function.to_s] << aspect.to_s
end
end
end
# Removes support for one or more aspects for the given simple function
# Example:
#
# Compass::BrowserSupport.remove_support("image", "o", "ms")
# # => Adds support for moz and webkit to the image() function.
#
# This function can be called one or more times in a compass configuration
# file in order to remove support for simple functions that no longer need to
# a prefix without waiting for a new compass release.
def remove_support(function, *aspects)
aspects.each do |aspect|
unless ASPECTS.include?(aspect)
Compass::Util.compass_warn "Unknown support aspect: #{aspect}"
next
end
SIMPLE_FUNCTIONS[function.to_s].reject!{|a| a == aspect.to_s}
end
end
def supports?(function, aspect)
SIMPLE_FUNCTIONS.has_key?(function.to_s) && SIMPLE_FUNCTIONS[function.to_s].include?(aspect.to_s)
end
def has_aspect?(function)
SIMPLE_FUNCTIONS.has_key?(function.to_s) && SIMPLE_FUNCTIONS[function.to_s].size > 0
end
end
end
module Sass::Script
module HasSimpleCrossBrowserFunctionSupport
def supports?(aspect)