sai 0.3.0 → 0.3.1

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 (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