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
@@ -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
|
+
|