sai 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -1
  3. data/README.md +11 -3
  4. data/docs/USAGE.md +57 -9
  5. data/lib/sai/ansi/color_parser.rb +109 -0
  6. data/lib/sai/ansi/sequence_processor.rb +15 -126
  7. data/lib/sai/ansi/style_parser.rb +66 -0
  8. data/lib/sai/ansi.rb +0 -27
  9. data/lib/sai/conversion/color_sequence.rb +4 -4
  10. data/lib/sai/conversion/rgb/color_classifier.rb +209 -0
  11. data/lib/sai/conversion/rgb/color_indexer.rb +48 -0
  12. data/lib/sai/conversion/rgb/color_space.rb +192 -0
  13. data/lib/sai/conversion/rgb/color_transformer.rb +140 -0
  14. data/lib/sai/conversion/rgb.rb +23 -269
  15. data/lib/sai/decorator/color_manipulations.rb +157 -0
  16. data/lib/sai/decorator/delegation.rb +84 -0
  17. data/lib/sai/decorator/gradients.rb +363 -0
  18. data/lib/sai/decorator/hex_colors.rb +56 -0
  19. data/lib/sai/decorator/named_colors.rb +780 -0
  20. data/lib/sai/decorator/named_styles.rb +276 -0
  21. data/lib/sai/decorator/rgb_colors.rb +64 -0
  22. data/lib/sai/decorator.rb +29 -775
  23. data/lib/sai/named_colors.rb +437 -0
  24. data/lib/sai.rb +731 -23
  25. data/sig/sai/ansi/color_parser.rbs +77 -0
  26. data/sig/sai/ansi/sequence_processor.rbs +0 -75
  27. data/sig/sai/ansi/style_parser.rbs +59 -0
  28. data/sig/sai/ansi.rbs +0 -10
  29. data/sig/sai/conversion/rgb/color_classifier.rbs +165 -0
  30. data/sig/sai/conversion/rgb/color_indexer.rbs +41 -0
  31. data/sig/sai/conversion/rgb/color_space.rbs +129 -0
  32. data/sig/sai/conversion/rgb/color_transformer.rbs +99 -0
  33. data/sig/sai/conversion/rgb.rbs +15 -198
  34. data/sig/sai/decorator/color_manipulations.rbs +125 -0
  35. data/sig/sai/decorator/delegation.rbs +47 -0
  36. data/sig/sai/decorator/gradients.rbs +267 -0
  37. data/sig/sai/decorator/hex_colors.rbs +48 -0
  38. data/sig/sai/decorator/named_colors.rbs +1491 -0
  39. data/sig/sai/decorator/named_styles.rbs +72 -0
  40. data/sig/sai/decorator/rgb_colors.rbs +52 -0
  41. data/sig/sai/decorator.rbs +21 -195
  42. data/sig/sai/named_colors.rbs +65 -0
  43. data/sig/sai.rbs +1468 -44
  44. metadata +32 -4
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'sai/ansi'
4
+ require 'sai/conversion/rgb/color_classifier'
5
+ require 'sai/conversion/rgb/color_indexer'
6
+ require 'sai/conversion/rgb/color_space'
7
+ require 'sai/conversion/rgb/color_transformer'
4
8
 
5
9
  module Sai
6
10
  module Conversion
@@ -12,60 +16,30 @@ module Sai
12
16
  # @api private
13
17
  module RGB
14
18
  class << self
15
- # Get closest ANSI color for RGB values
19
+ # Color classification utilities
16
20
  #
17
21
  # @author {https://aaronmallen.me Aaron Allen}
18
- # @since 0.1.0
22
+ # @since 0.3.1
19
23
  #
20
24
  # @api private
21
25
  #
