pastel 0.5.3 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|