haml-edge 2.3.100 → 2.3.148

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.yardopts +3 -0
  2. data/EDGE_GEM_VERSION +1 -1
  3. data/Rakefile +14 -2
  4. data/VERSION +1 -1
  5. data/extra/haml-mode.el +97 -11
  6. data/extra/sass-mode.el +2 -2
  7. data/lib/haml/engine.rb +2 -2
  8. data/lib/haml/exec.rb +121 -25
  9. data/lib/haml/filters.rb +1 -1
  10. data/lib/haml/helpers/action_view_mods.rb +2 -1
  11. data/lib/haml/helpers/xss_mods.rb +43 -13
  12. data/lib/haml/helpers.rb +38 -17
  13. data/lib/haml/html.rb +13 -4
  14. data/lib/haml/precompiler.rb +24 -3
  15. data/lib/haml/template/plugin.rb +7 -3
  16. data/lib/haml/template.rb +3 -3
  17. data/lib/haml/util.rb +40 -0
  18. data/lib/sass/callbacks.rb +50 -0
  19. data/lib/sass/css.rb +1 -1
  20. data/lib/sass/engine.rb +45 -5
  21. data/lib/sass/error.rb +6 -3
  22. data/lib/sass/files.rb +8 -1
  23. data/lib/sass/plugin/rails.rb +2 -2
  24. data/lib/sass/plugin.rb +260 -28
  25. data/lib/sass/script/color.rb +216 -30
  26. data/lib/sass/script/functions.rb +356 -74
  27. data/lib/sass/script/lexer.rb +7 -4
  28. data/lib/sass/script/number.rb +2 -0
  29. data/lib/sass/script/parser.rb +1 -1
  30. data/lib/sass/script.rb +3 -0
  31. data/lib/sass/tree/node.rb +1 -1
  32. data/lib/sass/tree/root_node.rb +6 -0
  33. data/lib/sass/tree/rule_node.rb +1 -0
  34. data/lib/sass.rb +4 -0
  35. data/test/haml/engine_test.rb +25 -0
  36. data/test/haml/helper_test.rb +81 -1
  37. data/test/haml/html2haml_test.rb +13 -0
  38. data/test/haml/spec/README.md +97 -0
  39. data/test/haml/spec/lua_haml_spec.lua +30 -0
  40. data/test/haml/spec/ruby_haml_test.rb +19 -0
  41. data/test/haml/spec/tests.json +534 -0
  42. data/test/haml/spec_test.rb +0 -0
  43. data/test/haml/template_test.rb +18 -4
  44. data/test/haml/util_test.rb +0 -0
  45. data/test/sass/callbacks_test.rb +61 -0
  46. data/test/sass/css2sass_test.rb +1 -0
  47. data/test/sass/engine_test.rb +70 -14
  48. data/test/sass/functions_test.rb +223 -3
  49. data/test/sass/plugin_test.rb +193 -25
  50. data/test/sass/results/options.css +1 -0
  51. data/test/sass/script_test.rb +5 -5
  52. data/test/sass/templates/options.sass +2 -0
  53. data/test/test_helper.rb +12 -5
  54. metadata +19 -9
data/lib/sass/plugin.rb CHANGED
@@ -1,4 +1,8 @@
1
+ require 'fileutils'
2
+ require 'rbconfig'
3
+
1
4
  require 'sass'
5
+ require 'sass/callbacks'
2
6
 
3
7
  module Sass
4
8
  # This module handles the compilation of Sass files.
@@ -10,8 +14,23 @@ module Sass
10
14
  # All Rack-enabled frameworks are supported out of the box.
11
15
  # The plugin is {file:SASS_REFERENCE.md#rails_merb_plugin automatically activated for Rails and Merb}.
12
16
  # Other frameworks must enable it explicitly; see {Sass::Plugin::Rack}.