22
- # @param red [Float] the red component (0-1)
23
- # @param green [Float] the green component (0-1)
24
- # @param blue [Float] the blue component (0-1)
25
- #
26
- # @return [Symbol] the closest ANSI color name
27
- # @rbs (Float red, Float green, Float blue) -> Symbol
28
- def closest_ansi_color(red, green, blue)
29
- return :black if dark?(red, green, blue)
30
- return :white if grayscale?(red, green, blue)
31
- return primary_color(red, green, blue) if primary?(red, green, blue)
32
- return secondary_color(red, green, blue) if secondary?(red, green, blue)
33
-
34
- :white
26
+ # @return [Module<ColorClassifier>] the ColorClassifier module
27
+ # @rbs () -> singleton(ColorClassifier)
28
+ def classify
29
+ ColorClassifier
35
30
  end
36
31
 
37
- # Determine if a color is dark
32
+ # Color indexing utilities
38
33
  #
39
34
  # @author {https://aaronmallen.me Aaron Allen}
40
- # @since 0.1.0
35
+ # @since 0.3.1
41
36
  #
42
37
  # @api private
43
38
  #
44
- # @param red [Float] the red component (0-1)
45
- # @param green [Float] the green component (0-1)
46
- # @param blue [Float] the blue component (0-1)
47
- #
48
- # @return [Boolean] true if color is dark
49
- # @rbs (Float red, Float green, Float blue) -> bool
50
- def dark?(red, green, blue)
51
- [red, green, blue].max < 0.3
52
- end
53
-
54
- # Determine if a color is grayscale
55
- #
56
- # @author {https://aaronmallen.me Aaron Allen}
57
- # @since 0.1.0
58
- #
59
- # @api private
60
- #
61
- # @param red [Float] the red component (0-1)
62
- # @param green [Float] the green component (0-1)
63
- # @param blue [Float] the blue component (0-1)
64
- #
65
- # @return [Boolean] true if color is grayscale
66
- # @rbs (Float red, Float green, Float blue) -> bool
67
- def grayscale?(red, green, blue)
68
- red == green && green == blue
39
+ # @return [Module<ColorIndexer>] the ColorIndexer module
40
+ # @rbs () -> singleton(ColorIndexer)
41
+ def index
42
+ ColorIndexer
69
43
  end
70
44
 
71
45
  # Convert a color value to RGB components
@@ -81,240 +55,20 @@ module Sai
81
55
  # @return [Array<Integer>] the RGB components
82
56
  # @rbs (Array[Integer] | String | Symbol color) -> Array[Integer]
83
57
  def resolve(color)
