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.
@@ -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