hansi 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rspec +4 -0
- data/.travis.yml +2 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +230 -0
- data/hansi.gemspec +22 -0
- data/hansi.png +0 -0
- data/lib/hansi.rb +53 -0
- data/lib/hansi/ansi_code.rb +16 -0
- data/lib/hansi/color.rb +115 -0
- data/lib/hansi/color_parser.rb +93 -0
- data/lib/hansi/color_renderer.rb +20 -0
- data/lib/hansi/mode_detector.rb +200 -0
- data/lib/hansi/palettes.rb +591 -0
- data/lib/hansi/sexp_renderer.rb +27 -0
- data/lib/hansi/special.rb +25 -0
- data/lib/hansi/string_renderer.rb +103 -0
- data/lib/hansi/theme.rb +66 -0
- data/lib/hansi/themes.rb +24 -0
- data/lib/hansi/version.rb +3 -0
- data/spec/hansi/color_parser_spec.rb +17 -0
- data/spec/hansi/color_renderer_spec.rb +6 -0
- data/spec/hansi/color_spec.rb +46 -0
- data/spec/hansi/mode_detector_spec.rb +35 -0
- data/spec/hansi/sexp_renderer_spec.rb +7 -0
- data/spec/hansi/special_spec.rb +6 -0
- data/spec/hansi/string_renderer_spec.rb +32 -0
- data/spec/hansi/theme_spec.rb +36 -0
- data/spec/hansi_spec.rb +41 -0
- data/spec/support.rb +6 -0
- data/spec/support/coverage.rb +14 -0
- data/spec/support/env.rb +10 -0
- data/spec/support/escape_matcher.rb +28 -0
- data/spec/support/parse_matcher.rb +28 -0
- data/spec/support/render_matcher.rb +28 -0
- metadata +56 -5
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Hansi
|
4
|
+
class SexpRenderer
|
5
|
+
def self.render(*args, **options)
|
6
|
+
new(**options).render(args)
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(theme: :default, mode: Hansi.mode, join: "")
|
10
|
+
@theme = Theme[theme]
|
11
|
+
@mode = mode
|
12
|
+
@join = join
|
13
|
+
end
|
14
|
+
|
15
|
+
def render(input, codes: nil)
|
16
|
+
return "#{codes}#{input}#{Hansi.reset}" unless input.respond_to? :to_ary and sexp = input.to_ary
|
17
|
+
return render(sexp.first) if sexp.size < 2
|
18
|
+
|
19
|
+
style, *content = sexp
|
20
|
+
codes ||= Hansi.reset
|
21
|
+
style &&= @theme[style]
|
22
|
+
codes += style.to_ansi(mode: @mode).to_s if style
|
23
|
+
|
24
|
+
content.map { |e| render(e, codes: codes) }.join(@join)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Hansi
|
2
|
+
class Special < AnsiCode
|
3
|
+
def initialize(ansi, css = nil)
|
4
|
+
ansi = "\e[#{ansi}m" unless ansi.is_a? String and ansi.start_with? "\e"
|
5
|
+
css = css.map { |a| a.map { |v| Symbol === v ? v.to_s.tr('_','-') : v.to_s }.join(': ') } if css.is_a? Hash
|
6
|
+
css = css.join("; ") if css.is_a? Array
|
7
|
+
css = css + ";" if css and not css.end_with? ";"
|
8
|
+
@ansi = ansi
|
9
|
+
@css = css
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_css_rule
|
13
|
+
@css || super
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_ansi(mode: Hansi.mode, **options)
|
17
|
+
mode &&= mode[/\d+/].to_i unless mode.is_a? Integer
|
18
|
+
@ansi if mode > 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
"<%p:%p>" % [self.class, @ansi]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Hansi
|
4
|
+
class StringRenderer
|
5
|
+
ParseError ||= Class.new(SyntaxError)
|
6
|
+
|
7
|
+
def self.render(*args, **options)
|
8
|
+
input, markup = [], {}
|
9
|
+
|
10
|
+
args.each do |arg|
|
11
|
+
if arg.respond_to? :to_hash
|
12
|
+
markup.merge! arg.to_hash
|
13
|
+
else
|
14
|
+
input << arg
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
new(markup, **options).render(*input)
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(markup = {}, theme: :default, escape: ?\\, tags: false, mode: Hansi.mode)
|
22
|
+
@theme = Theme[theme]
|
23
|
+
@escape = escape
|
24
|
+
@tags = tags
|
25
|
+
@markup = markup
|
26
|
+
@mode = mode
|
27
|
+
@simple = Regexp.union(@markup.keys)
|
28
|
+
reserved = @markup.keys
|
29
|
+
reserved += [?<, ?>] if @tags
|
30
|
+
reserved << @escape if @escape
|
31
|
+
@reserved = Regexp.union(reserved)
|
32
|
+
end
|
33
|
+
|
34
|
+
def render(input, *values)
|
35
|
+
scanner = StringScanner.new(input)
|
36
|
+
insert = true
|
37
|
+
stack = []
|
38
|
+
output = ""
|
39
|
+
|
40
|
+
until scanner.eos?
|
41
|
+
if scanner.scan(@simple)
|
42
|
+
stack.last == scanner[0] ? stack.pop : stack.push(scanner[0])
|
43
|
+
insert = true
|
44
|
+
elsif @escape and scanner.scan(/#{Regexp.escape(@escape)}(.)/)
|
45
|
+
output << color_codes(stack) if insert
|
46
|
+
output << scanner[1]
|
47
|
+
insert = false
|
48
|
+
elsif @tags and scanner.scan(/<(\/)?([^>\s]+)>/)
|
49
|
+
insert = true
|
50
|
+
if scanner[1]
|
51
|
+
unexpected(scanner[2], stack.last)
|
52
|
+
stack.pop
|
53
|
+
else
|
54
|
+
stack << scanner[2]
|
55
|
+
end
|
56
|
+
else
|
57
|
+
output << color_codes(stack) if insert
|
58
|
+
output << scanner.getch
|
59
|
+
insert = false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
unexpected(nil, stack.last)
|
64
|
+
output << Hansi.reset
|
65
|
+
values.any? ? output % values : output
|
66
|
+
end
|
67
|
+
|
68
|
+
def color_codes(stack)
|
69
|
+
codes = [Hansi.reset, :default, *stack].map { |element| ansi_for(element) }
|
70
|
+
codes.compact.join
|
71
|
+
end
|
72
|
+
|
73
|
+
def describe(element)
|
74
|
+
case element
|
75
|
+
when @simple then element.inspect
|
76
|
+
when nil then "end of string"
|
77
|
+
else "</#{element}>".inspect
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def unexpected(element, expected)
|
82
|
+
return if element == expected
|
83
|
+
return if element == "#" and expected.start_with?("#")
|
84
|
+
return if expected.start_with?("#{element}(") and expected.end_with?(")")
|
85
|
+
raise ParseError, "unexpected #{describe(element)}, expecting #{describe(expected)}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def ansi_for(input)
|
89
|
+
case input
|
90
|
+
when /^\e/ then input
|
91
|
+
when *@markup.keys then ansi_for(@markup[input])
|
92
|
+
when nil, false then nil
|
93
|
+
when AnsiCode then input.to_ansi(mode: @mode)
|
94
|
+
else ansi_for(@theme[input])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def escape(string)
|
99
|
+
return string unless @escape
|
100
|
+
string.gsub(@reserved) { |s| "#{@escape}#{s}" }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/hansi/theme.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module Hansi
|
2
|
+
class Theme
|
3
|
+
def self.[](key)
|
4
|
+
case key
|
5
|
+
when self then key
|
6
|
+
when Symbol then Themes.fetch(key)
|
7
|
+
when Hash then new(**key)
|
8
|
+
when Array then key.map { |k| self[k] }.inject(:merge)
|
9
|
+
else raise ArgumentError, "cannot convert %p into a %p" % [key, self]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.[]=(key, value)
|
14
|
+
Themes[key.to_sym] = self[value]
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :rules
|
18
|
+
def initialize(*inherit, **rules)
|
19
|
+
inherited = inherit.map { |t| Theme[t].rules }.inject({}, :merge)
|
20
|
+
@rules = inherited.merge(rules).freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
other.class == self.class and other.rules == self.rules
|
25
|
+
end
|
26
|
+
|
27
|
+
def eql?(other)
|
28
|
+
other.class.eql?(self.class) and other.rules.eql?(self.rules)
|
29
|
+
end
|
30
|
+
|
31
|
+
def hash
|
32
|
+
rules.hash
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](key)
|
36
|
+
return self[rules[key]] if rules.include? key
|
37
|
+
return self[rules[key.to_sym]] if key.respond_to? :to_sym and rules.include? key.to_sym
|
38
|
+
ColorParser.parse(key)
|
39
|
+
rescue ColorParser::IllegalValue
|
40
|
+
end
|
41
|
+
|
42
|
+
def merge(other)
|
43
|
+
other_rules = self.class[other].rules
|
44
|
+
self.class.new(**other_rules, **rules)
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_h
|
48
|
+
mapped = rules.keys.map { |key| [key, self[key]] if self[key] }
|
49
|
+
Hash[mapped.compact]
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_css(&block)
|
53
|
+
mapping = rules.keys.group_by { |key| self[key] }
|
54
|
+
mapping = mapping.map { |c,k| c.to_css(*k, &block) }
|
55
|
+
mapping.compact.join(?\n)
|
56
|
+
end
|
57
|
+
|
58
|
+
def theme_name
|
59
|
+
Themes.keys.detect { |key| Themes[key] == self }
|
60
|
+
end
|
61
|
+
|
62
|
+
def inspect
|
63
|
+
"%p[%p]" % [self.class, theme_name || rules]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/hansi/themes.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Hansi
|
2
|
+
Themes ||= {
|
3
|
+
default: Theme.new,
|
4
|
+
solarized: Theme.new({
|
5
|
+
base03: "#002b36",
|
6
|
+
base02: "#073642",
|
7
|
+
base01: "#586e75",
|
8
|
+
base00: "#657b83",
|
9
|
+
base0: "#839496",
|
10
|
+
base1: "#93a1a1",
|
11
|
+
base2: "#eee8d5",
|
12
|
+
base3: "#fdf6e3",
|
13
|
+
yellow: "#b58900",
|
14
|
+
orange: "#cb4b16",
|
15
|
+
red: "#dc322f",
|
16
|
+
magenta: "#d33682",
|
17
|
+
violet: "#6c71c4",
|
18
|
+
blue: "#268bd2",
|
19
|
+
cyan: "#2aa198",
|
20
|
+
green: "#859900",
|
21
|
+
default: :base00
|
22
|
+
})
|
23
|
+
}
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
describe Hansi::ColorParser do
|
2
|
+
subject(:parser) { Hansi::ColorParser }
|
3
|
+
it { should parse("#f00") .as(red: 255) }
|
4
|
+
it { should parse("00ff00") .as(green: 255) }
|
5
|
+
it { should parse("#f") .as(red: 255, green: 255, blue: 255) }
|
6
|
+
it { should parse("#f0") .as(red: 240, green: 240, blue: 240) }
|
7
|
+
it { should parse(red: 1.0) .as(red: 255) }
|
8
|
+
it { should parse(red: 300) .as(red: 255) }
|
9
|
+
it { should parse(red: -10) .as(red: 0) }
|
10
|
+
it { should parse(:rebeccapurple) .as(red: 102, green: 51, blue: 153) }
|
11
|
+
it { should parse("\e[31m") .as(red: 194, green: 54, blue: 33) }
|
12
|
+
it { should parse("\e[38;5;208m") .as(red: 255, green: 135, blue: 0) }
|
13
|
+
it { should parse("\e[38;2;255;0;0m") .as(red: 255) }
|
14
|
+
|
15
|
+
it { should_not parse("#ffff") }
|
16
|
+
it { should_not parse(Object.new) }
|
17
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
describe Hansi::Color do
|
2
|
+
subject(:color) { Hansi::Color.new(255, 0, 0) }
|
3
|
+
|
4
|
+
describe :hash do
|
5
|
+
specify { color.hash.should be == Hansi["f00"].hash }
|
6
|
+
end
|
7
|
+
|
8
|
+
describe :eql? do
|
9
|
+
specify { color.should be_eql Hansi["f00"] }
|
10
|
+
end
|
11
|
+
|
12
|
+
describe :to_s do
|
13
|
+
specify { color.to_s.should be == "#ff0000" }
|
14
|
+
end
|
15
|
+
|
16
|
+
describe :to_css_rule do
|
17
|
+
specify { color.to_css_rule.should be == "color: #ff0000;" }
|
18
|
+
end
|
19
|
+
|
20
|
+
describe :to_css do
|
21
|
+
specify { color.to_css("name") .should be == ".name {\n color: #ff0000;\n}\n" }
|
22
|
+
specify { color.to_css("name", &:to_s) .should be == "name {\n color: #ff0000;\n}\n" }
|
23
|
+
end
|
24
|
+
|
25
|
+
describe :to_web_name do
|
26
|
+
specify { color.to_web_name.should be == :red }
|
27
|
+
end
|
28
|
+
|
29
|
+
describe :closest do
|
30
|
+
specify { color.closest([color, Hansi[:orange]]) .should be == color }
|
31
|
+
specify { color.closest([Hansi[:blue], Hansi[:orange]]) .should be == Hansi[:orange] }
|
32
|
+
end
|
33
|
+
|
34
|
+
describe :to_ansi do
|
35
|
+
specify { color.to_ansi(mode: 0) .should be == "" }
|
36
|
+
specify { color.to_ansi(mode: 8) .should be == "\e[31m" }
|
37
|
+
specify { color.to_ansi(mode: 16) .should be == "\e[91m" }
|
38
|
+
specify { color.to_ansi(mode: 88) .should be == "\e[38;5;9m" }
|
39
|
+
specify { color.to_ansi(mode: 256) .should be == "\e[38;5;9m" }
|
40
|
+
specify { color.to_ansi(mode: Hansi::TRUE_COLOR) .should be == "\e[38;2;255;0;0m" }
|
41
|
+
|
42
|
+
specify "unknown mode" do
|
43
|
+
expect { color.to_ansi(mode: 99) }.to raise_error
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
describe Hansi::ModeDetector do
|
2
|
+
example do
|
3
|
+
detector = Hansi::ModeDetector.new(ENV)
|
4
|
+
expect(detector) .to be_shell_out
|
5
|
+
expect(detector.io) .to be == $stdout
|
6
|
+
expect(detector.mode) .to be_an(Integer)
|
7
|
+
end
|
8
|
+
|
9
|
+
example do
|
10
|
+
detector = Hansi::ModeDetector.new({})
|
11
|
+
expect(detector) .not_to be_shell_out
|
12
|
+
expect(detector.io) .to be_nil
|
13
|
+
expect(detector.mode) .to be == 0
|
14
|
+
end
|
15
|
+
|
16
|
+
example do
|
17
|
+
detector = Hansi::ModeDetector.new('TERM' => 'xterm')
|
18
|
+
expect(detector.mode).to be == 16
|
19
|
+
end
|
20
|
+
|
21
|
+
example do
|
22
|
+
detector = Hansi::ModeDetector.new('TERM' => 'footerm-256color')
|
23
|
+
expect(detector.mode).to be == 256
|
24
|
+
end
|
25
|
+
|
26
|
+
example do
|
27
|
+
detector = Hansi::ModeDetector.new('TERM' => 'footerm+24bit')
|
28
|
+
expect(detector.mode).to be == Hansi::TRUE_COLOR
|
29
|
+
end
|
30
|
+
|
31
|
+
example do
|
32
|
+
detector = Hansi::ModeDetector.new('TERM' => 'footerm+3byte')
|
33
|
+
expect(detector.mode).to be == Hansi::TRUE_COLOR
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
describe Hansi::SexpRenderer do
|
2
|
+
before { Hansi.mode = 16 }
|
3
|
+
subject(:renderer) { Hansi::SexpRenderer }
|
4
|
+
it { should render([:red, "foo"]) .as("\e[0m\e[91mfoo\e[0m") }
|
5
|
+
it { should render([:red, [:blue, "b"], "c"], join: " ") .as("\e[0m\e[91m\e[34mb\e[0m \e[0m\e[91mc\e[0m") }
|
6
|
+
it { should render([:default, "foo"], theme: :solarized) .as("\e[0m\e[90mfoo\e[0m") }
|
7
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
describe Hansi::StringRenderer do
|
2
|
+
before { Hansi.mode = 16 }
|
3
|
+
subject(:renderer) { Hansi::StringRenderer }
|
4
|
+
|
5
|
+
it { should render("foo *bar*") .as("\e[0m\e[10mfoo *bar*\e[0m") }
|
6
|
+
it { should render("<foo>") .as("\e[0m\e[10m<foo>\e[0m") }
|
7
|
+
it { should render("foo *bar*", "*" => :bold) .as("\e[0m\e[10mfoo \e[0m\e[10m\e[1mbar\e[0m") }
|
8
|
+
it { should render("foo \\*bar\\*", "*" => :bold) .as("\e[0m\e[10mfoo *bar*\e[0m") }
|
9
|
+
it { should render("<bold>foo</bold>", tags: true) .as("\e[0m\e[10m\e[1mfoo\e[0m") }
|
10
|
+
it { should render("foo", mode: 256, theme: :solarized) .as("\e[0m\e[38;5;66mfoo\e[0m") }
|
11
|
+
it { should render("*%s*", "*", "*" => :bold) .as("\e[0m\e[10m\e[1m*\e[0m") }
|
12
|
+
|
13
|
+
it { should_not render("<red>", tags: true) }
|
14
|
+
it { should_not render("*", "*" => :bold) }
|
15
|
+
|
16
|
+
describe :escape do
|
17
|
+
context "special character" do
|
18
|
+
subject(:renderer) { Hansi::StringRenderer.new("*" => :bold) }
|
19
|
+
it { should escape("foo *bar*").as("foo \\*bar\\*") }
|
20
|
+
end
|
21
|
+
|
22
|
+
context "nothing special" do
|
23
|
+
subject(:renderer) { Hansi::StringRenderer.new }
|
24
|
+
it { should escape("<foo> *bar*").as("<foo> *bar*") }
|
25
|
+
end
|
26
|
+
|
27
|
+
context "tags" do
|
28
|
+
subject(:renderer) { Hansi::StringRenderer.new(tags: true) }
|
29
|
+
it { should escape("<foo> bar").as("\\<foo\\> bar") }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
describe Hansi::Theme do
|
2
|
+
subject(:theme) { Hansi::Theme.new(foo: :bar, bar: :red) }
|
3
|
+
|
4
|
+
describe :[] do
|
5
|
+
specify { Hansi::Theme[:solarized][:yellow] .should be == Hansi[181, 137, 0] }
|
6
|
+
specify { Hansi::Theme[:default][:yellow] .should be == Hansi[255, 255, 0] }
|
7
|
+
specify { Hansi::Theme[Hansi::Theme[:solarized]] .should be == Hansi::Theme[:solarized] }
|
8
|
+
specify { Hansi::Theme[red: :blue] .should be == Hansi::Theme.new(red: :blue) }
|
9
|
+
specify { Hansi::Theme[[{red: :blue}, {green: :blue}]] .should be == Hansi::Theme.new(red: :blue, green: :blue) }
|
10
|
+
specify { expect { Hansi::Theme[Object.new] }.to raise_error(ArgumentError) }
|
11
|
+
end
|
12
|
+
|
13
|
+
describe :[]= do
|
14
|
+
specify do
|
15
|
+
Hansi::Theme[:foo] = :solarized
|
16
|
+
Hansi::Theme[:foo][:yellow].should be == Hansi[181, 137, 0]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe :hash do
|
21
|
+
specify { Hansi::Theme[:default].hash.should be == Hansi::Theme[{}].hash }
|
22
|
+
end
|
23
|
+
|
24
|
+
describe :eql? do
|
25
|
+
specify { Hansi::Theme[:default].should be_eql Hansi::Theme[{}] }
|
26
|
+
end
|
27
|
+
|
28
|
+
describe :to_h do
|
29
|
+
specify { theme.to_h.should be == { foo: Hansi[:red], bar: Hansi[:red] }}
|
30
|
+
end
|
31
|
+
|
32
|
+
describe :to_css do
|
33
|
+
specify { theme.to_css .should be == ".foo, .bar {\n color: #ff0000;\n}\n" }
|
34
|
+
specify { theme.to_css { |name| "##{name}" } .should be == "#foo, #bar {\n color: #ff0000;\n}\n" }
|
35
|
+
end
|
36
|
+
end
|