super_diff 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +174 -0
- data/lib/super_diff/csi/color_helper.rb +52 -0
- data/lib/super_diff/csi/eight_bit_color.rb +131 -0
- data/lib/super_diff/csi/eight_bit_sequence.rb +27 -0
- data/lib/super_diff/csi/four_bit_color.rb +80 -0
- data/lib/super_diff/csi/four_bit_sequence.rb +24 -0
- data/lib/super_diff/csi/reset_sequence.rb +9 -0
- data/lib/super_diff/csi/sequence.rb +22 -0
- data/lib/super_diff/csi/twenty_four_bit_color.rb +41 -0
- data/lib/super_diff/csi/twenty_four_bit_sequence.rb +27 -0
- data/lib/super_diff/csi.rb +29 -0
- data/lib/super_diff/diff_formatter.rb +37 -0
- data/lib/super_diff/diff_formatters/array.rb +21 -0
- data/lib/super_diff/diff_formatters/base.rb +37 -0
- data/lib/super_diff/diff_formatters/collection.rb +107 -0
- data/lib/super_diff/diff_formatters/hash.rb +34 -0
- data/lib/super_diff/diff_formatters/multi_line_string.rb +31 -0
- data/lib/super_diff/diff_formatters/object.rb +27 -0
- data/lib/super_diff/diff_formatters.rb +5 -0
- data/lib/super_diff/differ.rb +48 -0
- data/lib/super_diff/differs/array.rb +24 -0
- data/lib/super_diff/differs/base.rb +42 -0
- data/lib/super_diff/differs/empty.rb +13 -0
- data/lib/super_diff/differs/hash.rb +24 -0
- data/lib/super_diff/differs/multi_line_string.rb +27 -0
- data/lib/super_diff/differs/object.rb +68 -0
- data/lib/super_diff/differs.rb +5 -0
- data/lib/super_diff/equality_matcher.rb +45 -0
- data/lib/super_diff/equality_matchers/array.rb +44 -0
- data/lib/super_diff/equality_matchers/base.rb +42 -0
- data/lib/super_diff/equality_matchers/hash.rb +44 -0
- data/lib/super_diff/equality_matchers/multi_line_string.rb +44 -0
- data/lib/super_diff/equality_matchers/object.rb +18 -0
- data/lib/super_diff/equality_matchers/single_line_string.rb +28 -0
- data/lib/super_diff/equality_matchers.rb +5 -0
- data/lib/super_diff/errors.rb +20 -0
- data/lib/super_diff/helpers.rb +96 -0
- data/lib/super_diff/operation_sequences/array.rb +14 -0
- data/lib/super_diff/operation_sequences/base.rb +11 -0
- data/lib/super_diff/operation_sequences/hash.rb +14 -0
- data/lib/super_diff/operation_sequences/object.rb +14 -0
- data/lib/super_diff/operational_sequencer.rb +43 -0
- data/lib/super_diff/operational_sequencers/array.rb +127 -0
- data/lib/super_diff/operational_sequencers/base.rb +97 -0
- data/lib/super_diff/operational_sequencers/hash.rb +82 -0
- data/lib/super_diff/operational_sequencers/multi_line_string.rb +85 -0
- data/lib/super_diff/operational_sequencers/object.rb +96 -0
- data/lib/super_diff/operational_sequencers.rb +5 -0
- data/lib/super_diff/operations/binary_operation.rb +47 -0
- data/lib/super_diff/operations/unary_operation.rb +25 -0
- data/lib/super_diff/rspec/differ.rb +30 -0
- data/lib/super_diff/rspec/monkey_patches.rb +122 -0
- data/lib/super_diff/rspec.rb +19 -0
- data/lib/super_diff/value_inspection.rb +11 -0
- data/lib/super_diff/version.rb +3 -0
- data/lib/super_diff.rb +50 -0
- data/spec/examples.txt +46 -0
- data/spec/integration/rspec_spec.rb +261 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/support/color_helper.rb +49 -0
- data/spec/support/command_runner.rb +279 -0
- data/spec/support/integration/matchers/produce_output_when_run_matcher.rb +76 -0
- data/spec/support/person.rb +23 -0
- data/spec/support/person_diff_formatter.rb +15 -0
- data/spec/support/person_operation_sequence.rb +14 -0
- data/spec/support/person_operational_sequencer.rb +19 -0
- data/spec/unit/equality_matcher_spec.rb +1233 -0
- data/super_diff.gemspec +23 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a67c51aa528c0539aca579800b31aafc363fb85f532ba9a07723e073a2efb050
|
4
|
+
data.tar.gz: 593449f1083f6a121b51bc74b30c78682f48b9f55e28335613b0babdbde0d4b0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 867861728306a60b4ce417ac81836b933f6e5a6f36c27aa8dbe1735c3fd269a240c3901c2fa8cbec4158a65196053e0243da82d2c83567c8c5db12d6cb1b8627
|
7
|
+
data.tar.gz: 9148ff70d4edc39116fcd00100e5ab7cd3579c13f17eb6920ab6ba047df1e0dc02389b4600bfadd6ea7f73f686dc4881ff05d994c9574f4a97d04c13556eef09
|
data/README.md
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# SuperDiff [![Gem Version][version-badge]][rubygems] [![Build Status][travis-badge]][travis] ![Downloads][downloads-badge] [![Hound][hound-badge]][hound]
|
2
|
+
|
3
|
+
[version-badge]: http://img.shields.io/gem/v/super_diff.svg
|
4
|
+
[rubygems]: http://rubygems.org/gems/super_diff
|
5
|
+
[travis-badge]: http://img.shields.io/travis/mcmire/super_diff/master.svg
|
6
|
+
[travis]: http://travis-ci.org/mcmire/super_diff
|
7
|
+
[downloads-badge]: http://img.shields.io/gem/dtv/super_diff.svg
|
8
|
+
[hound-badge]: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
|
9
|
+
[hound]: https://houndci.com
|
10
|
+
|
11
|
+
## Concept
|
12
|
+
|
13
|
+
SuperDiff is a utility that helps you diff two complex data structures in Ruby
|
14
|
+
and gives you helpful output to show you exactly how the two data structures
|
15
|
+
differ.
|
16
|
+
|
17
|
+
Let's say you have two hashes and you want to compare them. Perhaps your first
|
18
|
+
hash looks like this:
|
19
|
+
|
20
|
+
``` ruby
|
21
|
+
expected = {
|
22
|
+
customer: {
|
23
|
+
name: "Marty McFly",
|
24
|
+
shipping_address: {
|
25
|
+
line_1: "123 Main St.",
|
26
|
+
city: "Hill Valley",
|
27
|
+
state: "CA",
|
28
|
+
zip: "90382",
|
29
|
+
},
|
30
|
+
},
|
31
|
+
items: [
|
32
|
+
{
|
33
|
+
name: "Fender Stratocaster",
|
34
|
+
cost: 100_000,
|
35
|
+
options: ["red", "blue", "green"],
|
36
|
+
},
|
37
|
+
{ name: "Chevy 4x4" },
|
38
|
+
],
|
39
|
+
}
|
40
|
+
```
|
41
|
+
|
42
|
+
and your second hash looks like this:
|
43
|
+
|
44
|
+
``` ruby
|
45
|
+
actual = {
|
46
|
+
customer: {
|
47
|
+
name: "Marty McFly, Jr.",
|
48
|
+
shipping_address: {
|
49
|
+
line_1: "456 Ponderosa Ct.",
|
50
|
+
city: "Hill Valley",
|
51
|
+
state: "CA",
|
52
|
+
zip: "90382",
|
53
|
+
},
|
54
|
+
},
|
55
|
+
items: [
|
56
|
+
{
|
57
|
+
name: "Fender Stratocaster",
|
58
|
+
cost: 100_000,
|
59
|
+
options: ["red", "blue", "green"],
|
60
|
+
},
|
61
|
+
{ name: "Mattel Hoverboard" },
|
62
|
+
],
|
63
|
+
}
|
64
|
+
```
|
65
|
+
|
66
|
+
If you want to know what the difference between them is, you could say:
|
67
|
+
|
68
|
+
``` ruby
|
69
|
+
SuperDiff::EqualityMatcher.call(expected, actual)
|
70
|
+
```
|
71
|
+
|
72
|
+
This will give you the following string:
|
73
|
+
|
74
|
+
```
|
75
|
+
Differing hashes.
|
76
|
+
|
77
|
+
Expected: { customer: { name: "Marty McFly", shipping_address: { line_1: "123 Main St.", city: "Hill Valley", state: "CA", zip: "90382" } }, items: [{ name: "Fender Stratocaster", cost: 100000, options: ["red", "blue", "green"] }, { name: "Chevy 4x4" }] }
|
78
|
+
Got: { customer: { name: "Marty McFly, Jr.", shipping_address: { line_1: "456 Ponderosa Ct.", city: "Hill Valley", state: "CA", zip: "90382" } }, items: [{ name: "Fender Stratocaster", cost: 100000, options: ["red", "blue", "green"] }, { name: "Mattel Hoverboard" }] }
|
79
|
+
|
80
|
+
Diff:
|
81
|
+
|
82
|
+
{
|
83
|
+
customer: {
|
84
|
+
- name: "Marty McFly",
|
85
|
+
+ name: "Marty McFly, Jr.",
|
86
|
+
shipping_address: {
|
87
|
+
- line_1: "123 Main St.",
|
88
|
+
+ line_1: "456 Ponderosa Ct.",
|
89
|
+
city: "Hill Valley",
|
90
|
+
state: "CA",
|
91
|
+
zip: "90382"
|
92
|
+
}
|
93
|
+
},
|
94
|
+
items: [
|
95
|
+
{
|
96
|
+
name: "Fender Stratocaster",
|
97
|
+
cost: 100000,
|
98
|
+
options: ["red", "blue", "green"]
|
99
|
+
},
|
100
|
+
{
|
101
|
+
- name: "Chevy 4x4"
|
102
|
+
+ name: "Mattel Hoverboard"
|
103
|
+
}
|
104
|
+
]
|
105
|
+
}
|
106
|
+
```
|
107
|
+
|
108
|
+
When printed to a terminal, this will display in color, so the "expected" value
|
109
|
+
in the summary and deleted lines in the diff will appear in red, while the
|
110
|
+
"actual" value in the summary and inserted lines in the diff will appear in
|
111
|
+
green.
|
112
|
+
|
113
|
+
By the way, SuperDiff doesn't just work with hashes, but arrays as well as other
|
114
|
+
objects, too!
|
115
|
+
|
116
|
+
## Usage
|
117
|
+
|
118
|
+
There are two ways to use this gem. One way is to use the API methods that this
|
119
|
+
gem provides, such as the method presented above.
|
120
|
+
|
121
|
+
However, this gem was really designed for use specifically with RSpec. In recent
|
122
|
+
years, RSpec has added a feature where if you're comparing two objects in a test
|
123
|
+
and your test fails, you will see a diff between those objects (provided the
|
124
|
+
objects are large enough). However, this diff is not always the most helpful.
|
125
|
+
It's very common when writing tests for API endpoints to work with giant JSON
|
126
|
+
hashes, and RSpec's diffs are not sufficient in highlighting changes between
|
127
|
+
such structures. Therefore, this gem provides an integration layer where you can
|
128
|
+
replace RSpec's differ with SuperDiff.
|
129
|
+
|
130
|
+
To get started, add the gem to your Gemfile under the `test` group:
|
131
|
+
|
132
|
+
``` ruby
|
133
|
+
gem "super_diff"
|
134
|
+
```
|
135
|
+
|
136
|
+
Then, open up `spec_helper` and add this line somewhere:
|
137
|
+
|
138
|
+
``` ruby
|
139
|
+
require "super_diff/rspec"
|
140
|
+
```
|
141
|
+
|
142
|
+
Now try writing a test using `eq` to compare two large data structures, and you
|
143
|
+
should see a diff similar to the one given above.
|
144
|
+
|
145
|
+
## Contributing
|
146
|
+
|
147
|
+
If you encounter a bug or have an idea for how this could be better, I'm all
|
148
|
+
ears! Feel free to create an issue.
|
149
|
+
|
150
|
+
If you'd like to submit a PR instead, here's how to get started. First, fork
|
151
|
+
this repo and then run:
|
152
|
+
|
153
|
+
```
|
154
|
+
bundle install
|
155
|
+
```
|
156
|
+
|
157
|
+
This will install dependencies. From here you can run all of the tests:
|
158
|
+
|
159
|
+
```
|
160
|
+
bundle exec rake
|
161
|
+
```
|
162
|
+
|
163
|
+
Or a single test:
|
164
|
+
|
165
|
+
```
|
166
|
+
bundle exec rspec spec/acceptance/...
|
167
|
+
bundle exec rspec spec/unit/...
|
168
|
+
```
|
169
|
+
|
170
|
+
Finally, submit your PR and I'll take a look at it when I get a chance.
|
171
|
+
|
172
|
+
## Copyright/License
|
173
|
+
|
174
|
+
© 2018 Elliot Winkler, released under the [MIT license](LICENSE).
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module SuperDiff
|
2
|
+
module Csi
|
3
|
+
module ColorHelper
|
4
|
+
BLACK = Csi::FourBitColor.new(:white)
|
5
|
+
# LIGHT_RED = Csi::TwentyFourBitColor.new(r: 63, g: 32, b: 32)
|
6
|
+
LIGHT_RED = Csi::TwentyFourBitColor.new(r: 73, g: 62, b: 71)
|
7
|
+
# RED = Csi::TwentyFourBitColor.new(r: 110, g: 56, b: 56)
|
8
|
+
# RED = Csi::TwentyFourBitColor.new(r: 116, g: 78, b: 84)
|
9
|
+
RED = Csi::FourBitColor.new(:red)
|
10
|
+
# LIGHT_GREEN = Csi::TwentyFourBitColor.new(r: 40, g: 48, b: 39)
|
11
|
+
LIGHT_GREEN = Csi::TwentyFourBitColor.new(r: 51, g: 81, b: 81)
|
12
|
+
# GREEN = Csi::TwentyFourBitColor.new(r: 73, g: 87, b: 71)
|
13
|
+
# GREEN = Csi::TwentyFourBitColor.new(r: 81, g: 115, b: 105)
|
14
|
+
GREEN = Csi::FourBitColor.new(:green)
|
15
|
+
DARK_GREY = Csi::TwentyFourBitColor.new(r: 134, g: 147, b: 149)
|
16
|
+
|
17
|
+
def self.plain(text)
|
18
|
+
text
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.line(given_text = "")
|
22
|
+
if block_given?
|
23
|
+
text = ""
|
24
|
+
yield text
|
25
|
+
text + "\n"
|
26
|
+
else
|
27
|
+
given_text + "\n"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.light_red_bg(text)
|
32
|
+
Csi.colorize(text, fg: BLACK, bg: LIGHT_RED)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.red(text)
|
36
|
+
Csi.colorize(text, fg: RED)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.light_green_bg(text)
|
40
|
+
Csi.colorize(text, fg: BLACK, bg: LIGHT_GREEN)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.green(text)
|
44
|
+
Csi.colorize(text, fg: GREEN)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.dark_grey(text)
|
48
|
+
Csi.colorize(text, fg: BLACK, bg: DARK_GREY)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module SuperDiff
|
2
|
+
module Csi
|
3
|
+
class EightBitColor
|
4
|
+
VALID_COMPONENT_RANGE = 0..6
|
5
|
+
VALID_CODE_RANGE = 0..255
|
6
|
+
VALID_CODES_BY_NAME = {
|
7
|
+
black: 0,
|
8
|
+
red: 1,
|
9
|
+
green: 2,
|
10
|
+
yellow: 3,
|
11
|
+
blue: 4,
|
12
|
+
magenta: 5,
|
13
|
+
cyan: 6,
|
14
|
+
white: 7,
|
15
|
+
bright_black: 8,
|
16
|
+
bright_red: 9,
|
17
|
+
bright_green: 10,
|
18
|
+
bright_yellow: 11,
|
19
|
+
bright_blue: 12,
|
20
|
+
bright_magenta: 13,
|
21
|
+
bright_cyan: 14,
|
22
|
+
bright_white: 15,
|
23
|
+
}.freeze
|
24
|
+
STARTING_INDICES = {
|
25
|
+
standard: 0,
|
26
|
+
high_intensity: 8,
|
27
|
+
grayscale: 232,
|
28
|
+
}.freeze
|
29
|
+
VALID_PAIR_TYPES = STARTING_INDICES.keys
|
30
|
+
VALID_PAIR_INDEX_RANGES = {
|
31
|
+
standard: 0..7,
|
32
|
+
high_intensity: 0..7,
|
33
|
+
grayscale: 0..23,
|
34
|
+
}.freeze
|
35
|
+
LEADING_CODES_BY_LAYER = { fg: 38, bg: 48 }.freeze
|
36
|
+
SERIAL_CODE = 5
|
37
|
+
|
38
|
+
def initialize(value)
|
39
|
+
@code =
|
40
|
+
case value
|
41
|
+
when Hash
|
42
|
+
interpret_triplet!(value)
|
43
|
+
when Symbol
|
44
|
+
interpret_color_name!(value)
|
45
|
+
when Array
|
46
|
+
interpret_pair!(value)
|
47
|
+
else
|
48
|
+
interpret_code!(value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def sequence_for(layer)
|
53
|
+
leading_code = LEADING_CODES_BY_LAYER.fetch(layer)
|
54
|
+
"\e[#{leading_code};#{SERIAL_CODE};#{code}m"
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :code
|
60
|
+
|
61
|
+
def interpret_triplet!(spec)
|
62
|
+
unless spec.include?(:r) && spec.include?(:g) && spec.include?(:b)
|
63
|
+
raise ArgumentError.new(
|
64
|
+
"#{spec.inspect} is not a valid color specification. " +
|
65
|
+
"Please provide a hash with :r, :g, and :b keys.",
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
if spec.values.none? { |component| VALID_COMPONENT_RANGE.cover?(component) }
|
70
|
+
raise ArgumentError.new(
|
71
|
+
"(#{spec[:r]},#{spec[:g]},#{spec[:b]}) is not a valid color " +
|
72
|
+
"specification. All components must be between " +
|
73
|
+
"#{VALID_COMPONENT_RANGE.begin} and #{VALID_COMPONENT_RANGE.end}.",
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
16 + 36 * spec[:r] + 6 * spec[:g] + spec[:b]
|
78
|
+
end
|
79
|
+
|
80
|
+
def interpret_color_name!(name)
|
81
|
+
if !VALID_CODES_BY_NAME.include?(name)
|
82
|
+
message =
|
83
|
+
"#{name.inspect} is not a valid color name.\n" +
|
84
|
+
"Valid names are:\n"
|
85
|
+
|
86
|
+
VALID_CODES_BY_NAME.keys.each do |valid_name|
|
87
|
+
message << "- #{valid_name}\n"
|
88
|
+
end
|
89
|
+
|
90
|
+
raise ArgumentError.new(message)
|
91
|
+
end
|
92
|
+
|
93
|
+
VALID_CODES_BY_NAME[name]
|
94
|
+
end
|
95
|
+
|
96
|
+
def interpret_pair!(pair)
|
97
|
+
type, index = pair
|
98
|
+
|
99
|
+
if !VALID_PAIR_TYPES.include?(type)
|
100
|
+
raise ArgumentError.new(
|
101
|
+
"Given pair did not have a valid type. " +
|
102
|
+
"Type must be one of: #{VALID_PAIR_TYPES}",
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
valid_range = VALID_PAIR_INDEX_RANGES[type]
|
107
|
+
|
108
|
+
if !valid_range.cover?(index)
|
109
|
+
raise ArgumentError.new(
|
110
|
+
"Given pair did not have a valid index. " +
|
111
|
+
"For #{type}, index must be between #{valid_range.begin} and " +
|
112
|
+
"#{valid_range.end}.",
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
STARTING_INDICES[type] + index
|
117
|
+
end
|
118
|
+
|
119
|
+
def interpret_code!(code)
|
120
|
+
if !VALID_CODE_RANGE.cover?(code)
|
121
|
+
raise ArgumentError.new(
|
122
|
+
"#{code.inspect} is not a valid color code " +
|
123
|
+
"(must be between 0 and 255).",
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
code
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module SuperDiff
|
2
|
+
module Csi
|
3
|
+
class EightBitSequence
|
4
|
+
LEADING_CODES = { fg: 38, bg: 48 }.freeze
|
5
|
+
SERIAL_CODE = 5
|
6
|
+
|
7
|
+
def initialize(fg: nil, bg: nil)
|
8
|
+
@fg = fg
|
9
|
+
@bg = bg
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
[sequence_for(:fg, fg), sequence_for(:bg, bg)].compact.join
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :fg, :bg
|
19
|
+
|
20
|
+
def sequence_for(type, color)
|
21
|
+
if color
|
22
|
+
"\e[#{LEADING_CODES.fetch(type)};#{SERIAL_CODE};#{color.code}m"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module SuperDiff
|
2
|
+
module Csi
|
3
|
+
class FourBitColor
|
4
|
+
VALID_TYPES = [:fg, :bg].freeze
|
5
|
+
VALID_CODES_BY_NAME = {
|
6
|
+
black: { fg: 30, bg: 40 },
|
7
|
+
red: { fg: 31, bg: 41 },
|
8
|
+
green: { fg: 32, bg: 42 },
|
9
|
+
yellow: { fg: 33, bg: 43 },
|
10
|
+
blue: { fg: 34, bg: 44 },
|
11
|
+
magenta: { fg: 35, bg: 45 },
|
12
|
+
cyan: { fg: 36, bg: 46 },
|
13
|
+
white: { fg: 37, bg: 47 },
|
14
|
+
bright_black: { fg: 90, bg: 100 },
|
15
|
+
bright_red: { fg: 91, bg: 101 },
|
16
|
+
bright_green: { fg: 92, bg: 102 },
|
17
|
+
bright_yellow: { fg: 93, bg: 103 },
|
18
|
+
bright_blue: { fg: 94, bg: 104 },
|
19
|
+
bright_magenta: { fg: 95, bg: 105 },
|
20
|
+
bright_cyan: { fg: 96, bg: 106 },
|
21
|
+
bright_white: { fg: 97, bg: 107 },
|
22
|
+
}.freeze
|
23
|
+
NAMES_BY_CODE = VALID_CODES_BY_NAME.reduce({}) do |hash, (key, value)|
|
24
|
+
hash.merge(value[:fg] => key, value[:bg] => key)
|
25
|
+
end
|
26
|
+
VALID_NAMES = VALID_CODES_BY_NAME.keys
|
27
|
+
VALID_CODE_RANGES = [30..37, 40..47, 90..97, 100..107].freeze
|
28
|
+
|
29
|
+
def initialize(value)
|
30
|
+
@name =
|
31
|
+
if value.is_a?(Symbol)
|
32
|
+
interpret_name!(value)
|
33
|
+
else
|
34
|
+
interpret_code!(value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def sequence_for(layer)
|
39
|
+
code = VALID_CODES_BY_NAME.fetch(name).fetch(layer)
|
40
|
+
"\e[#{code}m"
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_reader :name
|
46
|
+
|
47
|
+
def interpret_name!(name)
|
48
|
+
if !VALID_NAMES.include?(name)
|
49
|
+
message =
|
50
|
+
"#{name.inspect} is not a valid color name.\n" +
|
51
|
+
"Valid names are:\n"
|
52
|
+
|
53
|
+
VALID_NAMES.each do |valid_name|
|
54
|
+
message << "- #{valid_name}"
|
55
|
+
end
|
56
|
+
|
57
|
+
raise ArgumentError.new(message)
|
58
|
+
end
|
59
|
+
|
60
|
+
name
|
61
|
+
end
|
62
|
+
|
63
|
+
def interpret_code!(code)
|
64
|
+
if VALID_CODE_RANGES.none? { |range| range.cover?(code) }
|
65
|
+
message =
|
66
|
+
"#{code.inspect} is not a valid color code.\n" +
|
67
|
+
"Valid codes are:\n"
|
68
|
+
|
69
|
+
VALID_CODE_RANGES.each do |range|
|
70
|
+
message << "- #{range.begin} through #{range.end}\n"
|
71
|
+
end
|
72
|
+
|
73
|
+
raise ArgumentError.new(message)
|
74
|
+
end
|
75
|
+
|
76
|
+
NAMES_BY_CODE.fetch(code)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SuperDiff
|
2
|
+
module Csi
|
3
|
+
class FourBitSequence
|
4
|
+
def initialize(fg: nil, bg: nil)
|
5
|
+
@fg = fg
|
6
|
+
@bg = bg
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
[sequence_for(fg), sequence_for(bg)].compact.join
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
attr_reader :fg, :bg
|
16
|
+
|
17
|
+
def sequence_for(color)
|
18
|
+
if color
|
19
|
+
"\e[#{color.code}m"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module SuperDiff
|
2
|
+
module Csi
|
3
|
+
module Sequence
|
4
|
+
def self.for(color)
|
5
|
+
case color
|
6
|
+
when :reset
|
7
|
+
Csi::ResetSequence.new
|
8
|
+
when FourBitColor
|
9
|
+
Csi::FourBitSequence.new(color)
|
10
|
+
when EightBitColor
|
11
|
+
Csi::EightBitSequence.new(color)
|
12
|
+
when TwentyFourBitColor
|
13
|
+
Csi::TwentyFourBitSequence.new(color)
|
14
|
+
else
|
15
|
+
raise ArgumentError.new(
|
16
|
+
"Don't know how to interpret color: #{color.inspect}",
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module SuperDiff
|
2
|
+
module Csi
|
3
|
+
class TwentyFourBitColor
|
4
|
+
VALID_COMPONENT_RANGE = 0..255
|
5
|
+
LEADING_CODES_BY_LAYER = { fg: 38, bg: 48 }.freeze
|
6
|
+
SERIAL_CODE = 2
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
@code = interpret_triplet!(value)
|
10
|
+
end
|
11
|
+
|
12
|
+
def sequence_for(layer)
|
13
|
+
leading_code = LEADING_CODES_BY_LAYER.fetch(layer)
|
14
|
+
"\e[#{leading_code};#{SERIAL_CODE};#{code}m"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :code
|
20
|
+
|
21
|
+
def interpret_triplet!(spec)
|
22
|
+
if !spec.include?(:r) || !spec.include?(:g) || !spec.include?(:b)
|
23
|
+
raise ArgumentError.new(
|
24
|
+
"#{spec.inspect} is not a valid color specification. " +
|
25
|
+
"Please provide a hash with :r, :g, and :b keys.",
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
if spec.values.any? { |component| !VALID_COMPONENT_RANGE.cover?(component) }
|
30
|
+
raise ArgumentError.new(
|
31
|
+
"(#{spec[:r]},#{spec[:g]},#{spec[:b]}) is not a valid color " +
|
32
|
+
"specification. All components must be between " +
|
33
|
+
"#{VALID_COMPONENT_RANGE.begin} and #{VALID_COMPONENT_RANGE.end}.",
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
"#{spec[:r]};#{spec[:g]};#{spec[:b]}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module SuperDiff
|
2
|
+
module Csi
|
3
|
+
class TwentyFourBitSequence
|
4
|
+
LEADING_CODES = { fg: 38, bg: 48 }.freeze
|
5
|
+
SERIAL_CODE = 2
|
6
|
+
|
7
|
+
def initialize(fg: nil, bg: nil)
|
8
|
+
@fg = fg
|
9
|
+
@bg = bg
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
[sequence_for(:fg, fg), sequence_for(:bg, bg)].compact.join
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :fg, :bg
|
19
|
+
|
20
|
+
def sequence_for(type, color)
|
21
|
+
if color
|
22
|
+
"\e[#{LEADING_CODES.fetch(type)};#{SERIAL_CODE};#{color.code}m"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative "csi/reset_sequence"
|
2
|
+
|
3
|
+
# Source: <https://en.wikipedia.org/wiki/ANSI_escape_code>
|
4
|
+
module SuperDiff
|
5
|
+
module Csi
|
6
|
+
def self.reset_sequence
|
7
|
+
ResetSequence.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.colorize(text, fg: nil, bg: nil)
|
11
|
+
parts = []
|
12
|
+
|
13
|
+
if fg
|
14
|
+
parts << fg.sequence_for(:fg)
|
15
|
+
end
|
16
|
+
|
17
|
+
if bg
|
18
|
+
parts << bg.sequence_for(:bg)
|
19
|
+
end
|
20
|
+
|
21
|
+
(parts + [text, reset_sequence]).join
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require_relative "csi/four_bit_color"
|
27
|
+
require_relative "csi/eight_bit_color"
|
28
|
+
require_relative "csi/twenty_four_bit_color"
|
29
|
+
require_relative "csi/color_helper"
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module SuperDiff
|
2
|
+
class DiffFormatter
|
3
|
+
def self.call(*args)
|
4
|
+
new(*args).call
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(
|
8
|
+
operations,
|
9
|
+
indent_level:,
|
10
|
+
add_comma: false,
|
11
|
+
extra_classes: []
|
12
|
+
)
|
13
|
+
@operations = operations
|
14
|
+
@indent_level = indent_level
|
15
|
+
@add_comma = add_comma
|
16
|
+
@extra_classes = extra_classes
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
resolved_class.call(
|
21
|
+
operations,
|
22
|
+
indent_level: indent_level,
|
23
|
+
add_comma: add_comma,
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :operations, :indent_level, :add_comma, :extra_classes
|
30
|
+
|
31
|
+
def resolved_class
|
32
|
+
(DiffFormatters::DEFAULTS + extra_classes).find do |klass|
|
33
|
+
klass.applies_to?(operations)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module SuperDiff
|
2
|
+
module DiffFormatters
|
3
|
+
class Array < Base
|
4
|
+
def self.applies_to?(operations)
|
5
|
+
operations.is_a?(OperationSequences::Array)
|
6
|
+
end
|
7
|
+
|
8
|
+
def call
|
9
|
+
Collection.call(
|
10
|
+
open_token: "[",
|
11
|
+
close_token: "]",
|
12
|
+
collection_prefix: collection_prefix,
|
13
|
+
build_item_prefix: proc { "" },
|
14
|
+
operations: operations,
|
15
|
+
indent_level: indent_level,
|
16
|
+
add_comma: add_comma?,
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|