17
+ #
18
+ # This module has a large set of callbacks available
19
+ # to allow users to run code (such as logging) when certain things happen.
20
+ # All callback methods are of the form `on_#{name}`,
21
+ # and they all take a block that's called when the given action occurs.
22
+ #
23
+ # @example Using a callback
24
+ # Sass::Plugin.on_updating_stylesheet do |template, css|
25
+ # puts "Compiling #{template} to #{css}"
26
+ # end
27
+ # Sass::Plugin.update_stylesheets
28
+ # #=> Compiling app/sass/screen.sass to public/stylesheets/screen.css
29
+ # #=> Compiling app/sass/print.sass to public/stylesheets/print.css
30
+ # #=> Compiling app/sass/ie.sass to public/stylesheets/ie.css
13
31
  module Plugin
14
32
  include Haml::Util
33
+ include Sass::Callbacks
15
34
  extend self
16
35
 
17
36
  @options = {
@@ -22,6 +41,114 @@ module Sass
22
41
  }
23
42
  @checked_for_updates = false
24
43
 
44
+ # Register a callback to be run before stylesheets are mass-updated.
45
+ # This is run whenever \{#update\_stylesheets} is called,
46
+ # unless the \{file:SASS_REFERENCE.md#never_update-option `:never_update` option}
47
+ # is enabled.
48
+ #
49
+ # @yield [individual_files]
50
+ # @yieldparam individual_files [<(String, String)>]
51
+ # Individual files to be updated, in addition to the directories
52
+ # specified in the options.
53
+ # The first element of each pair is the source file,
54
+ # the second is the target CSS file.
55
+ define_callback :updating_stylesheets
56
+
57
+ # Register a callback to be run before a single stylesheet is updated.
58
+ # The callback is only run if the stylesheet is guaranteed to be updated;
59
+ # if the CSS file is fresh, this won't be run.
60
+ #
61
+ # Even if the \{file:SASS_REFERENCE.md#full_exception-option `:full_exception` option}
62
+ # is enabled, this callback won't be run
63
+ # when an exception CSS file is being written.
64
+ # To run an action for those files, use \{#on\_compilation\_error}.
65
+ #
66
+ # @yield [template, css]
67
+ # @yieldparam template [String]
68
+ # The location of the Sass file being updated.
69
+ # @yieldparam css [String]
70
+ # The location of the CSS file being generated.
71
+ define_callback :updating_stylesheet
72
+
73
+ # Register a callback to be run when Sass decides not to update a stylesheet.
74
+ # In particular, the callback is run when Sass finds that
75
+ # the template file and none of its dependencies
76
+ # have been modified since the last compilation.
77
+ #
78
+ # Note that this is **not** run when the
79
+ # \{file:SASS_REFERENCE.md#never-update_option `:never_update` option} is set,
80
+ # nor when Sass decides not to compile a partial.
81
+ #
82
+ # @yield [template, css]
83
+ # @yieldparam template [String]
84
+ # The location of the Sass file not being updated.
85
+ # @yieldparam css [String]
86
+ # The location of the CSS file not being generated.
87
+ define_callback :not_updating_stylesheet
88
+
89
+ # Register a callback to be run when there's an error
90
+ # compiling a Sass file.
91
+ # This could include not only errors in the Sass document,
92
+ # but also errors accessing the file at all.
93
+ #
94
+ # @yield [error, template, css]
95
+ # @yieldparam error [Exception] The exception that was raised.
96
+ # @yieldparam template [String]
97
+ # The location of the Sass file being updated.
98
+ # @yieldparam css [String]
99
+ # The location of the CSS file being generated.
100
+ define_callback :compilation_error
101
+
102
+ # Register a callback to be run when Sass creates a directory
103
+ # into which to put CSS files.
104
+ #
105
+ # Note that even if multiple levels of directories need to be created,
106
+ # the callback may only be run once.
107
+ # For example, if "foo/" exists and "foo/bar/baz/" needs to be created,
108
+ # this may only be run for "foo/bar/baz/".
109
+ # This is not a guarantee, however;
110
+ # it may also be run for "foo/bar/".
111
+ #
112
+ # @yield [dirname]
113
+ # @yieldparam dirname [String]
114
+ # The location of the directory that was created.
115
+ define_callback :creating_directory
116
+
117
+ # Register a callback to be run when Sass detects
118
+ # that a template has been modified.
119
+ # This is only run when using \{#watch}.
120
+ #
121
+ # @yield [template]
122
+ # @yieldparam template [String]
123
+ # The location of the template that was modified.
124
+ define_callback :template_modified
125
+
126
+ # Register a callback to be run when Sass detects
127
+ # that a new template has been created.
128
+ # This is only run when using \{#watch}.
129
+ #
130
+ # @yield [template]
131
+ # @yieldparam template [String]
132
+ # The location of the template that was created.
133
+ define_callback :template_created
134
+
135
+ # Register a callback to be run when Sass detects
136
+ # that a template has been deleted.
137
+ # This is only run when using \{#watch}.
138
+ #
139
+ # @yield [template]
140
+ # @yieldparam template [String]
141
+ # The location of the template that was deleted.
142
+ define_callback :template_deleted
143
+
144
+ # Register a callback to be run when Sass deletes a CSS file.
145
+ # This happens when the corresponding Sass file has been deleted.
146
+ #
147
+ # @yield [filename]
148
+ # @yieldparam filename [String]
149
+ # The location of the CSS file that was deleted.
150
+ define_callback :deleting_css
151
+
25
152
  # Whether or not Sass has **ever** checked if the stylesheets need to be updated
