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
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sai/ansi/sequenced_string'
4
+ require 'sai/conversion/rgb'
5
+
6
+ module Sai
7
+ class Decorator
8
+ # Color gradient methods for the {Decorator} class
9
+ #
10
+ # @author {https://aaronmallen.me Aaron Allen}
11
+ # @since 0.3.1
12
+ #
13
+ # @abstract This module is meant to be included in the {Decorator} class to provide color gradient methods
14
+ # @api private
15
+ module Gradients
16
+ # Build a foreground gradient between two colors for text decoration
17
+ #
18
+ # @author {https://aaronmallen.me Aaron Allen}
19
+ # @since 0.3.1
20
+ #
21
+ # @api public
22
+ #
23
+ # @example Create a foreground gradient from red to blue
24
+ # decorator.gradient(:red, :blue, 10).decorate('Hello, World!')
25
+ # #=> "\e[38;2;255;0;0mH\e[0m\e[38;2;204;0;51me\e[0m..."
26
+ #
27
+ # @param start_color [Array<Integer>, String, Symbol] the starting color
28
+ # @param end_color [Array<Integer>, String, Symbol] the ending color
29
+ # @param steps [Integer] the number of gradient steps (minimum 2)
30
+ #
31
+ # @raise [ArgumentError] if steps is less than 2
32
+ # @return [Decorator] a new instance of Decorator with foreground gradient colors
33
+ # @rbs (
34
+ # Array[Integer] | String | Symbol start_color,
35
+ # Array[Integer] | String | Symbol end_color,
36
+ # Integer steps
37
+ # ) -> Decorator
38
+ def gradient(start_color, end_color, steps)
39
+ colors = Conversion::RGB.transform.gradient(start_color, end_color, steps)
40
+ dup.tap { |duped| duped.instance_variable_set(:@foreground_sequence, colors) } #: Decorator
41
+ end
42
+
43
+ # Build a background gradient between two colors for text decoration
44
+ #
45
+ # @author {https://aaronmallen.me Aaron Allen}
46
+ # @since 0.3.1
47
+ #
48
+ # @api public
49
+ #
50
+ # @example Create a background gradient from red to blue
51
+ # decorator.on_gradient(:red, :blue, 10).decorate('Hello, World!')
52
+ # #=> "\e[48;2;255;0;0mH\e[0m\e[48;2;204;0;51me\e[0m..."
53
+ #
54
+ # @param start_color [Array<Integer>, String, Symbol] the starting color
55
+ # @param end_color [Array<Integer>, String, Symbol] the ending color
56
+ # @param steps [Integer] the number of gradient steps (minimum 2)
57
+ #
58
+ # @raise [ArgumentError] if steps is less than 2
59
+ # @return [Decorator] a new instance of Decorator with background gradient colors
60
+ # @rbs (
61
+ # Array[Integer] | String | Symbol start_color,
62
+ # Array[Integer] | String | Symbol end_color,
63
+ # Integer steps
64
+ # ) -> Decorator
65
+ def on_gradient(start_color, end_color, steps)
66
+ colors = Conversion::RGB.transform.gradient(start_color, end_color, steps)
67
+ dup.tap { |duped| duped.instance_variable_set(:@background_sequence, colors) } #: Decorator
68
+ end
69
+
70
+ # Build a background rainbow gradient for text decoration
71
+ #
72
+ # @author {https://aaronmallen.me Aaron Allen}
73
+ # @since 0.3.1
74
+ #
75
+ # @api public
76
+ #
77
+ # @example Create a rainbow background gradient
78
+ # decorator.on_rainbow(6).decorate('Hello, World!')
79
+ # #=> "\e[48;2;255;0;0mH\e[0m\e[48;2;255;255;0me\e[0m..."
80
+ #
81
+ # @param steps [Integer] the number of colors to generate (minimum 2)
82
+ #
83
+ # @raise [ArgumentError] if steps is less than 2
84
+ # @return [Decorator] a new instance of Decorator with background rainbow colors
85
+ # @rbs (Integer steps) -> Decorator
86
+ def on_rainbow(steps)
87
+ colors = Conversion::RGB.transform.rainbow_gradient(steps)
88
+ dup.tap { |duped| duped.instance_variable_set(:@background_sequence, colors) } #: Decorator
89
+ end
90
+
91
+ # Build a foreground rainbow gradient for text decoration
92
+ #
93
+ # @author {https://aaronmallen.me Aaron Allen}
94
+ # @since 0.3.1
95
+ #
96
+ # @api public
97
+ #
98
+ # @example Create a rainbow text gradient
99
+ # decorator.rainbow(6).decorate('Hello, World!')
100
+ # #=> "\e[38;2;255;0;0mH\e[0m\e[38;2;255;255;0me\e[0m..."
101
+ #
102
+ # @param steps [Integer] the number of colors to generate (minimum 2)
103
+ #
104
+ # @raise [ArgumentError] if steps is less than 2
105
+ # @return [Decorator] a new instance of Decorator with foreground rainbow colors
106
+ # @rbs (Integer steps) -> Decorator
107
+ def rainbow(steps)
108
+ colors = Conversion::RGB.transform.rainbow_gradient(steps)
109
+ dup.tap { |duped| duped.instance_variable_set(:@foreground_sequence, colors) } #: Decorator
110
+ end
111
+
112
+ private
113
+
114
+ # Adjust number of colors to match text length
115
+ #
116
+ # @author {https://aaronmallen.me Aaron Allen}
117
+ # @since 0.3.1
118
+ #
119
+ # @api private
120
+ #
121
+ # @param colors [Array<Array<Integer>>] original color sequence
122
+ # @param text_length [Integer] desired number of colors
123
+ #
124
+ # @return [Array<Array<Integer>>] adjusted color sequence
125
+ # @rbs (Array[Array[Integer]] colors, Integer text_length) -> Array[Array[Integer]]
126
+ def adjust_colors_to_text_length(colors, text_length)
127
+ return colors if colors.length == text_length
128
+ return stretch_colors(colors, text_length) if colors.length < text_length
129
+
130
+ shrink_colors(colors, text_length)
131
+ end
132
+
133
+ # Apply color sequence gradients to text
134
+ #
135
+ # @author {https://aaronmallen.me Aaron Allen}
136
+ # @since 0.3.1
137
+ #
138
+ # @api private
139
+ #
140
+ # @param text [String] the text to apply the gradient to
141
+ #
142
+ # @return [ANSI::SequencedString] the text with gradient applied
143
+ # @rbs (String text) -> ANSI::SequencedString
144
+ def apply_sequence_gradient(text)
145
+ # @type self: Decorator
146
+ return ANSI::SequencedString.new(text) unless should_decorate?
147
+
148
+ chars = text.chars
149
+ adjusted_colors = prepare_color_sequences(chars.length)
150
+ gradient_text = build_gradient_text(chars, adjusted_colors)
151
+
152
+ ANSI::SequencedString.new(gradient_text.join)
153
+ end
154
+
155
+ # Build color sequences for a single character
156
+ #
157
+ # @author {https://aaronmallen.me Aaron Allen}
158
+ # @since 0.3.1
159
+ #
160
+ # @api private
161
+ #
162
+ # @param colors [Hash] color sequences for foreground and background
163
+ # @param index [Integer] character position
164
+ #
165
+ # @return [Array<String>] ANSI sequences for the character
166
+ # @rbs (Hash[Symbol, Array[Array[Integer]]] colors, Integer index) -> Array[String]
167
+ def build_color_sequences(colors, index)
168
+ # @type self: Decorator
169
+ sequences = []
170
+ sequences << get_foreground_sequence(colors[:foreground], index) if colors[:foreground]
171
+ sequences << get_background_sequence(colors[:background], index) if colors[:background]
172
+ sequences += style_sequences
173
+ sequences.compact
174
+ end
175
+
176
+ # Build gradient text from characters and color sequences
177
+ #
178
+ # @author {https://aaronmallen.me Aaron Allen}
179
+ # @since 0.3.1
180
+ #
181
+ # @api private
182
+ #
183
+ # @param chars [Array<String>] text characters
184
+ # @param colors [Hash] color sequences for foreground and background
185
+ #
186
+ # @return [Array<String>] colored characters
187
+ # @rbs (Array[String] chars, Hash[Symbol, Array[Array[Integer]]] colors) -> Array[String]
188
+ def build_gradient_text(chars, colors)
189
+ chars.each_with_index.map do |char, i|
190
+ next char if char == ' '
191
+
192
+ sequences = build_color_sequences(colors, i)
193
+ "#{sequences.join}#{char}#{ANSI::RESET}"
194
+ end
195
+ end
196
+
197
+ # Calculate indices and progress for color interpolation
198
+ #
199
+ # @author {https://aaronmallen.me Aaron Allen}
200
+ # @since 0.3.1
201
+ #
202
+ # @api private
203
+ #
204
+ # @param position [Integer] current position in sequence
205
+ # @param step_size [Float] size of each step
206
+ # @param max_index [Integer] maximum index allowed
207
+ #
208
+ # @return [Hash] interpolation indices and progress
209
+ # @rbs (Integer position, Float step_size, Integer max_index) -> Hash[Symbol, Integer | Float]
210
+ def calculate_interpolation_indices(position, step_size, max_index)
211
+ position_in_sequence = position * step_size
212
+ lower = position_in_sequence.floor
213
+ upper = [lower + 1, max_index - 1].min
214
+ progress = position_in_sequence - lower
215
+
216
+ { lower: lower, upper: upper, progress: progress }
217
+ end
218
+
219
+ # Get background sequence for a character
220
+ #
221
+ # @author {https://aaronmallen.me Aaron Allen}
222
+ # @since 0.3.1
223
+ #
224
+ # @api private
225
+ #
226
+ # @param colors [Array<Array<Integer>>, nil] background color sequence
227
+ # @param index [Integer] character position
228
+ #
229
+ # @return [String, nil] ANSI sequence for background
230
+ # @rbs (Array[Array[Integer]]? colors, Integer index) -> String?
231
+ def get_background_sequence(colors, index)
232
+ if colors
233
+ Conversion::ColorSequence.resolve(colors[index], @mode, :background)
234
+ elsif @background_sequence
235
+ Conversion::ColorSequence.resolve(@background_sequence[index], @mode, :background)
236
+ end
237
+ end
238
+
239
+ # Get foreground sequence for a character
240
+ #
241
+ # @author {https://aaronmallen.me Aaron Allen}
242
+ # @since 0.3.1
243
+ #
244
+ # @api private
245
+ #
246
+ # @param colors [Array<Array<Integer>>, nil] foreground color sequence
247
+ # @param index [Integer] character position
248
+ #
249
+ # @return [String, nil] ANSI sequence for foreground
250
+ # @rbs (Array[Array[Integer]]? colors, Integer index) -> String?
251
+ def get_foreground_sequence(colors, index)
252
+ if colors
253
+ Conversion::ColorSequence.resolve(colors[index], @mode)
254
+ elsif @foreground_sequence
255
+ Conversion::ColorSequence.resolve(@foreground_sequence[index], @mode)
256
+ end
257
+ end
258
+
259
+ # Interpolate between two colors in a sequence
260
+ #
261
+ # @author {https://aaronmallen.me Aaron Allen}
262
+ # @since 0.3.1
263
+ #
264
+ # @api private
265
+ #
266
+ # @param colors [Array<Array<Integer>>] color sequence
267
+ # @param indices [Hash] interpolation indices and progress
268
+ #
269
+ # @return [Array<Integer>] interpolated color
270
+ # @rbs (Array[Array[Integer]] colors, Hash[Symbol, Integer | Float]) -> Array[Integer]
271
+ def interpolate_sequence_colors(colors, indices)
272
+ lower_index = indices[:lower].to_i
273
+ upper_index = indices[:upper].to_i
274
+ progress = indices[:progress].to_f
275
+
276
+ color1 = colors[lower_index]
277
+ color2 = colors[upper_index]
278
+
279
+ # Add nil guards
280
+ return [0, 0, 0] unless color1 && color2
281
+
282
+ color1.zip(color2).map do |c1, c2|
283
+ next 0 unless c1 && c2 # Add nil guard for individual components
284
+
285
+ (c1 + ((c2 - c1) * progress)).round
286
+ end
287
+ end
288
+
289
+ # Prepare foreground and background color sequences for text
290
+ #
291
+ # @author {https://aaronmallen.me Aaron Allen}
292
+ # @since 0.3.1
293
+ #
294
+ # @api private
295
+ #
296
+ # @param text_length [Integer] length of text to color
297
+ #
298
+ # @return [Hash] adjusted color sequences
299
+ # @rbs (Integer text_length) -> Hash[Symbol, Array[Array[Integer]]]
300
+ def prepare_color_sequences(text_length)
301
+ sequences = {}
302
+ sequences[:foreground] = prepare_sequence(@foreground_sequence, text_length) if @foreground_sequence
303
+ sequences[:background] = prepare_sequence(@background_sequence, text_length) if @background_sequence
304
+ sequences
305
+ end
306
+
307
+ # Prepare a single color sequence
308
+ #
309
+ # @author {https://aaronmallen.me Aaron Allen}
310
+ # @since 0.3.1
311
+ #
312
+ # @api private
313
+ #
314
+ # @param sequence [Array<Array<Integer>>, nil] color sequence to prepare
315
+ # @param text_length [Integer] length of text to color
316
+ #
317
+ # @return [Array<Array<Integer>>, nil] adjusted color sequence
318
+ # @rbs (Array[Array[Integer]]? sequence, Integer text_length) -> Array[Array[Integer]]?
319
+ def prepare_sequence(sequence, text_length)
320
+ sequence && adjust_colors_to_text_length(sequence, text_length)
321
+ end
322
+
323
+ # Shrink a color sequence to fit desired length
324
+ #
325
+ # @author {https://aaronmallen.me Aaron Allen}
326
+ # @since 0.3.1
327
+ #
328
+ # @api private
329
+ #
330
+ # @param colors [Array<Array<Integer>>] original color sequence
331
+ # @param target_length [Integer] desired number of colors
332
+ #
333
+ # @return [Array<Array<Integer>>] shrunk color sequence
334
+ # @rbs (Array[Array[Integer]] colors, Integer target_length) -> Array[Array[Integer]]
335
+ def shrink_colors(colors, target_length)
336
+ step_size = (target_length - 1).to_f / (colors.length - 1)
337
+ indices = (0...colors.length).select { |i| (i * step_size).round < target_length }
338
+ indices.map { |i| colors[i] }
339
+ end
340
+
341
+ # Stretch a color sequence to fit desired length
342
+ #
343
+ # @author {https://aaronmallen.me Aaron Allen}
344
+ # @since 0.3.1
345
+ #
346
+ # @api private
347
+ #
348
+ # @param colors [Array<Array<Integer>>] original color sequence
349
+ # @param target_length [Integer] desired number of colors
350
+ #
351
+ # @return [Array<Array<Integer>>] stretched color sequence
352
+ # @rbs (Array[Array[Integer]] colors, Integer target_length) -> Array[Array[Integer]]
353
+ def stretch_colors(colors, target_length)
354
+ step_size = (colors.length - 1).to_f / (target_length - 1)
355
+
356
+ (0...target_length).map do |i|
357
+ indices = calculate_interpolation_indices(i, step_size, colors.length)
358
+ interpolate_sequence_colors(colors, indices)
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sai
4
+ class Decorator
5
+ # Hexadecimal color 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 hexadecimal color methods
11
+ # @api private
12
+ module HexColors
13
+ # Apply a hexadecimal color to the foreground
14
+ #
15
+ # @author {https://aaronmallen.me Aaron Allen}
16
+ # @since 0.1.0
17
+ #
18
+ # @api public
19
+ #
20
+ # @example
21
+ # decorator.hex("#EB4133").decorate('Hello, world!').to_s #=> "\e[38;2;235;65;51mHello, world!\e[0m"
22
+ #
23
+ # @param code [String] the hex color code
24
+ #
25
+ # @raise [ArgumentError] if the hex code is invalid
26
+ # @return [Decorator] a new instance of Decorator with the hex color applied
27
+ # @rbs (String code) -> Decorator
28
+ def hex(code)
29
+ raise ArgumentError, "Invalid hex color code: #{code}" unless /^#?([A-Fa-f0-9]{6})$/.match?(code)
30
+
31
+ dup.tap { |duped| duped.instance_variable_set(:@foreground, code) } #: Decorator
32
+ end
33
+
34
+ # Apply a hexadecimal color to the background
35
+ #
36
+ # @author {https://aaronmallen.me Aaron Allen}
37
+ # @since 0.1.0
38
+ #
39
+ # @api public
40
+ #
41
+ # @example
42
+ # decorator.on_hex("#EB4133").decorate('Hello, world!').to_s #=> "\e[48;2;235;65;51mHello, world!\e[0m"
43
+ #
44
+ # @param code [String] the hex color code
45
+ #
46
+ # @raise [ArgumentError] if the hex code is invalid
47
+ # @return [Decorator] a new instance of Decorator with the hex color applied
48
+ # @rbs (String code) -> Decorator
49
+ def on_hex(code)
50
+ raise ArgumentError, "Invalid hex color code: #{code}" unless /^#?([A-Fa-f0-9]{6})$/.match?(code)
51
+
52
+ dup.tap { |duped| duped.instance_variable_set(:@background, code) } #: Decorator
53
+ end
54
+ end
55
+ end
56
+ end