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
@@ -0,0 +1,83 @@
|
|
1
|
+
module Styles
|
2
|
+
module Properties
|
3
|
+
class Margin < Base
|
4
|
+
sub_engine :layout
|
5
|
+
other_names :margin_left, :margin_right, :margin_top, :margin_bottom
|
6
|
+
|
7
|
+
attr_reader :top, :right, :bottom, :left
|
8
|
+
|
9
|
+
def initialize(*args)
|
10
|
+
if args.size == 1 && args.first.is_a?(Array)
|
11
|
+
@name = :margin
|
12
|
+
@sub_properties = args.first
|
13
|
+
else
|
14
|
+
if (val = args[2]).is_a?(String)
|
15
|
+
@name = :margin
|
16
|
+
@sub_properties = []
|
17
|
+
side_values = args[2].split.map do |side_value|
|
18
|
+
side_value == 'auto' ? :auto : side_value.to_i
|
19
|
+
end
|
20
|
+
%w[top right bottom left].each_with_index do |side, idx|
|
21
|
+
side_value = side_values[idx]
|
22
|
+
if side_value
|
23
|
+
@sub_properties << self.class.new(args[0], "margin_#{side}".to_sym, side_value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
else
|
27
|
+
super
|
28
|
+
@sub_properties = nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
compute_all_margins
|
32
|
+
end
|
33
|
+
|
34
|
+
def all_margins
|
35
|
+
[top, right, bottom, left]
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
attr_reader :sub_properties
|
41
|
+
|
42
|
+
# Hash of margins that have been defined (nil if not defined) for internal tracking and
|
43
|
+
# combination of sub-properties
|
44
|
+
attr_accessor :defined_margins
|
45
|
+
|
46
|
+
# If the value is an integer, returns that. For any other value, including the valid
|
47
|
+
# <tt>:none</tt> value, <tt>0</tt> is returned.
|
48
|
+
def normalized_value
|
49
|
+
(value.is_a?(Integer) || value == :auto) ? value : 0
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def compute_all_margins
|
55
|
+
@defined_margins = { top: nil, right: nil, bottom: nil, left: nil }
|
56
|
+
|
57
|
+
if @sub_properties
|
58
|
+
@sub_properties.each do |sub_prop|
|
59
|
+
if sub_prop.name == :margin
|
60
|
+
@defined_margins.merge!(sub_prop.defined_margins.dup.delete_if { |k,v| v.nil? })
|
61
|
+
else
|
62
|
+
@defined_margins.merge!(
|
63
|
+
{ sub_prop.name.to_s.sub(/^margin_/, '').to_sym => sub_prop.normalized_value }
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
else
|
68
|
+
if name == :margin
|
69
|
+
[:top, :right, :bottom, :left].each { |side| @defined_margins[side] = normalized_value }
|
70
|
+
elsif name.to_s =~ /margin_(\w+)/
|
71
|
+
@defined_margins[$1.to_sym] = normalized_value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
@defined_margins.each_pair { |side, val| set_margin(side, val) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def set_margin(which, val)
|
79
|
+
instance_variable_set("@#{which}".to_sym, (val.nil? ? 0 : val))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Styles
|
2
|
+
module Properties
|
3
|
+
class MatchBackgroundColor < Base
|
4
|
+
sub_engine :color
|
5
|
+
|
6
|
+
COLOR_PROPERTY_TYPE = :match
|
7
|
+
|
8
|
+
def valid_value?
|
9
|
+
[value].flatten.all? { |color| colors.is_basic_color?(color) || color == :none }
|
10
|
+
end
|
11
|
+
|
12
|
+
def color_to_use
|
13
|
+
if value.is_a? Array
|
14
|
+
value.map { |color| convert_to_background_color color }
|
15
|
+
elsif value.is_a? Symbol
|
16
|
+
convert_to_background_color value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def convert_to_background_color(color)
|
23
|
+
return :no_bg_color if color == :none
|
24
|
+
color =~ /^on_/ ? color : "on_#{color}".to_sym
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Styles
|
2
|
+
module Properties
|
3
|
+
class MatchColor < Base
|
4
|
+
sub_engine :color
|
5
|
+
|
6
|
+
COLOR_PROPERTY_TYPE = :match
|
7
|
+
|
8
|
+
def valid_value?
|
9
|
+
[value].flatten.all? { |color| colors.is_basic_color?(color) || color == :none }
|
10
|
+
end
|
11
|
+
|
12
|
+
def color_to_use
|
13
|
+
if value.is_a? Array
|
14
|
+
value.map { |color| color == :none ? :no_fg_color : color }
|
15
|
+
elsif value.is_a? Symbol
|
16
|
+
value == :none ? :no_fg_color : value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Styles
|
2
|
+
module Properties
|
3
|
+
class MatchFontWeight < Base
|
4
|
+
sub_engine :color
|
5
|
+
|
6
|
+
COLOR_PROPERTY_TYPE = :match
|
7
|
+
|
8
|
+
VALUES = [:normal, :bold].freeze
|
9
|
+
|
10
|
+
def valid_value?
|
11
|
+
[value].flatten.all? { |sub_value| VALUES.include?(sub_value) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def color_to_use
|
15
|
+
if value.is_a? Array
|
16
|
+
value.map { |sub_value| sub_value == :normal ? :no_bold : sub_value }
|
17
|
+
elsif value.is_a? Symbol
|
18
|
+
value == :normal ? :no_bold : value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Styles
|
2
|
+
module Properties
|
3
|
+
class MatchTextDecoration < Base
|
4
|
+
sub_engine :color
|
5
|
+
|
6
|
+
COLOR_PROPERTY_TYPE = :match
|
7
|
+
|
8
|
+
# CSS value is line-through and not strikethrough, but include strikethrough as well
|
9
|
+
VALUES = [:none, :underline, :line_through, :strikethrough, :blink].freeze
|
10
|
+
|
11
|
+
def valid_value?
|
12
|
+
[value].flatten.all? { |decoration| VALUES.include?(decoration) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def color_to_use
|
16
|
+
if value.is_a? Array
|
17
|
+
value.map { |decoration| normalize_value decoration }
|
18
|
+
elsif value.is_a? Symbol
|
19
|
+
normalize_value value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def normalize_value(value)
|
26
|
+
if value == :line_through
|
27
|
+
:strikethrough
|
28
|
+
elsif value == :none
|
29
|
+
:no_text_decoration
|
30
|
+
else
|
31
|
+
value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Styles
|
2
|
+
module Properties
|
3
|
+
class Padding < Base
|
4
|
+
sub_engine :layout
|
5
|
+
other_names :padding_left, :padding_right, :padding_top, :padding_bottom
|
6
|
+
|
7
|
+
attr_reader :top, :right, :bottom, :left
|
8
|
+
|
9
|
+
def initialize(*args)
|
10
|
+
if args.size == 1 && args.first.is_a?(Array)
|
11
|
+
@name = :padding
|
12
|
+
@sub_properties = args.first
|
13
|
+
else
|
14
|
+
if (val = args[2]).is_a?(String)
|
15
|
+
@name = :padding
|
16
|
+
@sub_properties = []
|
17
|
+
side_values = args[2].split.map(&:to_i)
|
18
|
+
%w[top right bottom left].each_with_index do |side, idx|
|
19
|
+
side_value = side_values[idx]
|
20
|
+
if side_value
|
21
|
+
@sub_properties << self.class.new(args[0], "padding_#{side}".to_sym, side_value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
else
|
25
|
+
super
|
26
|
+
@sub_properties = nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
compute_all_padding
|
30
|
+
end
|
31
|
+
|
32
|
+
def all_padding
|
33
|
+
[top, right, bottom, left]
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
attr_reader :sub_properties
|
39
|
+
|
40
|
+
# Hash of padding values that have been defined (nil if not defined) for internal
|
41
|
+
# tracking and combination of sub-properties
|
42
|
+
attr_accessor :defined_padding
|
43
|
+
|
44
|
+
# If the value is an integer, returns that. For any other value, including the valid
|
45
|
+
# <tt>:none</tt> value, <tt>0</tt> is returned.
|
46
|
+
def normalized_value
|
47
|
+
value.is_a?(Integer) ? value : 0
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def compute_all_padding
|
53
|
+
@defined_padding = { top: nil, right: nil, bottom: nil, left: nil }
|
54
|
+
|
55
|
+
if @sub_properties
|
56
|
+
@sub_properties.each do |sub_prop|
|
57
|
+
if sub_prop.name == :padding
|
58
|
+
@defined_padding.merge!(sub_prop.defined_padding.dup.delete_if { |k,v| v.nil? })
|
59
|
+
else
|
60
|
+
@defined_padding.merge!(
|
61
|
+
{ sub_prop.name.to_s.sub(/^padding_/, '').to_sym => sub_prop.normalized_value }
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
else
|
66
|
+
if name == :padding
|
67
|
+
[:top, :right, :bottom, :left].each { |side| @defined_padding[side] = normalized_value }
|
68
|
+
elsif name.to_s =~ /padding_(\w+)/
|
69
|
+
@defined_padding[$1.to_sym] = normalized_value
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
@defined_padding.each_pair { |side, val| set_padding(side, val) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def set_padding(which, val)
|
77
|
+
instance_variable_set("@#{which}".to_sym, (val.nil? ? 0 : val))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Styles
|
2
|
+
module Properties
|
3
|
+
class TextDecoration < Base
|
4
|
+
sub_engine :color
|
5
|
+
|
6
|
+
# CSS value is line-through and not strikethrough, but include strikethrough as well
|
7
|
+
VALUES = [:none, :underline, :line_through, :strikethrough, :blink].freeze
|
8
|
+
|
9
|
+
def color_to_use
|
10
|
+
if value == :line_through
|
11
|
+
:strikethrough
|
12
|
+
elsif value == :none
|
13
|
+
:no_text_decoration
|
14
|
+
else
|
15
|
+
value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/styles/rule.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'term/ansicolor'
|
2
|
+
|
3
|
+
module Styles
|
4
|
+
class Rule
|
5
|
+
|
6
|
+
# Special Selectors are Symbols that describe common special cases.
|
7
|
+
SPECIAL_SELECTORS = {
|
8
|
+
blank: /^\s*$/,
|
9
|
+
empty: /^$/,
|
10
|
+
any: /^/,
|
11
|
+
all: /^/ # Synonym of :any
|
12
|
+
}
|
13
|
+
|
14
|
+
attr_accessor :selector, :properties, :unrecognized_properties
|
15
|
+
|
16
|
+
def initialize(selector, properties_hash)
|
17
|
+
@selector = selector
|
18
|
+
@unrecognized_properties = {}
|
19
|
+
|
20
|
+
@properties = properties_hash.keys.map do |name|
|
21
|
+
property_class = ::Styles::Properties.find_class_by_property_name(name)
|
22
|
+
if property_class
|
23
|
+
property_class.new(selector, name, properties_hash[name])
|
24
|
+
else
|
25
|
+
unrecognized_properties[name] = properties_hash[name]
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end.compact
|
29
|
+
end
|
30
|
+
|
31
|
+
# Indicates whether this Rule is applicable to the given line, according to the selector. You
|
32
|
+
# could say the selector 'matches' the line for an applicable rule.
|
33
|
+
#
|
34
|
+
# A String selector matches if it appears anywhere in the line.
|
35
|
+
# A Regexp selector matches if it matches the line.
|
36
|
+
#
|
37
|
+
# ANSI color codes are ignored when matching to avoid false positives or negatives.
|
38
|
+
def applicable?(line)
|
39
|
+
uncolored_line = color.uncolor(line.chomp)
|
40
|
+
case selector
|
41
|
+
when String
|
42
|
+
uncolored_line.include?(selector)
|
43
|
+
when Regexp
|
44
|
+
selector.match(uncolored_line)
|
45
|
+
when Symbol
|
46
|
+
SPECIAL_SELECTORS[selector].match(uncolored_line)
|
47
|
+
else
|
48
|
+
false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Apply the rule to the given line
|
53
|
+
#
|
54
|
+
# If the line is nil then it has been hidden before this rule has been applied. Just return
|
55
|
+
# nil and leave it as hidden.
|
56
|
+
def apply(line)
|
57
|
+
return nil if line.nil?
|
58
|
+
properties.inject(line.dup) {|result, property| property.apply(result) }
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def color
|
64
|
+
::Term::ANSIColor
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Styles
|
2
|
+
class Stylesheet
|
3
|
+
|
4
|
+
attr_accessor :file_path, :rules
|
5
|
+
attr_reader :last_updated
|
6
|
+
|
7
|
+
# For creating a one off, temporary Stylesheet without an associated file. Mostly useful
|
8
|
+
# for testing.
|
9
|
+
def self.from_string(string)
|
10
|
+
stylesheet = new
|
11
|
+
stylesheet.rules = eval_rules string
|
12
|
+
stylesheet
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(stylesheet_file_path=nil)
|
16
|
+
@file_path = stylesheet_file_path
|
17
|
+
@rules = []
|
18
|
+
@last_updated = nil
|
19
|
+
load_rules_from_file if @file_path
|
20
|
+
end
|
21
|
+
|
22
|
+
def unrecognized_property_names
|
23
|
+
rules.map { |rule| rule.unrecognized_properties.keys }.flatten
|
24
|
+
end
|
25
|
+
|
26
|
+
def reload
|
27
|
+
load_rules_from_file
|
28
|
+
end
|
29
|
+
|
30
|
+
def outdated?
|
31
|
+
!last_updated || file_mtime > last_updated
|
32
|
+
end
|
33
|
+
|
34
|
+
def reload_if_outdated
|
35
|
+
reload if outdated?
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_writer :last_updated
|
41
|
+
|
42
|
+
def file_mtime
|
43
|
+
File.mtime file_path
|
44
|
+
end
|
45
|
+
|
46
|
+
def load_rules_from_file
|
47
|
+
begin
|
48
|
+
self.rules = self.class.eval_rules(IO.read(file_path), file_path)
|
49
|
+
rescue Errno::ENOENT => e
|
50
|
+
msg = "Stylesheet '#{File.basename file_path}' does not exist in #{File.dirname file_path}"
|
51
|
+
raise ::Styles::StylesheetLoadError, msg
|
52
|
+
end
|
53
|
+
self.last_updated = Time.now
|
54
|
+
end
|
55
|
+
|
56
|
+
# Evaluates rules specified in the DSL format and returns an array of Rule objects.
|
57
|
+
#
|
58
|
+
# This evaluates the stylesheet text in a throwaway execution context. A reference to an array
|
59
|
+
# to contain the parsed rules is place in the global variable <tt>$current_stylesheet_rules</tt>
|
60
|
+
# while this is happening. This is for DSL support and allows the use of the <tt>-</tt> method
|
61
|
+
# on core types to add rules to Stylesheets. See core_ext.rb for more details.
|
62
|
+
def self.eval_rules(string, file_path=nil)
|
63
|
+
$current_stylesheet_rules = []
|
64
|
+
context = EvalContext.new
|
65
|
+
|
66
|
+
begin
|
67
|
+
context.instance_eval(string)
|
68
|
+
rescue SyntaxError => se
|
69
|
+
msg = "Could not parse stylesheet: #{file_path || '(no file)'}\n#{se.message.sub(/^\(eval\):/, 'line ')}"
|
70
|
+
raise ::Styles::StylesheetLoadError, msg
|
71
|
+
end
|
72
|
+
|
73
|
+
# After eval the global variable will contain an array of selector and
|
74
|
+
# properties pairs. See core_ext.rb for how these are collected.
|
75
|
+
rules = $current_stylesheet_rules.map { |selector_and_properties| Rule.new *selector_and_properties }
|
76
|
+
|
77
|
+
$current_stylesheet_rules = nil
|
78
|
+
rules
|
79
|
+
end
|
80
|
+
|
81
|
+
# Instances serve as throwaway execution contexts for user-produced stylesheet text. This is
|
82
|
+
# useful so that methods that only serve to support the DSL do not pollute the resulting
|
83
|
+
# Stylesheet object. Also, method definitions or overrides in the stylesheet will not affect
|
84
|
+
# the Stylesheet (probably). Since we are executing Ruby code there is no guarantee that
|
85
|
+
# that something won't get messed up, but this makes inadvertently doing so much less likely.
|
86
|
+
class EvalContext
|
87
|
+
|
88
|
+
# Supports the DSL for stylesheets, making them more attractive and CSS-y by allowing
|
89
|
+
# property values to be specified like this
|
90
|
+
#
|
91
|
+
# color: red
|
92
|
+
#
|
93
|
+
# instead of like this
|
94
|
+
#
|
95
|
+
# color: :red
|
96
|
+
#
|
97
|
+
def method_missing(name)
|
98
|
+
name
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|