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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yaml +21 -0
  3. data/.github/workflows/daily.yaml +23 -0
  4. data/.gitignore +11 -0
  5. data/CHANGELOG.md +95 -0
  6. data/CONTRIBUTING.md +3 -0
  7. data/Gemfile +17 -0
  8. data/LICENSE +20 -0
  9. data/README.md +29 -0
  10. data/Rakefile +18 -0
  11. data/arbo.gemspec +25 -0
  12. data/docs/Gemfile +2 -0
  13. data/docs/_config.yml +7 -0
  14. data/docs/_includes/footer.html +8 -0
  15. data/docs/_includes/google-analytics.html +16 -0
  16. data/docs/_includes/head.html +7 -0
  17. data/docs/_includes/toc.html +12 -0
  18. data/docs/_includes/top-menu.html +8 -0
  19. data/docs/_layouts/default.html +21 -0
  20. data/docs/index.md +106 -0
  21. data/docs/stylesheets/main.css +1152 -0
  22. data/lib/arbo/component.rb +22 -0
  23. data/lib/arbo/context.rb +118 -0
  24. data/lib/arbo/element/builder_methods.rb +83 -0
  25. data/lib/arbo/element/proxy.rb +28 -0
  26. data/lib/arbo/element.rb +225 -0
  27. data/lib/arbo/element_collection.rb +31 -0
  28. data/lib/arbo/html/attributes.rb +41 -0
  29. data/lib/arbo/html/class_list.rb +28 -0
  30. data/lib/arbo/html/document.rb +31 -0
  31. data/lib/arbo/html/html5_elements.rb +47 -0
  32. data/lib/arbo/html/tag.rb +220 -0
  33. data/lib/arbo/html/text_node.rb +43 -0
  34. data/lib/arbo/rails/forms.rb +101 -0
  35. data/lib/arbo/rails/rendering.rb +17 -0
  36. data/lib/arbo/rails/template_handler.rb +35 -0
  37. data/lib/arbo/rails.rb +5 -0
  38. data/lib/arbo/version.rb +3 -0
  39. data/lib/arbo.rb +21 -0
  40. data/spec/arbo/integration/html_spec.rb +307 -0
  41. data/spec/arbo/unit/component_spec.rb +54 -0
  42. data/spec/arbo/unit/context_spec.rb +35 -0
  43. data/spec/arbo/unit/element_finder_methods_spec.rb +116 -0
  44. data/spec/arbo/unit/element_spec.rb +272 -0
  45. data/spec/arbo/unit/html/class_list_spec.rb +16 -0
  46. data/spec/arbo/unit/html/tag_attributes_spec.rb +104 -0
  47. data/spec/arbo/unit/html/tag_spec.rb +124 -0
  48. data/spec/arbo/unit/html/text_node_spec.rb +5 -0
  49. data/spec/rails/integration/forms_spec.rb +117 -0
  50. data/spec/rails/integration/rendering_spec.rb +98 -0
  51. data/spec/rails/rails_spec_helper.rb +46 -0
  52. data/spec/rails/stub_app/config/database.yml +3 -0
  53. data/spec/rails/stub_app/config/routes.rb +3 -0
  54. data/spec/rails/stub_app/db/schema.rb +3 -0
  55. data/spec/rails/stub_app/log/.gitignore +1 -0
  56. data/spec/rails/stub_app/public/favicon.ico +0 -0
  57. data/spec/rails/support/mock_person.rb +15 -0
  58. data/spec/rails/templates/arbo/_partial.arb +1 -0
  59. data/spec/rails/templates/arbo/_partial_with_assignment.arb +1 -0
  60. data/spec/rails/templates/arbo/empty.arb +0 -0
  61. data/spec/rails/templates/arbo/page_with_arb_partial_and_assignment.arb +3 -0
  62. data/spec/rails/templates/arbo/page_with_assignment.arb +1 -0
  63. data/spec/rails/templates/arbo/page_with_erb_partial.arb +3 -0
  64. data/spec/rails/templates/arbo/page_with_helpers.arb +7 -0
  65. data/spec/rails/templates/arbo/page_with_partial.arb +3 -0
  66. data/spec/rails/templates/arbo/simple_page.arb +8 -0
  67. data/spec/rails/templates/erb/_partial.erb +1 -0
  68. data/spec/spec_helper.rb +7 -0
  69. data/spec/support/bundle.rb +4 -0
  70. 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
@@ -0,0 +1,5 @@
1
+ require 'arbo/rails/template_handler'
2
+ require 'arbo/rails/forms'
3
+ require 'arbo/rails/rendering'
4
+
5
+ Arbo::Element.send :include, Arbo::Rails::Rendering
@@ -0,0 +1,3 @@
1
+ module Arbo
2
+ VERSION = '1.2.0'
3
+ end
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