arbre 0.0.1 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.DS_Store +0 -0
  2. data/.gitignore +3 -0
  3. data/Gemfile +10 -1
  4. data/README.rdoc +68 -2
  5. data/arbre.gemspec +2 -0
  6. data/lib/.DS_Store +0 -0
  7. data/lib/arbre.rb +18 -0
  8. data/lib/arbre/component.rb +22 -0
  9. data/lib/arbre/context.rb +81 -0
  10. data/lib/arbre/element.rb +182 -0
  11. data/lib/arbre/element/builder_methods.rb +92 -0
  12. data/lib/arbre/element_collection.rb +25 -0
  13. data/lib/arbre/html/attributes.rb +20 -0
  14. data/lib/arbre/html/class_list.rb +24 -0
  15. data/lib/arbre/html/document.rb +42 -0
  16. data/lib/arbre/html/html5_elements.rb +47 -0
  17. data/lib/arbre/html/tag.rb +175 -0
  18. data/lib/arbre/html/text_node.rb +35 -0
  19. data/lib/arbre/rails.rb +5 -0
  20. data/lib/arbre/rails/forms.rb +92 -0
  21. data/lib/arbre/rails/rendering.rb +17 -0
  22. data/lib/arbre/rails/template_handler.rb +15 -0
  23. data/lib/arbre/version.rb +1 -1
  24. data/spec/arbre/integration/html_spec.rb +241 -0
  25. data/spec/arbre/unit/component.rb +23 -0
  26. data/spec/arbre/unit/context_spec.rb +35 -0
  27. data/spec/arbre/unit/element_finder_methods_spec.rb +101 -0
  28. data/spec/arbre/unit/element_spec.rb +252 -0
  29. data/spec/arbre/unit/html/tag_attributes_spec.rb +60 -0
  30. data/spec/arbre/unit/html/tag_spec.rb +63 -0
  31. data/spec/arbre/unit/html/text_node_spec.rb +5 -0
  32. data/spec/rails/integration/forms_spec.rb +121 -0
  33. data/spec/rails/integration/rendering_spec.rb +73 -0
  34. data/spec/rails/rails_spec_helper.rb +45 -0
  35. data/spec/rails/stub_app/config/database.yml +3 -0
  36. data/spec/rails/stub_app/config/routes.rb +3 -0
  37. data/spec/rails/stub_app/db/schema.rb +3 -0
  38. data/spec/rails/stub_app/log/.gitignore +1 -0
  39. data/spec/rails/stub_app/public/favicon.ico +0 -0
  40. data/spec/rails/templates/arbre/_partial.arb +1 -0
  41. data/spec/rails/templates/arbre/empty.arb +0 -0
  42. data/spec/rails/templates/arbre/page_with_assignment.arb +1 -0
  43. data/spec/rails/templates/arbre/page_with_erb_partial.arb +3 -0
  44. data/spec/rails/templates/arbre/page_with_partial.arb +3 -0
  45. data/spec/rails/templates/arbre/simple_page.arb +8 -0
  46. data/spec/rails/templates/erb/_partial.erb +1 -0
  47. data/spec/spec_helper.rb +7 -0
  48. data/spec/support/bundle.rb +4 -0
  49. metadata +101 -45
Binary file
data/.gitignore CHANGED
@@ -2,3 +2,6 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ benchmarks
6
+ /.rvmrc
7
+ /tags
data/Gemfile CHANGED
@@ -1,4 +1,13 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- # Specify your gem's dependencies in arbre.gemspec
4
3
  gemspec
4
+
5
+ group :test do
6
+ gem "rspec"
7
+ end
8
+
9
+ group :rails do
10
+ gem "rspec-rails"
11
+ gem 'combustion', '0.3.2'
12
+ gem "capybara"
13
+ end
@@ -1,3 +1,69 @@
1
- Arbre - Coming Soon. For now, checkout Active Admin:
1
+ = Arbre - Ruby Object Oriented HTML Views
2
2
 
