ansi_chameleon 0.0.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.
- data/Changelog +2 -0
- data/LICENSE +20 -0
- data/README.md +106 -0
- data/lib/ansi_chameleon.rb +14 -0
- data/lib/ansi_chameleon/array_utils.rb +20 -0
- data/lib/ansi_chameleon/sequence_generator.rb +32 -0
- data/lib/ansi_chameleon/style_property_name_translator.rb +19 -0
- data/lib/ansi_chameleon/style_sheet_handler.rb +53 -0
- data/lib/ansi_chameleon/text_renderer.rb +62 -0
- data/lib/ansi_chameleon/text_rendering.rb +76 -0
- data/lib/ansi_chameleon/version.rb +3 -0
- metadata +67 -0
data/Changelog
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyrigth (c) 2012 Jacek Mikrut
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
AnsiChameleon
|
2
|
+
=============
|
3
|
+
|
4
|
+
AnsiChameleon is a Ruby Gem that **colorizes text terminal output** by converting custom HTML-like tags into color ANSI escape sequences.
|
5
|
+
|
6
|
+
Installation
|
7
|
+
------------
|
8
|
+
|
9
|
+
As a Ruby Gem, AnsiChameleon can be installed either by running
|
10
|
+
|
11
|
+
```bash
|
12
|
+
gem install ansi_chameleon
|
13
|
+
```
|
14
|
+
|
15
|
+
or adding
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem "ansi_chameleon"
|
19
|
+
```
|
20
|
+
|
21
|
+
to the Gemfile and then invoking `bundle install`.
|
22
|
+
|
23
|
+
Usage
|
24
|
+
-----
|
25
|
+
|
26
|
+
For **single use**:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
require "ansi_chameleon"
|
30
|
+
|
31
|
+
puts AnsiChameleon.render(
|
32
|
+
"There are <number>3</number> oranges on the table.",
|
33
|
+
:number => { :fg_color => :blue }
|
34
|
+
)
|
35
|
+
```
|
36
|
+
|
37
|
+
For **multiple use** this is more efficient:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
require "ansi_chameleon"
|
41
|
+
|
42
|
+
text_renderer = AnsiChameleon::TextRenderer.new(:number => { :fg_color => :blue })
|
43
|
+
|
44
|
+
puts text_renderer.render("There are <number>2</number> oranges on the table.")
|
45
|
+
puts text_renderer.render("Now, there is only <number>1</number> orange.")
|
46
|
+
```
|
47
|
+
|
48
|
+
Details
|
49
|
+
-------
|
50
|
+
|
51
|
+
In order to produce a colorized output AnsiChameleon needs a **text** and a **style sheet**.
|
52
|
+
|
53
|
+
### Text
|
54
|
+
|
55
|
+
Text to colorize is expected to include HTML-like tags of custom names, that will then be converted according to the rules defined in provided style sheet.
|
56
|
+
|
57
|
+
Tags not included in the style sheet but found in the text being rendered are put to the output without modification.
|
58
|
+
|
59
|
+
### Style sheet
|
60
|
+
|
61
|
+
A style sheet is simply a CSS-like Hash structure with keys that correspond to custom tag names and property names:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
style_sheet = {
|
65
|
+
# default style declarations (for text not wrapped in tags)
|
66
|
+
:fg_color => :green,
|
67
|
+
:bg_color => :black,
|
68
|
+
|
69
|
+
:number => {
|
70
|
+
# style declarations for <number> tag
|
71
|
+
:fg_color => :blue,
|
72
|
+
},
|
73
|
+
:message => {
|
74
|
+
# style declarations for <message> tag
|
75
|
+
:fg_color => :red,
|
76
|
+
:bg_color => :white,
|
77
|
+
|
78
|
+
:number => {
|
79
|
+
# style declarations for <number> tag nested in <message> tag
|
80
|
+
:fg_color => :inherit
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
```
|
85
|
+
|
86
|
+
Tag names in a style sheet can be written as either :symbols or "strings".
|
87
|
+
|
88
|
+
#### Properties
|
89
|
+
|
90
|
+
* `:foreground_color` (aliases: `:foreground`, `:fg_color`)
|
91
|
+
* `:background_color` (aliases: `:background`, `:bg_color`)
|
92
|
+
|
93
|
+
Available color values: `:black`, `:red`, `:green`, `:yellow`, `:blue`, `:magenta`, `:cyan`, `:white`.
|
94
|
+
|
95
|
+
* `:effect`
|
96
|
+
|
97
|
+
Available effect values: `:none`, `:bright`, `:underline`, `:blink`, `:reverse`.
|
98
|
+
|
99
|
+
Also, the `:inherit` value can be set for a property, which means that its value will be inherited from the surrounding tag (or be the default value).
|
100
|
+
|
101
|
+
Similarly to tag names, property names and values can be provided as :symbols or "strings".
|
102
|
+
|
103
|
+
License
|
104
|
+
-------
|
105
|
+
|
106
|
+
License is included in the LICENSE file.
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "ansi_chameleon/version"
|
2
|
+
require "ansi_chameleon/array_utils"
|
3
|
+
require "ansi_chameleon/sequence_generator"
|
4
|
+
require "ansi_chameleon/style_property_name_translator"
|
5
|
+
require "ansi_chameleon/style_sheet_handler"
|
6
|
+
require "ansi_chameleon/text_rendering"
|
7
|
+
require "ansi_chameleon/text_renderer"
|
8
|
+
|
9
|
+
module AnsiChameleon
|
10
|
+
|
11
|
+
def self.render(text, style_sheet)
|
12
|
+
TextRenderer.new(style_sheet).render(text)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AnsiChameleon
|
2
|
+
module ArrayUtils
|
3
|
+
|
4
|
+
def self.item_spread_map(array, items)
|
5
|
+
remaining_items = items.dup
|
6
|
+
|
7
|
+
map = array.inject('') do |agg, array_item|
|
8
|
+
if array_item == remaining_items.first
|
9
|
+
agg << '1'
|
10
|
+
remaining_items.shift
|
11
|
+
else
|
12
|
+
agg << '0'
|
13
|
+
end
|
14
|
+
agg
|
15
|
+
end
|
16
|
+
|
17
|
+
remaining_items.any? ? nil : map
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module AnsiChameleon
|
2
|
+
module SequenceGenerator
|
3
|
+
|
4
|
+
class UnknownColorName < ArgumentError; end
|
5
|
+
class UnknownEffectName < ArgumentError; end
|
6
|
+
|
7
|
+
COLORS = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
|
8
|
+
EFFECTS = { :none => 0, :bright => 1, :underline => 4, :blink => 5, :reverse => 7 }
|
9
|
+
|
10
|
+
def self.effect_code(name)
|
11
|
+
EFFECTS[name.to_sym] or raise UnknownEffectName.new("Unknown effect name #{name.inspect}")
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.foreground_color_code(name)
|
15
|
+
index = COLORS.index(name.to_sym) or raise UnknownColorName.new("Unknown foreground color name #{name.inspect}")
|
16
|
+
30 + index
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.background_color_code(name)
|
20
|
+
index = COLORS.index(name.to_sym) or raise UnknownColorName.new("Unknown background color name #{name.inspect}")
|
21
|
+
40 + index
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.generate(effect_name, foreground_color_name=nil, background_color_name=nil)
|
25
|
+
if effect_name.to_sym == :reset
|
26
|
+
"\033[0m"
|
27
|
+
else
|
28
|
+
"\033[#{effect_code(effect_name)};#{foreground_color_code(foreground_color_name)};#{background_color_code(background_color_name)}m"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module AnsiChameleon
|
2
|
+
module StylePropertyNameTranslator
|
3
|
+
|
4
|
+
def self.translate(property_name)
|
5
|
+
case property_name
|
6
|
+
|
7
|
+
when :background_color, :background, :bg_color
|
8
|
+
:background_color_name
|
9
|
+
|
10
|
+
when :foreground_color, :foreground, :fg_color
|
11
|
+
:foreground_color_name
|
12
|
+
|
13
|
+
when :effect
|
14
|
+
:effect_name
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module AnsiChameleon
|
2
|
+
class StyleSheetHandler
|
3
|
+
|
4
|
+
def initialize(style_sheet_hash, property_name_translator=nil)
|
5
|
+
@tag_names = []
|
6
|
+
@default_values = {}
|
7
|
+
@map = {}
|
8
|
+
@property_name_translator = property_name_translator
|
9
|
+
populate_map(style_sheet_hash)
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :tag_names, :default_values, :map, :property_name_translator
|
13
|
+
|
14
|
+
def value_for(*tag_names_chain, property_name)
|
15
|
+
strongest = strongest_style_definition(*(tag_names_chain.map { |tag_name| tag_name.to_sym }), property_name.to_sym)
|
16
|
+
strongest && strongest[:value]
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def populate_map(style_definition_hash, tag_names_chain=[])
|
22
|
+
style_definition_hash.each do |key, value|
|
23
|
+
|
24
|
+
case value
|
25
|
+
when Hash
|
26
|
+
tag_name = key.to_sym
|
27
|
+
|
28
|
+
@tag_names << tag_name unless @tag_names.include?(tag_name)
|
29
|
+
populate_map(value, tag_names_chain + [tag_name])
|
30
|
+
|
31
|
+
else
|
32
|
+
property_name = property_name_translator ? property_name_translator.translate(key) : key.to_sym
|
33
|
+
|
34
|
+
@map[property_name] ||= []
|
35
|
+
@map[property_name] << { :value => value, :tag_names_chain => tag_names_chain }
|
36
|
+
@default_values[property_name] = value if tag_names_chain.none?
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def strongest_style_definition(*tag_names_chain, property_name)
|
43
|
+
(@map[property_name] || [])
|
44
|
+
.map { |entry|
|
45
|
+
entry.merge(:item_spread_map => AnsiChameleon::ArrayUtils.item_spread_map(tag_names_chain, entry[:tag_names_chain])) }
|
46
|
+
.select { |entry|
|
47
|
+
not entry[:item_spread_map].nil? }
|
48
|
+
.compact
|
49
|
+
.max_by { |entry|
|
50
|
+
entry[:item_spread_map].reverse }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module AnsiChameleon
|
2
|
+
class TextRenderer
|
3
|
+
|
4
|
+
def initialize(style_sheet)
|
5
|
+
@style_sheet_handler = StyleSheetHandler.new(style_sheet, StylePropertyNameTranslator)
|
6
|
+
end
|
7
|
+
|
8
|
+
def render(text)
|
9
|
+
text_rendering = TextRendering.new(@style_sheet_handler)
|
10
|
+
|
11
|
+
chunks(text).each do |chunk|
|
12
|
+
case
|
13
|
+
when opening_tag?(chunk)
|
14
|
+
text_rendering.push_opening_tag(tag_name(chunk))
|
15
|
+
|
16
|
+
when closing_tag?(chunk)
|
17
|
+
text_rendering.push_closing_tag(tag_name(chunk))
|
18
|
+
|
19
|
+
else
|
20
|
+
text_rendering.push_text(chunk)
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
text_rendering.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def tag_names_regex
|
31
|
+
@tag_names_regex ||= @style_sheet_handler.tag_names.map { |tag_name| "(?<=<#{tag_name}>)|(?<=<\/#{tag_name}>)|(?=<\/?#{tag_name}>)" }.join('|')
|
32
|
+
end
|
33
|
+
|
34
|
+
def chunk_regex
|
35
|
+
@chunk_regex ||= /(?<= |\t)|(?= |\t)#{'|' + tag_names_regex unless tag_names_regex.empty?}/
|
36
|
+
end
|
37
|
+
|
38
|
+
def opening_tag_regex
|
39
|
+
@opening_tag_regex ||= /^<(?<=[^\/])(?:#{@style_sheet_handler.tag_names.join('|')})>$/
|
40
|
+
end
|
41
|
+
|
42
|
+
def closing_tag_regex
|
43
|
+
@closing_tag_regex ||= /^<\/#{@style_sheet_handler.tag_names.join('|')}>$/
|
44
|
+
end
|
45
|
+
|
46
|
+
def chunks(text)
|
47
|
+
text.split(chunk_regex)
|
48
|
+
end
|
49
|
+
|
50
|
+
def opening_tag?(chunk)
|
51
|
+
chunk =~ opening_tag_regex
|
52
|
+
end
|
53
|
+
|
54
|
+
def closing_tag?(chunk)
|
55
|
+
chunk =~ closing_tag_regex
|
56
|
+
end
|
57
|
+
|
58
|
+
def tag_name(chunk)
|
59
|
+
chunk.match(/^<\/?(?<tag_name>.*)>$/)[:tag_name].to_sym
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module AnsiChameleon
|
2
|
+
class TextRendering
|
3
|
+
|
4
|
+
DEFAULT_STYLE = {
|
5
|
+
:effect_name => :none,
|
6
|
+
:foreground_color_name => :white,
|
7
|
+
:background_color_name => :black
|
8
|
+
}
|
9
|
+
|
10
|
+
def initialize(style_sheet_handler)
|
11
|
+
@style_sheet_handler = style_sheet_handler
|
12
|
+
@stack = []
|
13
|
+
@current_style = DEFAULT_STYLE.merge(@style_sheet_handler.default_values)
|
14
|
+
|
15
|
+
@rendered_text = ''
|
16
|
+
@rendered_text << sequence_for(@current_style)
|
17
|
+
end
|
18
|
+
|
19
|
+
def push_opening_tag(tag_name)
|
20
|
+
@stack.push({ :tag_name => tag_name, :outer_style => @current_style })
|
21
|
+
|
22
|
+
@current_style = deduce_current_style
|
23
|
+
@rendered_text << sequence_for(@current_style)
|
24
|
+
end
|
25
|
+
|
26
|
+
def push_closing_tag(tag_name)
|
27
|
+
unless tag_names_chain.last == tag_name
|
28
|
+
raise SyntaxError.new("Encountered </#{tag_name}> tag that had not been opened yet")
|
29
|
+
end
|
30
|
+
|
31
|
+
@current_style = @stack.pop[:outer_style]
|
32
|
+
@rendered_text << sequence_for(@current_style)
|
33
|
+
end
|
34
|
+
|
35
|
+
def push_text(text)
|
36
|
+
@rendered_text << text
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
if @stack.any?
|
41
|
+
tag_names = @stack.map { |data| "<#{data[:tag_name]}>" }.join(', ')
|
42
|
+
msg_prefix = @stack.size == 1 ? "Tag #{tag_names} has" : "Tags #{tag_names} have"
|
43
|
+
raise SyntaxError.new(msg_prefix + " been opened but not closed yet")
|
44
|
+
end
|
45
|
+
|
46
|
+
@rendered_text + AnsiChameleon::SequenceGenerator.generate(:reset)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def sequence_for(style)
|
52
|
+
AnsiChameleon::SequenceGenerator.generate(
|
53
|
+
style[:effect_name],
|
54
|
+
style[:foreground_color_name],
|
55
|
+
style[:background_color_name]
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def tag_names_chain
|
60
|
+
@stack.map { |data| data[:tag_name] }
|
61
|
+
end
|
62
|
+
|
63
|
+
def property_names
|
64
|
+
DEFAULT_STYLE.keys
|
65
|
+
end
|
66
|
+
|
67
|
+
def deduce_current_style
|
68
|
+
property_names.inject({}) do |style, property_name|
|
69
|
+
property_value = @style_sheet_handler.value_for(*tag_names_chain, property_name)
|
70
|
+
style[property_name] = [:inherit, nil].include?(property_value) ? @current_style[property_name] : property_value
|
71
|
+
style
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ansi_chameleon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jacek Mikrut
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-30 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &86851630 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *86851630
|
25
|
+
description: Colorizes text terminal output by converting custom HTML-like tags into
|
26
|
+
color ANSI escape sequences.
|
27
|
+
email: jacekmikrut.software@gmail.com
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- lib/ansi_chameleon/array_utils.rb
|
33
|
+
- lib/ansi_chameleon/version.rb
|
34
|
+
- lib/ansi_chameleon/sequence_generator.rb
|
35
|
+
- lib/ansi_chameleon/text_rendering.rb
|
36
|
+
- lib/ansi_chameleon/style_property_name_translator.rb
|
37
|
+
- lib/ansi_chameleon/style_sheet_handler.rb
|
38
|
+
- lib/ansi_chameleon/text_renderer.rb
|
39
|
+
- lib/ansi_chameleon.rb
|
40
|
+
- README.md
|
41
|
+
- LICENSE
|
42
|
+
- Changelog
|
43
|
+
homepage: http://github.com/jacekmikrut/ansi_chameleon
|
44
|
+
licenses: []
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.8.12
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: Text terminal output coloring tool.
|
67
|
+
test_files: []
|