84
- case color
85
- when Array then validate_rgb(color)
86
- when /^#?([A-Fa-f0-9]{6})$/
87
- hex_to_rgb(
88
- Regexp.last_match(1) #: String
89
- )
90
- when String, Symbol then named_to_rgb(color.to_s.downcase)
91
- else
92
- raise ArgumentError, "Invalid color format: #{color}"
93
- end
94
- end
95
-
96
- # Convert RGB values to 256-color cube index
97
- #
98
- # @author {https://aaronmallen.me Aaron Allen}
99
- # @since 0.1.0
100
- #
101
- # @api private
102
- #
103
- # @param rgb [Array<Integer>] RGB values (0-255)
104
- #
105
- # @return [Integer] the color cube index
106
- # @rbs (Array[Integer] rgb) -> Integer
107
- def to_color_cube_index(rgb)
108
- r, g, b = rgb.map { |c| ((c / 255.0) * 5).round } #: [Integer, Integer, Integer]
109
- 16 + (r * 36) + (g * 6) + b
110
- end
111
-
112
- # Convert RGB values to grayscale index
113
- #
114
- # @author {https://aaronmallen.me Aaron Allen}
115
- # @since 0.1.0
116
- #
117
- # @api private
118
- #
119
- # @param rgb [Array<Integer>] RGB values
120
- #
121
- # @return [Integer] the grayscale index
122
- # @rbs (Array[Integer] rgb) -> Integer
123
- def to_grayscale_index(rgb)
124
- 232 + ((rgb[0] / 255.0) * 23).round
125
- end
126
-
127
- private
128
-
129
- # Check if RGB values represent cyan
130
- #
131
- # @author {https://aaronmallen.me Aaron Allen}
132
- # @since 0.1.0
133
- #
134
- # @api private
135
- #
136
- # @param red [Float] the red component (0-1)
137
- # @param green [Float] the green component (0-1)
138
- # @param blue [Float] the blue component (0-1)
139
- #
140
- # @return [Boolean] true if color is cyan
141
- # @rbs (Float red, Float green, Float blue) -> bool
142
- def cyan?(red, green, blue)
143
- green > red && blue > red
144
- end
145
-
146
- # Convert a hex string to RGB values
147
- #
148
- # @author {https://aaronmallen.me Aaron Allen}
149
- # @since 0.1.0
150
- #
151
- # @api private
152
- #
153
- # @param hex [String] the hex color code
154
- #
155
- # @return [Array<Integer>] the RGB components
156
- # @rbs (String hex) -> Array[Integer]
157
- def hex_to_rgb(hex)
158
- hex = hex.delete_prefix('#') #: String
159
- [
160
- hex[0..1].to_i(16), # steep:ignore UnexpectedPositionalArgument
161
- hex[2..3].to_i(16), # steep:ignore UnexpectedPositionalArgument
162
- hex[4..5].to_i(16) # steep:ignore UnexpectedPositionalArgument
163
- ]
164
- end
165
-
166
- # Check if RGB values represent magenta
167
- #
168
- # @author {https://aaronmallen.me Aaron Allen}
169
- # @since 0.1.0
170
- #
171
- # @api private
172
- #
173
- # @param red [Float] the red component (0-1)
174
- # @param green [Float] the green component (0-1)
175
- # @param blue [Float] the blue component (0-1)
176
- #
177
- # @return [Boolean] true if color is magenta
178
- # @rbs (Float red, Float green, Float blue) -> bool
179
- def magenta?(red, green, blue)
180
- red > green && blue > green
181
- end
182
-
183
- # Convert a named color to RGB values
184
- #
185
- # @author {https://aaronmallen.me Aaron Allen}
186
- # @since 0.1.0
187
- #
188
- # @api private
189
- #
190
- # @param color_name [String] the color name
191
- #
192
- # @raise [ArgumentError] if the color name is unknown
193
- # @return [Array<Integer>] the RGB components
194
- # @rbs (String color_name) -> Array[Integer]
195
- def named_to_rgb(color_name)
196
- ANSI::COLOR_NAMES.fetch(color_name.to_sym) do
197
- raise ArgumentError, "Unknown color name: #{color_name}"
198
- end
199
- end
200
-
201
- # Determine if RGB values represent a primary color
202
- #
203
- # @author {https://aaronmallen.me Aaron Allen}
204
- # @since 0.1.0
205
- #
206
- # @api private
207
- #
208
- # @param red [Float] the red component (0-1)
209
- # @param green [Float] the green component (0-1)
210
- # @param blue [Float] the blue component (0-1)
211
- #
212
- # @return [Boolean] true if color is primary
213
- # @rbs (Float red, Float green, Float blue) -> bool
214
- def primary?(red, green, blue)
215
- max = [red, green, blue].max
216
- mid = [red, green, blue].sort[1]
217
- (max - mid) > 0.3
218
- end
219
-
220
- # Get the closest primary color
221
- #
222
- # @author {https://aaronmallen.me Aaron Allen}
223
- # @since 0.1.0
224
- #
225
- # @api private
226
- #
227
- # @param red [Float] the red component (0-1)
228
- # @param green [Float] the green component (0-1)
229
- # @param blue [Float] the blue component (0-1)
230
- #
231
- # @return [Symbol] the primary color name
232
- # @rbs (Float red, Float green, Float blue) -> Symbol
233
- def primary_color(red, green, blue)
234
- max = [red, green, blue].max
235
- case max
236
- when red then :red
237
- when green then :green
238
- else :blue
239
- end
240
- end
241
-
242
- # Determine if RGB values represent a secondary color
243
- #
244
- # @author {https://aaronmallen.me Aaron Allen}
245
- # @since 0.1.0
246
- #
247
- # @api private
248
- #
249
- # @param red [Float] the red component (0-1)
250
- # @param green [Float] the green component (0-1)
251
- # @param blue [Float] the blue component (0-1)
252
- #
253
- # @return [Boolean] true if color is secondary
254
- # @rbs (Float red, Float green, Float blue) -> bool
255
- def secondary?(red, green, blue)
256
- return true if yellow?(red, green, blue)
257
- return true if magenta?(red, green, blue)
258
- return true if cyan?(red, green, blue)
259
-
260
- false
261
- end
262
-
263
- # Get the closest secondary color
264
- #
265
- # @author {https://aaronmallen.me Aaron Allen}
266
- # @since 0.1.0
267
- #
268
- # @api private
269
- #
270
- # @param red [Float] the red component (0-1)
271
- # @param green [Float] the green component (0-1)
272
- # @param blue [Float] the blue component (0-1)
273
- #
274
- # @return [Symbol] the secondary color name
275
- # @rbs (Float red, Float green, Float blue) -> Symbol
276
- def secondary_color(red, green, blue)
277
- return :yellow if yellow?(red, green, blue)
278
- return :magenta if magenta?(red, green, blue)
279
- return :cyan if cyan?(red, green, blue)
280
-
281
- :white
58
+ ColorSpace.resolve(color)
282
59
  end
