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.
- 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
@@ -0,0 +1,25 @@
|
|
1
|
+
module Arbre
|
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
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Arbre
|
2
|
+
module HTML
|
3
|
+
|
4
|
+
class Attributes < Hash
|
5
|
+
|
6
|
+
def to_s
|
7
|
+
self.collect do |name, value|
|
8
|
+
"#{html_escape(name)}=\"#{html_escape(value)}\""
|
9
|
+
end.join(" ")
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def html_escape(s)
|
15
|
+
ERB::Util.html_escape(s)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Arbre
|
4
|
+
module HTML
|
5
|
+
|
6
|
+
# Holds a set of classes
|
7
|
+
class ClassList < Set
|
8
|
+
|
9
|
+
def add(class_names)
|
10
|
+
class_names.to_s.split(" ").each do |class_name|
|
11
|
+
super(class_name)
|
12
|
+
end
|
13
|
+
self
|
14
|
+
end
|
15
|
+
alias :<< :add
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
to_a.join(" ")
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Arbre
|
2
|
+
module HTML
|
3
|
+
|
4
|
+
class Document < Tag
|
5
|
+
|
6
|
+
def build(*args)
|
7
|
+
super
|
8
|
+
build_head
|
9
|
+
build_body
|
10
|
+
end
|
11
|
+
|
12
|
+
def document
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def tag_name
|
17
|
+
'html'
|
18
|
+
end
|
19
|
+
|
20
|
+
def doctype
|
21
|
+
'<!DOCTYPE html>'.html_safe
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
doctype + super
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def build_head
|
31
|
+
@head = head do
|
32
|
+
meta :"http-equiv" => "Content-type", :content => "text/html; charset=utf-8"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_body
|
37
|
+
@body = body
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Arbre
|
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, :meta, :meter, :nav, :noscript,
|
11
|
+
:object, :ol, :optgroup, :option, :output, :pre, :progress, :q,
|
12
|
+
:s, :samp, :script, :section, :select, :small, :source, :span,
|
13
|
+
:strong, :style, :sub, :summary, :sup, :table, :tbody, :td,
|
14
|
+
:textarea, :tfoot, :th, :thead, :time, :title, :tr, :ul, :var, :video ]
|
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
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Arbre
|
4
|
+
module HTML
|
5
|
+
|
6
|
+
class Tag < Element
|
7
|
+
attr_reader :attributes
|
8
|
+
|
9
|
+
def initialize(*)
|
10
|
+
super
|
11
|
+
@attributes = Attributes.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def build(*args)
|
15
|
+
super
|
16
|
+
attributes = extract_arguments(args)
|
17
|
+
self.content = args.first if args.first
|
18
|
+
|
19
|
+
set_for_attribute(attributes.delete(:for))
|
20
|
+
|
21
|
+
attributes.each do |key, value|
|
22
|
+
set_attribute(key, value)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def extract_arguments(args)
|
27
|
+
if args.last.is_a?(Hash)
|
28
|
+
args.pop
|
29
|
+
else
|
30
|
+
{}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_attribute(name, value)
|
35
|
+
@attributes[name.to_sym] = value
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_attribute(name)
|
39
|
+
@attributes[name.to_sym]
|
40
|
+
end
|
41
|
+
alias :attr :get_attribute
|
42
|
+
|
43
|
+
def has_attribute?(name)
|
44
|
+
@attributes.has_key?(name.to_sym)
|
45
|
+
end
|
46
|
+
|
47
|
+
def remove_attribute(name)
|
48
|
+
@attributes.delete(name.to_sym)
|
49
|
+
end
|
50
|
+
|
51
|
+
def id
|
52
|
+
get_attribute(:id)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Generates and id for the object if it doesn't exist already
|
56
|
+
def id!
|
57
|
+
return id if id
|
58
|
+
self.id = object_id.to_s
|
59
|
+
id
|
60
|
+
end
|
61
|
+
|
62
|
+
def id=(id)
|
63
|
+
set_attribute(:id, id)
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_class(class_names)
|
67
|
+
class_list.add class_names
|
68
|
+
end
|
69
|
+
|
70
|
+
def remove_class(class_names)
|
71
|
+
class_list.delete(class_names)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns a string of classes
|
75
|
+
def class_names
|
76
|
+
class_list.to_s
|
77
|
+
end
|
78
|
+
|
79
|
+
def class_list
|
80
|
+
get_attribute(:class) || set_attribute(:class, ClassList.new)
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
indent(opening_tag, content, closing_tag).html_safe
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def opening_tag
|
90
|
+
"<#{tag_name}#{attributes_html}>"
|
91
|
+
end
|
92
|
+
|
93
|
+
def closing_tag
|
94
|
+
"</#{tag_name}>"
|
95
|
+
end
|
96
|
+
|
97
|
+
INDENT_SIZE = 2
|
98
|
+
|
99
|
+
def indent(open_tag, child_content, close_tag)
|
100
|
+
spaces = ' ' * indent_level * INDENT_SIZE
|
101
|
+
|
102
|
+
html = ""
|
103
|
+
|
104
|
+
if no_child? || child_is_text?
|
105
|
+
if self_closing_tag?
|
106
|
+
html << spaces << open_tag.sub( />$/, '/>' )
|
107
|
+
else
|
108
|
+
# one line
|
109
|
+
html << spaces << open_tag << child_content << close_tag
|
110
|
+
end
|
111
|
+
else
|
112
|
+
# multiple lines
|
113
|
+
html << spaces << open_tag << "\n"
|
114
|
+
html << child_content # the child takes care of its own spaces
|
115
|
+
html << spaces << close_tag
|
116
|
+
end
|
117
|
+
|
118
|
+
html << "\n"
|
119
|
+
|
120
|
+
html
|
121
|
+
end
|
122
|
+
|
123
|
+
def self_closing_tag?
|
124
|
+
%w|meta link|.include?(tag_name)
|
125
|
+
end
|
126
|
+
|
127
|
+
def no_child?
|
128
|
+
children.empty?
|
129
|
+
end
|
130
|
+
|
131
|
+
def child_is_text?
|
132
|
+
children.size == 1 && children.first.is_a?(TextNode)
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
def attributes_html
|
137
|
+
attributes.any? ? " " + attributes.to_s : nil
|
138
|
+
end
|
139
|
+
|
140
|
+
def set_for_attribute(record)
|
141
|
+
return unless record
|
142
|
+
# set_attribute :id, ActionController::RecordIdentifier.dom_id(record, default_id_for_prefix)
|
143
|
+
# add_class ActionController::RecordIdentifier.dom_class(record)
|
144
|
+
set_attribute :id, dom_id_for(record)
|
145
|
+
add_class dom_class_name_for(record)
|
146
|
+
end
|
147
|
+
|
148
|
+
def dom_class_name_for(record)
|
149
|
+
if record.class.respond_to?(:model_name)
|
150
|
+
record.class.model_name.singular
|
151
|
+
else
|
152
|
+
record.class.underscore.gsub("/", "_")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def dom_id_for(record)
|
157
|
+
id = if record.respond_to?(:to_key)
|
158
|
+
record.to_key
|
159
|
+
elsif record.respond_to?(:id)
|
160
|
+
record.id
|
161
|
+
else
|
162
|
+
record.object_id
|
163
|
+
end
|
164
|
+
|
165
|
+
[dom_class_name_for(record), id].join("_")
|
166
|
+
end
|
167
|
+
|
168
|
+
def default_id_for_prefix
|
169
|
+
nil
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Arbre
|
4
|
+
module HTML
|
5
|
+
|
6
|
+
class TextNode < Element
|
7
|
+
|
8
|
+
builder_method :text_node
|
9
|
+
|
10
|
+
# Builds a text node from a string
|
11
|
+
def self.from_string(string)
|
12
|
+
node = new
|
13
|
+
node.build(string)
|
14
|
+
node
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_child(*args)
|
18
|
+
raise "TextNodes do not have children"
|
19
|
+
end
|
20
|
+
|
21
|
+
def build(string)
|
22
|
+
@content = string
|
23
|
+
end
|
24
|
+
|
25
|
+
def tag_name
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
ERB::Util.html_escape(@content.to_s)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/arbre/rails.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
module SplitOpenAndCloseOn
|
2
|
+
|
3
|
+
def split_open_and_close_on(string, match)
|
4
|
+
return "" unless string && match
|
5
|
+
@opening_tag = string.split(Regexp.new("#{match}\\z")).first
|
6
|
+
@closing_tag = match
|
7
|
+
end
|
8
|
+
|
9
|
+
def opening_tag
|
10
|
+
@opening_tag || ""
|
11
|
+
end
|
12
|
+
|
13
|
+
def closing_tag
|
14
|
+
@closing_tag || ""
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class FormBuilderProxy < Arbre::Component
|
20
|
+
attr_reader :form_builder
|
21
|
+
|
22
|
+
# Since label and select are Arbre Elements already, we must
|
23
|
+
# override it here instead of letting method_missing
|
24
|
+
# deal with it
|
25
|
+
def label(*args)
|
26
|
+
proxy_call_to_form :label, *args
|
27
|
+
end
|
28
|
+
|
29
|
+
def select(*args)
|
30
|
+
proxy_call_to_form :select, *args
|
31
|
+
end
|
32
|
+
|
33
|
+
def respond_to?(name)
|
34
|
+
if form_builder && form_builder.respond_to?(name)
|
35
|
+
true
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def proxy_call_to_form(method, *args, &block)
|
44
|
+
text_node form_builder.send(method, *args, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
def method_missing(method, *args, &block)
|
48
|
+
if form_builder && form_builder.respond_to?(method)
|
49
|
+
proxy_call_to_form(method, *args, &block)
|
50
|
+
else
|
51
|
+
super
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
class FormForProxy < FormBuilderProxy
|
58
|
+
builder_method :form_for
|
59
|
+
include SplitOpenAndCloseOn
|
60
|
+
|
61
|
+
def build(resource, form_options = {}, &block)
|
62
|
+
form_string = helpers.form_for(resource, form_options) do |f|
|
63
|
+
@form_builder = f
|
64
|
+
end
|
65
|
+
|
66
|
+
split_open_and_close_on form_string, "</form>"
|
67
|
+
|
68
|
+
super(&block)
|
69
|
+
end
|
70
|
+
|
71
|
+
def fields_for(*args, &block)
|
72
|
+
insert_tag FieldsForProxy, form_builder, *args, &block
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
class FieldsForProxy < FormBuilderProxy
|
78
|
+
|
79
|
+
def build(form_builder, *args, &block)
|
80
|
+
form_builder.fields_for(*args) do |f|
|
81
|
+
@form_builder = f
|
82
|
+
end
|
83
|
+
|
84
|
+
super(&block)
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_s
|
88
|
+
children.to_s
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|