Skip to content

Commit 0e67387

Browse files
committed
Add ability to disable color
When using `super_diff/rspec` in a project that is tested on a service like CircleCI, if a test fails, then the test output will contain color sequences (because the gem produces them via the Csi module). This commit provides a way to disable color by configuring the gem like so: SuperDiff::RSpec.configure do |config| config.color_enabled = false end That said, this setting should take effect automatically if the output stream is not a TTY, just like RSpec does. So everything should "just work" on CI.
1 parent 8fa26cd commit 0e67387

16 files changed

+2375
-1981
lines changed

lib/super_diff.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
require "patience_diff"
44

55
module SuperDiff
6-
autoload :ColorizedDocument, "super_diff/colorized_document"
6+
autoload :ColorizedDocumentExtensions, "super_diff/colorized_document_extensions"
77
autoload :Csi, "super_diff/csi"
88
autoload :DiffFormatter, "super_diff/diff_formatter"
99
autoload :DiffFormatters, "super_diff/diff_formatters"

lib/super_diff/colorized_document.rb renamed to lib/super_diff/colorized_document_extensions.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
module SuperDiff
2-
class ColorizedDocument < Csi::ColorizedDocument
3-
alias_method :normal, :text
4-
# FIXME: Backward compatibility
5-
alias_method :plain, :normal
2+
module ColorizedDocumentExtensions
3+
def self.extended(extendee)
4+
extendee.singleton_class.class_eval do
5+
alias_method :normal, :text
6+
end
7+
end
68

79
def deleted(*args, **opts, &block)
810
colorize(*args, **opts, fg: :red, &block)

lib/super_diff/csi.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,32 @@
22
module SuperDiff
33
module Csi
44
autoload :Color, "super_diff/csi/color"
5+
autoload :ColorSequenceBlock, "super_diff/csi/color_sequence_block"
56
autoload :ColorizedDocument, "super_diff/csi/colorized_document"
7+
autoload :Document, "super_diff/csi/document"
68
autoload :EightBitColor, "super_diff/csi/eight_bit_color"
79
autoload :FourBitColor, "super_diff/csi/four_bit_color"
810
autoload :ResetSequence, "super_diff/csi/reset_sequence"
911
autoload :TwentyFourBitColor, "super_diff/csi/twenty_four_bit_color"
12+
autoload :UncolorizedDocument, "super_diff/csi/uncolorized_document"
13+
14+
class << self
15+
attr_writer :color_enabled
16+
end
1017

1118
def self.reset_sequence
1219
ResetSequence.new
1320
end
1421

22+
def self.color_enabled?
23+
@color_enabled
24+
end
25+
1526
def self.colorize(*args, **opts, &block)
16-
if block
17-
ColorizedDocument.new(&block)
27+
if color_enabled?
28+
ColorizedDocument.new(*args, **opts, &block)
1829
else
19-
ColorizedDocument.new { colorize(*args, **opts) }
30+
UncolorizedDocument.new(*args, **opts, &block)
2031
end
2132
end
2233

@@ -41,5 +52,7 @@ def self.inspect_colors_in(text)
4152
end
4253
end
4354
end
55+
56+
self.color_enabled = STDOUT.tty?
4457
end
4558
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module SuperDiff
2+
module Csi
3+
class ColorSequenceBlock
4+
include Enumerable
5+
6+
def initialize(colors = [])
7+
@colors = colors
8+
end
9+
10+
def each(&block)
11+
colors.each(&block)
12+
end
13+
14+
def push(color)
15+
colors.push(color)
16+
end
17+
alias_method :<<, :push
18+
19+
def to_s
20+
colors.map(&:to_s).join
21+
end
22+
23+
private
24+
25+
attr_reader :colors
26+
end
27+
end
28+
end
Lines changed: 22 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -1,256 +1,72 @@
11
module SuperDiff
22
module Csi
3-
class ColorizedDocument
4-
def self.colorize(*args, **opts, &block)
5-
if block
6-
new(&block)
7-
else
8-
new { colorize(*args, **opts) }
9-
end
10-
end
11-
12-
include Enumerable
13-
3+
class ColorizedDocument < Document
144
def initialize(&block)
15-
@parts = []
165
@color_sequences_open_in_parent = []
17-
@indentation_stack = []
18-
19-
evaluate_block(&block)
20-
end
21-
22-
def each(&block)
23-
parts.each(&block)
24-
end
25-
26-
def text(*contents, **, &block)
27-
if block
28-
evaluate_block(&block)
29-
elsif contents.any?
30-
parts.push(*contents)
31-
else
32-
raise ArgumentError.new(
33-
"Must have something to add to the document!",
34-
)
35-
end
6+
super
367
end
378

