styles 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +725 -0
  5. data/Rakefile +9 -0
  6. data/bin/styles +11 -0
  7. data/lib/styles.rb +18 -0
  8. data/lib/styles/application.rb +190 -0
  9. data/lib/styles/colors.rb +289 -0
  10. data/lib/styles/core_ext.rb +12 -0
  11. data/lib/styles/engine.rb +73 -0
  12. data/lib/styles/line.rb +55 -0
  13. data/lib/styles/properties.rb +34 -0
  14. data/lib/styles/properties/background_color.rb +15 -0
  15. data/lib/styles/properties/base.rb +68 -0
  16. data/lib/styles/properties/border.rb +147 -0
  17. data/lib/styles/properties/color.rb +16 -0
  18. data/lib/styles/properties/display.rb +10 -0
  19. data/lib/styles/properties/font_weight.rb +13 -0
  20. data/lib/styles/properties/function.rb +7 -0
  21. data/lib/styles/properties/margin.rb +83 -0
  22. data/lib/styles/properties/match_background_color.rb +28 -0
  23. data/lib/styles/properties/match_color.rb +21 -0
  24. data/lib/styles/properties/match_font_weight.rb +23 -0
  25. data/lib/styles/properties/match_text_decoration.rb +36 -0
  26. data/lib/styles/properties/padding.rb +81 -0
  27. data/lib/styles/properties/text_align.rb +10 -0
  28. data/lib/styles/properties/text_decoration.rb +20 -0
  29. data/lib/styles/properties/width.rb +11 -0
  30. data/lib/styles/rule.rb +67 -0
  31. data/lib/styles/stylesheet.rb +103 -0
  32. data/lib/styles/sub_engines.rb +4 -0
  33. data/lib/styles/sub_engines/base.rb +16 -0
  34. data/lib/styles/sub_engines/color.rb +115 -0
  35. data/lib/styles/sub_engines/layout.rb +158 -0
  36. data/lib/styles/sub_engines/pre_processor.rb +19 -0
  37. data/lib/styles/version.rb +3 -0
  38. data/styles.gemspec +26 -0
  39. data/test/application_test.rb +92 -0
  40. data/test/colors_test.rb +162 -0
  41. data/test/engine_test.rb +59 -0
  42. data/test/integration_test.rb +136 -0
  43. data/test/line_test.rb +24 -0
  44. data/test/properties/background_color_test.rb +36 -0
  45. data/test/properties/base_test.rb +11 -0
  46. data/test/properties/border_test.rb +154 -0
  47. data/test/properties/color_test.rb +28 -0
  48. data/test/properties/display_test.rb +26 -0
  49. data/test/properties/font_weight_test.rb +24 -0
  50. data/test/properties/function_test.rb +28 -0
  51. data/test/properties/margin_test.rb +98 -0
  52. data/test/properties/match_background_color_test.rb +71 -0
  53. data/test/properties/match_color_test.rb +79 -0
  54. data/test/properties/match_font_weight_test.rb +34 -0
  55. data/test/properties/match_text_decoration_test.rb +38 -0
  56. data/test/properties/padding_test.rb +87 -0
  57. data/test/properties/text_align_test.rb +107 -0
  58. data/test/properties/text_decoration_test.rb +25 -0
  59. data/test/properties/width_test.rb +41 -0
  60. data/test/rule_test.rb +39 -0
  61. data/test/stylesheet_test.rb +245 -0
  62. data/test/sub_engines/color_test.rb +144 -0
  63. data/test/sub_engines/layout_test.rb +110 -0
  64. data/test/test_helper.rb +5 -0
  65. metadata +184 -0
@@ -0,0 +1,7 @@
1
+ module Styles
2
+ module Properties
3
+ class Function < Base
4
+ sub_engine :pre_processor
5
+ end
6
+ end
7
+ end
@@ -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,10 @@
1
+ module Styles
2
+ module Properties
3
+ class TextAlign < Base
4
+ sub_engine :layout
5
+
6
+ # Not included: justify and inherit
7
+ VALUES = [:left, :right, :center]
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,11 @@
1
+ module Styles
2
+ module Properties
3
+ class Width < Base
4
+ sub_engine :layout
5
+
6
+ def width
7
+ value.to_i >= 0 ? value : 0
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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