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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yaml +21 -0
- data/.github/workflows/daily.yaml +23 -0
- data/.gitignore +11 -0
- data/CHANGELOG.md +95 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +17 -0
- data/LICENSE +20 -0
- data/README.md +29 -0
- data/Rakefile +18 -0
- data/arbo.gemspec +25 -0
- data/docs/Gemfile +2 -0
- data/docs/_config.yml +7 -0
- data/docs/_includes/footer.html +8 -0
- data/docs/_includes/google-analytics.html +16 -0
- data/docs/_includes/head.html +7 -0
- data/docs/_includes/toc.html +12 -0
- data/docs/_includes/top-menu.html +8 -0
- data/docs/_layouts/default.html +21 -0
- data/docs/index.md +106 -0
- data/docs/stylesheets/main.css +1152 -0
- data/lib/arbo/component.rb +22 -0
- data/lib/arbo/context.rb +118 -0
- data/lib/arbo/element/builder_methods.rb +83 -0
- data/lib/arbo/element/proxy.rb +28 -0
- data/lib/arbo/element.rb +225 -0
- data/lib/arbo/element_collection.rb +31 -0
- data/lib/arbo/html/attributes.rb +41 -0
- data/lib/arbo/html/class_list.rb +28 -0
- data/lib/arbo/html/document.rb +31 -0
- data/lib/arbo/html/html5_elements.rb +47 -0
- data/lib/arbo/html/tag.rb +220 -0
- data/lib/arbo/html/text_node.rb +43 -0
- data/lib/arbo/rails/forms.rb +101 -0
- data/lib/arbo/rails/rendering.rb +17 -0
- data/lib/arbo/rails/template_handler.rb +35 -0
- data/lib/arbo/rails.rb +5 -0
- data/lib/arbo/version.rb +3 -0
- data/lib/arbo.rb +21 -0
- data/spec/arbo/integration/html_spec.rb +307 -0
- data/spec/arbo/unit/component_spec.rb +54 -0
- data/spec/arbo/unit/context_spec.rb +35 -0
- data/spec/arbo/unit/element_finder_methods_spec.rb +116 -0
- data/spec/arbo/unit/element_spec.rb +272 -0
- data/spec/arbo/unit/html/class_list_spec.rb +16 -0
- data/spec/arbo/unit/html/tag_attributes_spec.rb +104 -0
- data/spec/arbo/unit/html/tag_spec.rb +124 -0
- data/spec/arbo/unit/html/text_node_spec.rb +5 -0
- data/spec/rails/integration/forms_spec.rb +117 -0
- data/spec/rails/integration/rendering_spec.rb +98 -0
- data/spec/rails/rails_spec_helper.rb +46 -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/support/mock_person.rb +15 -0
- data/spec/rails/templates/arbo/_partial.arb +1 -0
- data/spec/rails/templates/arbo/_partial_with_assignment.arb +1 -0
- data/spec/rails/templates/arbo/empty.arb +0 -0
- data/spec/rails/templates/arbo/page_with_arb_partial_and_assignment.arb +3 -0
- data/spec/rails/templates/arbo/page_with_assignment.arb +1 -0
- data/spec/rails/templates/arbo/page_with_erb_partial.arb +3 -0
- data/spec/rails/templates/arbo/page_with_helpers.arb +7 -0
- data/spec/rails/templates/arbo/page_with_partial.arb +3 -0
- data/spec/rails/templates/arbo/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 +169 -0
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Arbo
|
4
|
+
module HTML
|
5
|
+
|
6
|
+
class Tag < Element
|
7
|
+
attr_reader :attributes
|
8
|
+
|
9
|
+
# See: http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
|
10
|
+
SELF_CLOSING_ELEMENTS = [ :area, :base, :br, :col, :embed, :hr, :img, :input, :keygen, :link,
|
11
|
+
:menuitem, :meta, :param, :source, :track, :wbr ]
|
12
|
+
|
13
|
+
def initialize(*)
|
14
|
+
super
|
15
|
+
@attributes = Attributes.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def build(*args)
|
19
|
+
super
|
20
|
+
attributes = extract_arguments(args)
|
21
|
+
self.content = args.first if args.first
|
22
|
+
|
23
|
+
for_value = attributes[:for]
|
24
|
+
unless for_value.is_a?(String) || for_value.is_a?(Symbol)
|
25
|
+
set_for_attribute(attributes.delete(:for))
|
26
|
+
end
|
27
|
+
|
28
|
+
attributes.each do |key, value|
|
29
|
+
set_attribute(key, value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def extract_arguments(args)
|
34
|
+
if args.last.is_a?(Hash)
|
35
|
+
args.pop
|
36
|
+
else
|
37
|
+
{}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_attribute(name, value)
|
42
|
+
@attributes[name.to_sym] = value
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_attribute(name)
|
46
|
+
@attributes[name.to_sym]
|
47
|
+
end
|
48
|
+
alias :attr :get_attribute
|
49
|
+
|
50
|
+
def has_attribute?(name)
|
51
|
+
@attributes.has_key?(name.to_sym)
|
52
|
+
end
|
53
|
+
|
54
|
+
def remove_attribute(name)
|
55
|
+
@attributes.delete(name.to_sym)
|
56
|
+
end
|
57
|
+
|
58
|
+
def id
|
59
|
+
get_attribute(:id)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Generates and id for the object if it doesn't exist already
|
63
|
+
def id!
|
64
|
+
return id if id
|
65
|
+
self.id = object_id.to_s
|
66
|
+
id
|
67
|
+
end
|
68
|
+
|
69
|
+
def id=(id)
|
70
|
+
set_attribute(:id, id)
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_class(class_names)
|
74
|
+
class_list.add class_names
|
75
|
+
end
|
76
|
+
|
77
|
+
def remove_class(class_names)
|
78
|
+
class_list.delete(class_names)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns a string of classes
|
82
|
+
def class_names
|
83
|
+
class_list.to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
def class_list
|
87
|
+
list = get_attribute(:class)
|
88
|
+
|
89
|
+
case list
|
90
|
+
when ClassList
|
91
|
+
list
|
92
|
+
when String
|
93
|
+
set_attribute(:class, ClassList.build_from_string(list))
|
94
|
+
else
|
95
|
+
set_attribute(:class, ClassList.new)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_s
|
100
|
+
indent(opening_tag, content, closing_tag).html_safe
|
101
|
+
end
|
102
|
+
|
103
|
+
def render_in(context = arbo_context)
|
104
|
+
indent_in_context(context)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def opening_tag
|
110
|
+
"<#{tag_name}#{attributes_html}>"
|
111
|
+
end
|
112
|
+
|
113
|
+
def closing_tag
|
114
|
+
"</#{tag_name}>"
|
115
|
+
end
|
116
|
+
|
117
|
+
INDENT_SIZE = 2
|
118
|
+
|
119
|
+
def indent(open_tag, child_content, close_tag)
|
120
|
+
spaces = ' ' * indent_level * INDENT_SIZE
|
121
|
+
|
122
|
+
html = ""
|
123
|
+
|
124
|
+
if no_child? || child_is_text?
|
125
|
+
if self_closing_tag?
|
126
|
+
html << spaces << open_tag.sub( />$/, '/>' )
|
127
|
+
else
|
128
|
+
# one line
|
129
|
+
html << spaces << open_tag << child_content << close_tag
|
130
|
+
end
|
131
|
+
else
|
132
|
+
# multiple lines
|
133
|
+
html << spaces << open_tag << "\n"
|
134
|
+
html << child_content # the child takes care of its own spaces
|
135
|
+
html << spaces << close_tag
|
136
|
+
end
|
137
|
+
|
138
|
+
html << "\n"
|
139
|
+
|
140
|
+
html
|
141
|
+
end
|
142
|
+
|
143
|
+
def indent_in_context(context)
|
144
|
+
spaces = ' ' * indent_level * INDENT_SIZE
|
145
|
+
|
146
|
+
pos = context.output_buffer.length
|
147
|
+
|
148
|
+
if no_child? || child_is_text?
|
149
|
+
if self_closing_tag?
|
150
|
+
context.output_buffer << spaces << opening_tag.sub( />$/, '/>' ).html_safe
|
151
|
+
else
|
152
|
+
# one line
|
153
|
+
context.output_buffer << spaces << opening_tag.html_safe
|
154
|
+
children.render_in(context)
|
155
|
+
context.output_buffer << closing_tag.html_safe
|
156
|
+
end
|
157
|
+
else
|
158
|
+
# multiple lines
|
159
|
+
context.output_buffer << spaces << opening_tag.html_safe << "\n"
|
160
|
+
children.render_in(context)
|
161
|
+
context.output_buffer << spaces << closing_tag.html_safe
|
162
|
+
end
|
163
|
+
|
164
|
+
context.output_buffer << "\n"
|
165
|
+
|
166
|
+
context.output_buffer[pos..].html_safe
|
167
|
+
end
|
168
|
+
|
169
|
+
def self_closing_tag?
|
170
|
+
SELF_CLOSING_ELEMENTS.include?(tag_name.to_sym)
|
171
|
+
end
|
172
|
+
|
173
|
+
def no_child?
|
174
|
+
children.empty?
|
175
|
+
end
|
176
|
+
|
177
|
+
def child_is_text?
|
178
|
+
children.size == 1 && children.first.is_a?(TextNode)
|
179
|
+
end
|
180
|
+
|
181
|
+
def attributes_html
|
182
|
+
" #{attributes}" if attributes.any?
|
183
|
+
end
|
184
|
+
|
185
|
+
def set_for_attribute(record)
|
186
|
+
return unless record
|
187
|
+
# set_attribute :id, ActionController::RecordIdentifier.dom_id(record, default_id_for_prefix)
|
188
|
+
# add_class ActionController::RecordIdentifier.dom_class(record)
|
189
|
+
set_attribute :id, dom_id_for(record)
|
190
|
+
add_class dom_class_name_for(record)
|
191
|
+
end
|
192
|
+
|
193
|
+
def dom_class_name_for(record)
|
194
|
+
if record.class.respond_to?(:model_name)
|
195
|
+
record.class.model_name.singular
|
196
|
+
else
|
197
|
+
record.class.name.underscore.gsub("/", "_")
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def dom_id_for(record)
|
202
|
+
id = if record.respond_to?(:to_key)
|
203
|
+
record.to_key
|
204
|
+
elsif record.respond_to?(:id)
|
205
|
+
record.id
|
206
|
+
else
|
207
|
+
record.object_id
|
208
|
+
end
|
209
|
+
|
210
|
+
[default_id_for_prefix, dom_class_name_for(record), id].compact.join("_")
|
211
|
+
end
|
212
|
+
|
213
|
+
def default_id_for_prefix
|
214
|
+
nil
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Arbo
|
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 class_list
|
26
|
+
[]
|
27
|
+
end
|
28
|
+
|
29
|
+
def tag_name
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
ERB::Util.html_escape(@content.to_s)
|
35
|
+
end
|
36
|
+
|
37
|
+
def render_in(context)
|
38
|
+
to_s.tap { |s| context.output_buffer << s }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Arbo
|
2
|
+
module Rails
|
3
|
+
module Forms
|
4
|
+
|
5
|
+
class FormBuilderProxy < Arbo::Component
|
6
|
+
attr_reader :form_builder
|
7
|
+
|
8
|
+
# Since label and select are Arbo Elements already, we must
|
9
|
+
# override it here instead of letting method_missing
|
10
|
+
# deal with it
|
11
|
+
def label(*args)
|
12
|
+
proxy_call_to_form :label, *args
|
13
|
+
end
|
14
|
+
|
15
|
+
def select(*args)
|
16
|
+
proxy_call_to_form :select, *args
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond_to_missing?(method, include_all)
|
20
|
+
if form_builder && form_builder.respond_to?(method, include_all)
|
21
|
+
true
|
22
|
+
else
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def proxy_call_to_form(method, *args, &block)
|
30
|
+
text_node form_builder.send(method, *args, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(method, *args, &block)
|
34
|
+
if form_builder && form_builder.respond_to?(method)
|
35
|
+
proxy_call_to_form(method, *args, &block)
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
class FormForProxy < FormBuilderProxy
|
44
|
+
builder_method :form_for
|
45
|
+
|
46
|
+
def build(resource, form_options = {}, &block)
|
47
|
+
form_string = helpers.form_for(resource, form_options) do |f|
|
48
|
+
@form_builder = f
|
49
|
+
end
|
50
|
+
|
51
|
+
@opening_tag, @closing_tag = split_string_on(form_string, "</form>")
|
52
|
+
super(&block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def fields_for(*args, &block)
|
56
|
+
insert_tag FieldsForProxy, form_builder, *args, &block
|
57
|
+
end
|
58
|
+
|
59
|
+
def split_string_on(string, match)
|
60
|
+
return "" unless string && match
|
61
|
+
part_1 = string.split(Regexp.new("#{match}\\z")).first
|
62
|
+
[part_1, match]
|
63
|
+
end
|
64
|
+
|
65
|
+
def opening_tag
|
66
|
+
@opening_tag || ""
|
67
|
+
end
|
68
|
+
|
69
|
+
def closing_tag
|
70
|
+
@closing_tag || ""
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
class FieldsForProxy < FormBuilderProxy
|
76
|
+
|
77
|
+
def build(form_builder, *args, &block)
|
78
|
+
form_builder.fields_for(*args) do |f|
|
79
|
+
@form_builder = f
|
80
|
+
end
|
81
|
+
|
82
|
+
super(&block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_s
|
86
|
+
children.to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
def render_in(context)
|
90
|
+
children.collect do |element|
|
91
|
+
element.render_in_or_to_s(context)
|
92
|
+
end.join('')
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Arbo
|
2
|
+
module Rails
|
3
|
+
module Rendering
|
4
|
+
|
5
|
+
def render(*args)
|
6
|
+
rendered = helpers.render(*args)
|
7
|
+
case rendered
|
8
|
+
when Arbo::Context
|
9
|
+
current_arbo_element.add_child rendered
|
10
|
+
else
|
11
|
+
text_node rendered
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ActionView::Base.class_eval do
|
4
|
+
def capture(*args)
|
5
|
+
value = nil
|
6
|
+
buffer = with_output_buffer { value = yield(*args) }
|
7
|
+
|
8
|
+
# Override to handle Arbo elements inside helper blocks.
|
9
|
+
# See https://github.com/rails/rails/issues/17661
|
10
|
+
# and https://github.com/rails/rails/pull/18024#commitcomment-8975180
|
11
|
+
value = value.to_s if value.is_a?(Arbo::Element)
|
12
|
+
|
13
|
+
if (string = buffer.presence || value) && string.is_a?(String)
|
14
|
+
ERB::Util.html_escape string
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module Arbo
|
20
|
+
module Rails
|
21
|
+
class TemplateHandler
|
22
|
+
def call(template, source = nil)
|
23
|
+
source = template.source unless source
|
24
|
+
|
25
|
+
<<-END
|
26
|
+
Arbo::Context.new(assigns, self) {
|
27
|
+
#{source}
|
28
|
+
}.render_in(self).html_safe
|
29
|
+
END
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
ActionView::Template.register_template_handler :arb, Arbo::Rails::TemplateHandler.new
|
data/lib/arbo/rails.rb
ADDED
data/lib/arbo/version.rb
ADDED
data/lib/arbo.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'active_support/core_ext/string/output_safety'
|
2
|
+
require 'active_support/deprecation'
|
3
|
+
require 'active_support/hash_with_indifferent_access'
|
4
|
+
require 'active_support/inflector'
|
5
|
+
|
6
|
+
module Arbo
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'arbo/element'
|
10
|
+
require 'arbo/context'
|
11
|
+
require 'arbo/html/attributes'
|
12
|
+
require 'arbo/html/class_list'
|
13
|
+
require 'arbo/html/tag'
|
14
|
+
require 'arbo/html/text_node'
|
15
|
+
require 'arbo/html/document'
|
16
|
+
require 'arbo/html/html5_elements'
|
17
|
+
require 'arbo/component'
|
18
|
+
|
19
|
+
if defined?(Rails)
|
20
|
+
require 'arbo/rails'
|
21
|
+
end
|