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.
@@ -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 = Color.new)
19
+ def initialize(color)
18
20
  @color = color
19
21
  end
20
22
 
@@ -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
  #
@@ -1,5 +1,5 @@
1
1
  # coding: utf-8
2
2
 
3
3
  module Pastel
4
- VERSION = "0.5.3"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -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{^(test|spec|features)/})
18
+ spec.test_files = spec.files.grep(%r{^spec/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "equatable", "~> 0.5.0"
22
- spec.add_dependency "tty-screen", "~> 0.4.3"
21
+ spec.add_dependency 'equatable', '~> 0.5.0'
22
+ spec.add_dependency 'tty-color', '~> 0.3.0'
23
23
 
24
- spec.add_development_dependency "bundler", "~> 1.6"
24
+ spec.add_development_dependency 'bundler', '>= 1.5.0', '< 2.0'
25
+ spec.add_development_dependency 'rake'
25
26
  end
@@ -1,8 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
- RSpec.describe Pastel, '.alias_color' do
3
+ RSpec.describe Pastel, '#alias_color' do
6
4
 
7
5
  subject(:pastel) { described_class.new(enabled: true) }
8
6
 
@@ -1,31 +1,28 @@
1
1
  # coding: utf-8
2
2
 
3
- require 'spec_helper'
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
- allow(ENV).to receive(:[]).with('PASTEL_COLORS_ALIASES').
14
- and_return(color_aliases)
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).twice
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
- allow(ENV).to receive(:[]).with('PASTEL_COLORS_ALIASES').
24
- and_return(color_aliases)
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, '.alias_color' do
6
4
 
7
5
  subject(:color) { described_class.new(enabled: true) }
@@ -1,8 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
- RSpec.describe Pastel::Color, '.code' do
3
+ RSpec.describe Pastel::Color, '#code' do
6
4
  let(:string) { "This is a \e[1m\e[34mbold blue text\e[0m" }
7
5
 
8
6
  subject(:color) { described_class.new(enabled: true) }
@@ -1,8 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
- RSpec.describe Pastel::Color, '.colored?' do
3
+ RSpec.describe Pastel::Color, '#colored?' do
6
4
  subject(:color) { described_class.new(enabled: true) }
7
5
 
8
6
  it "checks if string has color codes" do
@@ -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
@@ -1,7 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
3
  RSpec.describe Pastel::Color, '#==' do
6
4
  it "is true with the same enabled and eachline attributes" do
7
5
  expect(Pastel::Color.new(enabled: false, eachline: "\n")).
@@ -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
@@ -1,24 +1,10 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
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('foobar')
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;tfoo\e[8;50;0t"
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
 
@@ -1,8 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
- RSpec.describe Pastel::Color do
3
+ RSpec.describe Pastel::Color, '#styles' do
6
4
 
7
5
  subject(:color) { described_class.new(enabled: true) }
8
6
 
@@ -1,7 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
3
  RSpec.describe Pastel::Color, '.valid?' do
6
4
  it "detects valid colors" do
7
5
  color = described_class.new
@@ -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