color_contrast_calc 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f14c1b61fcfcbab962811f5d6c2bee98841b5fc88aedbe3cdcb1f66747c7735
4
- data.tar.gz: 60c833fd6e0374f319172ff40307f496f3d3e31b7ae7e77b01105f2283855776
3
+ metadata.gz: 4eca556d535a8e8eda4ad0bf84e1891447b7065823b18cce4b4695f5a7e2beb6
4
+ data.tar.gz: 2548294c153d699792adceb71818b4a670da7f5faca6e48d67a1f511ae389f91
5
5
  SHA512:
6
- metadata.gz: 2406be8e787bc8730cc4297429f37081a21cfde4acd5fc82f53703eccce10f0d1868ce8072ee4f592c2ea7a4a8372c4ca7bdfa97a6866b4d632f30b7c2a49675
7
- data.tar.gz: 226e05b5b73f8f19e7589bed424cb1eb6c14dabfa1b87b2b8e1db84c646ce4110ad3cbd93970746a9df1d8df41fa5286801fea849168ac260e76724d55ab4cca
6
+ metadata.gz: cf7971d631606c8cd89f2c23661cb873f73874877c2668e742683c044f634b508262aca2745077831104bdaeeefb123ee30ca303c9cffbef2bc4298bb7518b35
7
+ data.tar.gz: 67851e719c99d0c047e86f8634c4c7b5ad6511c924b80f2246197bd01e7781c0ff6959dd05f16dba44a43792c916857c4fd698db4081bb6a06a242af5b0dbd1c
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.2
2
+ TargetRubyVersion: 2.3
3
3
  Layout/SpaceInsideBlockBraces:
4
4
  SpaceBeforeBlockParameters: false
5
5
  Layout/EmptyLineAfterGuardClause:
@@ -12,5 +12,5 @@ Style/FormatStringToken:
12
12
  Enabled: false
13
13
  Style/AccessModifierDeclarations:
14
14
  EnforcedStyle: inline
15
- Naming/UncommunicativeMethodParamName:
15
+ Naming/MethodParameterName:
16
16
  MinNameLength: 1
@@ -47,8 +47,8 @@ Or install it yourself as:
47
47
  require 'color_contrast_calc'
48
48
 
49
49
  # Create an instance of Color from a hex code
50
- # (You can pass 'red', [255, 0, 0], 'rgb(255, 0, 0)' or 'hsl(0deg, 100%, 50%)'
51
- # instead of '#ff0000')
50
+ # (You can pass 'red', [255, 0, 0], 'rgb(255, 0, 0)', 'hsl(0deg, 100%, 50%)' or
51
+ # hwb(60deg 0% 0%) instead of '#ff0000')
52
52
  red = ColorContrastCalc.color_from('#ff0000')
53
53
  puts red.class
54
54
  puts red.name
@@ -70,6 +70,17 @@ red
70
70
 
71
71
  ```
72
72
 
73
+ #### `ColorContrastCalc.color_from()`の引数に使える色の表現
74
+
75
+ `ColorContrastCalc.color_from()`の第1引数は以下の形式で指定できます。
76
+
77
+ * RGB値の16進数表記: #ff0, #ffff00, #FF0, etc.
78
+ * RGB値の関数形式表記: rgb(255, 255, 0), rgb(255 255 0), etc.
79
+ * Integerの配列で表したRGB値: [255, 255, 0], etc.
80
+ * HSL値の関数形式表記: hsl(60deg, 100%, 50%), hsl(60 100% 50%), etc.
81
+ * [実験的対応] HWB値の関数形式表記: hwb(60deg 0% 0%), hwb(60 0% 0%), etc.
82
+ * [拡張カラーキーワード](https://www.w3.org/TR/css-color-3/#svg-color): white, black, red, etc.
83
+
73
84
  ### 例1: 2つの色のコントラスト比を計算する
74
85
 
75
86
  #### 1.1: 最も簡便なやり方
data/README.md CHANGED
@@ -46,8 +46,8 @@ Save the following code as `color_instance.rb`:
46
46
  require 'color_contrast_calc'
47
47
 
48
48
  # Create an instance of Color from a hex code
49
- # (You can pass 'red', [255, 0, 0], 'rgb(255, 0, 0)' or 'hsl(0deg, 100%, 50%)'
50
- # instead of '#ff0000')
49
+ # (You can pass 'red', [255, 0, 0], 'rgb(255, 0, 0)', 'hsl(0deg, 100%, 50%)' or
50
+ # hwb(60deg 0% 0%) instead of '#ff0000')
51
51
  red = ColorContrastCalc.color_from('#ff0000')
52
52
  puts red.class
53
53
  puts red.name
@@ -69,6 +69,17 @@ red
69
69
 
70
70
  ```
