arbre 0.0.1 → 1.0.0.rc1
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.
- data/.DS_Store +0 -0
- data/.gitignore +3 -0
- data/Gemfile +10 -1
- data/README.rdoc +68 -2
- data/arbre.gemspec +2 -0
- data/lib/.DS_Store +0 -0
- data/lib/arbre.rb +18 -0
- data/lib/arbre/component.rb +22 -0
- data/lib/arbre/context.rb +81 -0
- data/lib/arbre/element.rb +182 -0
- data/lib/arbre/element/builder_methods.rb +92 -0
- data/lib/arbre/element_collection.rb +25 -0
- data/lib/arbre/html/attributes.rb +20 -0
- data/lib/arbre/html/class_list.rb +24 -0
- data/lib/arbre/html/document.rb +42 -0
- data/lib/arbre/html/html5_elements.rb +47 -0
- data/lib/arbre/html/tag.rb +175 -0
- data/lib/arbre/html/text_node.rb +35 -0
- data/lib/arbre/rails.rb +5 -0
- data/lib/arbre/rails/forms.rb +92 -0
- data/lib/arbre/rails/rendering.rb +17 -0
- data/lib/arbre/rails/template_handler.rb +15 -0
- data/lib/arbre/version.rb +1 -1
- data/spec/arbre/integration/html_spec.rb +241 -0
- data/spec/arbre/unit/component.rb +23 -0
- data/spec/arbre/unit/context_spec.rb +35 -0
- data/spec/arbre/unit/element_finder_methods_spec.rb +101 -0
- data/spec/arbre/unit/element_spec.rb +252 -0
- data/spec/arbre/unit/html/tag_attributes_spec.rb +60 -0
- data/spec/arbre/unit/html/tag_spec.rb +63 -0
- data/spec/arbre/unit/html/text_node_spec.rb +5 -0
- data/spec/rails/integration/forms_spec.rb +121 -0
- data/spec/rails/integration/rendering_spec.rb +73 -0
- data/spec/rails/rails_spec_helper.rb +45 -0
- data/spec/rails/stub_app/config/database.yml +3 -0
- data/spec/rails/stub_app/config/routes.rb +3 -0
- data/spec/rails/stub_app/db/schema.rb +3 -0
- data/spec/rails/stub_app/log/.gitignore +1 -0
- data/spec/rails/stub_app/public/favicon.ico +0 -0
- data/spec/rails/templates/arbre/_partial.arb +1 -0
- data/spec/rails/templates/arbre/empty.arb +0 -0
- data/spec/rails/templates/arbre/page_with_assignment.arb +1 -0
- data/spec/rails/templates/arbre/page_with_erb_partial.arb +3 -0
- data/spec/rails/templates/arbre/page_with_partial.arb +3 -0
- data/spec/rails/templates/arbre/simple_page.arb +8 -0
- data/spec/rails/templates/erb/_partial.erb +1 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/bundle.rb +4 -0
- metadata +101 -45
data/.DS_Store
ADDED
Binary file
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.rdoc
CHANGED
@@ -1,3 +1,69 @@
|
|
1
|
-
Arbre -
|
1
|
+
= Arbre - Ruby Object Oriented HTML Views
|
2
2
|
|
3
|
-
|
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>
|
data/arbre.gemspec
CHANGED
@@ -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
|
data/lib/.DS_Store
ADDED
Binary file
|
data/lib/arbre.rb
CHANGED
@@ -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
|