arbo 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yaml +21 -0
  3. data/.github/workflows/daily.yaml +23 -0
  4. data/.gitignore +11 -0
  5. data/CHANGELOG.md +95 -0
  6. data/CONTRIBUTING.md +3 -0
  7. data/Gemfile +17 -0
  8. data/LICENSE +20 -0
  9. data/README.md +29 -0
  10. data/Rakefile +18 -0
  11. data/arbo.gemspec +25 -0
  12. data/docs/Gemfile +2 -0
  13. data/docs/_config.yml +7 -0
  14. data/docs/_includes/footer.html +8 -0
  15. data/docs/_includes/google-analytics.html +16 -0
  16. data/docs/_includes/head.html +7 -0
  17. data/docs/_includes/toc.html +12 -0
  18. data/docs/_includes/top-menu.html +8 -0
  19. data/docs/_layouts/default.html +21 -0
  20. data/docs/index.md +106 -0
  21. data/docs/stylesheets/main.css +1152 -0
  22. data/lib/arbo/component.rb +22 -0
  23. data/lib/arbo/context.rb +118 -0
  24. data/lib/arbo/element/builder_methods.rb +83 -0
  25. data/lib/arbo/element/proxy.rb +28 -0
  26. data/lib/arbo/element.rb +225 -0
  27. data/lib/arbo/element_collection.rb +31 -0
  28. data/lib/arbo/html/attributes.rb +41 -0
  29. data/lib/arbo/html/class_list.rb +28 -0
  30. data/lib/arbo/html/document.rb +31 -0
  31. data/lib/arbo/html/html5_elements.rb +47 -0
  32. data/lib/arbo/html/tag.rb +220 -0
  33. data/lib/arbo/html/text_node.rb +43 -0
  34. data/lib/arbo/rails/forms.rb +101 -0
  35. data/lib/arbo/rails/rendering.rb +17 -0
  36. data/lib/arbo/rails/template_handler.rb +35 -0
  37. data/lib/arbo/rails.rb +5 -0
  38. data/lib/arbo/version.rb +3 -0
  39. data/lib/arbo.rb +21 -0
  40. data/spec/arbo/integration/html_spec.rb +307 -0
  41. data/spec/arbo/unit/component_spec.rb +54 -0
  42. data/spec/arbo/unit/context_spec.rb +35 -0
  43. data/spec/arbo/unit/element_finder_methods_spec.rb +116 -0
  44. data/spec/arbo/unit/element_spec.rb +272 -0
  45. data/spec/arbo/unit/html/class_list_spec.rb +16 -0
  46. data/spec/arbo/unit/html/tag_attributes_spec.rb +104 -0
  47. data/spec/arbo/unit/html/tag_spec.rb +124 -0
  48. data/spec/arbo/unit/html/text_node_spec.rb +5 -0
  49. data/spec/rails/integration/forms_spec.rb +117 -0
  50. data/spec/rails/integration/rendering_spec.rb +98 -0
  51. data/spec/rails/rails_spec_helper.rb +46 -0
  52. data/spec/rails/stub_app/config/database.yml +3 -0
  53. data/spec/rails/stub_app/config/routes.rb +3 -0
  54. data/spec/rails/stub_app/db/schema.rb +3 -0
  55. data/spec/rails/stub_app/log/.gitignore +1 -0
  56. data/spec/rails/stub_app/public/favicon.ico +0 -0
  57. data/spec/rails/support/mock_person.rb +15 -0
  58. data/spec/rails/templates/arbo/_partial.arb +1 -0
  59. data/spec/rails/templates/arbo/_partial_with_assignment.arb +1 -0
  60. data/spec/rails/templates/arbo/empty.arb +0 -0
  61. data/spec/rails/templates/arbo/page_with_arb_partial_and_assignment.arb +3 -0
  62. data/spec/rails/templates/arbo/page_with_assignment.arb +1 -0
  63. data/spec/rails/templates/arbo/page_with_erb_partial.arb +3 -0
  64. data/spec/rails/templates/arbo/page_with_helpers.arb +7 -0
  65. data/spec/rails/templates/arbo/page_with_partial.arb +3 -0
  66. data/spec/rails/templates/arbo/simple_page.arb +8 -0
  67. data/spec/rails/templates/erb/_partial.erb +1 -0
  68. data/spec/spec_helper.rb +7 -0
  69. data/spec/support/bundle.rb +4 -0
  70. metadata +169 -0