38-
def line(*contents, indent_by: 0, &block)
39-
indent(by: indent_by) do
40-
parts << indentation_stack.join
41-
42-
if block
43-
evaluate_block(&block)
44-
elsif contents.any?
45-
text(*contents)
46-
else
47-
raise ArgumentError.new(
48-
"Must have something to add to the document!",
49-
)
50-
end
51-
end
52-
53-
parts << "\n"
54-
end
55-
56-
def newline
57-
parts << "\n"
58-
end
59-
60-
def indent(by:, &block)
61-
# TODO: This won't work if using `text` manually to add lines
62-
indentation_stack << (by.is_a?(String) ? by : " " * by)
63-
evaluate_block(&block)
64-
indentation_stack.pop
65-
end
66-
67-
def colorize(*args, **opts, &block)
68-
contents, colors = args.partition do |arg|
69-
arg.is_a?(String) || arg.is_a?(self.class)
70-
end
71-
72-
if colors[0].is_a?(Symbol)
73-
if colors[0] == :colorize
74-
raise ArgumentError, "#colorize can't call itself!"
75-
else
76-
public_send(colors[0], *contents, *colors[1..-1], **opts, &block)
77-
end
78-
elsif !block && colors.empty? && opts.empty?
79-
text(*contents)
80-
elsif block
81-
colorize_block(colors, opts, &block)
82-
elsif contents.any?
83-
colorize_inline(contents, colors, opts)
84-
else
85-
raise ArgumentError, "Must have something to colorize!"
86-
end
87-
end
88-
alias_method :colored, :colorize
89-
90-
def method_missing(name, *args, **opts, &block)
91-
request = derive_color_request_from(name)
92-
93-
if request
94-
request.resolve(self, args, opts, &block)
95-
else
96-
super
97-
end
98-
end
99-
100-
def respond_to_missing?(name, include_private = false)
101-
request = derive_color_request_from(name)
102-
!request.nil? || super
103-
end
104-
105-
def to_s
106-
parts.map(&:to_s).join.rstrip
107-
end
108-
109-
private
110-
111-
attr_reader :parts, :color_sequences_open_in_parent, :indentation_stack
112-
113-
def derive_color_request_from(name)
114-
match = name.to_s.match(/\A(.+)_line\Z/)
115-
116-
if match
117-
if respond_to?(match[1].to_sym)
118-
return MethodRequest.new(name: match[1].to_sym, line: true)
119-
elsif Csi::Color.exists?(match[1].to_sym)
120-
return ColorRequest.new(name: match[1].to_sym, line: true)
121-
end
122-
elsif Csi::Color.exists?(name.to_sym)
123-
return ColorRequest.new(name: name.to_sym, line: false)
124-
end
125-
end
9+
protected
12610

12711
def colorize_block(colors, opts, &block)
12812
color_sequence = build_initial_color_sequence_from(colors, opts)
12913

13014
if color_sequences_open_in_parent.any?
131-
parts << Csi.reset_sequence
15+
add_part(Csi.reset_sequence)
13216
end
13317

134-
parts << color_sequence
18+
add_part(color_sequence)
13519
color_sequences_open_in_parent << color_sequence
13620
evaluate_block(&block)
137-
parts << Csi.reset_sequence
21+
add_part(Csi.reset_sequence)
13822

13923
color_sequence_to_reopen = color_sequences_open_in_parent.pop
14024
if color_sequences_open_in_parent.any?
141-
parts << color_sequence_to_reopen
25+
add_part(color_sequence_to_reopen)
14226
end
14327
end
14428

14529
def colorize_inline(contents, colors, opts)
14630
color_sequence = build_initial_color_sequence_from(colors, opts)
14731

148-
parts << color_sequence
32+
add_part(color_sequence)
14933

15034
contents.each do |content|
15135
if content.is_a?(self.class)
15236
content.each do |part|
153-
if part.is_a?(ColorSequence)
154-
parts << Csi.reset_sequence
37+
if part.is_a?(ColorSequenceBlock)
38+
add_part(Csi.reset_sequence)
15539
end
15640

157-
parts << part
41+
add_part(part)
15842

159-
if part.is_a?(Csi::ResetSequence)
160-
parts << color_sequence
43+
if part.is_a?(ResetSequence)
44+
add_part(color_sequence)
16145
end
16246
end
16347
else
164-
parts << content
48+
add_part(content)
16549
end
16650
end
16751

168-
parts << Csi.reset_sequence
52+
add_part(Csi.reset_sequence)
16953
end
17054

55+
private
56+
57+
attr_reader :color_sequences_open_in_parent
58+
17159
def build_initial_color_sequence_from(colors, opts)
172-
ColorSequence.new(colors).tap do |sequence|
60+
ColorSequenceBlock.new(colors).tap do |sequence|
17361
if opts[:fg]
174-
sequence << Csi::Color.resolve(opts[:fg], layer: :foreground)
62+
sequence << Color.resolve(opts[:fg], layer: :foreground)
17563
end
17664

17765
if opts[:bg]
178-
sequence << Csi::Color.resolve(opts[:bg], layer: :background)
179-
end
180-
end
181-
end
182-
183-
def evaluate_block(&block)
184-
if block.arity > 0
185-
block.call(self)
186-
else
187-
instance_eval(&block)
188-
end
189-
end
190-
191-
class Request
192-
def initialize(name:, line:)
193-
@name = name
194-
@line = line
195-
end
196-
197-
protected
198-
199-
attr_reader :name
200-
201-
def for_line?
202-
@line
203-
end
204-
205-
def wrapper
206-
if for_line?
207-
:line
208-
else
209-
:text
66+
sequence << Color.resolve(opts[:bg], layer: :background)
21067
end
21168
end
21269
end
213-
214-
class ColorRequest < Request
215-
def resolve(doc, args, opts, &block)
216-
doc.public_send(wrapper) do |d|
217-
d.colorize(*args, **opts, fg: name, &block)
218-
end
219-
end
220-
end
221-
222-
class MethodRequest < Request
223-
def resolve(doc, args, opts, &block)
224-
doc.public_send(wrapper) do |d|
225-
d.public_send(name, *args, **opts, &block)
226-
end
227-
end
228-
end
229-
230-
class ColorSequence
231-
include Enumerable
232-
233-
def initialize(colors = [])
234-
@colors = colors
235-
end
236-
237-
def each(&block)
238-
colors.each(&block)
239-
end
240-
241-
def push(color)
242-
colors.push(color)
243-
end
244-
alias_method :<<, :push
245-
246-
def to_s
247-
colors.map(&:to_s).join
248-
end
249-
250-
private
251-
252-
attr_reader :colors
253-
end
25470
end
25571
end
25672
end

0 commit comments

Comments
 (0)