hansi 0.1.0 → 0.1.1
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/.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
|