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