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,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
@@ -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
@@ -0,0 +1,16 @@
1
+ module Styles
2
+ module Properties
3
+ class Color < Base
4
+ sub_engine :color
5
+
6
+ def valid_value?
7
+ return true if value == :none
8
+ colors[value]
9
+ end
10
+
11
+ def color_to_use
12
+ value == :none ? :no_fg_color : value
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module Styles
2
+ module Properties
3
+ class Display < Base
4
+ sub_engine :layout
5
+
6
+ SHOW_VALUES = [:block, :inline, :inline_block, true].freeze
7
+ HIDE_VALUES = [:none, false].freeze
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ module Styles
2
+ module Properties
3
+ class FontWeight < Base
4
+ sub_engine :color
5
+
6
+ VALUES = [:normal, :bold].freeze
7
+
8
+ def color_to_use
9
+ value == :normal ? :no_bold : value
10
+ end
11
+ end
12
+ end
13
+ end