@@ -0,0 +1,22 @@
1
+ module Arbo
2
+ class Component < Arbo::HTML::Div
3
+
4
+ # By default components render a div
5
+ def tag_name
6
+ 'div'
7
+ end
8
+
9
+ def initialize(*)
10
+ super
11
+ add_class default_class_name
12
+ end
13
+
14
+ protected
15
+
16
+ # By default, add a css class named after the ruby class
17
+ def default_class_name
18
+ self.class.name.demodulize.underscore
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,118 @@
1
+ require 'arbo/element'
2
+ require 'ruby2_keywords'
3
+
4
+ module Arbo
5
+
6
+ # The Arbo::Context class is the frontend for using Arbo.
7
+ #
8
+ # The simplest example possible:
9
+ #
10
+ # html = Arbo::Context.new do
11
+ # h1 "Hello World"
12
+ # end
13
+ #
14
+ # html.to_s #=> "<h1>Hello World</h1>"
15
+ #
16
+ # The contents of the block are instance eval'd within the Context
17
+ # object. This means that you lose context to the outside world from
18
+ # within the block. To pass local variables into the Context, use the
19
+ # assigns param.
20
+ #
21
+ # html = Arbo::Context.new({one: 1}) do
22
+ # h1 "Your number #{one}"
23
+ # end
24
+ #
25
+ # html.to_s #=> "Your number 1"
26
+ #
27
+ class Context < Element
28
+
29
+ # Initialize a new Arbo::Context
30
+ #
31
+ # @param [Hash] assigns A hash of objecs that you would like to be
32
+ # availble as local variables within the Context
33
+ #
34
+ # @param [Object] helpers An object that has methods on it which will become
35
+ # instance methods within the context.
36
+ #
37
+ # @yield [] The block that will get instance eval'd in the context
38
+ def initialize(assigns = {}, helpers = nil, &block)
39
+ assigns = assigns || {}
40
+ @_assigns = assigns.symbolize_keys
41
+
42
+ @_helpers = helpers
43
+ @_current_arbo_element_buffer = [self]
44
+
45
+ super(self)
46
+ instance_eval(&block) if block_given?
47
+ end
48
+
49
+ def arbo_context
50
+ self
51
+ end
52
+
53
+ def assigns
54
+ @_assigns
55
+ end
56
+
57
+ def helpers
58
+ @_helpers
59
+ end
60
+
61
+ def indent_level
62
+ # A context does not increment the indent_level
63
+ super - 1
64
+ end
65
+
66
+ def bytesize
67
+ cached_html.bytesize
68
+ end
69
+ alias :length :bytesize
70
+
71
+ def respond_to_missing?(method, include_all)
72
+ super || cached_html.respond_to?(method, include_all)
73
+ end
74
+
75
+ # Webservers treat Arbo::Context as a string. We override
76
+ # method_missing to delegate to the string representation
77
+ # of the html.
78
+ ruby2_keywords def method_missing(method, *args, &block)
79
+ if cached_html.respond_to? method
80
+ cached_html.send method, *args, &block
81
+ else
82
+ super
83
+ end
84
+ end
85
+
86
+ def current_arbo_element
87
+ @_current_arbo_element_buffer.last
88
+ end
89
+
90
+ def with_current_arbo_element(tag)
91
+ raise ArgumentError, "Can't be in the context of nil. #{@_current_arbo_element_buffer.inspect}" unless tag
92
+ @_current_arbo_element_buffer.push tag
93
+ yield
94
+ @_current_arbo_element_buffer.pop
95
+ end
96
+ alias_method :within, :with_current_arbo_element
97
+
98
+ def output_buffer
99
+ @output_buffer ||= ActiveSupport::SafeBuffer.new
100
+ end
101
+
102
+ private
103
+
104
+
105
+ # Caches the rendered HTML so that we don't re-render just to
106
+ # get the content lenght or to delegate a method to the HTML
107
+ def cached_html
108
+ if defined?(@cached_html)
109
+ @cached_html
110
+ else
111
+ html = render_in(self)
112
+ @cached_html = html if html.length > 0
113
+ html
114
+ end
115
+ end
116
+
117
+ end
118
+ end
@@ -0,0 +1,83 @@
1
+ module Arbo
2
+ class Element
3
+
4
+ module BuilderMethods
5
+
6
+ def self.included(klass)
7
+ klass.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ def builder_method(method_name)
13
+ BuilderMethods.class_eval <<-EOF, __FILE__, __LINE__
14
+ def #{method_name}(*args, &block)
15
+ insert_tag ::#{self.name}, *args, &block
16
+ end
17
+ EOF
18
+ end
19
+
20
+ end
21
+
22
+ def build_tag(klass, *args, &block)
23
+ tag = klass.new(arbo_context)
24
+ tag.parent = current_arbo_element
25
+
26
+ with_current_arbo_element tag do
27
+ if block_given? && block.arity > 0
28
+ tag.build(*args, &block)
29
+ else
30
+ tag.build(*args)
31
+ append_return_block(yield) if block_given?
32
+ end
33
+ end
34
+
35
+ tag
36
+ end
37
+
38
+ def insert_tag(klass, *args, &block)
39
+ tag = build_tag(klass, *args, &block)
40
+ current_arbo_element.add_child(tag)
41
+ tag
42
+ end
43
+
44
+ def current_arbo_element
45
+ arbo_context.current_arbo_element
46
+ end
47
+
48
+ def with_current_arbo_element(tag, &block)
49
+ arbo_context.with_current_arbo_element(tag, &block)
50
+ end
51
+ alias_method :within, :with_current_arbo_element
52
+
53
+ private
54
+
55
+ # Appends the value to the current DOM element if there are no
56
+ # existing DOM Children and it responds to #to_s
57
+ def append_return_block(tag)
58
+ return nil if current_arbo_element.children?
59
+
60
+ if appendable_tag?(tag)
61
+ current_arbo_element << Arbo::HTML::TextNode.from_string(tag.to_s)
62
+ end
63
+ end
64
+
65
+ # Returns true if the object should be converted into a text node
66
+ # and appended into the DOM.
67
+ def appendable_tag?(tag)
68
+ is_appendable = !tag.is_a?(Arbo::Element) && tag.respond_to?(:to_s)
69
+
70
+ # In ruby 1.9, Arraay.new.to_s prints out an empty array ("[]"). In
71
+ # Arbo, we append the return value of blocks to the output, which
72
+ # can cause empty arrays to show up within the output. To get
73
+ # around this, we check if the object responds to #empty?
74
+ if tag.respond_to?(:empty?) && tag.empty?
75
+ is_appendable = false
76
+ end
77
+
78
+ is_appendable
79
+ end
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,28 @@
1
+ module Arbo
2
+ class Element
3
+ class Proxy < BasicObject
4
+ undef_method :==
5
+ undef_method :equal?
6
+
7
+ def initialize(element)
8
+ @element = element
9
+ end
10
+
11
+ def respond_to?(method, include_all = false)
12
+ if method.to_s == 'to_ary'
13
+ false
14
+ else
15
+ super || @element.respond_to?(method, include_all)
16
+ end
17
+ end
18
+
19
+ def method_missing(method, *args, &block)
20
+ if method.to_s == 'to_ary'
21
+ super
22
+ else
23
+ @element.__send__ method, *args, &block
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,225 @@
1
+ require 'arbo/element/builder_methods'
2
+ require 'arbo/element/proxy'
3
+ require 'arbo/element_collection'
4
+ require 'ruby2_keywords'
5
+
6
+ module Arbo
7
+
8
+ class Element
9
+ include BuilderMethods
10
+
11
+ attr_reader :parent
12
+ attr_reader :children, :arbo_context
13
+
14
+ def initialize(arbo_context = Arbo::Context.new)
15
+ @arbo_context = arbo_context
16
+ @children = ElementCollection.new
17
+ @parent = nil
18
+ end
19
+
20
+ def assigns
21
+ arbo_context.assigns
22
+ end
23
+
24
+ def helpers
25
+ arbo_context.helpers
26
+ end
27
+
28
+ def tag_name
29
+ @tag_name ||= self.class.name.demodulize.downcase
30
+ end
31
+
32
+ def build(*args, &block)
33
+ # Render the block passing ourselves in
34
+ append_return_block(block.call(self)) if block
35
+ end
36
+
37
+ def add_child(child)
38
+ return unless child
39
+
40
+ if child.is_a?(Array)
41
+ child.each{|item| add_child(item) }
42
+ return @children
43
+ end
44
+
45
+ # If its not an element, wrap it in a TextNode
46
+ unless child.is_a?(Element)
47
+ child = Arbo::HTML::TextNode.from_string(child)
48
+ end
49
+
50
+ if child.respond_to?(:parent)
51
+ # Remove the child
52
+ child.parent.remove_child(child) if child.parent && child.parent != self
53
+ # Set ourselves as the parent
54
+ child.parent = self
55
+ end
56
+
57
+ @children << child
58
+ end
59
+
60
+ def remove_child(child)
61
+ child.parent = nil if child.respond_to?(:parent=)
62
+ @children.delete(child)
63
+ end
64
+
65
+ def <<(child)
66
+ add_child(child)
67
+ end
68
+
69
+ def children?
70
+ @children.any?
71
+ end
72
+
73
+ def parent=(parent)
74
+ @parent = parent
75
+ end
76
+
77
+ def parent?
78
+ !@parent.nil?
79
+ end
80
+
81
+ def ancestors
82
+ if parent?
83
+ [parent] + parent.ancestors
84
+ else
85
+ []
86
+ end
87
+ end
88
+
89
+ # TODO: Shouldn't grab whole tree
90
+ def find_first_ancestor(type)
91
+ ancestors.find{|a| a.is_a?(type) }
92
+ end
93
+
94
+ def content=(contents)
95
+ clear_children!
96
+ add_child(contents)
97
+ end
98
+
99
+ def get_elements_by_tag_name(tag_name)
100
+ elements = ElementCollection.new
101
+ children.each do |child|
102
+ elements << child if child.tag_name == tag_name
103
+ elements.concat(child.get_elements_by_tag_name(tag_name))
104
+ end
105
+ elements
106
+ end
107
+ alias_method :find_by_tag, :get_elements_by_tag_name
108
+
109
+ def get_elements_by_class_name(class_name)
110
+ elements = ElementCollection.new
111
+ children.each do |child|
112
+ elements << child if child.class_list.include?(class_name)
113
+ elements.concat(child.get_elements_by_class_name(class_name))
114
+ end
115
+ elements
116
+ end
117
+ alias_method :find_by_class, :get_elements_by_class_name
118
+
119
+ def content
120
+ children.to_s
121
+ end
122
+
123
+ def html_safe
124
+ to_s
125
+ end
126
+
127
+ def indent_level
128
+ parent? ? parent.indent_level + 1 : 0
129
+ end
130
+
131
+ def each(&block)
132
+ [to_s].each(&block)
133
+ end
134
+
135
+ def inspect
136
+ content
137
+ end
138
+
139
+ def to_str
140
+ ActiveSupport::Deprecation.warn("don't rely on implicit conversion of Element to String")
141
+ content
142
+ end
143
+
144
+ def to_s
145
+ ActiveSupport::Deprecation.warn("#render_in should be defined for rendering #{method_owner(:to_s)} instead of #to_s")
146
+ content
147
+ end
148
+
149
+ # Rendering strategy that visits all elements and appends output to a buffer.
150
+ def render_in(context = arbo_context)
151
+ children.collect do |element|
152
+ element.render_in_or_to_s(context)
153
+ end.join('')
154
+ end
155
+
156
+ # Use render_in to render element unless closer ancestor overrides :to_s only.
157
+ def render_in_or_to_s(context)
158
+ if method_distance(:render_in) <= method_distance(:to_s)
159
+ render_in(context)
160
+ else
161
+ ActiveSupport::Deprecation.warn("#render_in should be defined for rendering #{method_owner(:to_s)} instead of #to_s")
162
+ to_s.tap { |s| context.output_buffer << s }
163
+ end
164
+ end
165
+
166
+ def +(element)
167
+ case element
168
+ when Element, ElementCollection
169
+ else
170
+ element = Arbo::HTML::TextNode.from_string(element)
171
+ end
172
+ to_ary + element
173
+ end
174
+
175
+ def to_ary
176
+ ElementCollection.new [Proxy.new(self)]
177
+ end
178
+ alias_method :to_a, :to_ary
179
+
180
+ private
181
+
182
+ # Resets the Elements children
183
+ def clear_children!
184
+ @children.clear
185
+ end
186
+
187
+ # Implements the method lookup chain. When you call a method that
188
+ # doesn't exist, we:
189
+ #
190
+ # 1. Try to call the method on the current DOM context
191
+ # 2. Return an assigned variable of the same name
192
+ # 3. Call the method on the helper object
193
+ # 4. Call super
194
+ #
195
+ ruby2_keywords def method_missing(name, *args, &block)
196
+ if current_arbo_element.respond_to?(name)
197
+ current_arbo_element.send name, *args, &block
198
+ elsif assigns && assigns.has_key?(name)
199
+ assigns[name]
200
+ elsif helpers.respond_to?(name)
201
+ helper_capture(name, *args, &block)
202
+ else
203
+ super
204
+ end
205
+ end
206
+
207
+ # The helper might have a block that builds Arbo elements
208
+ # which will be rendered (#to_s) inside ActionView::Base#capture.
209
+ # We do not want such elements added to self, so we push a dummy
210
+ # current_arbo_element.
211
+ ruby2_keywords def helper_capture(name, *args, &block)
212
+ s = ""
213
+ within(Element.new) { s = helpers.send(name, *args, &block) }
214
+ s.is_a?(Element) ? s.to_s : s
215
+ end
216
+
217
+ def method_distance(name)
218
+ self.class.ancestors.index method_owner(name)
219
+ end
220
+
221
+ def method_owner(name)
222
+ self.class.instance_method(name).owner
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,31 @@
1
+ module Arbo
2
+
3
+ # Stores a collection of Element objects
4
+ class ElementCollection < Array
5
+
6
+ def +(other)
7
+ self.class.new(super)
8
+ end
9
+
10
+ def -(other)
11
+ self.class.new(super)
12
+ end
13
+
14
+ def &(other)
15
+ self.class.new(super)
16
+ end
17
+
18
+ def to_s
19
+ self.collect do |element|
20
+ element.to_s
21
+ end.join('').html_safe
22
+ end
23
+
24
+ def render_in(context)
25
+ self.collect do |element|
26
+ element.render_in(context)
27
+ end.join('').html_safe
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,41 @@
1
+ module Arbo
2
+ module HTML
3
+
4
+ class Attributes < Hash
5
+
6
+ def to_s
7
+ flatten_hash.map do |name, value|
8
+ next if value_empty?(value)
9
+ "#{html_escape(name)}=\"#{html_escape(value)}\""
10
+ end.compact.join ' '
11
+ end
12
+
13
+ def any?
14
+ super{ |k,v| !value_empty?(v) }
15
+ end
16
+
17
+ protected
18
+
19
+ def flatten_hash(hash = self, old_path = [], accumulator = {})
20
+ hash.each do |key, value|
21
+ path = old_path + [key]
22
+ if value.is_a? Hash
23
+ flatten_hash(value, path, accumulator)
24
+ else
25
+ accumulator[path.join('-')] = value
26
+ end
27
+ end
28
+ accumulator
29
+ end
30
+
31
+ def value_empty?(value)
32
+ value.respond_to?(:empty?) ? value.empty? : !value
33
+ end
34
+
35
+ def html_escape(s)
36
+ ERB::Util.html_escape(s)
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,28 @@
1
+ require 'set'
2
+
3
+ module Arbo
4
+ module HTML
5
+
6
+ # Holds a set of classes
7
+ class ClassList < Set
8
+
9
+ def self.build_from_string(class_names)
10
+ new.add(class_names)
11
+ end
12
+
13
+ def add(class_names)
14
+ class_names.to_s.split(" ").each do |class_name|
15
+ super(class_name)
16
+ end
17
+ self
18
+ end
19
+ alias :<< :add
20
+
21
+ def to_s
22
+ to_a.join(" ")
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ module Arbo
2
+ module HTML
3
+
4
+ class Document < Tag
5
+
6
+ def document
7
+ self
8
+ end
9
+
10
+ def tag_name
11
+ 'html'
12
+ end
13
+
14
+ def doctype
15
+ '<!DOCTYPE html>'.html_safe
16
+ end
17
+
18
+ def to_s
19
+ doctype + super
20
+ end
21
+
22
+ def render_in(context = arbo_context)
23
+ context.output_buffer << doctype
24
+ super
25
+ context.output_buffer
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,47 @@
1
+ module Arbo
2
+ module HTML
3
+
4
+ AUTO_BUILD_ELEMENTS = [ :a, :abbr, :address, :area, :article, :aside, :audio, :b, :base,
5
+ :bdo, :blockquote, :body, :br, :button, :canvas, :caption, :cite,
6
+ :code, :col, :colgroup, :command, :datalist, :dd, :del, :details,
7
+ :dfn, :div, :dl, :dt, :em, :embed, :fieldset, :figcaption, :figure,
8
+ :footer, :form, :h1, :h2, :h3, :h4, :h5, :h6, :head, :header, :hgroup,
9
+ :hr, :html, :i, :iframe, :img, :input, :ins, :keygen, :kbd, :label,
10
+ :legend, :li, :link, :map, :mark, :menu, :menuitem, :meta, :meter, :nav, :noscript,
11
+ :object, :ol, :optgroup, :option, :output, :param, :pre, :progress, :q,
12
+ :s, :samp, :script, :section, :select, :small, :source, :span,
13
+ :strong, :style, :sub, :summary, :sup, :svg, :table, :tbody, :td,
14
+ :textarea, :tfoot, :th, :thead, :time, :title, :tr, :track, :ul, :var, :video, :wbr ]
15
+
16
+ HTML5_ELEMENTS = [ :p ] + AUTO_BUILD_ELEMENTS
17
+
18
+ AUTO_BUILD_ELEMENTS.each do |name|
19
+ class_eval <<-EOF
20
+ class #{name.to_s.capitalize} < Tag
21
+ builder_method :#{name}
22
+ end
23
+ EOF
24
+ end
25
+
26
+ class P < Tag
27
+ builder_method :para
28
+ end
29
+
30
+ class Table < Tag
31
+ def initialize(*)
32
+ super
33
+ set_table_tag_defaults
34
+ end
35
+
36
+ protected
37
+
38
+ # Set some good defaults for tables
39
+ def set_table_tag_defaults
40
+ set_attribute :border, 0
41
+ set_attribute :cellspacing, 0
42
+ set_attribute :cellpadding, 0
43
+ end
44
+ end
45
+
46
+ end
47
+ end