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