19 | {% include google-analytics.html %}
20 |
21 |
22 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | redirect_from: /docs/documentation.html
3 | ---
4 | # Arbre
5 | HTML Views in Ruby
6 |
7 | ### Introduction
8 |
9 | Arbre is a alternate template system for [Ruby on Rails Action View](http://guides.rubyonrails.org/action_view_overview.html).
10 | Arbre expresses HTML using a Ruby DSL, which makes it similar to the [Builder](https://github.com/tenderlove/builder) gem for XML.
11 | Arbre was extracted from [Active Admin](https://activeadmin.info/).
12 |
13 | An example `index.html.arb`:
14 |
15 | ```ruby
16 | html {
17 | head {
18 | title "Welcome page"
19 | }
20 | body {
21 | para "Hello, world"
22 | }
23 | }
24 | ```
25 |
26 | The purpose of Arbre is to leave the view as Ruby objects as long as possible,
27 | which allows an object-oriented approach including inheritance, composition, and encapsulation.
28 |
29 | ### Installation
30 |
31 | Add gem `arbre` to your `Gemfile` and `bundle install`.
32 |
33 | Arbre registers itself as a Rails template handler for files with an extension `.arb`.
34 |
35 | ### Tags
36 |
37 | Arbre DSL is composed of HTML tags. Tag attributes including `id` and HTML classes are passed as a hash parameter and the tag body is passed as a block. Most HTML5 tags are implemented, including `script`, `embed` and `video`.
38 |
39 | A special case is the paragraph tag,
, which is mapped to `para`.
40 |
41 | JavaScript can be included by using `script { raw ... }`
42 |
43 | To include text that is not immediately part of a tag use `text_node`.
44 |
45 | ### Components
46 |
47 | Arbre DSL can be extended by defining new tags composed of other, simpler tags.
48 | This provides a simpler alternative to nesting partials.
49 | The recommended approach is to subclass Arbre::Component and implement a new builder method.
50 |
51 | The builder_method defines the method that will be called to build this component
52 | when using the DSL. The arguments passed into the builder_method will be passed
53 | into the #build method for you.
54 |
55 | For example:
56 |
57 | ```ruby
58 | class Panel < Arbre::Component
59 | builder_method :panel
60 |
61 | def build(title, attributes = {})
62 | super(attributes)
63 |
64 | h3(title, class: "panel-title")
65 | end
66 | end
67 | ```
68 |
69 | By default, components are `div` tags. This can be overridden by redefining the `tag_name` method.
70 |
71 | Several examples of Arbre components are [included in Active Admin](https://activeadmin.info/12-arbre-components.html)
72 |
73 | ### Contexts
74 |
75 | An [Arbre::Context](http://www.rubydoc.info/gems/arbre/Arbre/Context) is an object in which Arbre DSL is interpreted, providing a root for the Ruby DOM that can be [searched and manipulated](http://www.rubydoc.info/gems/arbre/Arbre/Element). A context is automatically provided when a `.arb` template or partial is loaded. Contexts can be used when developing or testing a component. Contexts are rendered by calling to_s.
76 |
77 | ```ruby
78 | html = Arbre::Context.new do
79 | panel "Hello World", class: "panel", id: "my-panel" do
80 | span "Inside the panel"
81 | text_node "Plain text"
82 | end
83 | end
84 |
85 | puts html.to_s # =>
86 | ```
87 |
88 | ```html
89 |
"
15 | #
16 | # The contents of the block are instance eval'd within the Context
17 | # object. This means that you lose context to the outside world from
18 | # within the block. To pass local variables into the Context, use the
19 | # assigns param.
20 | #
21 | # html = Arbre::Context.new({one: 1}) do
22 | # h1 "Your number #{one}"
23 | # end
24 | #
25 | # html.to_s #=> "Your number 1"
26 | #
27 | class Context < Element
28 | # Initialize a new Arbre::Context
29 | #
30 | # @param [Hash] assigns A hash of objects that you would like to be
31 | # available as local variables within the Context
32 | #
33 | # @param [Object] helpers An object that has methods on it which will become
34 | # instance methods within the context.
35 | #
36 | # @yield [] The block that will get instance eval'd in the context
37 | def initialize(assigns = {}, helpers = nil, &block)
38 | assigns = assigns || {}
39 | @_assigns = assigns.symbolize_keys
40 |
41 | @_helpers = helpers
42 | @_current_arbre_element_buffer = [self]
43 |
44 | super(self)
45 | instance_eval(&block) if block
46 | end
47 |
48 | def arbre_context
49 | self
50 | end
51 |
52 | def assigns
53 | @_assigns
54 | end
55 |
56 | def helpers
57 | @_helpers
58 | end
59 |
60 | def indent_level
61 | # A context does not increment the indent_level
62 | super - 1
63 | end
64 |
65 | def bytesize
66 | cached_html.bytesize
67 | end
68 | alias :length :bytesize
69 |
70 | def respond_to_missing?(method, include_all)
71 | super || cached_html.respond_to?(method, include_all)
72 | end
73 |
74 | # Webservers treat Arbre::Context as a string. We override
75 | # method_missing to delegate to the string representation
76 | # of the html.
77 | ruby2_keywords def method_missing(method, *args, &block)
78 | if cached_html.respond_to? method
79 | cached_html.send method, *args, &block
80 | else
81 | super
82 | end
83 | end
84 |
85 | def current_arbre_element
86 | @_current_arbre_element_buffer.last
87 | end
88 |
89 | def with_current_arbre_element(tag)
90 | raise ArgumentError, "Can't be in the context of nil. #{@_current_arbre_element_buffer.inspect}" unless tag
91 | @_current_arbre_element_buffer.push tag
92 | yield
93 | @_current_arbre_element_buffer.pop
94 | end
95 | alias_method :within, :with_current_arbre_element
96 |
97 | private
98 |
99 | # Caches the rendered HTML so that we don't re-render just to
100 | # get the content length or to delegate a method to the HTML
101 | def cached_html
102 | if defined?(@cached_html)
103 | @cached_html
104 | else
105 | html = to_s
106 | @cached_html = html if html.length > 0
107 | html
108 | end
109 | end
110 | end
111 | end
112 |
--------------------------------------------------------------------------------
/lib/arbre/element.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'arbre/element/builder_methods'
3 | require 'arbre/element/proxy'
4 | require 'arbre/element_collection'
5 |
6 | module Arbre
7 |
8 | class Element
9 | include BuilderMethods
10 |
11 | attr_reader :parent
12 | attr_reader :children, :arbre_context
13 |
14 | def initialize(arbre_context = Arbre::Context.new)
15 | @arbre_context = arbre_context
16 | @children = ElementCollection.new
17 | @parent = nil
18 | end
19 |
20 | def assigns
21 | arbre_context.assigns
22 | end
23 |
24 | def helpers
25 | arbre_context.helpers
26 | end
27 |
28 | def tag_name
29 | @tag_name ||= self.class.name.demodulize.downcase
30 | end
31 |
32 | def build(*args, &block)
33 | # Render the block passing ourselves in
34 | append_return_block(block.call(self)) if block
35 | end
36 |
37 | def add_child(child)
38 | return unless child
39 |
40 | if child.is_a?(Array)
41 | child.each{|item| add_child(item) }
42 | return @children
43 | end
44 |
45 | # If its not an element, wrap it in a TextNode
46 | unless child.is_a?(Element)
47 | child = Arbre::HTML::TextNode.from_string(child)
48 | end
49 |
50 | if child.respond_to?(:parent)
51 | # Remove the child
52 | child.parent.remove_child(child) if child.parent && child.parent != self
53 | # Set ourselves as the parent
54 | child.parent = self
55 | end
56 |
57 | @children << child
58 | end
59 |
60 | def remove_child(child)
61 | child.parent = nil if child.respond_to?(:parent=)
62 | @children.delete(child)
63 | end
64 |
65 | def <<(child)
66 | add_child(child)
67 | end
68 |
69 | def children?
70 | @children.any?
71 | end
72 |
73 | def parent=(parent)
74 | @parent = parent
75 | end
76 |
77 | def parent?
78 | !@parent.nil?
79 | end
80 |
81 | def ancestors
82 | if parent?
83 | [parent] + parent.ancestors
84 | else
85 | []
86 | end
87 | end
88 |
89 | # TODO: Shouldn't grab whole tree
90 | def find_first_ancestor(type)
91 | ancestors.find{|a| a.is_a?(type) }
92 | end
93 |
94 | def content=(contents)
95 | clear_children!
96 | add_child(contents)
97 | end
98 |
99 | def get_elements_by_tag_name(tag_name)
100 | elements = ElementCollection.new
101 | children.each do |child|
102 | elements << child if child.tag_name == tag_name
103 | elements.concat(child.get_elements_by_tag_name(tag_name))
104 | end
105 | elements
106 | end
107 | alias_method :find_by_tag, :get_elements_by_tag_name
108 |
109 | def get_elements_by_class_name(class_name)
110 | elements = ElementCollection.new
111 | children.each do |child|
112 | elements << child if child.class_list.include?(class_name)
113 | elements.concat(child.get_elements_by_class_name(class_name))
114 | end
115 | elements
116 | end
117 | alias_method :find_by_class, :get_elements_by_class_name
118 |
119 | def content
120 | children.to_s
121 | end
122 |
123 | def html_safe
124 | to_s
125 | end
126 |
127 | def indent_level
128 | parent? ? parent.indent_level + 1 : 0
129 | end
130 |
131 | def each(&block)
132 | [to_s].each(&block)
133 | end
134 |
135 | def inspect
136 | to_s
137 | end
138 |
139 | def to_str
140 | to_s
141 | end
142 |
143 | def to_s
144 | content
145 | end
146 |
147 | def +(element)
148 | case element
149 | when Element, ElementCollection
150 | else
151 | element = Arbre::HTML::TextNode.from_string(element)
152 | end
153 | to_ary + element
154 | end
155 |
156 | def to_ary
157 | ElementCollection.new [Proxy.new(self)]
158 | end
159 | alias_method :to_a, :to_ary
160 |
161 | private
162 |
163 | # Resets the Elements children
164 | def clear_children!
165 | @children.clear
166 | end
167 |
168 | # Implements the method lookup chain. When you call a method that
169 | # doesn't exist, we:
170 | #
171 | # 1. Try to call the method on the current DOM context
172 | # 2. Return an assigned variable of the same name
173 | # 3. Call the method on the helper object
174 | # 4. Call super
175 | #
176 | ruby2_keywords def method_missing(name, *args, &block)
177 | if current_arbre_element.respond_to?(name)
178 | current_arbre_element.send name, *args, &block
179 | elsif assigns && assigns.has_key?(name)
180 | assigns[name]
181 | elsif helpers.respond_to?(name)
182 | helpers.send(name, *args, &block)
183 | else
184 | super
185 | end
186 | end
187 | end
188 | end
189 |
--------------------------------------------------------------------------------
/lib/arbre/element/builder_methods.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | module Arbre
3 | class Element
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 | with_current_arbre_element tag do
27 | if block && block.arity > 0
28 | tag.build(*args, &block)
29 | else
30 | tag.build(*args)
31 | append_return_block(yield) if block
32 | end
33 | end
34 |
35 | tag
36 | end
37 |
38 | def insert_tag(klass, *args, &block)
39 | tag = build_tag(klass, *args, &block)
40 | current_arbre_element.add_child(tag)
41 | tag
42 | end
43 |
44 | def current_arbre_element
45 | arbre_context.current_arbre_element
46 | end
47 |
48 | def with_current_arbre_element(tag, &block)
49 | arbre_context.with_current_arbre_element(tag, &block)
50 | end
51 | alias_method :within, :with_current_arbre_element
52 |
53 | private
54 |
55 | # Appends the value to the current DOM element if there are no
56 | # existing DOM Children and it responds to #to_s
57 | def append_return_block(tag)
58 | return nil if current_arbre_element.children?
59 |
60 | if appendable_tag?(tag)
61 | current_arbre_element << Arbre::HTML::TextNode.from_string(tag.to_s)
62 | end
63 | end
64 |
65 | # Returns true if the object should be converted into a text node
66 | # and appended into the DOM.
67 | def appendable_tag?(tag)
68 | # Array.new.to_s prints out an empty array ("[]"). In
69 | # Arbre, we append the return value of blocks to the output, which
70 | # can cause empty arrays to show up within the output. To get
71 | # around this, we check if the object responds to #empty?
72 | if tag.respond_to?(:empty?) && tag.empty?
73 | false
74 | else
75 | !tag.is_a?(Arbre::Element) && tag.respond_to?(:to_s)
76 | end
77 |
78 | end
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/arbre/element/proxy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | module Arbre
3 | class Element
4 | class Proxy < BasicObject
5 | undef_method :==
6 | undef_method :equal?
7 |
8 | def initialize(element)
9 | @element = element
10 | end
11 |
12 | def respond_to?(method, include_all = false)
13 | if method.to_s == 'to_ary'
14 | false
15 | else
16 | super || @element.respond_to?(method, include_all)
17 | end
18 | end
19 |
20 | def method_missing(method, *args, &block)
21 | if method.to_s == 'to_ary'
22 | super
23 | else
24 | @element.__send__ method, *args, &block
25 | end
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/arbre/element_collection.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | module Arbre
3 |
4 | # Stores a collection of Element objects
5 | class ElementCollection < Array
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
26 |
--------------------------------------------------------------------------------
/lib/arbre/html/attributes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | module Arbre
3 | module HTML
4 |
5 | class Attributes < Hash
6 | def to_s
7 | flatten_hash.compact.map do |name, value|
8 | "#{html_escape(name)}=\"#{html_escape(value)}\""
9 | end.join ' '
10 | end
11 |
12 | protected
13 |
14 | def flatten_hash(hash = self, old_path = [], accumulator = {})
15 | hash.each do |key, value|
16 | path = old_path + [key]
17 | if value.is_a? Hash
18 | flatten_hash(value, path, accumulator)
19 | else
20 | accumulator[path.join('-')] = value
21 | end
22 | end
23 | accumulator
24 | end
25 |
26 | def html_escape(s)
27 | ERB::Util.html_escape(s)
28 | end
29 | end
30 |
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/arbre/html/class_list.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'set'
3 |
4 | module Arbre
5 | module HTML
6 |
7 | # Holds a set of classes
8 | class ClassList < Set
9 | def self.build_from_string(class_names)
10 | new.add(class_names)
11 | end
12 |
13 | def add(class_names)
14 | class_names.to_s.split(" ").each do |class_name|
15 | super(class_name)
16 | end
17 | self
18 | end
19 | alias :<< :add
20 |
21 | def to_s
22 | to_a.join(" ")
23 | end
24 | end
25 |
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/arbre/html/document.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | module Arbre
3 | module HTML
4 |
5 | class Document < Tag
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 | ''.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
43 |
--------------------------------------------------------------------------------
/lib/arbre/html/html5_elements.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | module Arbre
3 | module HTML
4 |
5 | AUTO_BUILD_ELEMENTS = [
6 | :a,
7 | :abbr,
8 | :address,
9 | :area,
10 | :article,
11 | :aside,
12 | :audio,
13 | :b,
14 | :base,
15 | :bdi,
16 | :bdo,
17 | :blockquote,
18 | :body,
19 | :br,
20 | :button,
21 | :canvas,
22 | :caption,
23 | :cite,
24 | :code,
25 | :col,
26 | :colgroup,
27 | :command,
28 | :data,
29 | :datalist,
30 | :dd,
31 | :del,
32 | :details,
33 | :dfn,
34 | :dialog,
35 | :div,
36 | :dl,
37 | :dt,
38 | :em,
39 | :embed,
40 | :fieldset,
41 | :figcaption,
42 | :figure,
43 | :footer,
44 | :form,
45 | :h1,
46 | :h2,
47 | :h3,
48 | :h4,
49 | :h5,
50 | :h6,
51 | :head,
52 | :header,
53 | :hgroup,
54 | :hr,
55 | :html,
56 | :i,
57 | :iframe,
58 | :img,
59 | :input,
60 | :ins,
61 | :kbd,
62 | :keygen,
63 | :label,
64 | :legend,
65 | :li,
66 | :link,
67 | :main,
68 | :map,
69 | :mark,
70 | :menu,
71 | :menuitem,
72 | :meta,
73 | :meter,
74 | :nav,
75 | :noscript,
76 | :object,
77 | :ol,
78 | :optgroup,
79 | :option,
80 | :output,
81 | :param,
82 | :picture,
83 | :pre,
84 | :progress,
85 | :q,
86 | :rp,
87 | :rt,
88 | :ruby,
89 | :s,
90 | :samp,
91 | :script,
92 | :search,
93 | :section,
94 | :select,
95 | :slot,
96 | :small,
97 | :source,
98 | :span,
99 | :strong,
100 | :style,
101 | :sub,
102 | :summary,
103 | :sup,
104 | :svg,
105 | :table,
106 | :tbody,
107 | :td,
108 | :template,
109 | :textarea,
110 | :tfoot,
111 | :th,
112 | :thead,
113 | :time,
114 | :title,
115 | :tr,
116 | :track,
117 | :u,
118 | :ul,
119 | :var,
120 | :video,
121 | :wbr
122 | ]
123 |
124 | HTML5_ELEMENTS = [ :p ] + AUTO_BUILD_ELEMENTS
125 |
126 | AUTO_BUILD_ELEMENTS.each do |name|
127 | class_eval <<-EOF
128 | class #{name.to_s.capitalize} < Tag
129 | builder_method :#{name}
130 | end
131 | EOF
132 | end
133 |
134 | class P < Tag
135 | builder_method :para
136 | end
137 |
138 | end
139 | end
140 |
--------------------------------------------------------------------------------
/lib/arbre/html/tag.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'erb'
3 |
4 | module Arbre
5 | module HTML
6 |
7 | class Tag < Element
8 | attr_reader :attributes
9 |
10 | # See: https://html.spec.whatwg.org/multipage/syntax.html#void-elements
11 | SELF_CLOSING_ELEMENTS = [ :area, :base, :br, :col, :embed, :hr, :img, :input, :keygen, :link,
12 | :menuitem, :meta, :param, :source, :track, :wbr ]
13 |
14 | def initialize(*)
15 | super
16 | @attributes = Attributes.new
17 | end
18 |
19 | def build(*args)
20 | super
21 | attributes = extract_arguments(args)
22 | self.content = args.first if args.first
23 |
24 | for_value = attributes[:for]
25 | unless for_value.is_a?(String) || for_value.is_a?(Symbol)
26 | set_for_attribute(attributes.delete(:for))
27 | end
28 |
29 | attributes.each do |key, value|
30 | set_attribute(key, value)
31 | end
32 | end
33 |
34 | def extract_arguments(args)
35 | if args.last.is_a?(Hash)
36 | args.pop
37 | else
38 | {}
39 | end
40 | end
41 |
42 | def set_attribute(name, value)
43 | @attributes[name.to_sym] = value
44 | end
45 |
46 | def get_attribute(name)
47 | @attributes[name.to_sym]
48 | end
49 | alias :attr :get_attribute
50 |
51 | def has_attribute?(name)
52 | @attributes.has_key?(name.to_sym)
53 | end
54 |
55 | def remove_attribute(name)
56 | @attributes.delete(name.to_sym)
57 | end
58 |
59 | def id
60 | get_attribute(:id)
61 | end
62 |
63 | # Generates and id for the object if it doesn't exist already
64 | def id!
65 | return id if id
66 | self.id = object_id.to_s
67 | id
68 | end
69 |
70 | def id=(id)
71 | set_attribute(:id, id)
72 | end
73 |
74 | def add_class(class_names)
75 | class_list.add class_names
76 | end
77 |
78 | def remove_class(class_names)
79 | class_list.delete(class_names)
80 | end
81 |
82 | # Returns a string of classes
83 | def class_names
84 | class_list.to_s
85 | end
86 |
87 | def class_list
88 | list = get_attribute(:class)
89 |
90 | case list
91 | when ClassList
92 | list
93 | when String
94 | set_attribute(:class, ClassList.build_from_string(list))
95 | else
96 | set_attribute(:class, ClassList.new)
97 | end
98 | end
99 |
100 | def to_s
101 | indent(opening_tag, content, closing_tag).html_safe
102 | end
103 |
104 | private
105 |
106 | def opening_tag
107 | "<#{tag_name}#{attributes_html}>"
108 | end
109 |
110 | def closing_tag
111 | "#{tag_name}>"
112 | end
113 |
114 | INDENT_SIZE = 2
115 |
116 | def indent(open_tag, child_content, close_tag)
117 | spaces = ' ' * indent_level * INDENT_SIZE
118 |
119 | html = +""
120 |
121 | if no_child? || child_is_text?
122 | if self_closing_tag?
123 | html << spaces << open_tag.sub( />$/, '/>' )
124 | else
125 | # one line
126 | html << spaces << open_tag << child_content << close_tag
127 | end
128 | else
129 | # multiple lines
130 | html << spaces << open_tag << "\n"
131 | html << child_content # the child takes care of its own spaces
132 | html << spaces << close_tag
133 | end
134 |
135 | html << "\n"
136 | end
137 |
138 | def self_closing_tag?
139 | SELF_CLOSING_ELEMENTS.include?(tag_name.to_sym)
140 | end
141 |
142 | def no_child?
143 | children.empty?
144 | end
145 |
146 | def child_is_text?
147 | children.size == 1 && children.first.is_a?(TextNode)
148 | end
149 |
150 | def attributes_html
151 | " #{attributes}" if attributes.any?
152 | end
153 |
154 | def set_for_attribute(record)
155 | return unless record
156 | # set_attribute :id, ActionController::RecordIdentifier.dom_id(record, default_id_for_prefix)
157 | # add_class ActionController::RecordIdentifier.dom_class(record)
158 | set_attribute :id, dom_id_for(record)
159 | add_class dom_class_name_for(record)
160 | end
161 |
162 | def dom_class_name_for(record)
163 | if record.class.respond_to?(:model_name)
164 | record.class.model_name.singular
165 | else
166 | record.class.name.underscore.tr("/", "_")
167 | end
168 | end
169 |
170 | def dom_id_for(record)
171 | id = if record.respond_to?(:to_key)
172 | record.to_key
173 | elsif record.respond_to?(:id)
174 | record.id
175 | else
176 | record.object_id
177 | end
178 |
179 | [default_id_for_prefix, dom_class_name_for(record), id].compact.join("_")
180 | end
181 |
182 | def default_id_for_prefix
183 | nil
184 | end
185 | end
186 |
187 | end
188 | end
189 |
--------------------------------------------------------------------------------
/lib/arbre/html/text_node.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'erb'
3 |
4 | module Arbre
5 | module HTML
6 |
7 | class TextNode < Element
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 | end
37 |
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/arbre/rails/forms.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | module Arbre
3 | module Rails
4 | module Forms
5 |
6 | class FormBuilderProxy < Arbre::Component
7 | attr_reader :form_builder
8 |
9 | # Since label and select are Arbre Elements already, we must
10 | # override it here instead of letting method_missing
11 | # deal with it
12 | def label(*args)
13 | proxy_call_to_form :label, *args
14 | end
15 |
16 | def select(*args)
17 | proxy_call_to_form :select, *args
18 | end
19 |
20 | def respond_to_missing?(method, include_all)
21 | if form_builder && form_builder.respond_to?(method, include_all)
22 | true
23 | else
24 | super
25 | end
26 | end
27 |
28 | private
29 |
30 | def proxy_call_to_form(method, *args, &block)
31 | text_node form_builder.send(method, *args, &block)
32 | end
33 |
34 | ruby2_keywords def method_missing(method, *args, &block)
35 | if form_builder && form_builder.respond_to?(method)
36 | proxy_call_to_form(method, *args, &block)
37 | else
38 | super
39 | end
40 | end
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, "")
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 | end
73 |
74 | class FieldsForProxy < FormBuilderProxy
75 | def build(form_builder, *args, &block)
76 | form_builder.fields_for(*args) do |f|
77 | @form_builder = f
78 | end
79 |
80 | super(&block)
81 | end
82 |
83 | def to_s
84 | children.to_s
85 | end
86 | end
87 |
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/lib/arbre/rails/rendering.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | module Arbre
3 | module Rails
4 | module Rendering
5 |
6 | def render(*args, &block)
7 | rendered = helpers.render(*args, &block)
8 | case rendered
9 | when Arbre::Context
10 | current_arbre_element.add_child rendered
11 | else
12 | text_node rendered
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/arbre/rails/template_handler.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | module Arbre
3 | module Rails
4 | class TemplateHandler
5 | def call(template, source = nil)
6 | source = template.source unless source
7 |
8 | <<-END
9 | Arbre::Context.new(assigns, self) {
10 | #{source}
11 | }.to_s
12 | END
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/arbre/railtie.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require_relative 'rails/template_handler'
3 | require_relative 'rails/forms'
4 | require_relative 'rails/rendering'
5 | require 'rails'
6 |
7 | Arbre::Element.include(Arbre::Rails::Rendering)
8 |
9 | module Arbre
10 | class Railtie < ::Rails::Railtie
11 | initializer "arbre" do
12 | ActiveSupport.on_load(:action_view) do
13 | ActionView::Template.register_template_handler :arb, Arbre::Rails::TemplateHandler.new
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/arbre/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | module Arbre
3 | VERSION = "2.2.0"
4 | end
5 |
--------------------------------------------------------------------------------
/spec/arbre/integration/html_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'spec_helper'
3 |
4 | describe Arbre do
5 |
6 | let(:helpers){ nil }
7 | let(:assigns){ {} }
8 |
9 | it "renders a single element" do
10 | expect(arbre {
11 | span "Hello World"
12 | }.to_s).to eq("Hello World\n")
13 | end
14 |
15 | it "renders a child element" do
16 | expect(arbre {
17 | span do
18 | span "Hello World"
19 | end
20 | }.to_s).to eq <<~HTML
21 |
22 | Hello World
23 |
24 | HTML
25 | end
26 |
27 | it "renders an unordered list" do
28 | expect(arbre {
29 | ul do
30 | li "First"
31 | li "Second"
32 | li "Third"
33 | end
34 | }.to_s).to eq <<~HTML
35 |
36 |
First
37 |
Second
38 |
Third
39 |
40 | HTML
41 | end
42 |
43 | it "allows local variables inside the tags" do
44 | expect(arbre {
45 | first = "First"
46 | second = "Second"
47 | ul do
48 | li first
49 | li second
50 | end
51 | }.to_s).to eq <<~HTML
52 |
53 |
First
54 |
Second
55 |
56 | HTML
57 | end
58 |
59 | it "adds children and nested" do
60 | expect(arbre {
61 | div do
62 | ul
63 | li do
64 | li
65 | end
66 | end
67 | }.to_s).to eq <<~HTML
68 |
69 |
70 |
71 |
72 |
73 |
74 | HTML
75 | end
76 |
77 | it "passes the element in to the block if asked for" do
78 | expect(arbre {
79 | div do |d|
80 | d.ul do
81 | li
82 | end
83 | end
84 | }.to_s).to eq <<~HTML
85 |
86 |
87 |
88 |
89 |
90 | HTML
91 | end
92 |
93 | it "moves content tags between parents" do
94 | expect(arbre {
95 | div do
96 | span(ul(li))
97 | end
98 | }.to_s).to eq <<~HTML
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | HTML
107 | end
108 |
109 | it "adds content to the parent if the element is passed into block" do
110 | expect(arbre {
111 | div do |d|
112 | d.id = "my-tag"
113 | ul do
114 | li
115 | end
116 | end
117 | }.to_s).to eq <<~HTML
118 |
119 |
120 |
121 |
122 |
123 | HTML
124 | end
125 |
126 | it "has the parent set on it" do
127 | list, item = nil
128 | arbre {
129 | list = ul do
130 | li "Hello"
131 | item = li "World"
132 | end
133 | }
134 | expect(item.parent).to eq list
135 | end
136 |
137 | it "sets a string content return value with no children" do
138 | expect(arbre {
139 | li do
140 | "Hello World"
141 | end
142 | }.to_s).to eq <<~HTML
143 |
Hello World
144 | HTML
145 | end
146 |
147 | it "turns string return values into text nodes" do
148 | node = nil
149 | arbre {
150 | list = li do
151 | "Hello World"
152 | end
153 | node = list.children.first
154 | }
155 | expect(node).to be_a described_class::HTML::TextNode
156 | end
157 |
158 | it "does not render blank arrays" do
159 | expect(arbre {
160 | tbody do
161 | []
162 | end
163 | }.to_s).to eq <<~HTML
164 |
165 | HTML
166 | end
167 |
168 | describe "self-closing nodes" do
169 |
170 | it "does not self-close script tags" do
171 | expect(arbre {
172 | script type: 'text/javascript'
173 | }.to_s).to eq("\n")
174 | end
175 |
176 | it "self-closes meta tags" do
177 | expect(arbre {
178 | meta content: "text/html; charset=utf-8"
179 | }.to_s).to eq("\n")
180 | end
181 |
182 | it "self-closes link tags" do
183 | expect(arbre {
184 | link rel: "stylesheet"
185 | }.to_s).to eq("\n")
186 | end
187 |
188 | described_class::HTML::Tag::SELF_CLOSING_ELEMENTS.each do |tag|
189 | it "self-closes #{tag} tags" do
190 | expect(arbre {
191 | send(tag)
192 | }.to_s).to eq("<#{tag}/>\n")
193 | end
194 | end
195 |
196 | end
197 |
198 | describe "html safe" do
199 |
200 | it "escapes the contents" do
201 | expect(arbre {
202 | span(" ")
203 | }.to_s).to eq <<~HTML
204 | <br />
205 | HTML
206 | end
207 |
208 | it "returns html safe strings" do
209 | expect(arbre {
210 | span(" ")
211 | }.to_s).to be_html_safe
212 | end
213 |
214 | it "does not escape html passed in" do
215 | expect(arbre {
216 | span(span(" "))
217 | }.to_s).to eq <<~HTML
218 |
219 | <br />
220 |
221 | HTML
222 | end
223 |
224 | it "escapes string contents when passed in block" do
225 | expect(arbre {
226 | span {
227 | span {
228 | " "
229 | }
230 | }
231 | }.to_s).to eq <<~HTML
232 |
233 | <br />
234 |
235 | HTML
236 | end
237 |
238 | it "escapes the contents of attributes" do
239 | expect(arbre {
240 | span(class: " ")
241 | }.to_s).to eq <<~HTML
242 |
243 | HTML
244 | end
245 |
246 | end
247 |
248 | end
249 |
--------------------------------------------------------------------------------
/spec/arbre/unit/component_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'spec_helper'
3 |
4 | # A mock subclass to play with
5 | class MockComponent < Arbre::Component
6 | builder_method :mock_component
7 |
8 | def build
9 | h2 "Hello World"
10 | end
11 | end
12 |
13 | describe Arbre::Component do
14 | let(:assigns) { {} }
15 | let(:helpers) { nil }
16 | let(:component_class) { MockComponent }
17 | let(:component) { component_class.new }
18 |
19 | it "is a subclass of an html div" do
20 | expect(described_class.ancestors).to include(Arbre::HTML::Div)
21 | end
22 |
23 | it "renders to a div, even as a subclass" do
24 | expect(component.tag_name).to eq('div')
25 | end
26 |
27 | it "does not have a class list" do
28 | expect(component.class_list.to_s).to eq("")
29 | expect(component.class_list.empty?).to be(true)
30 | end
31 |
32 | it "renders the object using the builder method name" do
33 | expect(arbre {
34 | mock_component
35 | }.to_s).to eq <<~HTML
36 |
37 |
Hello World
38 |
39 | HTML
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/spec/arbre/unit/context_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'spec_helper'
3 |
4 | describe Arbre::Context do
5 |
6 | let(:context) do
7 | described_class.new do
8 | h1 "札幌市北区" # Add some HTML to the context
9 | end
10 | end
11 |
12 | it "does not increment the indent_level" do
13 | expect(context.indent_level).to eq(-1)
14 | end
15 |
16 | it "returns a bytesize" do
17 | expect(context.bytesize).to eq(25)
18 | end
19 |
20 | it "returns a length" do
21 | expect(context.length).to eq(25)
22 | end
23 |
24 | it "delegates missing methods to the html string" do
25 | expect(context).to respond_to(:index)
26 | expect(context.index('<')).to eq(0)
27 | end
28 |
29 | it "uses a cached version of the HTML for method delegation" do
30 | expect(context).to receive(:to_s).once.and_return("
札幌市北区
")
31 | expect(context.index('<')).to eq(0)
32 | expect(context.index('<')).to eq(0)
33 | end
34 |
35 | end
36 |
--------------------------------------------------------------------------------
/spec/arbre/unit/element_finder_methods_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'spec_helper'
3 |
4 | describe Arbre::Element, "Finder Methods" do
5 | let(:assigns){ {} }
6 | let(:helpers){ {} }
7 |
8 | describe "finding elements by tag name" do
9 |
10 | it "returns 0 when no elements exist" do
11 | expect(arbre {
12 | div
13 | }.get_elements_by_tag_name("li").size).to eq(0)
14 | end
15 |
16 | it "returns a child element" do
17 | html = arbre do
18 | ul
19 | li
20 | ul
21 | end
22 | elements = html.get_elements_by_tag_name("li")
23 | expect(elements.size).to eq(1)
24 | expect(elements[0]).to be_instance_of(Arbre::HTML::Li)
25 | end
26 |
27 | it "returns multiple child elements" do
28 | html = arbre do
29 | ul
30 | li
31 | ul
32 | li
33 | end
34 | elements = html.get_elements_by_tag_name("li")
35 | expect(elements.size).to eq(2)
36 | expect(elements[0]).to be_instance_of(Arbre::HTML::Li)
37 | expect(elements[1]).to be_instance_of(Arbre::HTML::Li)
38 | end
39 |
40 | it "returns children's child elements" do
41 | html = arbre do
42 | ul
43 | li do
44 | li
45 | end
46 | end
47 | elements = html.get_elements_by_tag_name("li")
48 | expect(elements.size).to eq(2)
49 | expect(elements[0]).to be_instance_of(Arbre::HTML::Li)
50 | expect(elements[1]).to be_instance_of(Arbre::HTML::Li)
51 | expect(elements[1].parent).to eq(elements[0])
52 | end
53 | end
54 |
55 | #TODO: describe "finding an element by id"
56 |
57 | describe "finding an element by a class name" do
58 |
59 | it "returns 0 when no elements exist" do
60 | expect(arbre {
61 | div
62 | }.get_elements_by_class_name("my_class").size).to eq(0)
63 | end
64 |
65 | it "allows text nodes on tree" do
66 | expect(arbre {
67 | text_node "text"
68 | }.get_elements_by_class_name("my_class").size).to eq(0)
69 | end
70 |
71 | it "returns a child element" do
72 | html = arbre do
73 | div class: "some_class"
74 | div class: "my_class"
75 | end
76 | elements = html.get_elements_by_class_name("my_class")
77 | expect(elements.size).to eq(1)
78 | expect(elements[0]).to be_instance_of(Arbre::HTML::Div)
79 | end
80 |
81 | it "returns multiple child elements" do
82 | html = arbre do
83 | div class: "some_class"
84 | div class: "my_class"
85 | div class: "my_class"
86 | end
87 | elements = html.get_elements_by_class_name("my_class")
88 | expect(elements.size).to eq(2)
89 | expect(elements[0]).to be_instance_of(Arbre::HTML::Div)
90 | expect(elements[1]).to be_instance_of(Arbre::HTML::Div)
91 | end
92 |
93 | it "returns elements that match one of several classes" do
94 | html = arbre do
95 | div class: "some_class this_class"
96 | div class: "some_class"
97 | div class: "other_class"
98 |
99 | end
100 | elements = html.get_elements_by_class_name("this_class")
101 | expect(elements.size).to eq(1)
102 | expect(elements[0]).to be_instance_of(Arbre::HTML::Div)
103 | end
104 |
105 | it "returns a grandchild element" do
106 | html = arbre do
107 | div class: "some_class" do
108 | div class: "my_class"
109 | end
110 | end
111 | elements = html.get_elements_by_class_name("my_class")
112 | expect(elements.size).to eq(1)
113 | expect(elements[0]).to be_instance_of(Arbre::HTML::Div)
114 | end
115 |
116 | end
117 | end
118 |
--------------------------------------------------------------------------------
/spec/arbre/unit/element_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'spec_helper'
3 |
4 | describe Arbre::Element do
5 |
6 | let(:element){ described_class.new }
7 |
8 | context "when initialized" do
9 |
10 | it "has no children" do
11 | expect(element.children).to be_empty
12 | end
13 |
14 | it "has no parent" do
15 | expect(element.parent).to be_nil
16 | end
17 |
18 | it "responds to the HTML builder methods" do
19 | expect(element).to respond_to(:span)
20 | end
21 |
22 | it "has a set of local assigns" do
23 | context = Arbre::Context.new hello: "World"
24 | element = described_class.new(context)
25 | expect(element.assigns[:hello]).to eq("World")
26 | end
27 |
28 | it "has an empty hash with no local assigns" do
29 | expect(element.assigns).to eq({})
30 | end
31 |
32 | end
33 |
34 | describe "passing in a helper object" do
35 |
36 | let(:helper) do
37 | Class.new do
38 | def helper_method
39 | "helper method"
40 | end
41 | end
42 | end
43 |
44 | let(:element){ described_class.new(Arbre::Context.new(nil, helper.new)) }
45 |
46 | it "calls methods on the helper object and return TextNode objects" do
47 | expect(element.helper_method).to eq("helper method")
48 | end
49 |
50 | it "raises a NoMethodError if not found" do
51 | expect {
52 | element.a_method_that_doesnt_exist
53 | }.to raise_error(NoMethodError)
54 | end
55 |
56 | end
57 |
58 | describe "passing in assigns" do
59 | let(:post) { double }
60 | let(:assigns){ {post: post} }
61 |
62 | it "is accessible via a method call" do
63 | element = described_class.new(Arbre::Context.new(assigns))
64 | expect(element.post).to eq(post)
65 | end
66 |
67 | end
68 |
69 | it "to_a.flatten should not infinitely recurse" do
70 | expect {
71 | Timeout.timeout(1) do
72 | element.to_a.flatten
73 | end
74 | }.not_to raise_error
75 | end
76 |
77 | describe "adding a child" do
78 |
79 | let(:child){ described_class.new }
80 |
81 | before do
82 | element.add_child child
83 | end
84 |
85 | it "adds the child to the parent" do
86 | expect(element.children.first).to eq(child)
87 | end
88 |
89 | it "sets the parent of the child" do
90 | expect(child.parent).to eq(element)
91 | end
92 |
93 | context "when the child is nil" do
94 |
95 | let(:child){ nil }
96 |
97 | it "does not add the child" do
98 | expect(element.children).to be_empty
99 | end
100 |
101 | end
102 |
103 | context "when the child is a string" do
104 |
105 | let(:child){ "Hello World" }
106 |
107 | it "adds as a TextNode" do
108 | expect(element.children.first).to be_instance_of(Arbre::HTML::TextNode)
109 | expect(element.children.first.to_s).to eq("Hello World")
110 | end
111 |
112 | end
113 | end
114 |
115 | describe "setting the content" do
116 |
117 | context "when a string" do
118 |
119 | before do
120 | element.add_child "Hello World"
121 | element.content = "Goodbye"
122 | end
123 |
124 | it "clears the existing children" do
125 | expect(element.children.size).to eq(1)
126 | end
127 |
128 | it "adds the string as a child" do
129 | expect(element.children.first.to_s).to eq("Goodbye")
130 | end
131 |
132 | it "htmls escape the string" do
133 | string = "Goodbye "
134 | element.content = string
135 | expect(element.content.to_s).to eq("Goodbye <br />")
136 | end
137 | end
138 |
139 | context "when an element" do
140 | let(:content_element){ described_class.new }
141 |
142 | before do
143 | element.content = content_element
144 | end
145 |
146 | it "sets the content tag" do
147 | expect(element.children.first).to eq(content_element)
148 | end
149 |
150 | it "sets the tags parent" do
151 | expect(content_element.parent).to eq(element)
152 | end
153 | end
154 |
155 | context "when an array of tags" do
156 | let(:first){ described_class.new }
157 | let(:second){ described_class.new }
158 |
159 | before do
160 | element.content = [first, second]
161 | end
162 |
163 | it "sets the content tag" do
164 | expect(element.children.first).to eq(first)
165 | end
166 |
167 | it "sets the tags parent" do
168 | expect(element.children.first.parent).to eq(element)
169 | end
170 | end
171 |
172 | end
173 |
174 | describe "rendering to html" do
175 |
176 | before { @separator = $, }
177 | after { $, = @separator } # rubocop:disable RSpec/InstanceVariable
178 |
179 | let(:collection){ element + "hello world" }
180 |
181 | it "renders the children collection" do
182 | expect(element.children).to receive(:to_s).and_return("content")
183 | expect(element.to_s).to eq("content")
184 | end
185 |
186 | it "renders collection when is set the default separator" do
187 | suppressing_27_warning { $, = "_" }
188 |
189 | expect(collection.to_s).to eq("hello world")
190 | end
191 |
192 | it "renders collection when is not set the default separator" do
193 | expect(collection.to_s).to eq("hello world")
194 | end
195 |
196 | private
197 |
198 | def suppressing_27_warning
199 | return yield unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7.a")
200 |
201 | begin
202 | old_verbose = $VERBOSE
203 | $VERBOSE = nil
204 | yield
205 | ensure
206 | $VERBOSE = old_verbose
207 | end
208 | end
209 | end
210 |
211 | describe "adding elements together" do
212 |
213 | context "when both elements are tags" do
214 | let(:first){ described_class.new }
215 | let(:second){ described_class.new }
216 | let(:collection){ first + second }
217 |
218 | it "returns an instance of Collection" do
219 | expect(collection).to be_an_instance_of(Arbre::ElementCollection)
220 | end
221 |
222 | it "returns the elements in the collection" do
223 | expect(collection.size).to eq(2)
224 | expect(collection.first).to eq(first)
225 | expect(collection[1]).to eq(second)
226 | end
227 | end
228 |
229 | context "when the left is a collection and the right is a tag" do
230 | let(:first){ described_class.new }
231 | let(:second){ described_class.new }
232 | let(:third){ described_class.new }
233 | let(:collection){ Arbre::ElementCollection.new([first, second]) + third }
234 |
235 | it "returns an instance of Collection" do
236 | expect(collection).to be_an_instance_of(Arbre::ElementCollection)
237 | end
238 |
239 | it "returns the elements in the collection flattened" do
240 | expect(collection.size).to eq(3)
241 | expect(collection[0]).to eq(first)
242 | expect(collection[1]).to eq(second)
243 | expect(collection[2]).to eq(third)
244 | end
245 | end
246 |
247 | context "when the right is a collection and the left is a tag" do
248 | let(:first){ described_class.new }
249 | let(:second){ described_class.new }
250 | let(:third){ described_class.new }
251 | let(:collection){ first + Arbre::ElementCollection.new([second,third]) }
252 |
253 | it "returns an instance of Collection" do
254 | expect(collection).to be_an_instance_of(Arbre::ElementCollection)
255 | end
256 |
257 | it "returns the elements in the collection flattened" do
258 | expect(collection.size).to eq(3)
259 | expect(collection[0]).to eq(first)
260 | expect(collection[1]).to eq(second)
261 | expect(collection[2]).to eq(third)
262 | end
263 | end
264 |
265 | context "when the left is a tag and the right is a string" do
266 | let(:element){ described_class.new }
267 | let(:collection){ element + "Hello World"}
268 |
269 | it "returns an instance of Collection" do
270 | expect(collection).to be_an_instance_of(Arbre::ElementCollection)
271 | end
272 |
273 | it "returns the elements in the collection" do
274 | expect(collection.size).to eq(2)
275 | expect(collection[0]).to eq(element)
276 | expect(collection[1]).to be_an_instance_of(Arbre::HTML::TextNode)
277 | end
278 | end
279 |
280 | context "when the left is a string and the right is a tag" do
281 | let(:collection){ "hello World" + described_class.new}
282 |
283 | it "returns a string" do
284 | expect(collection.strip.chomp).to eq("hello World")
285 | end
286 | end
287 | end
288 |
289 | end
290 |
--------------------------------------------------------------------------------
/spec/arbre/unit/html/class_list_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'spec_helper'
3 |
4 | describe Arbre::HTML::ClassList do
5 |
6 | describe ".build_from_string" do
7 |
8 | it "builds a new list from a string of classes" do
9 | list = described_class.build_from_string("first second")
10 | expect(list.size).to eq(2)
11 |
12 | expect(list).to match_array(%w{first second})
13 | end
14 |
15 | end
16 |
17 | end
18 |
--------------------------------------------------------------------------------
/spec/arbre/unit/html/document_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'spec_helper'
3 |
4 | describe Arbre::HTML::Document do
5 | let(:document){ described_class.new }
6 |
7 | describe "#to_s" do
8 | subject { document.to_s }
9 |
10 | before do
11 | document.build
12 | end
13 |
14 | it { is_expected.to eq "\n" }
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/arbre/unit/html/tag_attributes_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'spec_helper'
3 |
4 | describe Arbre::HTML::Tag, "Attributes" do
5 |
6 | let(:tag){ described_class.new }
7 |
8 | describe "attributes" do
9 |
10 | before { tag.build id: "my_id" }
11 |
12 | it "has an attributes hash" do
13 | expect(tag.attributes).to eq({id: "my_id"})
14 | end
15 |
16 | describe "#to_s" do
17 | it "renders the attributes to html" do
18 | expect(tag.to_s).to eq "\n"
19 | end
20 |
21 | it "renders attributes that are empty but not nil" do
22 | tag.class_list # initializes an empty ClassList
23 | tag.set_attribute :foo, ''
24 | tag.set_attribute :bar, nil
25 |
26 | expect(tag.to_s).to eq "\n"
27 | end
28 |
29 | context "with hyphenated attributes" do
30 | before { tag.build id: "my_id", "data-method" => "get", "data-remote" => true }
31 |
32 | it "renders the attributes to html" do
33 | expect(tag.to_s).to eq "\n"
34 | end
35 | end
36 |
37 | context "when there is a nested attribute" do
38 | before { tag.build id: "my_id", data: { action: 'some_action' } }
39 |
40 | it "flattens the attributes when rendering to html" do
41 | expect(tag.to_s).to eq "\n"
42 | end
43 |
44 | it "renders attributes that are empty but not nil" do
45 | tag.class_list # initializes an empty ClassList
46 | tag.set_attribute :foo, { bar: '' }
47 | tag.set_attribute :bar, { baz: nil }
48 |
49 | expect(tag.to_s).to eq "\n"
50 | end
51 | end
52 |
53 | context "when there is a deeply nested attribute" do
54 | before { tag.build id: "my_id", foo: { bar: { bat: nil, baz: 'foozle' } } }
55 |
56 | it "flattens the attributes when rendering to html" do
57 | expect(tag.to_s).to eq "\n"
58 | end
59 | end
60 |
61 | context "when there are multiple nested attributes" do
62 | before { tag.build id: "my_id", foo: { bar: 'foozle1', bat: nil, baz: '' } }
63 |
64 | it "flattens the attributes when rendering to html" do
65 | expect(tag.to_s).to eq "\n"
66 | end
67 | end
68 | end
69 |
70 | it "gets an attribute value" do
71 | expect(tag.attr(:id)).to eq("my_id")
72 | end
73 |
74 | describe "#has_attribute?" do
75 | context "when the attribute exists" do
76 | it "returns true" do
77 | expect(tag.has_attribute?(:id)).to be(true)
78 | end
79 | end
80 |
81 | context "when the attribute does not exist" do
82 | it "returns false" do
83 | expect(tag.has_attribute?(:class)).to be(false)
84 | end
85 | end
86 | end
87 |
88 | it "removes an attribute" do
89 | expect(tag.attributes).to eq({id: "my_id"})
90 | expect(tag.remove_attribute(:id)).to eq("my_id")
91 | expect(tag.attributes).to eq({})
92 | end
93 | end
94 |
95 | describe "rendering attributes" do
96 | it "escapes attribute values" do
97 | tag.set_attribute(:class, '">bad things!')
98 | expect(tag.to_s).to eq "\n"
99 | end
100 |
101 | it "escapes attribute names" do
102 | tag.set_attribute(">bad", "things")
103 | expect(tag.to_s).to eq "\n"
104 | end
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/spec/arbre/unit/html/tag_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'spec_helper'
3 |
4 | describe Arbre::HTML::Tag do
5 |
6 | let(:tag){ described_class.new }
7 |
8 | describe "building a new tag" do
9 | before { tag.build "Hello World", id: "my_id" }
10 |
11 | it "sets the contents to a string" do
12 | expect(tag.content).to eq("Hello World")
13 | end
14 |
15 | it "sets the hash of options to the attributes" do
16 | expect(tag.attributes).to eq({ id: "my_id" })
17 | end
18 | end
19 |
20 | describe "creating a tag 'for' an object" do
21 | # rubocop:disable RSpec/VerifiedDoubles
22 | let(:model_name){ double(singular: "resource_class")}
23 | let(:resource_class){ double(model_name: model_name) }
24 | let(:resource){ double(class: resource_class, to_key: ['5'])}
25 | # rubocop:enable RSpec/VerifiedDoubles
26 |
27 | before do
28 | tag.build for: resource
29 | end
30 |
31 | it "sets the id to the type and id" do
32 | expect(tag.id).to eq("resource_class_5")
33 | end
34 |
35 | it "adds a class name" do
36 | expect(tag.class_list).to include("resource_class")
37 | end
38 |
39 | describe "for an object that doesn't have a model_name" do
40 | let(:resource_class){ double(name: 'ResourceClass') } # rubocop:disable RSpec/VerifiedDoubles
41 |
42 | before do
43 | tag.build for: resource
44 | end
45 |
46 | it "sets the id to the type and id" do
47 | expect(tag.id).to eq("resource_class_5")
48 | end
49 |
50 | it "adds a class name" do
51 | expect(tag.class_list).to include("resource_class")
52 | end
53 | end
54 |
55 | describe "with a default_id_for_prefix" do
56 |
57 | let(:tag) do
58 | Class.new(Arbre::HTML::Tag) do
59 | def default_id_for_prefix
60 | "a_prefix"
61 | end
62 | end.new
63 | end
64 |
65 | it "sets the id to the type and id" do
66 | expect(tag.id).to eq("a_prefix_resource_class_5")
67 | end
68 |
69 | end
70 | end
71 |
72 | describe "creating a tag with a for attribute" do
73 | it "sets the `for` attribute when a string is given" do
74 | tag.build for: "email"
75 | expect(tag.attributes[:for]).to eq "email"
76 | end
77 |
78 | it "sets the `for` attribute when a symbol is given" do
79 | tag.build for: :email
80 | expect(tag.attributes[:for]).to eq :email
81 | end
82 | end
83 |
84 | describe "css class names" do
85 |
86 | it "adds a class" do
87 | tag.add_class "hello_world"
88 | expect(tag.class_names).to eq("hello_world")
89 | end
90 |
91 | it "removes classes" do
92 | tag.add_class "hello_world"
93 | expect(tag.class_names).to eq("hello_world")
94 | tag.remove_class "hello_world"
95 | expect(tag.class_names).to eq("")
96 | end
97 |
98 | it "does not add a class if it already exists" do
99 | tag.add_class "hello_world"
100 | tag.add_class "hello_world"
101 | expect(tag.class_names).to eq("hello_world")
102 | end
103 |
104 | it "separates classes with space" do
105 | tag.add_class "hello world"
106 | expect(tag.class_list.size).to eq(2)
107 | end
108 |
109 | it "creates a class list from a string" do
110 | tag = described_class.new
111 | tag.build(class: "first-class")
112 | tag.add_class "second-class"
113 | expect(tag.class_list.size).to eq(2)
114 | end
115 |
116 | end
117 |
118 | end
119 |
--------------------------------------------------------------------------------
/spec/arbre/unit/html/text_node_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'spec_helper'
3 |
4 | describe Arbre::HTML::TextNode do
5 | let(:text_node){ described_class.new }
6 |
7 | describe '#class_list' do
8 | subject { text_node.class_list }
9 |
10 | it { is_expected.to be_empty }
11 | end
12 |
13 | describe '#tag_name' do
14 | subject { text_node.tag_name }
15 |
16 | it { is_expected.to be_nil }
17 | end
18 |
19 | describe '#to_s' do
20 | subject { text_node.build('Test').to_s }
21 |
22 | it { is_expected.to eq 'Test' }
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/changelog_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'spec_helper'
3 |
4 | RSpec.describe "Changelog" do
5 | subject(:changelog) do
6 | path = File.join(File.dirname(__dir__), "CHANGELOG.md")
7 | File.read(path)
8 | end
9 |
10 | it 'has definitions for all implicit links' do
11 | implicit_link_names = changelog.scan(/\[([^\]]+)\]\[\]/).flatten.uniq
12 | implicit_link_names.each do |name|
13 | expect(changelog).to include("[#{name}]: https")
14 | end
15 | end
16 |
17 | describe 'entry' do
18 | subject(:entries) { lines.grep(/^\*/) }
19 |
20 | let(:lines) { changelog.each_line }
21 |
22 | it 'does not end with a punctuation' do
23 | entries.each do |entry|
24 | expect(entry).not_to match(/\.$/)
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/gemspec_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require "spec_helper"
3 | require "open3"
4 | require "arbre/version"
5 |
6 | RSpec.describe "Gemspec" do
7 | after do
8 | File.delete("arbre-#{Arbre::VERSION}.gem")
9 | end
10 |
11 | let(:build) do
12 | Bundler.with_original_env do
13 | Open3.capture3("gem build arbre")
14 | end
15 | end
16 |
17 | it "succeeds" do
18 | expect(build[2]).to be_success
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/rails/integration/forms_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'rails/rails_spec_helper'
3 |
4 | RSpec.describe "Forms" do
5 |
6 | let(:assigns){ {} }
7 | let(:helpers){ mock_action_view }
8 | let(:html) { form.to_s }
9 |
10 | describe "building a simple form for" do
11 |
12 | let(:form) do
13 | arbre do
14 | form_for MockPerson.new, url: "/" do |f|
15 | f.label :name
16 | f.text_field :name
17 | end
18 | end
19 | end
20 |
21 | it "builds a form" do
22 | expect(html).to have_css("form")
23 | end
24 |
25 | it "includes the hidden authenticity token" do
26 | expect(html).to have_field("authenticity_token", type: :hidden, with: "AUTH_TOKEN")
27 | end
28 |
29 | it "creates a label" do
30 | expect(html).to have_css("form label[for=mock_person_name]")
31 | end
32 |
33 | it "creates a text field" do
34 | expect(html).to have_css("form input[type=text]")
35 | end
36 |
37 | end
38 |
39 | describe "building a form with fields for" do
40 |
41 | let(:form) do
42 | arbre do
43 | form_for MockPerson.new, url: "/" do |f|
44 | f.label :name
45 | f.text_field :name
46 | f.fields_for :permission do |pf|
47 | pf.label :admin
48 | pf.check_box :admin
49 | end
50 | end
51 | end
52 | end
53 |
54 | it "renders nested label" do
55 | expect(html).to have_css("form label[for=mock_person_permission_admin]", text: "Admin")
56 | end
57 |
58 | it "renders nested input" do
59 | expect(html).to have_css("form input[type=checkbox][name='mock_person[permission][admin]']")
60 | end
61 |
62 | it "does not render a div for the proxy" do
63 | expect(html).to have_no_css("form div.fields_for_proxy")
64 | end
65 |
66 | end
67 |
68 | describe "forms with other elements" do
69 | let(:form) do
70 | arbre do
71 | form_for MockPerson.new, url: "/" do |f|
72 |
73 | div do
74 | f.label :name
75 | f.text_field :name
76 | end
77 |
78 | para do
79 | f.label :name
80 | f.text_field :name
81 | end
82 |
83 | div class: "permissions" do
84 | f.fields_for :permission do |pf|
85 | div class: "permissions_label" do
86 | pf.label :admin
87 | end
88 | pf.check_box :admin
89 | end
90 | end
91 |
92 | end
93 | end
94 | end
95 |
96 | it "nests elements" do
97 | expect(html).to have_css("form > p > label")
98 | end
99 |
100 | it "nests elements within fields for" do
101 | expect(html).to have_css("form > div.permissions > div.permissions_label label")
102 | end
103 | end
104 |
105 | end
106 |
--------------------------------------------------------------------------------
/spec/rails/integration/rendering_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'rails/rails_spec_helper'
3 |
4 | ARBRE_VIEWS_PATH = File.expand_path("../../templates", __FILE__)
5 |
6 | class TestController < ActionController::Base
7 | append_view_path ARBRE_VIEWS_PATH
8 |
9 | def render_empty
10 | render "arbre/empty"
11 | end
12 |
13 | def render_simple_page
14 | render "arbre/simple_page"
15 | end
16 |
17 | def render_partial
18 | render "arbre/page_with_partial"
19 | end
20 |
21 | def render_erb_partial
22 | render "arbre/page_with_erb_partial"
23 | end
24 |
25 | def render_with_instance_variable
26 | @my_instance_var = "From Instance Var"
27 | render "arbre/page_with_assignment"
28 | end
29 |
30 | def render_partial_with_instance_variable
31 | @my_instance_var = "From Instance Var"
32 | render "arbre/page_with_arb_partial_and_assignment"
33 | end
34 |
35 | def render_with_block
36 | render "arbre/page_with_render_with_block"
37 | end
38 | end
39 |
40 | RSpec.describe TestController, "Rendering with Arbre", type: :request do
41 | let(:body){ response.body }
42 |
43 | before do
44 | Rails.application.routes.draw do
45 | get 'test/render_empty', controller: "test"
46 | get 'test/render_simple_page', controller: "test"
47 | get 'test/render_partial', controller: "test"
48 | get 'test/render_erb_partial', controller: "test"
49 | get 'test/render_with_instance_variable', controller: "test"
50 | get 'test/render_partial_with_instance_variable', controller: "test"
51 | get 'test/render_page_with_helpers', controller: "test"
52 | get 'test/render_with_block', controller: "test"
53 | end
54 | end
55 |
56 | after do
57 | Rails.application.reload_routes!
58 | end
59 |
60 | it "renders the empty template" do
61 | get "/test/render_empty"
62 | expect(response).to be_successful
63 | end
64 |
65 | it "renders a simple page" do
66 | get "/test/render_simple_page"
67 | expect(response).to be_successful
68 | expect(body).to have_css("h1", text: "Hello World")
69 | expect(body).to have_css("p", text: "Hello again!")
70 | end
71 |
72 | it "renders an arb partial" do
73 | get "/test/render_partial"
74 | expect(response).to be_successful
75 | expect(body).to eq <<~HTML
76 |
Before Partial
77 |
Hello from a partial
78 |
After Partial
79 | HTML
80 | end
81 |
82 | it "renders an erb (or other) partial" do
83 | get "/test/render_erb_partial"
84 | expect(response).to be_successful
85 | expect(body).to eq <<~HTML
86 |
Before Partial
87 |
Hello from an erb partial
88 |
After Partial
89 | HTML
90 | end
91 |
92 | it "renders with instance variables" do
93 | get "/test/render_with_instance_variable"
94 | expect(response).to be_successful
95 | expect(body).to have_css("h1", text: "From Instance Var")
96 | end
97 |
98 | it "renders an arbre partial with assignments" do
99 | get "/test/render_partial_with_instance_variable"
100 | expect(response).to be_successful
101 | expect(body).to have_css("p", text: "Partial: From Instance Var")
102 | end
103 |
104 | it "renders with a block" do
105 | get "/test/render_with_block"
106 | expect(response).to be_successful
107 | expect(body).to eq <<~HTML
108 |