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,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
|