styles 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +725 -0
- data/Rakefile +9 -0
- data/bin/styles +11 -0
- data/lib/styles.rb +18 -0
- data/lib/styles/application.rb +190 -0
- data/lib/styles/colors.rb +289 -0
- data/lib/styles/core_ext.rb +12 -0
- data/lib/styles/engine.rb +73 -0
- data/lib/styles/line.rb +55 -0
- data/lib/styles/properties.rb +34 -0
- data/lib/styles/properties/background_color.rb +15 -0
- data/lib/styles/properties/base.rb +68 -0
- data/lib/styles/properties/border.rb +147 -0
- data/lib/styles/properties/color.rb +16 -0
- data/lib/styles/properties/display.rb +10 -0
- data/lib/styles/properties/font_weight.rb +13 -0
- data/lib/styles/properties/function.rb +7 -0
- data/lib/styles/properties/margin.rb +83 -0
- data/lib/styles/properties/match_background_color.rb +28 -0
- data/lib/styles/properties/match_color.rb +21 -0
- data/lib/styles/properties/match_font_weight.rb +23 -0
- data/lib/styles/properties/match_text_decoration.rb +36 -0
- data/lib/styles/properties/padding.rb +81 -0
- data/lib/styles/properties/text_align.rb +10 -0
- data/lib/styles/properties/text_decoration.rb +20 -0
- data/lib/styles/properties/width.rb +11 -0
- data/lib/styles/rule.rb +67 -0
- data/lib/styles/stylesheet.rb +103 -0
- data/lib/styles/sub_engines.rb +4 -0
- data/lib/styles/sub_engines/base.rb +16 -0
- data/lib/styles/sub_engines/color.rb +115 -0
- data/lib/styles/sub_engines/layout.rb +158 -0
- data/lib/styles/sub_engines/pre_processor.rb +19 -0
- data/lib/styles/version.rb +3 -0
- data/styles.gemspec +26 -0
- data/test/application_test.rb +92 -0
- data/test/colors_test.rb +162 -0
- data/test/engine_test.rb +59 -0
- data/test/integration_test.rb +136 -0
- data/test/line_test.rb +24 -0
- data/test/properties/background_color_test.rb +36 -0
- data/test/properties/base_test.rb +11 -0
- data/test/properties/border_test.rb +154 -0
- data/test/properties/color_test.rb +28 -0
- data/test/properties/display_test.rb +26 -0
- data/test/properties/font_weight_test.rb +24 -0
- data/test/properties/function_test.rb +28 -0
- data/test/properties/margin_test.rb +98 -0
- data/test/properties/match_background_color_test.rb +71 -0
- data/test/properties/match_color_test.rb +79 -0
- data/test/properties/match_font_weight_test.rb +34 -0
- data/test/properties/match_text_decoration_test.rb +38 -0
- data/test/properties/padding_test.rb +87 -0
- data/test/properties/text_align_test.rb +107 -0
- data/test/properties/text_decoration_test.rb +25 -0
- data/test/properties/width_test.rb +41 -0
- data/test/rule_test.rb +39 -0
- data/test/stylesheet_test.rb +245 -0
- data/test/sub_engines/color_test.rb +144 -0
- data/test/sub_engines/layout_test.rb +110 -0
- data/test/test_helper.rb +5 -0
- metadata +184 -0
data/Rakefile
ADDED
data/bin/styles
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'styles'
|
5
|
+
rescue LoadError
|
6
|
+
executable = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
|
7
|
+
$:.unshift File.join(File.dirname(executable), '..', 'lib')
|
8
|
+
require 'styles'
|
9
|
+
end
|
10
|
+
|
11
|
+
Styles::Application.new.run
|
data/lib/styles.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Styles
|
2
|
+
|
3
|
+
# Raised when a stylesheet cannot be loaded, either because it does not
|
4
|
+
# exist or because it is malformed.
|
5
|
+
class StylesheetLoadError < LoadError
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'styles/version'
|
10
|
+
require 'styles/core_ext'
|
11
|
+
require 'styles/colors'
|
12
|
+
require 'styles/line'
|
13
|
+
require 'styles/engine'
|
14
|
+
require 'styles/sub_engines'
|
15
|
+
require 'styles/properties'
|
16
|
+
require 'styles/rule'
|
17
|
+
require 'styles/stylesheet'
|
18
|
+
require 'styles/application'
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Styles
|
5
|
+
class Application
|
6
|
+
STYLESHEET_CHECK_INTERVAL_SECONDS = 2
|
7
|
+
|
8
|
+
def initialize(options={})
|
9
|
+
@input_stream = options[:input_stream] || $stdin
|
10
|
+
@output_stream = options[:output_stream] || $stdout
|
11
|
+
@quiet = false
|
12
|
+
@last_reload_error_message = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
create_stylesheets_dir
|
17
|
+
parse_args
|
18
|
+
read_stylesheets
|
19
|
+
process
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_accessor :last_stylesheet_check_time
|
25
|
+
attr_reader :input_stream, :output_stream
|
26
|
+
|
27
|
+
def process
|
28
|
+
['INT', 'TERM'].each { |signal| trap(signal) { exit } }
|
29
|
+
|
30
|
+
while process_next_line
|
31
|
+
# no-op
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def process_next_line
|
36
|
+
reload_stylesheets_if_outdated if check_interval_elapsed?
|
37
|
+
|
38
|
+
line = input_stream.gets
|
39
|
+
return nil unless line
|
40
|
+
result = engine.process line
|
41
|
+
output_stream.puts result if result
|
42
|
+
line
|
43
|
+
end
|
44
|
+
|
45
|
+
def engine
|
46
|
+
@engine ||= Styles::Engine.new(stylesheets)
|
47
|
+
end
|
48
|
+
|
49
|
+
def stylesheets
|
50
|
+
@stylesheets ||= []
|
51
|
+
end
|
52
|
+
|
53
|
+
def stylesheet_names
|
54
|
+
@stylesheet_names ||= []
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_args
|
58
|
+
OptionParser.new do |opts|
|
59
|
+
opts.on('--edit [NAME]', 'Edit stylesheet with given NAME (\'default\' by default)') do |name|
|
60
|
+
safe_exec which_editor, stylesheet_file(name ||= 'default')
|
61
|
+
end
|
62
|
+
opts.on('--list', 'List names of available stylesheets') do
|
63
|
+
Dir.entries(stylesheets_dir).grep(/\.rb$/).each { |ss| puts ss.sub(/\.rb$/, '') }
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
opts.on('--quiet', 'Suppress stylesheet warning messages') do
|
67
|
+
@quiet = true
|
68
|
+
end
|
69
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
70
|
+
puts opts
|
71
|
+
exit
|
72
|
+
end
|
73
|
+
opts.on_tail('--version', 'Show version') do
|
74
|
+
puts "Styles version: #{Styles::VERSION}"
|
75
|
+
exit
|
76
|
+
end
|
77
|
+
end.parse!
|
78
|
+
|
79
|
+
stylesheet_names.concat ARGV.dup
|
80
|
+
end
|
81
|
+
|
82
|
+
def read_stylesheets
|
83
|
+
stylesheet_names << 'default' if stylesheet_names.empty?
|
84
|
+
|
85
|
+
stylesheet_names.each do |name|
|
86
|
+
file = stylesheet_file(name)
|
87
|
+
begin
|
88
|
+
stylesheets << ::Styles::Stylesheet.new(file)
|
89
|
+
self.last_stylesheet_check_time = Time.now
|
90
|
+
print_stylesheet_warnings
|
91
|
+
rescue ::Styles::StylesheetLoadError => e
|
92
|
+
$stderr.puts e.message
|
93
|
+
exit 1
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def reload_stylesheets_if_outdated
|
99
|
+
before_times = stylesheets.map(&:last_updated).sort
|
100
|
+
|
101
|
+
begin
|
102
|
+
stylesheets.each &:reload_if_outdated
|
103
|
+
@last_reload_error_message = nil
|
104
|
+
rescue ::Styles::StylesheetLoadError => e
|
105
|
+
unless @last_reload_error_message == e.message
|
106
|
+
$stderr.puts e.message
|
107
|
+
@last_reload_error_message = e.message
|
108
|
+
|
109
|
+
$stderr.puts "\nFix the above errors to have output formatted correctly\n" +
|
110
|
+
"Proceeding with previously working stylesheet rules in 5 seconds..."
|
111
|
+
sleep 5
|
112
|
+
end
|
113
|
+
end
|
114
|
+
self.last_stylesheet_check_time = Time.now
|
115
|
+
|
116
|
+
after_times = stylesheets.map(&:last_updated).sort
|
117
|
+
print_stylesheet_warnings unless before_times == after_times
|
118
|
+
end
|
119
|
+
|
120
|
+
def check_interval_elapsed?
|
121
|
+
(last_stylesheet_check_time + STYLESHEET_CHECK_INTERVAL_SECONDS) <= Time.now
|
122
|
+
end
|
123
|
+
|
124
|
+
def which_editor
|
125
|
+
editor = ENV['STYLES_EDITOR'] || ENV['EDITOR']
|
126
|
+
return editor unless editor.nil?
|
127
|
+
|
128
|
+
%w[subl mate].each {|e| return e if which(e) }
|
129
|
+
|
130
|
+
'/usr/bin/vim'
|
131
|
+
end
|
132
|
+
|
133
|
+
def which(cmd)
|
134
|
+
dir = ENV['PATH'].split(':').find {|p| File.executable? File.join(p, cmd)}
|
135
|
+
Pathname.new(File.join(dir, cmd)) unless dir.nil?
|
136
|
+
end
|
137
|
+
|
138
|
+
# Properly quote and evaluate of environment variables in the cmd parameter. This
|
139
|
+
# and a few other things related to firing up an editor are borrowed or pretty much
|
140
|
+
# cribbed from Homebrew (github.com/mxcl/homebrew).
|
141
|
+
def safe_exec(cmd, *args)
|
142
|
+
exec "/bin/sh", "-i", "-c", cmd + ' "$@"', "--", *args
|
143
|
+
end
|
144
|
+
|
145
|
+
def create_stylesheets_dir
|
146
|
+
return if File.directory? stylesheets_dir
|
147
|
+
if File.exist? stylesheets_dir
|
148
|
+
# TODO: raise a custom exception that is caught and outputs something nice
|
149
|
+
raise "Not a directory: #{stylesheets_dir}"
|
150
|
+
else
|
151
|
+
Dir.mkdir stylesheets_dir
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def stylesheet_file(name)
|
156
|
+
File.join(stylesheets_dir, "#{name}.rb")
|
157
|
+
end
|
158
|
+
|
159
|
+
def stylesheets_dir
|
160
|
+
@stylesheets_dir ||= ENV['STYLES_DIR'] || File.join(home_dir, '.styles')
|
161
|
+
end
|
162
|
+
|
163
|
+
def home_dir
|
164
|
+
@home_dir ||= begin
|
165
|
+
home = ENV['HOME']
|
166
|
+
home = ENV['USERPROFILE'] unless home
|
167
|
+
if !home && (ENV['HOMEDRIVE'] && ENV['HOMEPATH'])
|
168
|
+
home = File.join(ENV['HOMEDRIVE'], ENV['HOMEPATH'])
|
169
|
+
end
|
170
|
+
home = File.expand_path('~') unless home
|
171
|
+
home = 'C:/' if !home && RUBY_PLATFORM =~ /mswin|mingw/
|
172
|
+
home
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def quiet?; @quiet end
|
177
|
+
|
178
|
+
def print_stylesheet_warnings
|
179
|
+
unless quiet?
|
180
|
+
stylesheets.each do |sheet|
|
181
|
+
unless sheet.unrecognized_property_names.empty?
|
182
|
+
props = sheet.unrecognized_property_names
|
183
|
+
name_list = props.map { |p| "'#{p.to_s}'" }.join(' ')
|
184
|
+
$stderr.puts "(styles) Warning: unrecognized #{props.size > 1 ? 'properties' : 'property'} #{name_list} in #{sheet.file_path}"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,289 @@
|
|
1
|
+
require 'term/ansicolor'
|
2
|
+
|
3
|
+
module Styles
|
4
|
+
# Basically a wrapper around Term::ANSIColor but also adds combination foreground and background
|
5
|
+
# colors (e.g. :red_on_white). Returns nil with an invalid color specification.
|
6
|
+
class Colors
|
7
|
+
# Map CSS-style value to ANSI code name, where they are different
|
8
|
+
CSS_TO_ANSI_VALUES = {
|
9
|
+
:line_through => :strikethrough
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
FOREGROUND_COLOR_VALUES = [
|
13
|
+
:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
BACKGROUND_COLOR_VALUES = [
|
17
|
+
:on_black, :on_red, :on_green, :on_yellow, :on_blue, :on_magenta, :on_cyan, :on_white
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
TEXT_DECORATION_VALUES = [:underline, :strikethrough, :blink].freeze
|
21
|
+
|
22
|
+
COLOR_VALUES = (FOREGROUND_COLOR_VALUES + BACKGROUND_COLOR_VALUES).freeze
|
23
|
+
OTHER_STYLE_VALUES = [:bold, :italic, :underline, :underscore, :blink, :strikethrough]
|
24
|
+
|
25
|
+
# Only :reset is available to represent the complete absence of color and styling. There are no
|
26
|
+
# fine-grained negative codes to just remove foreground color or just remove bold. Our API
|
27
|
+
# should provide these to allow these kind of fine-grained transitions to other color states.
|
28
|
+
NEGATIVE_PSEUDO_VALUES = [
|
29
|
+
:no_fg_color, :no_bg_color, :no_bold, :no_italic, :no_text_decoration,
|
30
|
+
:no_underline, :no_blink, :no_strikethrough
|
31
|
+
].freeze
|
32
|
+
|
33
|
+
VALID_VALUES = (::Term::ANSIColor.attributes + [:none] + CSS_TO_ANSI_VALUES.keys).freeze
|
34
|
+
VALID_VALUES_AND_PSEUDO_VALUES = (VALID_VALUES + NEGATIVE_PSEUDO_VALUES).freeze
|
35
|
+
|
36
|
+
# Retrieve color codes with the corresponding symbol. Can be basic colors like :red or
|
37
|
+
# "compound" colors specifying foreground and background colors like :red_on_blue.
|
38
|
+
#
|
39
|
+
# Any number of colors can be specified, either as multiple arguments or in an array.
|
40
|
+
def self.[](*colors)
|
41
|
+
colors.flatten!
|
42
|
+
valid_colors = []
|
43
|
+
colors.each do |color|
|
44
|
+
if is_valid_basic_value? color
|
45
|
+
valid_colors << (CSS_TO_ANSI_VALUES[color] || color)
|
46
|
+
elsif color_parts = is_compound_color?(color)
|
47
|
+
valid_colors += color_parts
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
unless valid_colors.empty?
|
52
|
+
valid_colors.uniq!
|
53
|
+
valid_colors.sort!
|
54
|
+
valid_colors.unshift(:reset) if valid_colors.delete(:reset)
|
55
|
+
|
56
|
+
valid_colors.inject('') { |str, color| str += ansi_color.send(color) }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class << self
|
61
|
+
alias_method :c, :[]
|
62
|
+
end
|
63
|
+
|
64
|
+
# Apply any valid colors to a string and auto-reset (if any colors applied). Does not apply
|
65
|
+
# colors to an empty string.
|
66
|
+
def self.color(string, *colors)
|
67
|
+
return string if string.nil? or string.empty?
|
68
|
+
colors.flatten!
|
69
|
+
colors.reject! { |col| col == :none || !VALID_VALUES.include?(col) }
|
70
|
+
if colors.any?
|
71
|
+
"#{colors.map { |col| c(col) }.join}#{string}#{c(:reset)}"
|
72
|
+
else
|
73
|
+
string
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Apply any valid colors to a string and auto-reset (if any colors applied). If there are any
|
78
|
+
# resets in the middle of the string, reapply the colors after them.
|
79
|
+
def self.force_color(string, *colors)
|
80
|
+
return string if string.nil? or string.empty?
|
81
|
+
colors.flatten!
|
82
|
+
colors.reject! { |col| col == :none || !VALID_VALUES.include?(col) }
|
83
|
+
if colors.any?
|
84
|
+
codes = colors.map { |col| c(col) }.join
|
85
|
+
"#{codes}#{string.gsub(c(:reset), c(:reset) + codes)}#{c(:reset)}"
|
86
|
+
else
|
87
|
+
string
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.valid?(color)
|
92
|
+
is_valid_basic_value?(color) || is_compound_color?(color)
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.is_basic_color?(color)
|
96
|
+
COLOR_VALUES.include?(color)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns an array of colors if the given symbol represents a compound color.
|
100
|
+
# Returns nil otherwise.
|
101
|
+
def self.is_compound_color?(color)
|
102
|
+
if color.to_s =~ /(\w+)_on_(\w+)/
|
103
|
+
colors = [$1.to_sym, "on_#{$2}".to_sym]
|
104
|
+
if colors.all? { |c| COLOR_VALUES.include? c }
|
105
|
+
colors
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Gives a pair of color codes for transitions into and out of a colored substring in the
|
111
|
+
# middle of a possibly differently colored line.
|
112
|
+
def self.line_substring_color_transitions(line_colors, substring_colors)
|
113
|
+
line_colors, substring_colors = Array(line_colors), Array(substring_colors)
|
114
|
+
|
115
|
+
implied_substring_colors = []
|
116
|
+
line_colors.each do |line_col|
|
117
|
+
cat = category(line_col)
|
118
|
+
replaced = substring_colors.any? { |substr_col| category(substr_col) == cat}
|
119
|
+
implied_substring_colors << line_col unless replaced
|
120
|
+
end
|
121
|
+
|
122
|
+
[
|
123
|
+
color_transition(line_colors, substring_colors, false),
|
124
|
+
color_transition(substring_colors + implied_substring_colors, line_colors)
|
125
|
+
]
|
126
|
+
end
|
127
|
+
|
128
|
+
# Produces a string of color codes to transition from one set of colors to another.
|
129
|
+
#
|
130
|
+
# If hard is true then all foregound and background colors are reset before adding the after
|
131
|
+
# colors. In other words, no colors are allowed to continue, even if not replaced.
|
132
|
+
#
|
133
|
+
# If hard is false then colors that are not explicitly replaced by new colors are not reset.
|
134
|
+
# This means that if there are foreground and background before colors and only a foreground
|
135
|
+
# after color then even though the foreground color is replaced by the new one the background
|
136
|
+
# color is allowed to continue and is not explicitly reset.
|
137
|
+
#
|
138
|
+
# Regardless of whether all colors are reset, output of unnecessary codes is avoided. This
|
139
|
+
# means, for example, that if any before colors are replaced by new colors of the same
|
140
|
+
# category (foreground, background, underline, etc.) then there will never be an explicit
|
141
|
+
# reset because that would be redundant and merely add more characters.
|
142
|
+
def self.color_transition(before_colors, after_colors, hard=true)
|
143
|
+
before_colors, after_colors = Array(before_colors), Array(after_colors)
|
144
|
+
|
145
|
+
before_categories, after_categories = categorize(before_colors), categorize(after_colors)
|
146
|
+
|
147
|
+
# Nothing to do if before and after colors are the same
|
148
|
+
return '' if before_categories == after_categories
|
149
|
+
|
150
|
+
transition = ''
|
151
|
+
should_reset = false
|
152
|
+
colors_to_apply = []
|
153
|
+
|
154
|
+
# Explicit reset is necessary if we want a hard transition and all colors in all
|
155
|
+
# categories are not replaced.
|
156
|
+
if hard
|
157
|
+
before_categories.each_pair do |cat, before_color|
|
158
|
+
next if negative?(before_color)
|
159
|
+
after_color = after_categories[cat]
|
160
|
+
if !after_color || negative?(after_color)
|
161
|
+
should_reset = true
|
162
|
+
break
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# If soft transition then the only time we need an explicit reset is when we have a color
|
168
|
+
# in a category that is explicitly turned off with a negative value. This also applies
|
169
|
+
# to hard transitions.
|
170
|
+
unless should_reset
|
171
|
+
after_categories.each_pair do |cat, after_color|
|
172
|
+
before_color = before_categories[cat]
|
173
|
+
if before_color && negative?(after_color) && !negative?(before_color)
|
174
|
+
should_reset = true
|
175
|
+
break
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
after_categories.each_pair do |cat, after_color|
|
181
|
+
before_color = before_categories[cat]
|
182
|
+
if !negative?(after_color)
|
183
|
+
transition << c(after_color) unless before_color == after_color && !should_reset
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# If we are resetting but using a soft transition then all colors execept negated ones
|
188
|
+
# need to be set again after the reset.
|
189
|
+
if should_reset && !hard
|
190
|
+
before_categories.values.each do |color|
|
191
|
+
unless negative?(color) || after_categories.values.include?(negate(color))
|
192
|
+
transition << c(color) unless after_categories.keys.include?(category(color))
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
transition.prepend(c(:reset)) if should_reset
|
198
|
+
transition
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.negative?(color)
|
202
|
+
NEGATIVE_PSEUDO_VALUES.include?(color)
|
203
|
+
end
|
204
|
+
|
205
|
+
def self.uncolor(string)
|
206
|
+
ansi_color.uncolor(string)
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
# Exists to support color_transition - it's a bit specialized. Takes an array of colors and
|
212
|
+
# puts them in a hash, category => value. The extra things that this does is
|
213
|
+
# 1. translates the no_text_decoration negative pseudo-value into all of the negative values
|
214
|
+
# in the text_decoration category
|
215
|
+
# 2. if there is any text_decoration category value then it adds negations for the other
|
216
|
+
# text_decoration values - this is because even though they don't replace each other in
|
217
|
+
# terminals (for example: adding blink doesn't turn off existing underline) but we want
|
218
|
+
# them to in order to match CSS-style behavior so we need to add this in
|
219
|
+
#
|
220
|
+
# TODO: refactor this text_decoration stuff and probably move it elsewhere so it makes more
|
221
|
+
# sense - it could be cleaner and this probably isn't the place for it
|
222
|
+
def self.categorize(colors)
|
223
|
+
categories = {}
|
224
|
+
|
225
|
+
if categories.delete(:no_text_decoration)
|
226
|
+
TEXT_DECORATION_VALUES.each do |td|
|
227
|
+
categories[td] = negate(td)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
colors.each { |color| categories[category(color)] = color }
|
232
|
+
|
233
|
+
TEXT_DECORATION_VALUES.each do |td|
|
234
|
+
if categories.values.include?(td)
|
235
|
+
(TEXT_DECORATION_VALUES - [td]).each do |other_td|
|
236
|
+
categories[other_td] = negate(other_td) unless categories.values.include?(other_td)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
categories
|
242
|
+
end
|
243
|
+
|
244
|
+
# Get the category of a color.
|
245
|
+
#
|
246
|
+
# Foreground colors are in the :fg_color category, background colors in :bg_color. Other style
|
247
|
+
# "colors" are in their own category (:bold, :underline, etc.). Negative pseudo-values are in
|
248
|
+
# the category they negate, so :no_fg_color is in :fg_color and :no_bold is in :bold.
|
249
|
+
def self.category(color)
|
250
|
+
return nil unless VALID_VALUES_AND_PSEUDO_VALUES.include?(color)
|
251
|
+
|
252
|
+
if FOREGROUND_COLOR_VALUES.include?(color) || color == :no_fg_color
|
253
|
+
:fg_color
|
254
|
+
elsif BACKGROUND_COLOR_VALUES.include?(color) || color == :no_bg_color
|
255
|
+
:bg_color
|
256
|
+
else
|
257
|
+
color.to_s.sub(/^no_/, '').to_sym
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# Get the negative pseudo-value for a color or style. Compound colors and already
|
262
|
+
# negative values cannot be negated.
|
263
|
+
def self.negate(color)
|
264
|
+
return nil unless is_valid_basic_value?(color)
|
265
|
+
|
266
|
+
if FOREGROUND_COLOR_VALUES.include?(color)
|
267
|
+
:no_fg_color
|
268
|
+
elsif BACKGROUND_COLOR_VALUES.include?(color)
|
269
|
+
:no_bg_color
|
270
|
+
else
|
271
|
+
color.to_s.prepend('no_').to_sym
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# Is this a valid non-compound value? Includes colors but also stuff like :bold and
|
276
|
+
# other non-color things.
|
277
|
+
def self.is_valid_basic_value?(color)
|
278
|
+
VALID_VALUES.include?(color)
|
279
|
+
end
|
280
|
+
|
281
|
+
def self.valid_value_or_pseudo_value?(value)
|
282
|
+
VALID_VALUES_AND_PSEUDO_VALUES.include?(value)
|
283
|
+
end
|
284
|
+
|
285
|
+
def self.ansi_color
|
286
|
+
::Term::ANSIColor
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|