styles 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/.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
|