26
153
  # (in this Ruby instance).
27
154
  #
@@ -67,58 +194,163 @@ module Sass
67
194
  #
68
195
  # Checks each Sass file in {file:SASS_REFERENCE.md#template_location-option `:template_location`}
69
196
  # to see if it's been modified more recently than the corresponding CSS file
70
- # in {file:SASS_REFERENCE.md#css_location-option} `:css_location`}.
197
+ # in {file:SASS_REFERENCE.md#css_location-option `:css_location`}.
71
198
  # If it has, it updates the CSS file.
72
- def update_stylesheets
199
+ #
200
+ # @param individual_files [Array<(String, String)>]
201
+ # A list of files to check for updates
202
+ # **in addition to those specified by the
203
+ # {file:SASS_REFERENCE.md#template_location-option `:template_location` option}.**
204
+ # The first string in each pair is the location of the Sass file,
205
+ # the second is the location of the CSS file that it should be compiled to.
206
+ def update_stylesheets(individual_files = [])
73
207
  return if options[:never_update]
74
208
 
209
+ run_updating_stylesheets individual_files
210
+
211
+ individual_files.each {|t, c| update_stylesheet(t, c)}
212
+
75
213
  @checked_for_updates = true
76
214
  template_locations.zip(css_locations).each do |template_location, css_location|
77
215
 
78
216
  Dir.glob(File.join(template_location, "**", "*.sass")).each do |file|
79
217
  # Get the relative path to the file with no extension