71
71
 
72
+ #### Color units for the argument of `ColorContrastCalc.color_from()`
73
+
74
+ The following formats are supported for the first argument of `ColorContrastCalc.color_from()`.
75
+
76
+ * RGB values in hexadecimal notation: #ff0, #ffff00, #FF0, etc.
77
+ * RGB values in functional notation: rgb(255, 255, 0), rgb(255 255 0), etc.
78
+ * RGB values as an Array of Integers: [255, 255, 0], etc.
79
+ * HSL colors in functional notation: hsl(60deg, 100%, 50%), hsl(60 100% 50%), etc.
80
+ * [Experimental] HWB colors in functional notation: hwb(60deg 0% 0%), hwb(60 0% 0%), etc.
81
+ * [Extended color keywords](https://www.w3.org/TR/css-color-3/#svg-color): white, black, red, etc.
82
+
72
83
  ### Example 1: Calculate the contrast ratio between two colors
73
84
 
74
85
  #### 1.1: The easiest way
@@ -26,6 +26,6 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency 'bundler', '~> 2.0'
27
27
  spec.add_development_dependency 'rake', '~> 10.0'
28
28
  spec.add_development_dependency 'rspec', '~> 3.0'
29
- spec.add_development_dependency 'rubocop', '~> 0.49.1'
29
+ spec.add_development_dependency 'rubocop', '~> 0.79'
30
30
  spec.add_development_dependency 'yard', '~> 0.9.9'
31
31
  end
@@ -3,8 +3,8 @@ require 'color_contrast_calc'
3
3
  require 'color_contrast_calc'
4
4
 
5
5
  # Create an instance of Color from a hex code
6
- # (You can pass 'red', [255, 0, 0], 'rgb(255, 0, 0)' or 'hsl(0deg, 100%, 50%)'
7
- # instead of '#ff0000')
6
+ # (You can pass 'red', [255, 0, 0], 'rgb(255, 0, 0)', 'hsl(0deg, 100%, 50%)' or
7
+ # hwb(60deg 0% 0%) instead of '#ff0000')
8
8
  red = ColorContrastCalc.color_from('#ff0000')
9
9
  puts red.class
10
10
  puts red.name
@@ -12,12 +12,14 @@ module ColorContrastCalc
12
12
  ##
13
13
  # Return an instance of Color.
14
14
  #
15
- # As +color_value+, you can pass a predefined color name, or an
16
- # RGB value represented as an array of integers or a hex code such
17
- # as [255, 255, 0] or "#ffff00". +name+ is assigned to the returned
18
- # instance.
15
+ # As +color_value+, you can pass a predefined color name, an
16
+ # RGB value represented as an array of integers like [255, 255, 0],
17
+ # or a string such as a hex code like "#ffff00". +name+ is assigned
18
+ # to the returned instance.
19
19
  # @param color_value [String, Array<Integer>] Name of a predefined
20
- # color, hex color code, rgb/hsl functions or RGB value
20
+ # color, hex color code, rgb/hsl/hwb functions or RGB value.
21
+ # Yellow, for example, can be given as [255, 255, 0], "#ffff00",
22
+ # "rgb(255, 255, 255)", "hsl(60deg, 100% 50%)" or "hwb(60deg 0% 0%)".
21
23
  # @param name [String] Without specifying a name, a color keyword name
22
24
  # (if exists) or the value of normalized hex color code is assigned
23
25
  # to Color#name
@@ -63,7 +65,8 @@ module ColorContrastCalc
63
65
  #
64
66
  # @param color1 [String, Array<Integer>] RGB color given as a string or
65
67
  # an array of integers. Yellow, for example, can be given as "#ffff00",
66
- # "#ff0", "rgb(255, 255, 0)", "hsl(60deg, 100%, 50%)" or [255, 255, 0].
68
+ # "#ff0", "rgb(255, 255, 0)", "hsl(60deg, 100%, 50%)", "hwb(60deg 0% 0%)"
69
+ # or [255, 255, 0].
67
70
  # @param color2 [String, Array<Integer>] RGB color given as a string or
68
71
  # an array of integers.
69
72
  # @return [Float] Contrast ratio
@@ -72,6 +75,32 @@ module ColorContrastCalc
72
75
  Color.as_color(color1).contrast_ratio_against(Color.as_color(color2))
73
76
  end
74
77
 
78
+ ##
79
+ # Select from two colors the one of which the contrast ratio is higher
80
+ # than the other's, against a given color.
81
+ #
82
+ # Note that this method is tentatively provided and may be changed later
83
+ # including its name.
84
+ #
85
+ # @param color [String, Array<Integer>, Color] A color against which
86
+ # the contrast ratio of other two colors will be calculated
87
+ # @param light_base [String, Array<Integer>, Color] One of two colors
88
+ # which will be returned depending their contrast ratio: This one
89
+ # will be returned when the contast ratio of the colors happen to
90
+ # be same.
91
+ # @param dark_base [String, Array<Integer>, Color] One of two colors
92
+ # which will be returned depending their contrast ratio
93
+ # @return [String, Array<Integer>, Color] One of the values
94
+ # specified as +light_base+ and +dark_base+
95
+
96
+ def self.higher_contrast_base_color_for(color,
97
+ light_base: Color::WHITE,
98
+ dark_base: Color::BLACK)
99
+ ratio_with_light = contrast_ratio(color, light_base)
100
+ ratio_with_dark = contrast_ratio(color, dark_base)
101
+ ratio_with_light < ratio_with_dark ? dark_base : light_base
102
+ end
103
+
75
104
  ##
76
105
  # Return an array of named colors.
77
106
  #
@@ -78,12 +78,14 @@ module ColorContrastCalc
78
78
  ##
79
79
  # Return an instance of Color.
80
80
  #
81
- # As +color_value+, you can pass a predefined color name, or an
82
- # RGB value represented as an array of integers or a hex code such
83
- # as [255, 255, 0] or "#ffff00". +name+ is assigned to the returned
84
- # instance.
81
+ # As +color_value+, you can pass a predefined color name, an
82
+ # RGB value represented as an array of integers like [255, 255, 0],
83
+ # or a string such as a hex code like "#ffff00". +name+ is assigned
84
+ # to the returned instance.
85
85
  # @param color_value [String, Array<Integer>] Name of a predefined
86
- # color, hex color code, rgb/hsl functions or RGB value
86
+ # color, hex color code, rgb/hsl/hwb functions or RGB value.
87
+ # Yellow, for example, can be given as [255, 255, 0], "#ffff00",
88
+ # "rgb(255, 255, 255)", "hsl(60deg, 100% 50%)" or "hwb(60deg 0% 0%)".
87
89
  # @param name [String] Without specifying a name, a color keyword name
88
90
  # (if exists) or the value of normalized hex color code is assigned
89
91
  # to Color#name
@@ -96,7 +98,7 @@ module ColorContrastCalc
96
98
 
97
99
  return color_from_rgb(color_value, name) if color_value.is_a?(Array)
98
100
 
99
- if /\A(?:rgb|hsl)/i =~ color_value
101
+ if /\A(?:rgb|hsl|hwb)/i =~ color_value
100
102
  return color_from_func(color_value, name)
101
103
  end
102
104
  color_from_str(color_value, name)
@@ -106,11 +108,12 @@ module ColorContrastCalc
106
108
  # Return an instance of Color.
107
109
  #
108
110
  # As +color_value+, you can pass a Color instance, a predefined color
109
- # name, or an RGB value represented as an array of integers or a hex
110
- # code such as [255, 255, 0] or "#ffff00". +name+ is assigned to the
111
- # returned instance.
112
- # @param color_value [Color, String, Array<Integer>] An instance of
113
- # Color, a name of a predefined, color, hex color code or RGB value
111
+ # name, an RGB value represented as an array of integers like
112
+ # [255, 255, 0], or a string such as a hex code like "#ffff00".
113
+ # +name+ is assigned to the returned instance.
114
+ # @param color_value [String, Array<Integer>] An instance of Color,
115
+ # a predefined color name, hex color code, rgb/hsl/hwb functions
116
+ # or RGB value
114
117
  # @param name [String] Without specifying a name, a color keyword name
115
118
  # (if exists) or the value of normalized hex color code is assigned
116
119
  # to Color#name
@@ -138,11 +141,11 @@ module ColorContrastCalc
138
141
 
139
142
  def color_from_func(color_value, name = nil)
140
143
  conv = ColorFunctionParser.parse(color_value)
141
- if conv.scheme == ColorFunctionParser::Scheme::RGB
142
- return color_from_rgb(conv.to_a, name || color_value)
144
+ if conv.scheme == ColorFunctionParser::Scheme::HSL
145
+ return from_hsl(conv.to_a, name || color_value)
143
146
  end
144
147
 
145
- from_hsl(conv.to_a, name || color_value)
148
+ color_from_rgb(conv.rgb, name || color_value)
146
149
  end
147
150
 
148
151
  private :color_from_func
@@ -7,7 +7,7 @@ require 'color_contrast_calc/invalid_color_representation_error'
7
7
 
8
8
  module ColorContrastCalc
9
9
  ##
10
- # Module that converts RGB/HSL functions into data apt for calculation.
10
+ # Module that converts RGB/HSL/HWB functions into data apt for calculation.
11
11
 
12
12
  module ColorFunctionParser
13
13
  ##
@@ -16,21 +16,154 @@ module ColorContrastCalc
16
16
  module Scheme
17
17
  RGB = 'rgb'
18
18
  HSL = 'hsl'
19
+ HWB = 'hwb'
19
20
  end
20
21
 
21
22
  ##
22
- # Hold information about a parsed RGB/HSL function.
23
+ # Supported units
24
+
25
+ module Unit
26
+ PERCENT = '%'
27
+ DEG = 'deg'
28
+ GRAD = 'grad'
29
+ RAD = 'rad'
30
+ TURN = 'turn'
31
+ end
32
+
33
+ ##
34
+ # Validate the unit of each parameter in a color functions.
35
+
36
+ class Validator
37
+ include Unit
38
+
39
+ POS = %w[1st 2nd 3rd].freeze
40
+
41
+ private_constant :POS
42
+
43
+ def initialize
44
+ @config = yield
45
+ @scheme = @config[:scheme]
46
+ end
47
+
48
+ def format_to_function(parameters)
49
+ params = parameters.map {|param| "#{param[:number]}#{param[:unit]}" }
50
+ "#{@scheme}(#{params.join(' ')})"
51
+ end
52
+
53
+ private :format_to_function
54
+
55
+ def error_message(parameters, passed_unit, pos, original_value = nil)
56
+ color_func = original_value || format_to_function(parameters)
57
+
58
+ if passed_unit
59
+ return format('"%s" is not allowed for %s.',
60
+ passed_unit, format_to_function(parameters))
61
+ end
62
+
63
+ format('A unit is required for the %s parameter of %s.',
64
+ POS[pos], color_func)
65
+ end
66
+
67
+ private :error_message
68
+
69
+ # @private
70
+ def validate_units(parameters, original_value = nil)
71
+ @config[:units].each_with_index do |unit, i|
72
+ passed_unit = parameters[i][:unit]
73
+
74
+ unless unit.include? passed_unit
75
+ raise InvalidColorRepresentationError,
76
+ error_message(parameters, passed_unit, i, original_value)
77
+ end
78
+ end
79
+
80
+ true
81
+ end
82
+
83
+ # @private
84
+ RGB = Validator.new do
85
+ {
86
+ scheme: Scheme::RGB,
87
+ units: [
88
+ [nil, PERCENT],
89
+ [nil, PERCENT],
90
+ [nil, PERCENT]
91
+ ]
92
+ }
93
+ end
94
+
95
+ # @private
96
+ HSL = Validator.new do
97
+ {
98
+ scheme: Scheme::HSL,
99
+ units: [
100
+ [nil, DEG, GRAD, RAD, TURN],
101
+ [PERCENT],
102
+ [PERCENT]
103
+ ]
104
+ }
105
+ end
106
+
107
+ # @private
108
+ HWB = Validator.new do
109
+ {
110
+ scheme: Scheme::HWB,
111
+ units: [
112
+ [nil, DEG, GRAD, RAD, TURN],
113
+ [PERCENT],
114
+ [PERCENT]
115
+ ]
116
+ }
117
+ end
118
+
119
+ VALIDATORS = {
120
+ Scheme::RGB => RGB,
121
+ Scheme::HSL => HSL,
122
+ Scheme::HWB => HWB
123
+ }.freeze
124
+
125
+ private_constant :VALIDATORS
126
+
127
+ def self.validate(parsed_value, original_value = nil)
128
+ scheme = parsed_value[:scheme]
129
+ params = parsed_value[:parameters]
130
+ VALIDATORS[scheme].validate_units(params, original_value)
131
+ end
132
+ end
133
+
134
+ ##
135
+ # Hold information about a parsed RGB/HSL/HWB function.
23
136
  #
24
137
  # This class is intended to be used internally in ColorFunctionParser,
25
138
  # so do not rely on the current class name and its interfaces.
26
139
  # They may change in the future.
27
140
 
28
141
  class Converter
142
+ UNIT_CONV = {
143
+ Unit::PERCENT => proc do |n, base|
144
+ if base == 255
145
+ (n.to_f * base / 100.0).round
146
+ else
147
+ n.to_f
148
+ end
149
+ end,
150
+ Unit::DEG => proc {|n| n.to_f },
151
+ Unit::GRAD => proc {|n| n.to_f * 9 / 10 },
152
+ Unit::TURN => proc {|n| n.to_f * 360 },
153
+ Unit::RAD => proc {|n| n.to_f * 180 / Math::PI }
154
+ }
155
+
156
+ UNIT_CONV.default = proc {|n| /\./ =~ n ? n.to_f : n.to_i }
157
+ UNIT_CONV.freeze
158
+
159
+ private_constant :UNIT_CONV
160
+
29
161
  ##
30
162
  # @!attribute [r] scheme
31
163
  # @return [String] Type of function: 'rgb' or 'hsl'
32
164
  # @!attribute [r] source
33
- # @return [String] The original RGB/HSL function before the conversion
165
+ # @return [String] The original RGB/HSL/HWB function before
166
+ # the conversion
34
167
 
35
168
  attr_reader :scheme, :source
36
169
 
@@ -55,7 +188,7 @@ module ColorContrastCalc
55
188
  private :normalize_params
56
189
 
57
190
  ##
58
- # Return the RGB value gained from a RGB/HSL function.
191
+ # Return the RGB value gained from a RGB/HSL/HWB function.
59
192
  #
60
193
  # @return [Array<Integer>] RGB value represented as an array
61
194
 
@@ -64,11 +197,12 @@ module ColorContrastCalc
64
197
  end
65
198
 
66
199
  ##
67
- # Return the parameters of a RGB/HSL function as an array of
200
+ # Return the parameters of a RGB/HSL/HWB function as an array of
68
201
  # Integer/Float.
69
202
  # The unit for H, S, L is assumed to be deg, %, % respectively.
70
203
  #
71
- # @return [Array<Integer, Float>] RGB/HSL value represented as an array
204
+ # @return [Array<Integer, Float>] RGB/HSL/HWB value represented
205
+ # as an array
72
206
 
73
207
  def to_a
74
208
  @normalized
@@ -78,11 +212,7 @@ module ColorContrastCalc
78
212
  class Rgb < self
79
213
  def normalize_params
80
214
  @params.map do |param|
81
- if param[:unit] == '%'
82
- (param[:number] * 255.0 / 100).round
83
- else
84
- param[:number].to_i
85
- end
215
+ UNIT_CONV[param[:unit]][param[:number], 255]
86
216
  end
87
217
  end
88
218
 
@@ -93,7 +223,7 @@ module ColorContrastCalc
93
223
  class Hsl < self
94
224
  def normalize_params
95
225
  @params.map do |param|
96
- param[:number].to_f
226
+ UNIT_CONV[param[:unit]][param[:number]]
97
227
  end
98
228
  end
99
229
 
@@ -102,13 +232,29 @@ module ColorContrastCalc
102
232
  end
103
233
  end
104
234
 
235
+ # @private
236
+ class Hwb < self
237
+ def normalize_params
238
+ @params.map do |param|
239
+ UNIT_CONV[param[:unit]][param[:number]]
240
+ end
241
+ end
242
+
243
+ def rgb
244
+ Utils.hwb_to_rgb(to_a)
245
+ end
246
+ end
247
+
105
248
  # @private
106
249
  def self.create(parsed_value, original_value)
250
+ Validator.validate(parsed_value, original_value)
107
251
  case parsed_value[:scheme]
108
252
  when Scheme::RGB
109
253
  Rgb.new(parsed_value, original_value)
110
254
  when Scheme::HSL
111
255
  Hsl.new(parsed_value, original_value)
256
+ when Scheme::HWB
257
+ Hwb.new(parsed_value, original_value)
112
258
  end
113
259
  end
114
260
  end
@@ -116,127 +262,197 @@ module ColorContrastCalc
116
262
  # @private
117
263
  module TokenRe
118
264
  SPACES = /\s+/.freeze
119
- SCHEME = /(rgb|hsl)/i.freeze
265
+ SCHEME = /(rgb|hsl|hwb)/i.freeze
120
266
  OPEN_PAREN = /\(/.freeze
121
267
  CLOSE_PAREN = /\)/.freeze
122
268
  COMMA = /,/.freeze
123
269
  NUMBER = /(\d+)(:?\.\d+)?/.freeze
124
- UNIT = /(%|deg)/.freeze
270
+ UNIT = /(%|deg|grad|rad|turn)/.freeze
125
271
  end
126
272
 
127
- def self.format_error_message(scanner, re)
128
- out = StringIO.new
129
- color_value = scanner.string
130
- [
131
- format('"%s" is not a valid code. An error occurred at:', color_value),
132
- color_value,
133
- "#{' ' * scanner.charpos}^ while searching with #{re}"
134
- ].each do |line|
135
- out.puts line
273
+ class Parser
274
+ class << self
275
+ attr_accessor :parsers
136
276
  end
137
277
 
138
- out.string
139
- end
278
+ def skip_spaces!(scanner)
279
+ scanner.scan(TokenRe::SPACES)
280
+ end
140
281
 
141
- private_class_method :format_error_message
282
+ private :skip_spaces!
142
283
 
143
- def self.skip_spaces!(scanner)
144
- scanner.scan(TokenRe::SPACES)
145
- end
284
+ def read_scheme!(scanner)
285
+ scheme = read_token!(scanner, TokenRe::SCHEME).downcase
146
286
 
147
- private_class_method :skip_spaces!
287
+ parsed_value = {
288
+ scheme: scheme,
289
+ parameters: []
290
+ }
148
291
 
149
- def self.read_token!(scanner, re)
150
- skip_spaces!(scanner)
151
- token = scanner.scan(re)
292
+ parser = Parser.parsers[scheme] || self
152
293
 
153
- return token if token
294
+ parser.read_open_paren!(scanner, parsed_value)
295
+ end
154
296
 
155
- error_message = format_error_message(scanner, re)
156
- raise InvalidColorRepresentationError, error_message
157
- end
297
+ def format_error_message(scanner, re)
298
+ out = StringIO.new
299
+ color_value = scanner.string
158
300
 
159
- private_class_method :read_token!
301
+ out.print format('"%s" is not a valid code. ', color_value)
302
+ print_error_pos!(out, color_value, scanner.charpos)
303
+ out.puts " while searching with #{re}"
160
304
 
161
- def self.read_scheme!(scanner)
162
- scheme = read_token!(scanner, TokenRe::SCHEME)
305
+ out.string
306
+ end
163
307
 
164
- parsed_value = {
165
- scheme: scheme.downcase,
166
- parameters: []
167
- }
308
+ private :format_error_message
168
309
 
169
- read_open_paren!(scanner, parsed_value)
170
- end
310
+ def print_error_pos!(out, color_value, pos)
311
+ out.puts 'An error occurred at:'
312
+ out.puts color_value
313
+ out.print "#{' ' * pos}^"
314
+ end
171
315
 
172
- private_class_method :read_scheme!
316
+ private :print_error_pos!
173
317
 
174
- def self.read_open_paren!(scanner, parsed_value)
175
- read_token!(scanner, TokenRe::OPEN_PAREN)
318
+ def read_token!(scanner, re)
319
+ skip_spaces!(scanner)
320
+ token = scanner.scan(re)
176
321
 
177
- read_parameters!(scanner, parsed_value)
178
- end
322
+ return token if token
179
323
 
180
- private_class_method :read_open_paren!
324
+ error_message = format_error_message(scanner, re)
325
+ raise InvalidColorRepresentationError, error_message
326
+ end
181
327
 
182
- def self.read_close_paren!(scanner)
183
- scanner.scan(TokenRe::CLOSE_PAREN)
184
- end
328
+ private :read_token!
185
329
 
186
- private_class_method :read_close_paren!
330
+ def read_open_paren!(scanner, parsed_value)
331
+ read_token!(scanner, TokenRe::OPEN_PAREN)
187
332
 
188
- def self.read_parameters!(scanner, parsed_value)
189
- read_number!(scanner, parsed_value)
190
- end
333
+ read_parameters!(scanner, parsed_value)
334
+ end
191
335
 
192
- private_class_method :read_parameters!
336
+ protected :read_open_paren!
193
337
 
194
- def self.read_number!(scanner, parsed_value)
195
- number = read_token!(scanner, TokenRe::NUMBER)
338
+ def read_close_paren!(scanner)
339
+ scanner.scan(TokenRe::CLOSE_PAREN)
340
+ end
196
341
 
197
- parsed_value[:parameters].push({ number: number, unit: nil })
342
+ private :read_close_paren!
198
343
 
199
- read_unit!(scanner, parsed_value)
200
- end
344
+ def read_parameters!(scanner, parsed_value)
345
+ read_number!(scanner, parsed_value)
346
+ end
347
+
348
+ private :read_parameters!
349
+
350
+ def read_number!(scanner, parsed_value)
351
+ number = read_token!(scanner, TokenRe::NUMBER)
352
+
353
+ parsed_value[:parameters].push({ number: number, unit: nil })
201
354
 
202
- private_class_method :read_number!
355
+ read_unit!(scanner, parsed_value)
356
+ end
357
+
358
+ private :read_number!
203
359
 
204
- def self.read_unit!(scanner, parsed_value)
205
- unit = scanner.scan(TokenRe::UNIT)
360
+ def read_unit!(scanner, parsed_value)
361
+ unit = scanner.scan(TokenRe::UNIT)
206
362
 
207
- parsed_value[:parameters].last[:unit] = unit if unit
363
+ parsed_value[:parameters].last[:unit] = unit if unit
364
+
365
+ read_comma!(scanner, parsed_value)
366
+ end
367
+
368
+ private :read_unit!
369
+
370
+ def next_spaces_as_separator?(scanner)
371
+ cur_pos = scanner.pos
372
+ spaces = skip_spaces!(scanner)
373
+ next_token_is_number = scanner.check(TokenRe::NUMBER)
374
+ scanner.pos = cur_pos
375
+ spaces && next_token_is_number
376
+ end
208
377
 
209
- read_comma!(scanner, parsed_value)
378
+ private :next_spaces_as_separator?
379
+
380
+ def read_comma!(scanner, parsed_value)
381
+ if next_spaces_as_separator?(scanner)
382
+ return read_number!(scanner, parsed_value)
383
+ end
384
+
385
+ skip_spaces!(scanner)
386
+
387
+ return parsed_value if read_close_paren!(scanner)
388
+
389
+ read_token!(scanner, TokenRe::COMMA)
390
+ read_number!(scanner, parsed_value)
391
+ end
392
+
393
+ private :read_comma!
210
394
  end
211
395
 
212
- private_class_method :read_unit!
396
+ class FunctionParser < Parser
397
+ def read_comma!(scanner, parsed_value)
398
+ if next_spaces_as_separator?(scanner)
399
+ return read_number!(scanner, parsed_value)
400
+ end
401
+
402
+ skip_spaces!(scanner)
403
+
404
+ if scanner.check(TokenRe::COMMA)
405
+ wrong_separator_error(scanner, parsed_value)
406
+ end
407
+
408
+ return parsed_value if read_close_paren!(scanner)
213
409
 
214
- def self.read_comma!(scanner, parsed_value)
215
- skip_spaces!(scanner)
410
+ read_number!(scanner, parsed_value)
411
+ end
412
+
413
+ def report_wrong_separator!(scanner, parsed_value)
414
+ out = StringIO.new
415
+ color_value = scanner.string
416
+ scheme = parsed_value[:scheme].upcase
417
+ # The trailing space after the first message is intentional,
418
+ # because it is immediately followed by another message.
419
+ out.print "\",\" is not a valid separator for #{scheme} functions. "
420
+ print_error_pos!(out, color_value, scanner.charpos)
421
+ out.puts
422
+ out.string
423
+ end
216
424
 
217
- return parsed_value if read_close_paren!(scanner)
425
+ private :report_wrong_separator!
218
426
 
219
- read_token!(scanner, TokenRe::COMMA)
220
- read_number!(scanner, parsed_value)
427
+ def wrong_separator_error(scanner, parsed_value)
428
+ error_message = report_wrong_separator!(scanner, parsed_value)
429
+ raise InvalidColorRepresentationError, error_message
430
+ end
431
+
432
+ private :wrong_separator_error
221
433
  end
222
434
 
223
- private_class_method :read_comma!
435
+ Parser.parsers = {
436
+ Scheme::HWB => FunctionParser.new
437
+ }
438
+
439
+ MAIN_PARSER = Parser.new
224
440
 
225
441
  ##
226
- # Parse an RGB/HSL function and store the result as an instance of
442
+ # Parse an RGB/HSL/HWB function and store the result as an instance of
227
443
  # ColorFunctionParser::Converter.
228
444
  #
229
- # @param color_value [String] RGB/HSL function defined at
230
- # https://www.w3.org/TR/css-color-4/
445
+ # @param color_value [String] RGB/HSL/HWB function defined at
446
+ # https://www.w3.org/TR/2019/WD-css-color-4-20191105/
231
447
  # @return [Converter] An instance of ColorFunctionParser::Converter
232
448
 
233
449
  def self.parse(color_value)
234
- parsed_value = read_scheme!(StringScanner.new(color_value))
450
+ parsed_value = MAIN_PARSER.read_scheme!(StringScanner.new(color_value))
235
451
  Converter.create(parsed_value, color_value)
236
452
  end
237
453
 
238
454
  ##
239
- # Return An RGB value gained from an RGB/HSL function.
455
+ # Return An RGB value gained from an RGB/HSL/HWB function.
240
456
  #
241
457
  # @return [Array<Integer>] RGB value represented as an array
242
458
 
@@ -118,6 +118,7 @@
118
118
  ["plum", "#dda0dd"],
119
119
  ["powderblue", "#b0e0e6"],
120
120
  ["purple", "#800080"],
121
+ ["rebeccapurple", "#663399"],
121
122
  ["red", "#ff0000"],
122
123
  ["rosybrown", "#bc8f8f"],
123
124
  ["royalblue", "#4169e1"],
@@ -6,7 +6,7 @@ module ColorContrastCalc
6
6
 
7
7
  module Deprecated
8
8
  def self.warn(old_method, new_method)
9
- STDERR.puts "##{old_method} is deprecated. Use ##{new_method} instead"
9
+ Kernel.warn "##{old_method} is deprecated. Use ##{new_method} instead"
10
10
  end
11
11
 
12
12
  module Color
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ColorContrastCalc
2
4
  ##
3
5
  # Error raised if creating a Color instance with invalid value.
@@ -236,6 +236,40 @@ module ColorContrastCalc
236
236
  def self.uppercase?(str)
237
237
  !/[[:lower:]]/.match?(str)
238
238
  end
239
+
240
+ module Hwb
241
+ ##
242
+ # ref: https://www.w3.org/TR/2019/WD-css-color-4-20191105/
243
+
244
+ def normalize_hwb(hwb)
245
+ h, w, b = hwb
246
+
247
+ achromatic_percent = w + b
248
+ denominator = achromatic_percent > 100 ? achromatic_percent : 100
249
+
250
+ normalized_w = w.to_f / denominator
251
+ normalized_b = b.to_f / denominator
252
+
253
+ [h, normalized_w, normalized_b]
254
+ end
255
+
256
+ private :normalize_hwb
257
+
258
+ def hwb_to_rgb(hwb)
259
+ hue, white, black = normalize_hwb(hwb)
260
+ rgb = Utils.hsl_to_rgb([hue, 100, 50])
261
+
262
+ rgb.map do |c|
263
+ ((c * (1.0 - white - black)) + white * 255).round
264
+ end
265
+ end
266
+
267
+ def rgb_to_hwb(_rgb)
268
+ raise Notimplementederror, 'Must be implemented later'
269
+ end
270
+ end
271
+
272
+ extend Hwb
239
273
  end
240
274
 
241
275
  ##
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ColorContrastCalc
4
- VERSION = '0.6.1'
4
+ VERSION = '0.7.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: color_contrast_calc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - HASHIMOTO, Naoki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-01-05 00:00:00.000000000 Z
11
+ date: 2020-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 0.49.1
61
+ version: '0.79'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 0.49.1
68
+ version: '0.79'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: yard
71
71
  requirement: !ruby/object:Gem::Requirement