sai 0.2.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +38 -1
  4. data/README.md +39 -241
  5. data/docs/USAGE.md +351 -0
  6. data/lib/sai/ansi/color_parser.rb +109 -0
  7. data/lib/sai/ansi/sequence_processor.rb +269 -0
  8. data/lib/sai/ansi/sequenced_string.rb +475 -0
  9. data/lib/sai/ansi/style_parser.rb +66 -0
  10. data/lib/sai/ansi.rb +0 -27
  11. data/lib/sai/conversion/color_sequence.rb +4 -4
  12. data/lib/sai/conversion/rgb/color_classifier.rb +209 -0
  13. data/lib/sai/conversion/rgb/color_indexer.rb +48 -0
  14. data/lib/sai/conversion/rgb/color_space.rb +192 -0
  15. data/lib/sai/conversion/rgb/color_transformer.rb +140 -0
  16. data/lib/sai/conversion/rgb.rb +23 -269
  17. data/lib/sai/decorator/color_manipulations.rb +157 -0
  18. data/lib/sai/decorator/delegation.rb +84 -0
  19. data/lib/sai/decorator/gradients.rb +363 -0
  20. data/lib/sai/decorator/hex_colors.rb +56 -0
  21. data/lib/sai/decorator/named_colors.rb +780 -0
  22. data/lib/sai/decorator/named_styles.rb +276 -0
  23. data/lib/sai/decorator/rgb_colors.rb +64 -0
  24. data/lib/sai/decorator.rb +35 -795
  25. data/lib/sai/mode_selector.rb +19 -19
  26. data/lib/sai/named_colors.rb +437 -0
  27. data/lib/sai.rb +753 -23
  28. data/sig/manifest.yaml +3 -0
  29. data/sig/sai/ansi/color_parser.rbs +77 -0
  30. data/sig/sai/ansi/sequence_processor.rbs +178 -0
  31. data/sig/sai/ansi/sequenced_string.rbs +380 -0
  32. data/sig/sai/ansi/style_parser.rbs +59 -0
  33. data/sig/sai/ansi.rbs +0 -10
  34. data/sig/sai/conversion/rgb/color_classifier.rbs +165 -0
  35. data/sig/sai/conversion/rgb/color_indexer.rbs +41 -0
  36. data/sig/sai/conversion/rgb/color_space.rbs +129 -0
  37. data/sig/sai/conversion/rgb/color_transformer.rbs +99 -0
  38. data/sig/sai/conversion/rgb.rbs +15 -198
  39. data/sig/sai/decorator/color_manipulations.rbs +125 -0
  40. data/sig/sai/decorator/delegation.rbs +47 -0
  41. data/sig/sai/decorator/gradients.rbs +267 -0
  42. data/sig/sai/decorator/hex_colors.rbs +48 -0
  43. data/sig/sai/decorator/named_colors.rbs +1491 -0
  44. data/sig/sai/decorator/named_styles.rbs +72 -0
  45. data/sig/sai/decorator/rgb_colors.rbs +52 -0
  46. data/sig/sai/decorator.rbs +25 -202
  47. data/sig/sai/mode_selector.rbs +19 -19
  48. data/sig/sai/named_colors.rbs +65 -0
  49. data/sig/sai.rbs +1485 -44
  50. metadata +38 -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