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
@@ -0,0 +1,12 @@
|
|
1
|
+
|
2
|
+
[String, Regexp, Symbol].each do |klass|
|
3
|
+
klass.class_eval do
|
4
|
+
|
5
|
+
# Associates the receiver object with a hash of properties and adds the
|
6
|
+
# receiver and properties to the array of rules being parsed, in the global
|
7
|
+
# variable <tt>$current_stylesheet_rules</tt>.
|
8
|
+
def -(properties_hash)
|
9
|
+
$current_stylesheet_rules << [self, properties_hash]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'term/ansicolor'
|
2
|
+
|
3
|
+
module Styles
|
4
|
+
|
5
|
+
# Takes one or more Stylesheets and applies the rules from them to lines of text.
|
6
|
+
class Engine
|
7
|
+
attr_reader :stylesheets
|
8
|
+
|
9
|
+
def initialize(*stylesheets)
|
10
|
+
@stylesheets = [stylesheets].flatten
|
11
|
+
end
|
12
|
+
|
13
|
+
# Process a line according to the rules that comprise all of the Stylesheets.
|
14
|
+
#
|
15
|
+
# For all the rules that are applicable to this line, find the last defined of each type of
|
16
|
+
# property and apply it.
|
17
|
+
#
|
18
|
+
# Returns nil if the line is hidden or otherwise should not be displayed.
|
19
|
+
def process(line)
|
20
|
+
applicable_rules = rules.find_all { |rule| rule.applicable?(line) }
|
21
|
+
|
22
|
+
simple_properties = {}
|
23
|
+
multiple_names_properties = {}
|
24
|
+
applicable_rules.each do |rule|
|
25
|
+
rule.properties.each do |property|
|
26
|
+
if property.class.multiple_names?
|
27
|
+
(multiple_names_properties[property.class.to_sym] ||= []) << property
|
28
|
+
else
|
29
|
+
simple_properties[property.class.to_sym] = property
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
properties = simple_properties.values
|
35
|
+
|
36
|
+
multiple_names_properties.keys.each do |basic_name|
|
37
|
+
props = multiple_names_properties[basic_name]
|
38
|
+
prop_class = props.first.class
|
39
|
+
properties << prop_class.new(props)
|
40
|
+
end
|
41
|
+
|
42
|
+
line_obj = ::Styles::Line.new(line, properties)
|
43
|
+
|
44
|
+
sub_engines.each do |sub_engine|
|
45
|
+
line_obj = sub_engine.process(line_obj)
|
46
|
+
return nil if line_obj.text.nil?
|
47
|
+
end
|
48
|
+
|
49
|
+
line_obj.to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Returns instances SubEngines in the order that they should be used in processing.
|
55
|
+
def sub_engines
|
56
|
+
@sub_engines ||= begin
|
57
|
+
[
|
58
|
+
::Styles::SubEngines::PreProcessor.new,
|
59
|
+
::Styles::SubEngines::Color.new,
|
60
|
+
::Styles::SubEngines::Layout.new
|
61
|
+
]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def rules
|
66
|
+
stylesheets.map(&:rules).flatten
|
67
|
+
end
|
68
|
+
|
69
|
+
def color
|
70
|
+
::Term::ANSIColor
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/styles/line.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module Styles
|
2
|
+
class Line
|
3
|
+
attr_accessor :applicable_properties, :top, :right, :bottom, :left
|
4
|
+
attr_reader :original
|
5
|
+
attr_writer :text
|
6
|
+
|
7
|
+
def initialize(line, properties=nil)
|
8
|
+
@original = line
|
9
|
+
@original.freeze
|
10
|
+
@applicable_properties = properties
|
11
|
+
|
12
|
+
@text = @original.dup
|
13
|
+
@top = @right = @bottom = @left = ''
|
14
|
+
end
|
15
|
+
|
16
|
+
# The current content of the line, possibly already altered by processing. By content we mean
|
17
|
+
# the line without additional layout characters (margin, padding, border) being applied around
|
18
|
+
# it. This does, however, include color information that may have been added.
|
19
|
+
def text
|
20
|
+
@text.nil? ? nil : @text.dup
|
21
|
+
end
|
22
|
+
|
23
|
+
# The line's main text content surrounded by any extra layout characters that may have been
|
24
|
+
# applied (margin, padding, border). When processing is complete this represents the complete
|
25
|
+
# result to be written to the output stream.
|
26
|
+
def to_s
|
27
|
+
return '' unless text
|
28
|
+
bottom_and_newline = "\n#{bottom}" if bottom && !bottom.empty?
|
29
|
+
"#{top}#{left}#{text}#{right}#{bottom_and_newline}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def total_width
|
33
|
+
colors.uncolor("#{left}#{text}#{right}").size
|
34
|
+
end
|
35
|
+
|
36
|
+
def content_width
|
37
|
+
colors.uncolor(text).size
|
38
|
+
end
|
39
|
+
|
40
|
+
def prop(property_name)
|
41
|
+
prop_name = property_name.to_sym
|
42
|
+
applicable_properties.find { |prop| prop.class.to_sym == prop_name }
|
43
|
+
end
|
44
|
+
|
45
|
+
def prop?(property_name)
|
46
|
+
!!prop(property_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def colors
|
52
|
+
::Styles::Colors
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
%w[
|
2
|
+
base
|
3
|
+
display
|
4
|
+
color
|
5
|
+
background_color
|
6
|
+
font_weight
|
7
|
+
text_decoration
|
8
|
+
match_color
|
9
|
+
match_background_color
|
10
|
+
match_font_weight
|
11
|
+
match_text_decoration
|
12
|
+
text_align
|
13
|
+
padding
|
14
|
+
margin
|
15
|
+
border
|
16
|
+
width
|
17
|
+
function
|
18
|
+
].each { |property| require "styles/properties/#{property}" }
|
19
|
+
|
20
|
+
module Styles
|
21
|
+
module Properties
|
22
|
+
|
23
|
+
def self.all_property_classes
|
24
|
+
constants = ::Styles::Properties.constants - [:Base]
|
25
|
+
constants.map { |con| ::Styles::Properties.const_get(con) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.find_class_by_property_name(name)
|
29
|
+
name = name.to_sym
|
30
|
+
all_property_classes.find { |klass| klass.names.include? name }
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Styles
|
2
|
+
module Properties
|
3
|
+
class BackgroundColor < Base
|
4
|
+
sub_engine :color
|
5
|
+
|
6
|
+
VALUES = (Styles::Colors::COLOR_VALUES + [:none]).freeze
|
7
|
+
|
8
|
+
# Convert foreground colors to background
|
9
|
+
def color_to_use
|
10
|
+
return :no_bg_color if value == :none
|
11
|
+
value =~ /^on_/ ? value : "on_#{value}".to_sym
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'term/ansicolor'
|
2
|
+
|
3
|
+
module Styles
|
4
|
+
module Properties
|
5
|
+
class Base
|
6
|
+
attr_accessor :selector, :name, :value
|
7
|
+
|
8
|
+
def self.sub_engines
|
9
|
+
@sub_engines ||= []
|
10
|
+
end
|
11
|
+
|
12
|
+
# Macro to specify a sub-engine that this property is processed by
|
13
|
+
def self.sub_engine(name)
|
14
|
+
sub_engine_class = ::Styles::SubEngines.const_get(camelize(name.to_s))
|
15
|
+
sub_engines << sub_engine_class
|
16
|
+
|
17
|
+
begin
|
18
|
+
include sub_engine_class::PropertyMixin
|
19
|
+
rescue NameError
|
20
|
+
# do nothing if PropertyMixin does not exist for SubEngine
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# The name of this property, for use in stylesheets, as a Symbol
|
25
|
+
def self.to_sym
|
26
|
+
underscore(name).split('/').last.to_sym
|
27
|
+
end
|
28
|
+
|
29
|
+
# Macro to specify other names that a property class uses, besides the main +to_sym+ version
|
30
|
+
def self.other_names(*names)
|
31
|
+
@other_names = names
|
32
|
+
@names = self.names
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.names
|
36
|
+
@names ||= [to_sym, instance_variable_get('@other_names')].flatten.compact
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.multiple_names?
|
40
|
+
names.size > 1
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(selector, name, value)
|
44
|
+
@selector, @name, @value = selector, name, value
|
45
|
+
end
|
46
|
+
|
47
|
+
def colors
|
48
|
+
::Styles::Colors
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def self.underscore(word)
|
54
|
+
word = word.dup
|
55
|
+
word.gsub!(/::/, '/')
|
56
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
57
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
58
|
+
word.tr!("-", "_")
|
59
|
+
word.downcase!
|
60
|
+
word
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.camelize(word)
|
64
|
+
word.capitalize.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module Styles
|
2
|
+
module Properties
|
3
|
+
class Border < Base
|
4
|
+
# :dashed the same as :dotted
|
5
|
+
STYLES = [:solid, :dashed, :dotted, :double]
|
6
|
+
|
7
|
+
SOLID_CHARS = { top: "\u2500", right: "\u2502", bottom: "\u2500", left: "\u2502",
|
8
|
+
top_left: "\u250C", top_right: "\u2510", bottom_left: "\u2514", bottom_right: "\u2518" }.freeze
|
9
|
+
SOLID_BOLD_CHARS = { top: "\u2501", right: "\u2503", bottom: "\u2501", left: "\u2503",
|
10
|
+
top_left: "\u250F", top_right: "\u2513", bottom_left: "\u2517", bottom_right: "\u251B" }.freeze
|
11
|
+
DOUBLE_CHARS = { top: "\u2550", right: "\u2551", bottom: "\u2550", left: "\u2551",
|
12
|
+
top_left: "\u2554", top_right: "\u2557", bottom_left: "\u255A", bottom_right: "\u255D" }.freeze
|
13
|
+
|
14
|
+
# dotted doesn't have its own corners, reuse solid
|
15
|
+
DOTTED_CHARS = { top: "\u2504", right: "\u2506", bottom: "\u2504", left: "\u2506",
|
16
|
+
top_left: "\u250F", top_right: "\u2513", bottom_left: "\u2517", bottom_right: "\u251B" }.freeze
|
17
|
+
DASHED_CHARS = DOTTED_CHARS
|
18
|
+
|
19
|
+
STYLE_CHARS = {
|
20
|
+
solid: SOLID_CHARS, dotted: DOTTED_CHARS, dashed: DASHED_CHARS, double: DOUBLE_CHARS
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
sub_engine :layout
|
24
|
+
other_names :border_left, :border_right, :border_top, :border_bottom
|
25
|
+
|
26
|
+
attr_reader :top, :right, :bottom, :left
|
27
|
+
|
28
|
+
def initialize(*args)
|
29
|
+
if args.size == 1 && args.first.is_a?(Array)
|
30
|
+
@sub_properties = args.first
|
31
|
+
else
|
32
|
+
super
|
33
|
+
@sub_properties = nil
|
34
|
+
end
|
35
|
+
compute_all_border_values
|
36
|
+
end
|
37
|
+
|
38
|
+
def all_border_values
|
39
|
+
[top, right, bottom, left]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Generate methods for returning the proper characters to make up this border
|
43
|
+
[
|
44
|
+
:top_char, :right_char, :bottom_char, :left_char,
|
45
|
+
:top_left_char, :top_right_char, :bottom_left_char, :bottom_right_char
|
46
|
+
].each do |method_name|
|
47
|
+
str = method_name.to_s
|
48
|
+
position = str.sub(/_char$/, '')
|
49
|
+
|
50
|
+
define_method(method_name) do |*args|
|
51
|
+
raise ArgumentError, "Wrong number of arguments to #{method_name}: #{args.size}" if args.size > 1
|
52
|
+
|
53
|
+
return '' if position.split('_').any? { |side| send(side) == :none }
|
54
|
+
|
55
|
+
side_style = send(str.split('_').first)
|
56
|
+
return '' if side_style == :none
|
57
|
+
times = args.size == 1 ? args.first.to_i : 1
|
58
|
+
# TODO: check color validity
|
59
|
+
style, color = side_style
|
60
|
+
|
61
|
+
begin_color, end_color = if color != :default
|
62
|
+
[colors[color], colors[:reset]]
|
63
|
+
else
|
64
|
+
['', '']
|
65
|
+
end
|
66
|
+
|
67
|
+
"#{begin_color}#{STYLE_CHARS[style][position.to_sym] * times}#{end_color}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Draws the top line portion of a border with the specified inner width
|
72
|
+
def top_line_chars(width)
|
73
|
+
return '' if top == :none
|
74
|
+
tl = top_left_char.empty? ? (left == :none ? '' : ' ') : top_left_char
|
75
|
+
tr = top_right_char.empty? ? (right == :none ? '' : ' ') : top_right_char
|
76
|
+
"#{tl}#{top_char(width)}#{tr}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Draws the bottom line portion of a border with the specified inner width
|
80
|
+
def bottom_line_chars(width)
|
81
|
+
return '' if bottom == :none
|
82
|
+
bl = bottom_left_char.empty? ? (left == :none ? '' : ' ') : bottom_left_char
|
83
|
+
br = bottom_right_char.empty? ? (right == :none ? '' : ' ') : bottom_right_char
|
84
|
+
"#{bl}#{bottom_char(width)}#{br}"
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def compute_all_border_values
|
90
|
+
if @sub_properties
|
91
|
+
set_all_border_values(:none)
|
92
|
+
|
93
|
+
@sub_properties.each do |sub_prop|
|
94
|
+
if sub_prop.name == :border
|
95
|
+
set_all_border_values(parse_value(sub_prop.value))
|
96
|
+
else
|
97
|
+
set_border_value($1, parse_value(value))
|
98
|
+
set_border_value(sub_prop.name.to_s.sub(/^border_/, ''), parse_value(sub_prop.value))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
else
|
102
|
+
if name == :border
|
103
|
+
set_all_border_values(parse_value(value))
|
104
|
+
elsif name.to_s =~ /border_(\w+)/
|
105
|
+
set_all_border_values(:none)
|
106
|
+
set_border_value($1, parse_value(value))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Converts a "raw" value into an array of values specifying a border. Returns an array
|
112
|
+
# of the parsed sub-values, the style first and color second,
|
113
|
+
#
|
114
|
+
# Values can be in the following forms.
|
115
|
+
#
|
116
|
+
# Only a style, with default color assumed
|
117
|
+
# 'solid'
|
118
|
+
# or
|
119
|
+
# :solid
|
120
|
+
#
|
121
|
+
# A style and a color
|
122
|
+
# 'dotted red'
|
123
|
+
def parse_value(val)
|
124
|
+
return val if val == :none
|
125
|
+
default = [:solid, :default]
|
126
|
+
if val.is_a?(Symbol) && STYLES.include?(val)
|
127
|
+
[val, :default]
|
128
|
+
elsif val.is_a?(String)
|
129
|
+
parts = val.split
|
130
|
+
style = (parts[0] && STYLES.include?(parts[0].to_sym)) ? parts[0].to_sym : :solid
|
131
|
+
color = parts[1] ? parts[1].to_sym : :default
|
132
|
+
[style, color]
|
133
|
+
else
|
134
|
+
default
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def set_all_border_values(val)
|
139
|
+
@top = @right = @bottom = @left = val
|
140
|
+
end
|
141
|
+
|
142
|
+
def set_border_value(which, val)
|
143
|
+
instance_variable_set("@#{which}".to_sym, val)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|