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.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/CHANGELOG.md +38 -1
- data/README.md +39 -241
- data/docs/USAGE.md +351 -0
- data/lib/sai/ansi/color_parser.rb +109 -0
- data/lib/sai/ansi/sequence_processor.rb +269 -0
- data/lib/sai/ansi/sequenced_string.rb +475 -0
- data/lib/sai/ansi/style_parser.rb +66 -0
- data/lib/sai/ansi.rb +0 -27
- data/lib/sai/conversion/color_sequence.rb +4 -4
- data/lib/sai/conversion/rgb/color_classifier.rb +209 -0
- data/lib/sai/conversion/rgb/color_indexer.rb +48 -0
- data/lib/sai/conversion/rgb/color_space.rb +192 -0
- data/lib/sai/conversion/rgb/color_transformer.rb +140 -0
- data/lib/sai/conversion/rgb.rb +23 -269
- data/lib/sai/decorator/color_manipulations.rb +157 -0
- data/lib/sai/decorator/delegation.rb +84 -0
- data/lib/sai/decorator/gradients.rb +363 -0
- data/lib/sai/decorator/hex_colors.rb +56 -0
- data/lib/sai/decorator/named_colors.rb +780 -0
- data/lib/sai/decorator/named_styles.rb +276 -0
- data/lib/sai/decorator/rgb_colors.rb +64 -0
- data/lib/sai/decorator.rb +35 -795
- data/lib/sai/mode_selector.rb +19 -19
- data/lib/sai/named_colors.rb +437 -0
- data/lib/sai.rb +753 -23
- data/sig/manifest.yaml +3 -0
- data/sig/sai/ansi/color_parser.rbs +77 -0
- data/sig/sai/ansi/sequence_processor.rbs +178 -0
- data/sig/sai/ansi/sequenced_string.rbs +380 -0
- data/sig/sai/ansi/style_parser.rbs +59 -0
- data/sig/sai/ansi.rbs +0 -10
- data/sig/sai/conversion/rgb/color_classifier.rbs +165 -0
- data/sig/sai/conversion/rgb/color_indexer.rbs +41 -0
- data/sig/sai/conversion/rgb/color_space.rbs +129 -0
- data/sig/sai/conversion/rgb/color_transformer.rbs +99 -0
- data/sig/sai/conversion/rgb.rbs +15 -198
- data/sig/sai/decorator/color_manipulations.rbs +125 -0
- data/sig/sai/decorator/delegation.rbs +47 -0
- data/sig/sai/decorator/gradients.rbs +267 -0
- data/sig/sai/decorator/hex_colors.rbs +48 -0
- data/sig/sai/decorator/named_colors.rbs +1491 -0
- data/sig/sai/decorator/named_styles.rbs +72 -0
- data/sig/sai/decorator/rgb_colors.rbs +52 -0
- data/sig/sai/decorator.rbs +25 -202
- data/sig/sai/mode_selector.rbs +19 -19
- data/sig/sai/named_colors.rbs +65 -0
- data/sig/sai.rbs +1485 -44
- 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
|