80
- name = file.sub(template_location + "/", "")[0...-5]
218
+ name = file.sub(template_location.sub(/\/*$/, '/'), "")[0...-5]
219
+
220
+ next if forbid_update?(name)
81
221
 
82
- if !forbid_update?(name) && (options[:always_update] || stylesheet_needs_update?(name, template_location, css_location))
83
- update_stylesheet(name, template_location, css_location)
222
+ filename = template_filename(name, template_location)
223
+ css = css_filename(name, css_location)
224
+ if options[:always_update] || stylesheet_needs_update?(name, template_location, css_location)
225
+ update_stylesheet filename, css
226
+ else
227
+ run_not_updating_stylesheet filename, css
84
228
  end
85
229
  end
86
230
  end
87
231
  end
88
232
 
89
- private
233
+ # Watches the template directory (or directories)
234
+ # and updates the CSS files whenever the related Sass files change.
235
+ # `watch` never returns.
236
+ #
237
+ # Whenever a change is detected to a Sass file in
238
+ # {file:SASS_REFERENCE.md#template_location-option `:template_location`},
239
+ # the corresponding CSS file in {file:SASS_REFERENCE.md#css_location-option `:css_location`}
240
+ # will be recompiled.
241
+ # The CSS files of any Sass files that import the changed file will also be recompiled.
242
+ #
243
+ # Before the watching starts in earnest, `watch` calls \{#update\_stylesheets}.
244
+ #
245
+ # Note that `watch` uses the [FSSM](http://github.com/ttilley/fssm) library
246
+ # to monitor the filesystem for changes.
247
+ # FSSM isn't loaded until `watch` is run.
248
+ # The version of FSSM distributed with Sass is loaded by default,
249
+ # but if another version has already been loaded that will be used instead.
250
+ #
251
+ # @param individual_files [Array<(String, String)>]
252
+ # A list of files to watch for updates
253
+ # **in addition to those specified by the
254
+ # {file:SASS_REFERENCE.md#template_location-option `:template_location` option}.**
255
+ # The first string in each pair is the location of the Sass file,
256
+ # the second is the location of the CSS file that it should be compiled to.
257
+ def watch(individual_files = [])
258
+ update_stylesheets(individual_files)
90
259
 
91
- def update_stylesheet(name, template_location, css_location)
92
- css = css_filename(name, css_location)
93
- File.delete(css) if File.exists?(css)
260
+ begin
261
+ require 'fssm'
262
+ rescue LoadError => e
263
+ e.message << "\n" <<
264
+ if File.exists?(scope(".git"))
265
+ 'Run "git submodule update --init" to get the recommended version.'
266
+ else
267
+ 'Run "gem install fssm" to get it.'
268
+ end
269
+ raise e
270
+ end
94
271
 
95
- filename = template_filename(name, template_location)
96
- result = begin
97
- Sass::Files.tree_for(filename, engine_options(:css_filename => css, :filename => filename)).render
98
- rescue Exception => e
99
- Sass::SyntaxError.exception_to_css(e, options)
100
- end
272
+ # TODO: Keep better track of what depends on what
273
+ # so we don't have to run a global update every time anything changes.
274
+ FSSM.monitor do |mon|
275
+ template_locations.zip(css_locations).each do |template_location, css_location|
276
+ mon.path template_location do |path|
277
+ path.glob '**/*.sass'
101
278
 
102
- # Create any directories that might be necessary
103
- mkpath(css_location, name)
279
+ path.update do |base, relative|
280
+ run_template_modified File.join(base, relative)
281
+ update_stylesheets(individual_files)
282
+ end
104
283
 
105
- # Finally, write the file
106
- File.open(css, 'w') do |file|
107
- file.print(result)
284
+ path.create do |base, relative|
285
+ run_template_created File.join(base, relative)
286
+ update_stylesheets(individual_files)
287
+ end
288
+
289
+ path.delete do |base, relative|
290
+ run_template_deleted File.join(base, relative)
291
+ css = File.join(css_location, relative.gsub(/\.sass$/, '.css'))
292
+ try_delete_css css
293
+ update_stylesheets(individual_files)
294
+ end
295
+ end
296
+ end
297
+
298
+ individual_files.each do |template, css|
299
+ mon.file template do |path|
300
+ path.update do
301
+ run_template_modified template
302
+ update_stylesheets(individual_files)
303
+ end
304
+
305
+ path.create do
306
+ run_template_created template
307
+ update_stylesheets(individual_files)
308
+ end
309
+
310
+ path.delete do
311
+ run_template_deleted template
312
+ try_delete_css css
313
+ update_stylesheets(individual_files)
314
+ end
315
+ end
316
+ end
317
+ end
318
+ end
319
+
320
+ private
321
+
322
+ def update_stylesheet(filename, css)
323
+ dir = File.dirname(css)
324
+ unless File.exists?(dir)
325
+ run_creating_directory dir
326
+ FileUtils.mkdir_p dir
327
+ end
328
+
329
+ begin
330
+ result = Sass::Files.tree_for(filename, engine_options(:css_filename => css, :filename => filename)).render
331
+ rescue Exception => e
332
+ run_compilation_error e, filename, css
333
+ result = Sass::SyntaxError.exception_to_css(e, options)
334
+ else
335
+ run_updating_stylesheet filename, css
108
336
  end
337
+
338
+ # Finally, write the file
339
+ flag = 'w'
340
+ flag = 'wb' if RbConfig::CONFIG['host_os'] =~ /mswin|windows/i && options[:unix_newlines]
341
+ File.open(css, flag) {|file| file.print(result)}
109
342
  end
110
-
111
- # Create any successive directories required to be able to write a file to: File.join(base,name)
112
- def mkpath(base, name)
113
- dirs = [base]
114
- name.split(File::SEPARATOR)[0...-1].each { |dir| dirs << File.join(dirs[-1],dir) }
115
- dirs.each { |dir| Dir.mkdir(dir) unless File.exist?(dir) }
343
+
344
+ def try_delete_css(css)
345
+ return unless File.exists?(css)
346
+ run_deleting_css css
347
+ File.delete css
116
348
  end
117
349
 
118
350
  def load_paths(opts = options)
119
351
  (opts[:load_paths] || []) + template_locations
120
352
  end
121
-
353
+
122
354
  def template_locations
123
355
  location = (options[:template_location] || File.join(options[:css_location],'sass'))
124
356
  if location.is_a?(String)
@@ -127,7 +359,7 @@ module Sass
127
359
  location.to_a.map { |l| l.first }
128
360
  end
129
361
  end
130
-
362
+
131
363
  def css_locations
132
364
  if options[:template_location] && !options[:template_location].is_a?(String)
133
365
  options[:template_location].to_a.map { |l| l.last }
@@ -2,10 +2,24 @@ require 'sass/script/literal'
2
2
 
3
3
  module Sass::Script
4
4
  # A SassScript object representing a CSS color.
5
+ #
6
+ # A color may be represented internally as RGBA, HSLA, or both.
7
+ # It's originally represented as whatever its input is;
8
+ # if it's created with RGB values, it's represented as RGBA,
9
+ # and if it's created with HSL values, it's represented as HSLA.
10
+ # Once a property is accessed that requires the other representation --
11
+ # for example, \{#red} for an HSL color --
12
+ # that component is calculated and cached.
13
+ #
14
+ # The alpha channel of a color is independent of its RGB or HSL representation.
15
+ # It's always stored, as 1 if nothing else is specified.
16
+ # If only the alpha channel is modified using \{#with},
17
+ # the cached RGB and HSL values are retained.
5
18
  class Color < Literal
6
19
  class << self; include Haml::Util; end
7
20
 
8
21
  # A hash from color names to `[red, green, blue]` value arrays.
22
+ # @private
9
23
  HTML4_COLORS = map_vals({
10
24
  'black' => 0x000000,
11
25
  'silver' => 0xc0c0c0,
@@ -25,52 +39,139 @@ module Sass::Script
25
39
  'aqua' => 0x00ffff
26
40
  }) {|color| (0..2).map {|n| color >> (n << 3) & 0xff}.reverse}
27
41
  # A hash from `[red, green, blue]` value arrays to color names.
42
+ # @private
28
43
  HTML4_COLORS_REVERSE = map_hash(HTML4_COLORS) {|k, v| [v, k]}
29
44
 
30
- # Constructs an RGB or RGBA color object.
31
- # The RGB values must be between 0 and 255,
32
- # and the alpha value is generally expected to be between 0 and 1.
33
- # However, the alpha value can be greater than 1
34
- # in order to allow it to be used for color multiplication.
35
- #
36
- # @param rgba [Array<Numeric>] A three-element array of the red, green, blue,
37
- # and optionally alpha values (respectively) of the color
38
- # @raise [Sass::SyntaxError] if any color value isn't between 0 and 255,
39
- # or the alpha value is negative
40
- def initialize(rgba)
41
- @red, @green, @blue = rgba[0...3].map {|c| c.to_i}
42
- @alpha = rgba[3] ? rgba[3].to_f : 1
45
+ # Constructs an RGB or HSL color object,
46
+ # optionally with an alpha channel.
47
+ #
48
+ # The RGB values must be between 0 and 255.
49
+ # The saturation and lightness values must be between 0 and 100.
50
+ # The alpha value must be between 0 and 1.
51
+ #
52
+ # @raise [Sass::SyntaxError] if any color value isn't in the specified range
53
+ #
54
+ # @overload initialize(attrs)
55
+ # The attributes are specified as a hash.
56
+ # This hash must contain either `:hue`, `:saturation`, and `:value` keys,
57
+ # or `:red`, `:green`, and `:blue` keys.
58
+ # It cannot contain both HSL and RGB keys.
59
+ # It may also optionally contain an `:alpha` key.
60
+ #
61
+ # @param attrs [{Symbol => Numeric}] A hash of color attributes to values
62
+ # @raise [ArgumentError] if not enough attributes are specified,
63
+ # or both RGB and HSL attributes are specified
64
+ #
65
+ # @overload initialize(rgba)
66
+ # The attributes are specified as an array.
67
+ # This overload only supports RGB or RGBA colors.
68
+ #
69
+ # @param rgba [Array<Numeric>] A three- or four-element array
70
+ # of the red, green, blue, and optionally alpha values (respectively)
71
+ # of the color
72
+ # @raise [ArgumentError] if not enough attributes are specified
73
+ def initialize(attrs, allow_both_rgb_and_hsl = false)
43
74
  super(nil)
44
75
 
45
- unless rgb.all? {|c| (0..255).include?(c)}
46
- raise Sass::SyntaxError.new("Color values must be between 0 and 255")
76
+ if attrs.is_a?(Array)
77
+ unless (3..4).include?(attrs.size)
78
+ raise ArgumentError.new("Color.new(array) expects a three- or four-element array")
79
+ end
80
+
81
+ red, green, blue = attrs[0...3].map {|c| c.to_i}
82
+ @attrs = {:red => red, :green => green, :blue => blue}
83
+ @attrs[:alpha] = attrs[3] ? attrs[3].to_f : 1
84
+ else
85
+ attrs = attrs.reject {|k, v| v.nil?}
86
+ hsl = [:hue, :saturation, :lightness] & attrs.keys
87
+ rgb = [:red, :green, :blue] & attrs.keys
88
+ if !allow_both_rgb_and_hsl && !hsl.empty? && !rgb.empty?
89
+ raise ArgumentError.new("Color.new(hash) may not have both HSL and RGB keys specified")
90
+ elsif hsl.empty? && rgb.empty?
91
+ raise ArgumentError.new("Color.new(hash) must have either HSL or RGB keys specified")
92
+ elsif !hsl.empty? && hsl.size != 3
93
+ raise ArgumentError.new("Color.new(hash) must have all three HSL values specified")
94
+ elsif !rgb.empty? && rgb.size != 3
95
+ raise ArgumentError.new("Color.new(hash) must have all three RGB values specified")
96
+ end
97
+
98
+ @attrs = attrs
99
+ @attrs[:hue] %= 360 if @attrs[:hue]
100
+ @attrs[:alpha] ||= 1
101
+ end
102
+
103
+ [:red, :green, :blue].each do |k|
104
+ next if @attrs[k].nil?
105
+ @attrs[k] = @attrs[k].to_i
106
+ next if (0..255).include?(@attrs[k])
107
+ raise Sass::SyntaxError.new("#{k.to_s.capitalize} value must be between 0 and 255")
108
+ end
109
+
110
+ [:saturation, :lightness].each do |k|
111
+ next if @attrs[k].nil? || (0..100).include?(@attrs[k])
112
+ raise Sass::SyntaxError.new("#{k.to_s.capitalize} must be between 0 and 100")
47
113
  end
48
114
 
49
- unless (0..1).include?(alpha)
50
- raise Sass::SyntaxError.new("Color opacity value must between 0 and 1")
115
+ unless (0..1).include?(@attrs[:alpha])
116
+ raise Sass::SyntaxError.new("Alpha channel must between 0 and 1")
51
117
  end
52
118
  end
53
119
 
54
120
  # The red component of the color.
55
121
  #
56
122
  # @return [Fixnum]
57
- attr_reader :red
123
+ def red
124
+ hsl_to_rgb!
125
+ @attrs[:red]
126
+ end
58
127
 
59
128
  # The green component of the color.
60
129
  #
61
130
  # @return [Fixnum]
62
- attr_reader :green
131
+ def green
132
+ hsl_to_rgb!
133
+ @attrs[:green]
134
+ end
63
135
 
64
136
  # The blue component of the color.
65
137
  #
66
138
  # @return [Fixnum]
67
- attr_reader :blue
139
+ def blue
140
+ hsl_to_rgb!
141
+ @attrs[:blue]
142
+ end
143
+
144
+ # The hue component of the color.
145
+ #
146
+ # @return [Numeric]
147
+ def hue
148
+ rgb_to_hsl!
149
+ @attrs[:hue]
150
+ end
151
+
152
+ # The saturation component of the color.
153
+ #
154
+ # @return [Numeric]
155
+ def saturation
156
+ rgb_to_hsl!
157
+ @attrs[:saturation]
158
+ end
159
+
160
+ # The lightness component of the color.
161
+ #
162
+ # @return [Numeric]
163
+ def lightness
164
+ rgb_to_hsl!
165
+ @attrs[:lightness]
166
+ end
68
167
 
69
168
  # The alpha channel (opacity) of the color.
70
169
  # This is 1 unless otherwise defined.
71
170
  #
72
171
  # @return [Fixnum]
73
- attr_reader :alpha
172
+ def alpha
173
+ @attrs[:alpha]
174
+ end
74
175
 
75
176
  # Returns whether this color object is translucent;
76
177
  # that is, whether the alpha channel is non-1.
@@ -80,13 +181,13 @@ module Sass::Script
80
181
  alpha < 1
81
182
  end
82
183
 
83
- # @deprecated This will be removed in version 2.6.
184
+ # @deprecated This will be removed in version 3.2.
84
185
  # @see #rgb
85
186
  def value
86
187
  warn <<END
87
188
  DEPRECATION WARNING:
88
189
  The Sass::Script::Color #value attribute is deprecated and will be
89
- removed in version 2.6. Use the #rgb attribute instead.
190
+ removed in version 3.2. Use the #rgb attribute instead.
90
191
  END
91
192
  rgb
92
193
  end
@@ -99,6 +200,14 @@ END
99
200
  [red, green, blue].freeze
100
201
  end
101
202
 
203
+ # Returns the hue, saturation, and lightness components of the color.
204
+ #
205
+ # @return [Array<Fixnum>] A frozen three-element array of the
206
+ # hue, saturation, and lightness values (respectively) of the color
207
+ def hsl
208
+ [hue, saturation, lightness].freeze
209
+ end
210
+
102
211
  # The SassScript `==` operation.
103
212
  # **Note that this returns a {Sass::Script::Bool} object,
104
213
  # not a Ruby boolean**.
@@ -112,6 +221,7 @@ END
112
221
  end
113
222
 
114
223
  # Returns a copy of this color with one or more channels changed.
224
+ # RGB or HSL colors may be changed, but not both at once.
115
225
  #
116
226
  # For example:
117
227
  #
@@ -119,19 +229,36 @@ END
119
229
  # #=> rgb(10, 40, 30)
120
230
  # Color.new([126, 126, 126]).with(:red => 0, :green => 255)
121
231
  # #=> rgb(0, 255, 126)
232
+ # Color.new([255, 0, 127]).with(:saturation => 60)
233
+ # #=> rgb(204, 51, 127)
122
234
  # Color.new([1, 2, 3]).with(:alpha => 0.4)
123
235
  # #=> rgba(1, 2, 3, 0.4)
124
236
  #
125
237
  # @param attrs [{Symbol => Numeric}]
126
- # A map of channel names (`:red`, `:green`, `:blue`, or `:alpha`) to values
238
+ # A map of channel names (`:red`, `:green`, `:blue`,
239
+ # `:hue`, `:saturation`, `:lightness`, or `:alpha`) to values
127
240
  # @return [Color] The new Color object
241
+ # @raise [ArgumentError] if both RGB and HSL keys are specified
128
242
  def with(attrs)
129
- Color.new([
130
- attrs[:red] || red,
131
- attrs[:green] || green,
132
- attrs[:blue] || blue,
133
- attrs[:alpha] || alpha,
134
- ])
243
+ attrs = attrs.reject {|k, v| v.nil?}
244
+ hsl = !([:hue, :saturation, :lightness] & attrs.keys).empty?
245
+ rgb = !([:red, :green, :blue] & attrs.keys).empty?
246
+ if hsl && rgb
247
+ raise ArgumentError.new("Color#with may not have both HSL and RGB keys specified")
248
+ end
249
+
250
+ if hsl
251
+ [:hue, :saturation, :lightness].each {|k| attrs[k] ||= send(k)}
252
+ elsif rgb
253
+ [:red, :green, :blue].each {|k| attrs[k] ||= send(k)}
254
+ else
255
+ # If we're just changing the alpha channel,
256
+ # keep all the HSL/RGB stuff we've calculated
257
+ attrs = @attrs.merge(attrs)
258
+ end
259
+ attrs[:alpha] ||= alpha
260
+
261
+ Color.new(attrs, :allow_both_rgb_and_hsl)
135
262
  end
136
263
 
137
264
  # The SassScript `+` operation.
@@ -299,5 +426,64 @@ END
299
426
 
300
427
  with(:red => result[0], :green => result[1], :blue => result[2])
301
428
  end
429
+
430
+ def hsl_to_rgb!
431
+ return if @attrs[:red] && @attrs[:blue] && @attrs[:green]
432
+
433
+ h = @attrs[:hue] / 360.0
434
+ s = @attrs[:saturation] / 100.0
435
+ l = @attrs[:lightness] / 100.0
436
+
437
+ # Algorithm from the CSS3 spec: http://www.w3.org/TR/css3-color/#hsl-color.
438
+ m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s
439
+ m1 = l * 2 - m2
440
+ @attrs[:red], @attrs[:green], @attrs[:blue] = [
441
+ hue_to_rgb(m1, m2, h + 1.0/3),
442
+ hue_to_rgb(m1, m2, h),
443
+ hue_to_rgb(m1, m2, h - 1.0/3)
444
+ ].map {|c| (c * 0xff).round}
445
+ end
446
+
447
+ def hue_to_rgb(m1, m2, h)
448
+ h += 1 if h < 0
449
+ h -= 1 if h > 1
450
+ return m1 + (m2 - m1) * h * 6 if h * 6 < 1
451
+ return m2 if h * 2 < 1
452
+ return m1 + (m2 - m1) * (2.0/3 - h) * 6 if h * 3 < 2
453
+ return m1
454
+ end
455
+
456
+ def rgb_to_hsl!
457
+ return if @attrs[:hue] && @attrs[:saturation] && @attrs[:lightness]
458
+ r, g, b = [:red, :green, :blue].map {|k| @attrs[k] / 255.0}
459
+
460
+ # Algorithm from http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV
461
+ max = [r, g, b].max
462
+ min = [r, g, b].min
463
+ d = max - min
464
+
465
+ h =
466
+ case max
467
+ when min; 0
468
+ when r; 60 * (g-b)/d
469
+ when g; 60 * (b-r)/d + 120
470
+ when b; 60 * (r-g)/d + 240
471
+ end
472
+
473
+ l = (max + min)/2.0
474
+
475
+ s =
476
+ if max == min
477
+ 0
478
+ elsif l < 0.5
479
+ d/(2*l)
480
+ else
481
+ d/(2 - 2*l)
482
+ end
483
+
484
+ @attrs[:hue] = h % 360
485
+ @attrs[:saturation] = s * 100
486
+ @attrs[:lightness] = l * 100
487
+ end
302
488
  end
303
489
  end