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,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sai/ansi'
4
+ require 'sai/ansi/color_parser'
5
+ require 'sai/ansi/style_parser'
6
+ require 'strscan'
7
+
8
+ module Sai
9
+ module ANSI
10
+ # Extract ANSI sequence information from a string
11
+ #
12
+ # @author {https://aaronmallen.me Aaron Allen}
13
+ # @since 0.3.0
14
+ #
15
+ # @api private
16
+ class SequenceProcessor
17
+ # The pattern to extract ANSI sequences from a string
18
+ #
19
+ # @author {https://aaronmallen.me Aaron Allen}
20
+ # @since 0.3.0
21
+ #
22
+ # @api private
23
+ #
24
+ # @return [Regexp] the pattern
25
+ SEQUENCE_PATTERN = /\e\[([0-9;]*)m/ #: Regexp
26
+ private_constant :SEQUENCE_PATTERN
27
+
28
+ # Initialize a new instance of SequenceProcessor and parse the provided string
29
+ #
30
+ # @author {https://aaronmallen.me Aaron Allen}
31
+ # @since 0.3.0
32
+ #
33
+ # @api private
34
+ #
35
+ # @param string [String] the string to parse
36
+ #
37
+ # @return [Array<Hash{Symbol => Object}>] the segments
38
+ # @rbs (String string) -> Array[Hash[Symbol, untyped]]
39
+ def self.process(string)
40
+ new(string).process
41
+ end
42
+
43
+ # Initialize a new instance of SequenceProcessor
44
+ #
45
+ # @author {https://aaronmallen.me Aaron Allen}
46
+ # @since 0.3.0
47
+ #
48
+ # @api private
49
+ #
50
+ # @param string [String] the string to parse
51
+ #
52
+ # @return [SequenceProcessor] the new instance of SequenceProcessor
53
+ # @rbs (String string) -> void
54
+ def initialize(string)
55
+ @scanner = StringScanner.new(string)
56
+ @segments = []
57
+ @current_segment = { text: +'', foreground: nil, background: nil, styles: [] }
58
+ @encoded_pos = 0
59
+ @stripped_pos = 0
60
+ @color_parser = ColorParser.new(@current_segment)
61
+ @style_parser = StyleParser.new(@current_segment)
62
+ end
63
+
64
+ # Parse a string and return a hash of segments
65
+ #
66
+ # @author {https://aaronmallen.me Aaron Allen}
67
+ # @since 0.3.0
68
+ #
69
+ # @api private
70
+ #
71
+ # @return [Array<Hash{Symbol => Object}>] the segments
72
+ # @rbs () -> Array[Hash[Symbol, untyped]]
73
+ def process
74
+ consume_tokens
75
+ finalize_segment_if_text!
76
+
77
+ @segments
78
+ end
79
+
80
+ private
81
+
82
+ # Applies the appropriate action for the provided ANSI sequence
83
+ #
84
+ # @author {https://aaronmallen.me Aaron Allen}
85
+ # @since 0.3.0
86
+ #
87
+ # @api private
88
+ #
89
+ # @param sequence [String] an ANSI sequence (e.g. "\e[31m", "\e[0m")
90
+ #
91
+ # @return [void]
92
+ # @rbs (String sequence) -> void
93
+ def apply_ansi_sequence(sequence)
94
+ return reset_segment! if sequence == ANSI::RESET
95
+
96
+ codes = sequence.match(SEQUENCE_PATTERN)&.[](1)
97
+ return unless codes
98
+
99
+ apply_codes(codes)
100
+ end
101
+
102
+ # Parse all numeric codes in the provided string, applying them in order (just like a real ANSI terminal)
103
+ #
104
+ # @author {https://aaronmallen.me Aaron Allen}
105
+ # @since 0.3.0
106
+ #
107
+ # @api private
108
+ #
109
+ # @param codes_string [String] e.g. "38;5;160;48;5;21;1"
110
+ #
111
+ # @return [void]
112
+ # @rbs (String codes_string) -> void
113
+ def apply_codes(codes_string)
114
+ codes_array = codes_string.split(';').map(&:to_i)
115
+ i = 0
116
+ i = apply_single_code(codes_array, i) while i < codes_array.size
117
+ end
118
+
119
+ # Applies a single code (or group) from the array. This might be:
120
+ # - 0 => reset
121
+ # - 30..37 => basic FG color
122
+ # - 40..47 => basic BG color
123
+ # - 38 or 48 => extended color sequence
124
+ # - otherwise => style code (bold, underline, etc.)
125
+ #
126
+ # @author {https://aaronmallen.me Aaron Allen}
127
+ # @since 0.3.0
128
+ #
129
+ # @api private
130
+ #
131
+ # @param codes_array [Array<Integer>] the list of numeric codes
132
+ # @param index [Integer] the current index
133
+ #
134
+ # @return [Integer] the updated index after consuming needed codes
135
+ # @rbs (Array[Integer] codes_array, Integer index) -> Integer
136
+ def apply_single_code(codes_array, index) # rubocop:disable Metrics/MethodLength
137
+ code = codes_array[index]
138
+ return index + 1 if code.nil?
139
+
140
+ case code
141
+ when 0
142
+ reset_segment!
143
+ index + 1
144
+ when 30..37, 40..47
145
+ @color_parser.parse_basic(code)
146
+ index + 1
147
+ when 38, 48
148
+ parse_extended_color(codes_array, index)
149
+ else
150
+ @style_parser.parse(code)
151
+ index + 1
152
+ end
153
+ end
154
+
155
+ # Scans the string for ANSI sequences or individual characters
156
+ #
157
+ # @author {https://aaronmallen.me Aaron Allen}
158
+ # @since 0.3.0
159
+ #
160
+ # @api private
161
+ #
162
+ # @return [void]
163
+ # @rbs () -> void
164
+ def consume_tokens
165
+ handle_ansi_sequence || handle_character until @scanner.eos?
166
+ end
167
+
168
+ # Finalizes the current segment if any text is present, then resets it
169
+ #
170
+ # @author {https://aaronmallen.me Aaron Allen}
171
+ # @since 0.3.0
172
+ #
173
+ # @api private
174
+ #
175
+ # @return [void]
176
+ # @rbs () -> void
177
+ def finalize_segment_if_text!
178
+ return if @current_segment[:text].empty?
179
+
180
+ seg_len = @current_segment[:text].length
181
+ segment = @current_segment.merge(
182
+ encoded_start: @encoded_pos - seg_len,
183
+ encoded_end: @encoded_pos,
184
+ stripped_start: @stripped_pos - seg_len,
185
+ stripped_end: @stripped_pos
186
+ )
187
+
188
+ @segments << segment
189
+ reset_segment!
190
+ end
191
+
192
+ # Attempts to capture an ANSI sequence from the scanner If found, finalizes
193
+ # the current text segment and applies the sequence
194
+ #
195
+ # @author {https://aaronmallen.me Aaron Allen}
196
+ # @since 0.3.0
197
+ #
198
+ # @api private
199
+ #
200
+ # @return [Boolean] `true` if a sequence was found, `false` if otherwise
201
+ # @rbs () -> bool
202
+ def handle_ansi_sequence
203
+ sequence = @scanner.scan(SEQUENCE_PATTERN)
204
+ return false unless sequence
205
+
206
+ finalize_segment_if_text!
207
+ apply_ansi_sequence(sequence)
208
+ @encoded_pos += sequence.length
209
+ true
210
+ end
211
+
212
+ # Reads a single character from the scanner and appends it to the current segment
213
+ #
214
+ # @author {https://aaronmallen.me Aaron Allen}
215
+ # @since 0.3.0
216
+ #
217
+ # @api private
218
+ #
219
+ # @return [void]
220
+ # @rbs () -> void
221
+ def handle_character
222
+ char = @scanner.getch
223
+ @current_segment[:text] << char
224
+ @encoded_pos += 1
225
+ @stripped_pos += 1
226
+ end
227
+
228
+ # Parse extended color codes from the array, e.g. 38;5;160 (256-color) or 38;2;R;G;B (24-bit),
229
+ # and apply them to foreground or background
230
+ #
231
+ # @author {https://aaronmallen.me Aaron Allen}
232
+ # @since 0.3.0
233
+ #
234
+ # @api private
235
+ #
236
+ # @param codes_array [Array<Integer>] the array of codes
237
+ # @param index [Integer] the current position (where we saw 38 or 48)
238
+ #
239
+ # @return [Integer] the updated position in the codes array
240
+ # @rbs (Array[Integer] codes_array, Integer index) -> Integer
241
+ def parse_extended_color(codes_array, index)
242
+ mode_code = codes_array[index + 1]
243
+ return index + 1 unless mode_code
244
+
245
+ case mode_code
246
+ when 5 then @color_parser.parse256(codes_array, index)
247
+ when 2 then @color_parser.parse_24bit(codes_array, index)
248
+ else index + 1
249
+ end
250
+ end
251
+
252
+ # Resets the current segment to a fresh, blank state
253
+ #
254
+ # @author {https://aaronmallen.me Aaron Allen}
255
+ # @since 0.3.0
256
+ #
257
+ # @api private
258
+ #
259
+ # @return [void]
260
+ # @rbs () -> void
261
+ def reset_segment!
262
+ @current_segment[:text] = +''
263
+ @current_segment[:foreground] = nil
264
+ @current_segment[:background] = nil
265
+ @current_segment[:styles] = []
266
+ end
267
+ end
268
+ end
269
+ end