pastel 0.5.3 → 0.6.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.
- checksums.yaml +4 -4
- data/.travis.yml +7 -7
- data/CHANGELOG.md +59 -10
- data/Gemfile +0 -1
- data/README.md +55 -19
- data/benchmarks/nesting_speed.rb +15 -0
- data/benchmarks/speed.rb +26 -2
- data/lib/pastel.rb +6 -2
- data/lib/pastel/alias_importer.rb +5 -4
- data/lib/pastel/ansi.rb +14 -0
- data/lib/pastel/color.rb +45 -73
- data/lib/pastel/color_parser.rb +117 -0
- data/lib/pastel/color_resolver.rb +3 -1
- data/lib/pastel/delegator.rb +4 -1
- data/lib/pastel/version.rb +1 -1
- data/pastel.gemspec +5 -4
- data/spec/unit/alias_color_spec.rb +1 -3
- data/spec/unit/alias_importer_spec.rb +8 -11
- data/spec/unit/color/alias_color_spec.rb +0 -2
- data/spec/unit/color/code_spec.rb +1 -3
- data/spec/unit/color/colored_spec.rb +1 -3
- data/spec/unit/color/decorate_spec.rb +5 -3
- data/spec/unit/color/equal_spec.rb +0 -2
- data/spec/unit/color/lookup_spec.rb +17 -0
- data/spec/unit/color/new_spec.rb +1 -15
- data/spec/unit/color/strip_spec.rb +3 -5
- data/spec/unit/color/styles_spec.rb +1 -3
- data/spec/unit/color/valid_spec.rb +0 -2
- data/spec/unit/color_parser_spec.rb +67 -0
- data/spec/unit/decorate_dsl_spec.rb +85 -0
- data/spec/unit/decorator_chain_spec.rb +0 -2
- data/spec/unit/delegator_spec.rb +0 -2
- data/spec/unit/detach_spec.rb +0 -2
- data/spec/unit/new_spec.rb +18 -80
- data/spec/unit/respond_to_spec.rb +0 -2
- data/spec/unit/undecorate_spec.rb +12 -0
- metadata +38 -10
- data/.ruby-gemset +0 -1
@@ -0,0 +1,117 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Pastel
|
4
|
+
# Responsible for parsing color symbols out of text with color escapes
|
5
|
+
#
|
6
|
+
# Used internally by {Color}.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class ColorParser
|
10
|
+
include ANSI
|
11
|
+
|
12
|
+
ESC = "\x1b".freeze
|
13
|
+
CSI = "\[".freeze
|
14
|
+
|
15
|
+
# Parse color escape sequences into a list of hashes
|
16
|
+
# corresponding to the color attributes being set by these
|
17
|
+
# sequences
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# parse("\e[32mfoo\e[0m") # => [{colors: [:green], text: 'foo'}
|
21
|
+
#
|
22
|
+
# @param [String] text
|
23
|
+
# the text to parse for presence of color ansi codes
|
24
|
+
#
|
25
|
+
# @return [Array[Hash[Symbol,String]]]
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
def self.parse(text)
|
29
|
+
scanner = StringScanner.new(text)
|
30
|
+
state = {}
|
31
|
+
result = []
|
32
|
+
ansi_stack = []
|
33
|
+
text_chunk = ''
|
34
|
+
|
35
|
+
until scanner.eos?
|
36
|
+
char = scanner.getch
|
37
|
+
# match control
|
38
|
+
if char == ESC && (delim = scanner.getch) == CSI
|
39
|
+
if scanner.scan(/^0m/)
|
40
|
+
unpack_ansi(ansi_stack) { |attr, name| state[attr] = name }
|
41
|
+
ansi_stack = []
|
42
|
+
elsif scanner.scan(/^([1-9;:]+)m/)
|
43
|
+
# ansi codes separated by text
|
44
|
+
if !text_chunk.empty? && !ansi_stack.empty?
|
45
|
+
unpack_ansi(ansi_stack) { |attr, name| state[attr] = name }
|
46
|
+
ansi_stack = []
|
47
|
+
end
|
48
|
+
scanner[1].split(/:|;/).each do |code|
|
49
|
+
ansi_stack << code
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if !text_chunk.empty?
|
54
|
+
state[:text] = text_chunk
|
55
|
+
result.push(state)
|
56
|
+
state = {}
|
57
|
+
text_chunk = ''
|
58
|
+
end
|
59
|
+
elsif char == ESC # broken escape
|
60
|
+
text_chunk << char + delim.to_s
|
61
|
+
else
|
62
|
+
text_chunk << char
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
if !text_chunk.empty?
|
67
|
+
state[:text] = text_chunk
|
68
|
+
end
|
69
|
+
if !ansi_stack.empty?
|
70
|
+
unpack_ansi(ansi_stack) { |attr, name| state[attr] = name}
|
71
|
+
end
|
72
|
+
if state.values.any? { |val| !val.empty? }
|
73
|
+
result.push(state)
|
74
|
+
end
|
75
|
+
result
|
76
|
+
end
|
77
|
+
|
78
|
+
# Remove from current stack all ansi codes
|
79
|
+
#
|
80
|
+
# @param [Array[Integer]] ansi_stack
|
81
|
+
# the stack with all the ansi codes
|
82
|
+
#
|
83
|
+
# @yield [Symbol, Symbol] attr, name
|
84
|
+
#
|
85
|
+
# @api private
|
86
|
+
def self.unpack_ansi(ansi_stack)
|
87
|
+
ansi_stack.each do |ansi|
|
88
|
+
name = ansi_for(ansi)
|
89
|
+
attr = attribute_for(ansi)
|
90
|
+
yield attr, name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Decide attribute name for ansi
|
95
|
+
#
|
96
|
+
# @param [Integer] ansi
|
97
|
+
# the ansi escape code
|
98
|
+
#
|
99
|
+
# @return [Symbol]
|
100
|
+
#
|
101
|
+
# @api private
|
102
|
+
def self.attribute_for(ansi)
|
103
|
+
if ANSI.foreground?(ansi)
|
104
|
+
:foreground
|
105
|
+
elsif ANSI.background?(ansi)
|
106
|
+
:background
|
107
|
+
elsif ANSI.style?(ansi)
|
108
|
+
:style
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# @api private
|
113
|
+
def self.ansi_for(ansi)
|
114
|
+
ATTRIBUTES.key(ansi.to_i)
|
115
|
+
end
|
116
|
+
end # Parser
|
117
|
+
end # Pastel
|
@@ -7,6 +7,8 @@ module Pastel
|
|
7
7
|
#
|
8
8
|
# @api private
|
9
9
|
class ColorResolver
|
10
|
+
# The color instance
|
11
|
+
# @api public
|
10
12
|
attr_reader :color
|
11
13
|
|
12
14
|
# Initialize ColorResolver
|
@@ -14,7 +16,7 @@ module Pastel
|
|
14
16
|
# @param [Color] color
|
15
17
|
#
|
16
18
|
# @api private
|
17
|
-
def initialize(color
|
19
|
+
def initialize(color)
|
18
20
|
@color = color
|
19
21
|
end
|
20
22
|
|
data/lib/pastel/delegator.rb
CHANGED
@@ -10,7 +10,10 @@ module Pastel
|
|
10
10
|
include Equatable
|
11
11
|
|
12
12
|
def_delegators '@resolver.color', :valid?, :styles, :strip, :decorate,
|
13
|
-
:enabled?, :colored?, :alias_color
|
13
|
+
:enabled?, :colored?, :alias_color, :lookup
|
14
|
+
|
15
|
+
def_delegators ColorParser, :parse
|
16
|
+
alias_method :undecorate, :parse
|
14
17
|
|
15
18
|
# Create Delegator
|
16
19
|
#
|
data/lib/pastel/version.rb
CHANGED
data/pastel.gemspec
CHANGED
@@ -15,11 +15,12 @@ Gem::Specification.new do |spec|
|
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
-
spec.test_files = spec.files.grep(%r{^
|
18
|
+
spec.test_files = spec.files.grep(%r{^spec/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_dependency
|
22
|
-
spec.add_dependency
|
21
|
+
spec.add_dependency 'equatable', '~> 0.5.0'
|
22
|
+
spec.add_dependency 'tty-color', '~> 0.3.0'
|
23
23
|
|
24
|
-
spec.add_development_dependency
|
24
|
+
spec.add_development_dependency 'bundler', '>= 1.5.0', '< 2.0'
|
25
|
+
spec.add_development_dependency 'rake'
|
25
26
|
end
|
@@ -1,31 +1,28 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
RSpec.describe Pastel::AliasImporter, '.import' do
|
3
|
+
RSpec.describe Pastel::AliasImporter, '#import' do
|
6
4
|
let(:color) { spy(:color, alias_color: true) }
|
7
5
|
let(:output) { StringIO.new }
|
8
6
|
|
9
|
-
subject(:importer) { described_class.new(color, output) }
|
10
|
-
|
11
7
|
it "imports aliases from environment" do
|
12
8
|
color_aliases = "funky=red,base=bright_yellow"
|
13
|
-
|
14
|
-
|
9
|
+
env = {'PASTEL_COLORS_ALIASES' => color_aliases}
|
10
|
+
importer = described_class.new(color, env)
|
15
11
|
|
16
12
|
importer.import
|
17
13
|
|
18
|
-
expect(color).to have_received(:alias_color).
|
14
|
+
expect(color).to have_received(:alias_color).with(:funky, :red)
|
15
|
+
expect(color).to have_received(:alias_color).with(:base, :bright_yellow)
|
19
16
|
end
|
20
17
|
|
21
18
|
it "fails to import incorrectly formatted colors" do
|
22
19
|
color_aliases = "funky red,base=bright_yellow"
|
23
|
-
|
24
|
-
|
20
|
+
env = {'PASTEL_COLORS_ALIASES' => color_aliases}
|
21
|
+
importer = described_class.new(color, env, output)
|
22
|
+
output.rewind
|
25
23
|
|
26
24
|
importer.import
|
27
25
|
|
28
|
-
output.rewind
|
29
26
|
expect(output.string).to eq("Bad color mapping `funky red`\n")
|
30
27
|
expect(color).to have_received(:alias_color).with(:base, :bright_yellow)
|
31
28
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require 'spec_helper'
|
4
|
-
|
5
3
|
RSpec.describe Pastel::Color, '.decorate' do
|
6
4
|
let(:string) { 'string' }
|
7
5
|
|
@@ -16,6 +14,10 @@ RSpec.describe Pastel::Color, '.decorate' do
|
|
16
14
|
expect(color.decorate('')).to eq('')
|
17
15
|
end
|
18
16
|
|
17
|
+
it "doesn't decorate without color" do
|
18
|
+
expect(color.decorate(string)).to eq(string)
|
19
|
+
end
|
20
|
+
|
19
21
|
it 'applies green text to string' do
|
20
22
|
expect(color.decorate(string, :green)).to eq("\e[32m#{string}\e[0m")
|
21
23
|
end
|
@@ -35,7 +37,7 @@ RSpec.describe Pastel::Color, '.decorate' do
|
|
35
37
|
|
36
38
|
it "applies styles to nested text" do
|
37
39
|
decorated = color.decorate(string + color.decorate(string, :red) + string, :green)
|
38
|
-
expect(decorated).to eq("\e[32m#{string}\e[31m#{string}\e[32m#{string}\e[0m")
|
40
|
+
expect(decorated).to eq("\e[32m#{string}\e[31m#{string}\e[0m\e[32m#{string}\e[0m")
|
39
41
|
end
|
40
42
|
|
41
43
|
it "decorates multiline string as regular by default" do
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
RSpec.describe Pastel::Color, '#lookup' do
|
4
|
+
it "looksup colors" do
|
5
|
+
color = described_class.new(enabled: true)
|
6
|
+
expect(color.lookup(:red, :on_green, :bold)).to eq("\e[31;42;1m")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "caches color lookups" do
|
10
|
+
color = described_class.new(enabled: true)
|
11
|
+
allow(color).to receive(:code).and_return([31])
|
12
|
+
color.lookup(:red, :on_green)
|
13
|
+
color.lookup(:red, :on_green)
|
14
|
+
color.lookup(:red, :on_green)
|
15
|
+
expect(color).to have_received(:code).once
|
16
|
+
end
|
17
|
+
end
|
data/spec/unit/color/new_spec.rb
CHANGED
@@ -1,24 +1,10 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
RSpec.describe Pastel::Color, '.new' do
|
6
|
-
it "is immutable" do
|
7
|
-
expect(described_class.new).to be_frozen
|
8
|
-
end
|
9
|
-
|
3
|
+
RSpec.describe Pastel::Color, '::new' do
|
10
4
|
it "allows to disable coloring" do
|
11
5
|
color = described_class.new(enabled: false)
|
12
6
|
|
13
7
|
expect(color.enabled?).to eq(false)
|
14
8
|
expect(color.decorate("Unicorn", :red)).to eq("Unicorn")
|
15
9
|
end
|
16
|
-
|
17
|
-
it "invokes screen dependency to check color support" do
|
18
|
-
allow(TTY::Screen).to receive(:color?).and_return(true)
|
19
|
-
color = described_class.new
|
20
|
-
|
21
|
-
expect(color.enabled?).to eq(true)
|
22
|
-
expect(TTY::Screen).to have_received(:color?)
|
23
|
-
end
|
24
10
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require 'spec_helper'
|
4
|
-
|
5
3
|
RSpec.describe Pastel::Color, '.strip' do
|
6
4
|
|
7
5
|
subject(:color) { described_class.new(enabled: true) }
|
@@ -18,7 +16,7 @@ RSpec.describe Pastel::Color, '.strip' do
|
|
18
16
|
|
19
17
|
it 'preserves movement characters' do
|
20
18
|
# [176A - move cursor up n lines
|
21
|
-
expect(color.strip("foo\e[176Abar")).to eq(
|
19
|
+
expect(color.strip("foo\e[176Abar")).to eq("foo\e[176Abar")
|
22
20
|
end
|
23
21
|
|
24
22
|
it 'strips reset/setfg/setbg/italics/strike/underline sequence' do
|
@@ -28,7 +26,7 @@ RSpec.describe Pastel::Color, '.strip' do
|
|
28
26
|
|
29
27
|
it 'strips octal in encapsulating brackets' do
|
30
28
|
string = "\[\033[01;32m\]u@h \[\033[01;34m\]W $ \[\033[00m\]"
|
31
|
-
expect(color.strip(string)).to eq('u@h W $ ')
|
29
|
+
expect(color.strip(string)).to eq('[]u@h []W $ []')
|
32
30
|
end
|
33
31
|
|
34
32
|
it 'strips octal codes without brackets' do
|
@@ -37,7 +35,7 @@ RSpec.describe Pastel::Color, '.strip' do
|
|
37
35
|
end
|
38
36
|
|
39
37
|
it 'strips octal with multiple colors' do
|
40
|
-
string = "\e[3;0;0;
|
38
|
+
string = "\e[3;0;0;mfoo\e[8;50;0m"
|
41
39
|
expect(color.strip(string)).to eq('foo')
|
42
40
|
end
|
43
41
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
RSpec.describe Pastel::ColorParser, '::parse' do
|
4
|
+
subject(:parser) { described_class }
|
5
|
+
|
6
|
+
it "parses string with no color" do
|
7
|
+
expect(parser.parse("foo")).to eq([{text: 'foo'}])
|
8
|
+
end
|
9
|
+
|
10
|
+
it "parses simple color" do
|
11
|
+
expect(parser.parse("\e[32mfoo\e[0m")).to eq([
|
12
|
+
{foreground: :green, text: 'foo'}
|
13
|
+
])
|
14
|
+
end
|
15
|
+
|
16
|
+
it "parses simple color and style" do
|
17
|
+
expect(parser.parse("\e[32;1mfoo\e[0m")).to eq([
|
18
|
+
{foreground: :green, style: :bold, text: 'foo'}
|
19
|
+
])
|
20
|
+
end
|
21
|
+
|
22
|
+
it "parses chained colors in shorthand syntax" do
|
23
|
+
expect(parser.parse("\e[32;44mfoo\e[0m")).to eq([
|
24
|
+
{foreground: :green, background: :on_blue, text: 'foo'}
|
25
|
+
])
|
26
|
+
end
|
27
|
+
|
28
|
+
it "parses chained colors in regular syntax" do
|
29
|
+
expect(parser.parse("\e[32m\e[44mfoo\e[0m")).to eq([
|
30
|
+
{foreground: :green, background: :on_blue, text: 'foo'}
|
31
|
+
])
|
32
|
+
end
|
33
|
+
|
34
|
+
it "parses many colors" do
|
35
|
+
expect(parser.parse("\e[32mfoo\e[0m \e[31mbar\e[0m")).to eq([
|
36
|
+
{foreground: :green, text: 'foo'},
|
37
|
+
{text: ' '},
|
38
|
+
{foreground: :red, text: 'bar'}
|
39
|
+
])
|
40
|
+
end
|
41
|
+
|
42
|
+
it "parses nested colors with one reset" do
|
43
|
+
expect(parser.parse("\e[32mfoo\e[31mbar\e[0m")).to eq([
|
44
|
+
{foreground: :green, text: 'foo'},
|
45
|
+
{foreground: :red, text: 'bar'}
|
46
|
+
])
|
47
|
+
end
|
48
|
+
|
49
|
+
it "parses nested colors with two resets" do
|
50
|
+
expect(parser.parse("\e[32mfoo\e[31mbar\e[0m\e[0m")).to eq([
|
51
|
+
{foreground: :green, text: 'foo'},
|
52
|
+
{foreground: :red, text: 'bar'}
|
53
|
+
])
|
54
|
+
end
|
55
|
+
|
56
|
+
it "parses unrest color" do
|
57
|
+
expect(parser.parse("\e[32mfoo")).to eq([
|
58
|
+
{foreground: :green, text: 'foo'}
|
59
|
+
])
|
60
|
+
end
|
61
|
+
|
62
|
+
it "parses malformed control sequence" do
|
63
|
+
expect(parser.parse("\eA foo bar ESC\e")).to eq([
|
64
|
+
{text: "\eA foo bar ESC\e"}
|
65
|
+
])
|
66
|
+
end
|
67
|
+
end
|