haml-edge 2.3.100 → 2.3.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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