3
- http://github.com/gregbell/active_admin
3
+ Arbre is the DOM implemented in Ruby. This project is primarily used as
4
+ the object oriented view layer in Active Admin.
5
+
6
+ == Simple Usage
7
+
8
+ A simple example of setting up an Arbre context and creating some html
9
+
10
+ html = Arbre::Context.new do
11
+ h2 "Why Arbre is awesome?"
12
+
13
+ ul do
14
+ li "The DOM is implemented in ruby"
15
+ li "You can create object oriented views"
16
+ li "Templates suck"
17
+ end
18
+ end
19
+
20
+ puts html.to_s #=> <h2>Why</h2><ul><li></li></ul>
21
+
22
+
23
+ == The DOM in Ruby
24
+
25
+ The purpose of Arbre is to leave the view as ruby objects as long
26
+ as possible. This allows OO Design to be used to implement the view layer.
27
+
28
+
29
+ html = Arbre::Context.new do
30
+ h2 "Why Arbre is awesome?"
31
+ end
32
+
33
+ html.children.size #=> 1
34
+ html.children.first #=> #<Arbre::HTML::H2>
35
+
36
+ == Components
37
+
38
+ The purpose of Arbre is to be able to create shareable and extendable HTML
39
+ components. To do this, you create a subclass of Arbre::Component.
40
+
41
+ For example:
42
+
43
+ class Panel < Arbre::Component
44
+ builder_method :panel
45
+
46
+ def build(title, attributes = {})
47
+ super(attributes)
48
+
49
+ h3(title, :class => "panel-title")
50
+ end
51
+ end
52
+
53
+ The builder_method defines the method that will be called to build this component
54
+ when using the DSL. The arguments passed into the builder_method will be passed
55
+ into the #build method for you.
56
+
57
+ You can now use this panel in any Arbre context:
58
+
59
+ html = Arbre::Context.new do
60
+ panel "Hello World", :id => "my-panel" do
61
+ span "Inside the panel"
62
+ end
63
+ end
64
+
65
+ html.to_s #=>
66
+ # <div class='panel' id="my-panel">
67
+ # <h3 class='panel-title'>Hello World</h3>
68
+ # <span>Inside the panel</span>
69
+ # </div>
@@ -16,4 +16,6 @@ Gem::Specification.new do |s|
16
16
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
18
  s.require_paths = ["lib"]
19
+
20
+ s.add_dependency("activesupport", ">= 3.0.0")
19
21
  end
Binary file
@@ -1,2 +1,20 @@
1
+ require 'active_support/core_ext/string/output_safety'
2
+ require 'active_support/hash_with_indifferent_access'
3
+ require 'active_support/inflector'
4
+
1
5
  module Arbre
2
6
  end