283
60
 
284
- # Validate RGB values
61
+ # Transform RGB values
285
62
  #
286
63
  # @author {https://aaronmallen.me Aaron Allen}
287
- # @since 0.1.0
64
+ # @since 0.3.1
288
65
  #
289
66
  # @api private
290
67
  #
291
- # @param color [Array<Integer>] the RGB components to validate
292
- # @return [Array<Integer>] the validated RGB components
293
- # @raise [ArgumentError] if the RGB values are invalid
294
- # @rbs (Array[Integer] color) -> Array[Integer]
295
- def validate_rgb(color)
296
- unless color.size == 3 && color.all? { |c| c.is_a?(Integer) && c.between?(0, 255) }
297
- raise ArgumentError, "Invalid RGB values: #{color}"
298
- end
299
-
300
- color
301
- end
302
-
303
- # Check if RGB values represent yellow
304
- #
305
- # @author {https://aaronmallen.me Aaron Allen}
306
- # @since 0.1.0
307
- #
308
- # @api private
309
- #
310
- # @param red [Float] the red component (0-1)
311
- # @param green [Float] the green component (0-1)
312
- # @param blue [Float] the blue component (0-1)
313
- #
314
- # @return [Boolean] true if color is yellow
315
- # @rbs (Float red, Float green, Float blue) -> bool
316
- def yellow?(red, green, blue)
317
- red > blue && green > blue && (red - green).abs < 0.3
68
+ # @return [Module<ColorTransformer>] the color transformer
69
+ # @rbs () -> singleton(ColorTransformer)
70
+ def transform
71
+ ColorTransformer
318
72
  end
319
73
  end
320
74
  end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sai
