hansi 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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,3 @@
1
+ module Hansi
2
+ VERSION = '0.1.1'
3
+ 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,6 @@
1
+ describe Hansi::ColorRenderer do
2
+ before { Hansi.mode = 16 }
3
+ subject(:renderer) { Hansi::ColorRenderer }
4
+ it { should render(Hansi[:red]).as("\e[91m") }
5
+ it { should render(Hansi[:red], "foo").as("\e[91mfoo\e[0m") }
6
+ 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,6 @@
1
+ describe Hansi::Special do
2
+ describe :to_css_rule do
3
+ example { Hansi[:bold] .to_css_rule.should be == 'font-weight: bold;' }
4
+ example { Hansi[:fraktur] .to_css_rule.should be == '/* cannot convert <Hansi::Special:"\e[20m"> to css */' }
5
+ end
6
+ 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