7
+
8
+ require 'arbre/element'
9
+ require 'arbre/context'
10
+ require 'arbre/html/attributes'
11
+ require 'arbre/html/class_list'
12
+ require 'arbre/html/tag'
13
+ require 'arbre/html/text_node'
14
+ require 'arbre/html/document'
15
+ require 'arbre/html/html5_elements'
16
+ require 'arbre/component'
17
+
18
+ if defined?(Rails)
19
+ require 'arbre/rails'
20
+ end
@@ -0,0 +1,22 @@
1
+ module Arbre
2
+ class Component < Arbre::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,81 @@
1
+ require 'arbre/element'
2
+
3
+ module Arbre
4
+ class Context < Element
5
+
6
+ def initialize(assigns = {}, helpers = nil, &block)
7
+ @_assigns = assigns || {}
8
+ @_assigns.symbolize_keys!
9
+ @_helpers = helpers
10
+ @_current_arbre_element_buffer = [self]
11
+
12
+ super(self)
13
+ instance_eval &block if block_given?
14
+ end
15
+
16
+ def arbre_context
17
+ self
18
+ end
19
+
20
+ def assigns
21
+ @_assigns
22
+ end
23
+
24
+ def helpers
25
+ @_helpers
26
+ end
27
+
28
+ def indent_level
29
+ # A context does not increment the indent_level
30
+ super - 1
31
+ end
32
+
33
+ def bytesize
34
+ cached_html.bytesize
35
+ end
36
+ alias :length :bytesize
37
+
38
+ def respond_to?(method)
39
+ super || cached_html.respond_to?(method)
40
+ end
41
+
42
+ # Webservers treat Arbre::Context as a string. We override
43
+ # method_missing to delegate to the string representation
44
+ # of the html.
45
+ def method_missing(method, *args, &block)
46
+ if cached_html.respond_to? method
47
+ cached_html.send method, *args, &block
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ def current_arbre_element
54
+ @_current_arbre_element_buffer.last
55
+ end
56
+
57
+ def with_current_arbre_element(tag)
58
+ raise ArgumentError, "Can't be in the context of nil. #{@_current_arbre_element_buffer.inspect}" unless tag
59
+ @_current_arbre_element_buffer.push tag
60
+ yield
61
+ @_current_arbre_element_buffer.pop
62
+ end
63
+ alias_method :within, :with_current_arbre_element
64
+
65
+ private
66
+
67
+
68
+ # Caches the rendered HTML so that we don't re-render just to
69
+ # get the content lenght or to delegate a method to the HTML
70
+ def cached_html
71
+ if defined?(@cached_html)
72
+ @cached_html
73
+ else
74
+ html = to_s
75
+ @cached_html = html if html.length > 0
76
+ html
77
+ end
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,182 @@
1
+ require 'arbre/element/builder_methods'
2
+ require 'arbre/element_collection'
3
+
4
+ module Arbre
5
+
6
+ class Element
7
+ include BuilderMethods
8
+
9
+ attr_accessor :parent
10
+ attr_reader :children, :arbre_context
11
+
12
+ def initialize(arbre_context = Arbre::Context.new)
13
+ @arbre_context = arbre_context
14
+ @children = ElementCollection.new
15
+ end
16
+
17
+ def assigns
18
+ arbre_context.assigns
19
+ end
20
+
21
+ def helpers
22
+ arbre_context.helpers
23
+ end
24
+
25
+ def tag_name
26
+ @tag_name ||= self.class.name.demodulize.downcase
27
+ end
28
+
29
+ def build(*args, &block)
30
+ # Render the block passing ourselves in
31
+ append_return_block(block.call(self)) if block
32
+ end
33
+
34
+ def add_child(child)
35
+ return unless child
36
+
37
+ if child.is_a?(Array)
38
+ child.each{|item| add_child(item) }
39
+ return @children
40
+ end
41
+
42
+ # If its not an element, wrap it in a TextNode
43
+ unless child.is_a?(Element)
44
+ child = Arbre::HTML::TextNode.from_string(child)
45
+ end
46
+
47
+ if child.respond_to?(:parent)
48
+ # Remove the child
49
+ child.parent.remove_child(child) if child.parent
50
+ # Set ourselves as the parent
51
+ child.parent = self
52
+ end
53
+
54
+ @children << child
55
+ end
56
+
57
+ def remove_child(child)
58
+ child.parent = nil if child.respond_to?(:parent=)
59
+ @children.delete(child)
60
+ end
61
+
62
+ def <<(child)
63
+ add_child(child)
64
+ end
65
+
66
+ def children?
67
+ @children.any?
68
+ end
69
+
70
+ def parent=(parent)
71
+ @parent = parent
72
+ end
73
+
74
+ def parent?
75
+ !@parent.nil?
76
+ end
77
+
78
+ def ancestors
79
+ if parent?
80
+ [parent] + parent.ancestors
81
+ else
82
+ []
83
+ end
84
+ end
85
+
86
+ # TODO: Shouldn't grab whole tree
87
+ def find_first_ancestor(type)
88
+ ancestors.find{|a| a.is_a?(type) }
89
+ end
90
+
91
+ def content=(contents)
92
+ clear_children!
93
+ add_child(contents)
94
+ end
95
+
96
+ def get_elements_by_tag_name(tag_name)
97
+ elements = ElementCollection.new
98
+ children.each do |child|
99
+ elements << child if child.tag_name == tag_name
100
+ elements.concat(child.get_elements_by_tag_name(tag_name))
101
+ end
102
+ elements
103
+ end
104
+ alias_method :find_by_tag, :get_elements_by_tag_name
105
+
106
+ def get_elements_by_class_name(class_name)
107
+ elements = ElementCollection.new
108
+ children.each do |child|
109
+ elements << child if child.class_list =~ /#{class_name}/
110
+ elements.concat(child.get_elements_by_tag_name(tag_name))
111
+ end
112
+ elements
113
+ end
114
+ alias_method :find_by_class, :get_elements_by_class_name
115
+
116
+ def content
117
+ children.to_s
118
+ end
119
+
120
+ def html_safe
121
+ to_s
122
+ end
123
+
124
+ def indent_level
125
+ parent? ? parent.indent_level + 1 : 0
126
+ end
127
+
128
+ def each(&block)
129
+ [to_s].each(&block)
130
+ end
131
+
132
+ def to_str
133
+ to_s
134
+ end
135
+
136
+ def to_s
137
+ content
138
+ end
139
+
140
+ def +(element)
141
+ case element
142
+ when Element, ElementCollection
143
+ else
144
+ element = Arbre::HTML::TextNode.from_string(element)
145
+ end
146
+ ElementCollection.new([self]) + element
147
+ end
148
+
149
+ def to_ary
150
+ ElementCollection.new [self]
151
+ end
152
+ alias_method :to_a, :to_ary
153
+
154
+ private
155
+
156
+ # Resets the Elements children
157
+ def clear_children!
158
+ @children.clear
159
+ end
160
+
161
+ # Implements the method lookup chain. When you call a method that
162
+ # doesn't exist, we:
163
+ #
164
+ # 1. Try to call the method on the current DOM context
165
+ # 2. Return an assigned variable of the same name
166
+ # 3. Call the method on the helper object
167
+ # 4. Call super
168
+ #
169
+ def method_missing(name, *args, &block)
170
+ if current_arbre_element.respond_to?(name)
171
+ current_arbre_element.send name, *args, &block
172
+ elsif assigns && assigns.has_key?(name)
173
+ assigns[name]
174
+ elsif helpers.respond_to?(name)
175
+ helpers.send(name, *args, &block)
176
+ else
177
+ super
178
+ end
179
+ end
180
+
181
+ end
182
+ end
@@ -0,0 +1,92 @@
1
+ module Arbre
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(arbre_context)
24
+ tag.parent = current_arbre_element
25
+
26
+ # If you passed in a block and want the object
27
+ if block_given? && block.arity > 0
28
+ # Set out context to the tag, and pass responsibility to the tag
29
+ with_current_arbre_element tag do
30
+ tag.build(*args, &block)
31
+ end
32
+ else
33
+ # Build the tag
34
+ tag.build(*args)
35
+
36
+ # Render the blocks contents
37
+ if block_given?
38
+ with_current_arbre_element tag do
39
+ append_return_block(yield)
40
+ end
41
+ end
42
+ end
43
+
44
+ tag
45
+ end
46
+
47
+ def insert_tag(klass, *args, &block)
48
+ tag = build_tag(klass, *args, &block)
49
+ current_arbre_element.add_child(tag)
50
+ tag
51
+ end
52
+
53
+ def current_arbre_element
54
+ arbre_context.current_arbre_element
55
+ end
56
+
57
+ def with_current_arbre_element(tag, &block)
58
+ arbre_context.with_current_arbre_element(tag, &block)
59
+ end
60
+ alias_method :within, :with_current_arbre_element
61
+
62
+ private
63
+
64
+ # Appends the value to the current DOM element if there are no
65
+ # existing DOM Children and it responds to #to_s
66
+ def append_return_block(tag)
67
+ return nil if current_arbre_element.children?
68
+
69
+ if appendable_tag?(tag)
70
+ current_arbre_element << Arbre::HTML::TextNode.from_string(tag.to_s)
71
+ end
72
+ end
73
+
74
+ # Returns true if the object should be converted into a text node
75
+ # and appended into the DOM.
76
+ def appendable_tag?(tag)
77
+ is_appendable = !tag.is_a?(Arbre::Element) && tag.respond_to?(:to_s)
78
+
79
+ # In ruby 1.9, Arraay.new.to_s prints out an empty array ("[]"). In
80
+ # Arbre, we append the return value of blocks to the output, which
81
+ # can cause empty arrays to show up within the output. To get
82
+ # around this, we check if the object responds to #empty?
83
+ if tag.respond_to?(:empty?) && tag.empty?
84
+ is_appendable = false
85
+ end
86
+
87
+ is_appendable
88
+ end
89
+ end
90
+
91
+ end
92
+ end