4
+ class Decorator
5
+ # Color manipulation methods for the {Decorator} class
6
+ #
7
+ # @author {https://aaronmallen.me Aaron Allen}
8
+ # @since 0.3.1
9
+ #
10
+ # @abstract This module is meant to be included in the {Decorator} class to provide color manipulation methods
11
+ # @api private
12
+ module ColorManipulations
13
+ # Darken the background color by a percentage
14
+ #
15
+ # @author {https://aaronmallen.me Aaron Allen}
16
+ # @since 0.3.1
17
+ #
18
+ # @api public
19
+ #
20
+ # @example
21
+ # decorator.on_blue.darken_text(0.5).decorate('Hello, world!').to_s #=> "\e[48;2;0;0;238mHello, world!\e[0m"
22
+ #
23
+ # @param amount [Float] the amount to darken the background color (0.0...1.0)
24
+ #
25
+ # @raise [ArgumentError] if the percentage is out of range
26
+ # @return [Decorator] a new instance of Decorator with the darkened background color
27
+ # @rbs (Float amount) -> Decorator
28
+ def darken_background(amount)
29
+ raise ArgumentError, "Invalid percentage: #{amount}" unless amount >= 0.0 && amount <= 1.0
30
+
31
+ darken(amount, :background)
32
+ end
33
+ alias darken_bg darken_background
34
+
35
+ # Darken the text color by a percentage
36
+ #
37
+ # @author {https://aaronmallen.me Aaron Allen}
38
+ # @since 0.3.1
39
+ #
40
+ # @api public
41
+ #
42
+ # @example
43
+ # decorator.blue.darken_text(0.5).decorate('Hello, world!').to_s #=> "\e[38;2;0;0;119mHello, world!\e[0m"
44
+ #
45
+ # @param amount [Float] the amount to darken the text color (0.0...1.0)
46
+ #
47
+ # @raise [ArgumentError] if the percentage is out of range
48
+ # @return [Decorator] a new instance of Decorator with the darkened text color
49
+ # @rbs (Float amount) -> Decorator
50
+ def darken_text(amount)
51
+ raise ArgumentError, "Invalid percentage: #{amount}" unless amount >= 0.0 && amount <= 1.0
52
+
53
+ darken(amount, :foreground)
54
+ end
55
+ alias darken_fg darken_text
56
+ alias darken_foreground darken_text
57
+
58
+ # Lighten the background color by a percentage
59
+ #
60
+ # @author {https://aaronmallen.me Aaron Allen}
61
+ # @since 0.3.1
62
+ #
63
+ # @api public
64
+ #
65
+ # @example
66
+ # decorator.on_blue.lighten_background(0.5).decorate('Hello, world!').to_s
67
+ # #=> "\e[48;2;0;0;255mHello, world!\e[0m"
68
+ #
69
+ # @param amount [Float] the amount to lighten the background color (0.0...1.0)
70
+ #
71
+ # @raise [ArgumentError] if the percentage is out of range
72
+ # @return [Decorator] a new instance of Decorator with the lightened background color
73
+ # @rbs (Float amount) -> Decorator
74
+ def lighten_background(amount)
75
+ raise ArgumentError, "Invalid percentage: #{amount}" unless amount >= 0.0 && amount <= 1.0
76
+
77
+ lighten(amount, :background)
78
+ end
79
+ alias lighten_bg lighten_background
80
+
81
+ # Lighten the text color by a percentage
82
+ #
83
+ # @author {https://aaronmallen.me Aaron Allen}
84
+ # @since 0.3.1
85
+ #
86
+ # @api public
87
+ #
88
+ # @example
89
+ # decorator.blue.lighten_text(0.5).decorate('Hello, world!').to_s #=> "\e[38;2;0;0;127mHello, world!\e[0m"
90
+ #
91
+ # @param amount [Float] the amount to lighten the text color (0.0...1.0)
92
+ #
93
+ # @raise [ArgumentError] if the percentage is out of range
94
+ # @return [Decorator] a new instance of Decorator with the lightened text color
95
+ # @rbs (Float amount) -> Decorator
96
+ def lighten_text(amount)
97
+ raise ArgumentError, "Invalid percentage: #{amount}" unless amount >= 0.0 && amount <= 1.0
98
+
99
+ lighten(amount, :foreground)
100
+ end
101
+ alias lighten_fg lighten_text
102
+ alias lighten_foreground lighten_text
103
+
104
+ private
105
+
106
+ # Darken the foreground or background color by a specified amount
107
+ #
108
+ # @author {https://aaronmallen.me Aaron Allen}
109
+ # @since 0.3.1
110
+ #
111
+ # @api private
112
+ #
113
+ # @param amount [Float] a value between 0.0 and 1.0 to darken the color by
114
+ # @param component [Symbol] the color component to darken
115
+ #
116
+ # @return [Decorator] a new instance of Decorator with the color darkened
117
+ # @rbs (Float amount, Symbol component) -> Decorator
118
+ def darken(amount, component)
119
+ color = instance_variable_get(:"@#{component}")
120
+
121
+ # @type self: Decorator
122
+
123
+ dup.tap do |duped|
124
+ if color
125
+ rgb = Conversion::RGB.transform.darken(color, amount)
126
+ duped.instance_variable_set(:"@#{component}", rgb)
127
+ end
128
+ end
129
+ end
130
+
131
+ # Lighten the foreground or background color by a specified amount
132
+ #
133
+ # @author {https://aaronmallen.me Aaron Allen}
134
+ # @since 0.3.1
135
+ #
136
+ # @api private
137
+ #
138
+ # @param amount [Float] a value between 0.0 and 1.0 to lighten the color by
139
+ # @param component [Symbol] the color component to lighten
140
+ #
141
+ # @return [Decorator] a new instance of Decorator with the color lightened
142
+ # @rbs (Float amount, Symbol component) -> Decorator
143
+ def lighten(amount, component)
144
+ color = instance_variable_get(:"@#{component}")
145
+
146
+ # @type self: Decorator
147
+
148
+ dup.tap do |duped|
149
+ if color
150
+ rgb = Conversion::RGB.transform.lighten(color, amount)
151
+ duped.instance_variable_set(:"@#{component}", rgb)
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sai/decorator'
4
+ require 'sai/decorator/color_manipulations'
5
+ require 'sai/decorator/gradients'
6
+ require 'sai/decorator/hex_colors'
7
+ require 'sai/decorator/named_colors'
8
+ require 'sai/decorator/named_styles'
9
+ require 'sai/decorator/rgb_colors'
10
+
11
+ module Sai
12
+ class Decorator
13
+ # Delegates all methods from the Decorator class and its component modules
14
+ #
15
+ # @author {https://aaronmallen.me Aaron Allen}
16
+ # @since 0.3.1
17
+ #
18
+ # @api private
19
+ module Delegation
20
+ # The list of component modules to delegate methods from
21
+ #
22
+ # @author {https://aaronmallen.me Aaron Allen}
23
+ # @since 0.3.1
24
+ #
25
+ # @api private
26
+ #
27
+ # @return [Array<Symbol>] the list of component modules
28
+ COMPONENT_MODULES = %i[
29
+ ColorManipulations
30
+ Gradients
31
+ HexColors
32
+ NamedColors
33
+ NamedStyles
34
+ RGBColors
35
+ ].freeze #: Array[Symbol]
36
+ private_constant :COMPONENT_MODULES
37
+
38
+ class << self
39
+ # Install delegated methods on the given class or module
40
+ #
41
+ # @author {https://aaronmallen.me Aaron Allen}
42
+ # @since 0.3.1
43
+ #
44
+ # @api private
45
+ #
46
+ # @param klass [Class, Module] the class or module to install the methods on
47
+ #
48
+ # @return [void]
49
+ # @rbs (Class | Module) -> void
50
+ def install(klass)
51
+ ignored_methods = %i[apply call decorate encode]
52
+ methods = collect_delegatable_methods.reject { |m| ignored_methods.include?(m) }
53
+
54
+ methods.each do |method|
55
+ klass.define_method(method) do |*args, **kwargs|
56
+ Decorator.new(mode: Sai.mode.auto).public_send(method, *args, **kwargs)
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Collect all methods from the Decorator class and its component modules
64
+ #
65
+ # @author {https://aaronmallen.me Aaron Allen}
66
+ # @since 0.3.1
67
+ #
68
+ # @api private
69
+ #
70
+ # @return [Array<Symbol>] the list of methods to delegate
71
+ # @rbs () -> Array[Symbol]
72
+ def collect_delegatable_methods
73
+ methods = Decorator.instance_methods(false)
74
+
75
+ COMPONENT_MODULES.each do |mod|
76
+ methods.concat(Decorator.const_get(mod).instance_methods(false))
77
+ end
78
+
79
+ methods.uniq
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end