ruby-ansi-formatter 0.1.0

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 (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ansi.rb +248 -0
  3. metadata +43 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1b078adce12fac81b76b16aa90260a235bf855f2082ffd1a268fa7cd8abff7eb
4
+ data.tar.gz: c00bbaf68b69b8ea0f807a91541e6ca5db5caf29a7e0ce8c25b3cbb8471fc803
5
+ SHA512:
6
+ metadata.gz: c0e0503b5d526dddc7a440565d2a64700e7d890af38d7b50d13ae780f0a35dce16144c99d4226b7ec8a00556ffc49d40bad9edc00daa9172a1b5e002f17f2b32
7
+ data.tar.gz: '08459dcc670e3c0d58dc71a6c0f8dabd35ef3618eededa7ec3ddb5e756f58e8357d0a1d82de70ab328ec75abb6f04b6fb8c29a7335795e1b6d5249cf50ed5e43'
data/lib/ansi.rb ADDED
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Helpers for wrapping text in ANSI escape codes and expanding inline DSL tokens.
4
+ module Ansi
5
+ NAME = "ruby-ansi-formatter"
6
+ VERSION = "0.1.0"
7
+
8
+ # Maps public formatter names to their DSL prefixes and ANSI escape codes.
9
+ FORMATS = {
10
+ reset: {
11
+ prefix: "R",
12
+ code: "\e[0m",
13
+ },
14
+ bold: {
15
+ prefix: "B",
16
+ code: "\e[1m",
17
+ },
18
+ dim: {
19
+ prefix: "D",
20
+ code: "\e[2m",
21
+ },
22
+ italic: {
23
+ prefix: "I",
24
+ code: "\e[3m",
25
+ },
26
+ underline: {
27
+ prefix: "U",
28
+ code: "\e[4m",
29
+ },
30
+ invert: {
31
+ prefix: "X",
32
+ code: "\e[7m",
33
+ },
34
+ hidden: {
35
+ prefix: "H",
36
+ code: "\e[8m",
37
+ },
38
+ strikethrough: {
39
+ prefix: "S",
40
+ code: "\e[9m",
41
+ },
42
+ double_underline: {
43
+ prefix: "DU",
44
+ code: "\e[21m",
45
+ },
46
+ overline: {
47
+ prefix: "OV",
48
+ code: "\e[53m",
49
+ },
50
+ red: {
51
+ prefix: "r",
52
+ code: "\e[31m",
53
+ },
54
+ green: {
55
+ prefix: "g",
56
+ code: "\e[32m",
57
+ },
58
+ yellow: {
59
+ prefix: "y",
60
+ code: "\e[33m",
61
+ },
62
+ blue: {
63
+ prefix: "b",
64
+ code: "\e[34m",
65
+ },
66
+ magenta: {
67
+ prefix: "m",
68
+ code: "\e[35m",
69
+ },
70
+ cyan: {
71
+ prefix: "c",
72
+ code: "\e[36m",
73
+ },
74
+ white: {
75
+ prefix: "w",
76
+ code: "\e[37m",
77
+ },
78
+ bright_red: {
79
+ prefix: "br",
80
+ code: "\e[91m",
81
+ },
82
+ bright_green: {
83
+ prefix: "bg",
84
+ code: "\e[92m",
85
+ },
86
+ bright_yellow: {
87
+ prefix: "by",
88
+ code: "\e[93m",
89
+ },
90
+ bright_blue: {
91
+ prefix: "bb",
92
+ code: "\e[94m",
93
+ },
94
+ bright_magenta: {
95
+ prefix: "bm",
96
+ code: "\e[95m",
97
+ },
98
+ bright_cyan: {
99
+ prefix: "bc",
100
+ code: "\e[96m",
101
+ },
102
+ bright_white: {
103
+ prefix: "bw",
104
+ code: "\e[97m",
105
+ },
106
+ }.freeze
107
+
108
+ class << self
109
+ # Defines formatter helpers for each entry in {FORMATS}.
110
+ #
111
+ # Each generated method accepts text and returns it wrapped in matching ANSI
112
+ # format code followed by reset code. Helpers are exposed under both format
113
+ # names like `bold` and DSL prefixes like `B`.
114
+ FORMATS.each do |format, data|
115
+ define_method(format) do |text|
116
+ "#{data[:code]}#{text}#{FORMATS[:reset][:code]}"
117
+ end
118
+
119
+ define_method(data[:prefix]) do |text|
120
+ "#{data[:code]}#{text}#{FORMATS[:reset][:code]}"
121
+ end
122
+ end
123
+
124
+ # Returns registered formatter names.
125
+ #
126
+ # @return [Array<Symbol>]
127
+ def formats
128
+ @formats ||= FORMATS.keys
129
+ end
130
+
131
+ # Returns DSL prefixes for all registered formatters.
132
+ #
133
+ # @return [Array<String>]
134
+ def prefixes
135
+ @prefixes ||= FORMATS.map do |_format, data|
136
+ data[:prefix]
137
+ end
138
+ end
139
+
140
+ # Returns ANSI escape codes for all registered formatters.
141
+ #
142
+ # @return [Array<String>]
143
+ def codes
144
+ @codes ||= FORMATS.map do |_format, data|
145
+ data[:code]
146
+ end
147
+ end
148
+
149
+ # Expands `%<prefix>(text)` and `%<format_name>(text)` DSL tokens in string.
150
+ #
151
+ # Supports multiline content, nested tokens, and restoration of outer active
152
+ # formatting after nested token resets.
153
+ #
154
+ # @param string [String]
155
+ # @return [String]
156
+ def format(string)
157
+ parse_text(string)
158
+ end
159
+
160
+ private
161
+
162
+ # Recursively expands DSL tokens while preserving outer active ANSI codes.
163
+ #
164
+ # @param string [String]
165
+ # @param active_codes [Array<String>]
166
+ # @return [String]
167
+ def parse_text(string, active_codes = [])
168
+ result = +""
169
+ index = 0
170
+
171
+ while index < string.length
172
+ token = extract_token(string, index)
173
+
174
+ unless token
175
+ result << string[index]
176
+ index += 1
177
+ next
178
+ end
179
+
180
+ code = token_data.fetch(token[:token]).fetch(:code)
181
+
182
+ result << code
183
+ result << parse_text(token[:text], active_codes + [code])
184
+ result << FORMATS.fetch(:reset).fetch(:code)
185
+ result << active_codes.join
186
+
187
+ index = token[:next_index]
188
+ end
189
+
190
+ result
191
+ end
192
+
193
+ # Extracts next complete DSL token starting at start_index.
194
+ #
195
+ # @param string [String]
196
+ # @param start_index [Integer]
197
+ # @return [Hash, nil]
198
+ def extract_token(string, start_index)
199
+ return unless string[start_index] == "%"
200
+
201
+ token = sorted_tokens.find do |candidate|
202
+ string[start_index + 1, candidate.length] == candidate &&
203
+ string[start_index + candidate.length + 1] == "("
204
+ end
205
+ return unless token
206
+
207
+ content_start = start_index + token.length + 2
208
+ depth = 1
209
+ index = content_start
210
+
211
+ while index < string.length
212
+ case string[index]
213
+ when "("
214
+ depth += 1
215
+ when ")"
216
+ depth -= 1
217
+
218
+ if depth.zero?
219
+ return {
220
+ token:,
221
+ text: string[content_start...index],
222
+ next_index: index + 1,
223
+ }
224
+ end
225
+ end
226
+
227
+ index += 1
228
+ end
229
+ end
230
+
231
+ # Builds lookup map from DSL token to formatter metadata.
232
+ #
233
+ # @return [Hash{String => Hash}]
234
+ def token_data
235
+ @token_data ||= FORMATS.each_with_object({}) do |(format, data), result|
236
+ result[data[:prefix]] = data
237
+ result[format.to_s] = data
238
+ end
239
+ end
240
+
241
+ # Returns DSL tokens sorted longest-first for greedy token matching.
242
+ #
243
+ # @return [Array<String>]
244
+ def sorted_tokens
245
+ @sorted_tokens ||= token_data.keys.sort_by { |token| -token.length }
246
+ end
247
+ end
248
+ end
metadata ADDED
@@ -0,0 +1,43 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-ansi-formatter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Greenfield
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Provides helper methods to apply ANSI formatting to text as well as a
13
+ DSL for applying several formats in a single string
14
+ email:
15
+ - mattgreenfield1@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/ansi.rb
21
+ homepage: https://github.com/omgreenfield/ruby-ansi-formatter
22
+ licenses:
23
+ - MIT
24
+ metadata:
25
+ rubygems_mfa_required: 'true'
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 3.4.0
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubygems_version: 4.0.10
41
+ specification_version: 4
42
+ summary: Ruby ANSI formatter
43
+ test_files: []