arbo 1.2.0

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