From 15f760e0020a74e219f745211bc8f17fb0237fde Mon Sep 17 00:00:00 2001 From: Chris Eppstein Date: Thu, 30 Dec 2010 12:19:00 -0800 Subject: [PATCH] Infrastructure for cross browser support of simple, experimental functions --- doc-src/content/CHANGELOG.markdown | 15 ++ .../functions/gradient_support.rb | 50 +++--- lib/compass/sass_extensions/monkey_patches.rb | 2 +- .../monkey_patches/browser_support.rb | 152 ++++++++++++++++++ .../stylesheets/compass/css/gradients.css | 15 ++ test/fixtures/stylesheets/compass/css/pie.css | 1 - .../stylesheets/compass/sass/gradients.sass | 9 ++ 7 files changed, 223 insertions(+), 21 deletions(-) create mode 100644 lib/compass/sass_extensions/monkey_patches/browser_support.rb diff --git a/doc-src/content/CHANGELOG.markdown b/doc-src/content/CHANGELOG.markdown index 349b9f14..07c24743 100644 --- a/doc-src/content/CHANGELOG.markdown +++ b/doc-src/content/CHANGELOG.markdown @@ -26,6 +26,21 @@ The Documentation for the [latest preview release](http://beta.compass-style.org The CLI options will still override the values set within the config file, but they might inform other values. For instance `compass compile -e production` will have the environment parameter preset to `:production` so that you can set other values in the project accordingly. +* New infrastructure for supporting experimental css3 functions that have the same syntax + across all browsers. It is now possible to configure which browsers support which experimental + functions outside of the compass release cycle by calling + `Compass::BrowserSupport.add_support('function', 'prefix')` and + `Compass::BrowserSupport.remove_support('function', 'prefix')`. + Once properly configured, the browser specific aspect can be inspected + via the `prefixed()` helper function and accessed via the various support + aspect helpers like `-moz()` and `-webkit()`. For example, if you call: + `Compass::BrowserSupport.add_support('image', 'webkit')` in your config + then in your stylesheet, `prefixed(-webkit, image("foo.png", "foo.gif"))` would + return true and `prefixed(-moz, image("foo.png", "foo.gif"))` would return false. + Additionally `-webkit(image("foo.png", "foo.gif"))` will return + `-webkit-image("foo.png", "foo.gif")`. This is very useful for creating mixins + that can support a range of inputs and vary the outputs according to the + support needs. 0.11.alpha.4 (12/08/2010) diff --git a/lib/compass/sass_extensions/functions/gradient_support.rb b/lib/compass/sass_extensions/functions/gradient_support.rb index ee3d27d3..7ebdae4c 100644 --- a/lib/compass/sass_extensions/functions/gradient_support.rb +++ b/lib/compass/sass_extensions/functions/gradient_support.rb @@ -133,29 +133,29 @@ module Compass::SassExtensions::Functions::GradientSupport # Check if any of the arguments passed have a tendency towards vendor prefixing. def prefixed(prefix, *args) - method = prefix.value.sub(/^-/,"to_").to_sym + method = prefix.value.sub(/^-/,"") args.map!{|a| a.is_a?(List) ? a.values : a}.flatten! - Sass::Script::Bool.new(args.any?{|a| a.respond_to?(method)}) + 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(:to_#{prefix}, a)}) + List.new(*args.map! {|a| add_prefix("#{prefix}", a)}) end RUBY end protected - def add_prefix(prefix_method, object) + def add_prefix(prefix, object) if object.is_a?(List) object.class.new(object.value.map{|e| - add_prefix(prefix_method, e) + add_prefix(prefix, e) }) - elsif object.respond_to?(prefix_method) + elsif object.respond_to?(:supports?) && object.supports?(prefix) object.options = options - object.send(prefix_method) + object.send(:"to_#{prefix_method}") else object end @@ -269,33 +269,31 @@ module Compass::SassExtensions::Functions::GradientSupport Sass::Script::List.new list.values[start_index..end_index], list.separator end - # Check if any of the arguments passed have a tendency towards vendor prefixing. + # Check if any of the arguments passed have require the vendor prefix. def prefixed(prefix, *args) - method = prefix.value.sub(/^-/,"to_").to_sym - 2.times do - args.map!{|a| a.is_a?(Sass::Script::List) ? a.value : a}.flatten! - end - Sass::Script::Bool.new(args.any?{|a| a.respond_to?(method)}) + 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(:to_#{prefix}, a)}, :comma) + Sass::Script::List.new(args.map! {|a| add_prefix("#{prefix}", a)}, :comma) end RUBY end protected - def add_prefix(prefix_method, object) + def add_prefix(prefix, object) if object.is_a?(Sass::Script::List) Sass::Script::List.new(object.value.map{|e| - add_prefix(prefix_method, e) + add_prefix(prefix, e) }, object.separator) - elsif object.respond_to?(prefix_method) + elsif object.respond_to?(:supports?) && object.supports?(prefix) object.options = options - object.send(prefix_method) + object.send(:"to_#{prefix}") else object end @@ -317,7 +315,6 @@ module Compass::SassExtensions::Functions::GradientSupport end - class ColorStop < Sass::Script::Literal attr_accessor :color, :stop def children @@ -349,6 +346,8 @@ 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 @@ -372,6 +371,12 @@ module Compass::SassExtensions::Functions::GradientSupport s << color_stops.to_s(options) s << ")" end + def supports?(aspect) + GRADIENT_ASPECTS.include?(aspect) + end + def has_aspect? + true + end def to_webkit(options = self.options) args = [ grad_point(position_and_angle || _center_position), @@ -421,6 +426,12 @@ module Compass::SassExtensions::Functions::GradientSupport s << color_stops.to_s(options) s << ")" end + def supports?(aspect) + GRADIENT_ASPECTS.include?(aspect) + end + def has_aspect? + true + end def to_webkit(options = self.options) args = [] args << grad_point(position_and_angle || Sass::Script::String.new("top")) @@ -726,6 +737,7 @@ EOS end end + class LinearGradient < Sass::Script::Literal include Functions include Compass::SassExtensions::Functions::Constants diff --git a/lib/compass/sass_extensions/monkey_patches.rb b/lib/compass/sass_extensions/monkey_patches.rb index f0c04196..9762ac31 100644 --- a/lib/compass/sass_extensions/monkey_patches.rb +++ b/lib/compass/sass_extensions/monkey_patches.rb @@ -1,3 +1,3 @@ -%w(traversal).each do |patch| +%w(traversal browser_support).each do |patch| require "compass/sass_extensions/monkey_patches/#{patch}" end diff --git a/lib/compass/sass_extensions/monkey_patches/browser_support.rb b/lib/compass/sass_extensions/monkey_patches/browser_support.rb new file mode 100644 index 00000000..c76aa90c --- /dev/null +++ b/lib/compass/sass_extensions/monkey_patches/browser_support.rb @@ -0,0 +1,152 @@ +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) + return true if Compass::BrowserSupport.supports?(name, aspect) + children.any? {|child| child.respond_to?(:supports?) && child.supports?(aspect) } + end + + def has_aspect?(children = nil) + children ||= self.children + return true if Compass::BrowserSupport.has_aspect?(name) + children.any? {|child| child.respond_to?(:has_aspect?) && child.has_aspect? } + end + end + + class CrossBrowserFunctionCall < Literal + + attr_accessor :name, :args + + include HasSimpleCrossBrowserFunctionSupport + + def initialize(name, args) + self.name = name + self.args = args + end + + def children + args + end + + def inspect + to_s + end + + def to_s(options = self.options) + s = "#{name}(#{args.join(", ")})" + end + + %w(webkit moz o ms svg pie css2).each do |prefix| + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def to_#{prefix}(options = self.options) + prefixed_args = args.map do |arg| + arg.respond_to?(:to_#{prefix}) ? arg.to_#{prefix}(options) : arg + end + prefixed_name = if Compass::BrowserSupport.supports?(name, "#{prefix}") + "-#{prefix}-\#{name}" + else + name + end + contents = prefixed_args.join(', ') + if contents.size > 0 + opts(Sass::Script::String.new("\#{prefixed_name}(\#{contents})")) + else + opts(Sass::Script::String.new("")) + end + end + RUBY + end + + end + + class Funcall < Node + include HasSimpleCrossBrowserFunctionSupport + + alias sass_to_literal to_literal + + def to_literal(args) + if has_aspect?(args) + CrossBrowserFunctionCall.new(name, args) + else + sass_to_literal(args) + end + end + end + + class List < Literal + def supports?(aspect) + children.any? {|child| child.respond_to?(:supports?) && child.supports?(aspect) } + end + + def has_aspect? + children.any? {|child| child.respond_to?(:has_aspect?) && child.has_aspect? } + end + end + +end diff --git a/test/fixtures/stylesheets/compass/css/gradients.css b/test/fixtures/stylesheets/compass/css/gradients.css index 60f1cdba..c3586f46 100644 --- a/test/fixtures/stylesheets/compass/css/gradients.css +++ b/test/fixtures/stylesheets/compass/css/gradients.css @@ -96,6 +96,21 @@ background-image: -moz-radial-gradient(#dddddd, #aaaaaa 100px); background-image: radial-gradient(#dddddd, #aaaaaa 100px); } +.image-fallback { + background-image: image(-webkit-gradient(radial, 50% 50%, 0, 50% 50%, 100, color-stop(0%, #dddddd), color-stop(100%, #aaaaaa)), url('/images/4x6.png?busted=true'), #cc0000); + background-image: image(-moz-radial-gradient(#dddddd, #aaaaaa 100px), url('/images/4x6.png?busted=true'), #cc0000); + background-image: image(radial-gradient(#dddddd, #aaaaaa 100px), url('/images/4x6.png?busted=true'), #cc0000); } + +.cross-fade { + background-image: cross-fade(-webkit-gradient(radial, 50% 50%, 0, 50% 50%, 100, color-stop(0%, #dddddd), color-stop(100%, #aaaaaa)), url('/images/4x6.png?busted=true')); + background-image: cross-fade(-moz-radial-gradient(#dddddd, #aaaaaa 100px), url('/images/4x6.png?busted=true')); + background-image: cross-fade(radial-gradient(#dddddd, #aaaaaa 100px), url('/images/4x6.png?busted=true')); } + +.unknown-function-wrapper { + background: foo(-webkit-gradient(radial, 50% 50%, 0, 50% 50%, 100, color-stop(0%, #dddddd), color-stop(100%, #aaaaaa))); + background: foo(-moz-radial-gradient(#dddddd, #aaaaaa 100px)); + background: foo(radial-gradient(#dddddd, #aaaaaa 100px)); } + .linear-1 { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #dddddd), color-stop(100%, #aaaaaa)); background-image: -moz-linear-gradient(top, #dddddd, #aaaaaa); diff --git a/test/fixtures/stylesheets/compass/css/pie.css b/test/fixtures/stylesheets/compass/css/pie.css index d7898353..c911df07 100644 --- a/test/fixtures/stylesheets/compass/css/pie.css +++ b/test/fixtures/stylesheets/compass/css/pie.css @@ -1,4 +1,3 @@ -@charset "UTF-8"; .pie-element, .bordered, .gradient { behavior: url('/tmp/PIE.htc'); position: relative; } diff --git a/test/fixtures/stylesheets/compass/sass/gradients.sass b/test/fixtures/stylesheets/compass/sass/gradients.sass index 8c8a656c..fd0ec96a 100644 --- a/test/fixtures/stylesheets/compass/sass/gradients.sass +++ b/test/fixtures/stylesheets/compass/sass/gradients.sass @@ -58,6 +58,15 @@ $experimental-support-for-svg: false .bg-radial-gradient-no-position +background-image(radial-gradient(#ddd, #aaa 100px)) +.image-fallback + +background-image(image(radial-gradient(#ddd, #aaa 100px), image-url("4x6.png"), #c00)) + +.cross-fade + +background-image(cross-fade(radial-gradient(#ddd, #aaa 100px), image-url("4x6.png"))) + +.unknown-function-wrapper + +background(foo(radial-gradient(#ddd, #aaa 100px))) + .linear-1 +linear-gradient(color-stops(#dddddd, #aaaaaa))