arbre2 2.1.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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +30 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +75 -0
  7. data/Gemfile +2 -0
  8. data/Gemfile.lock +93 -0
  9. data/LICENSE +20 -0
  10. data/README.md +92 -0
  11. data/Rakefile +7 -0
  12. data/arbre.gemspec +28 -0
  13. data/lib/arbre/child_element_collection.rb +86 -0
  14. data/lib/arbre/container.rb +20 -0
  15. data/lib/arbre/context.rb +83 -0
  16. data/lib/arbre/element/building.rb +151 -0
  17. data/lib/arbre/element.rb +194 -0
  18. data/lib/arbre/element_collection.rb +93 -0
  19. data/lib/arbre/html/attributes.rb +91 -0
  20. data/lib/arbre/html/class_list.rb +53 -0
  21. data/lib/arbre/html/comment.rb +47 -0
  22. data/lib/arbre/html/document.rb +93 -0
  23. data/lib/arbre/html/html_tags.rb +67 -0
  24. data/lib/arbre/html/querying.rb +256 -0
  25. data/lib/arbre/html/tag.rb +317 -0
  26. data/lib/arbre/rails/layouts.rb +126 -0
  27. data/lib/arbre/rails/legacy_document.rb +29 -0
  28. data/lib/arbre/rails/rendering.rb +76 -0
  29. data/lib/arbre/rails/rspec/arbre_support.rb +61 -0
  30. data/lib/arbre/rails/rspec.rb +2 -0
  31. data/lib/arbre/rails/template_handler.rb +32 -0
  32. data/lib/arbre/rails.rb +35 -0
  33. data/lib/arbre/rspec/be_rendered_as_matcher.rb +103 -0
  34. data/lib/arbre/rspec/be_scripted_as_matcher.rb +68 -0
  35. data/lib/arbre/rspec/contain_script_matcher.rb +64 -0
  36. data/lib/arbre/rspec.rb +3 -0
  37. data/lib/arbre/text_node.rb +35 -0
  38. data/lib/arbre/version.rb +3 -0
  39. data/lib/arbre.rb +27 -0
  40. data/spec/arbre/integration/html_document_spec.rb +90 -0
  41. data/spec/arbre/integration/html_spec.rb +283 -0
  42. data/spec/arbre/integration/querying_spec.rb +187 -0
  43. data/spec/arbre/integration/rails_spec.rb +183 -0
  44. data/spec/arbre/rails/rspec/arbre_support_spec.rb +75 -0
  45. data/spec/arbre/rspec/be_rendered_as_matcher_spec.rb +80 -0
  46. data/spec/arbre/rspec/be_scripted_as_matcher_spec.rb +61 -0
  47. data/spec/arbre/rspec/contain_script_matcher_spec.rb +40 -0
  48. data/spec/arbre/support/arbre_example_group.rb +0 -0
  49. data/spec/arbre/unit/child_element_collection_spec.rb +146 -0
  50. data/spec/arbre/unit/container_spec.rb +23 -0
  51. data/spec/arbre/unit/context_spec.rb +95 -0
  52. data/spec/arbre/unit/element/building_spec.rb +300 -0
  53. data/spec/arbre/unit/element_collection_spec.rb +169 -0
  54. data/spec/arbre/unit/element_spec.rb +297 -0
  55. data/spec/arbre/unit/html/attributes_spec.rb +219 -0
  56. data/spec/arbre/unit/html/class_list_spec.rb +109 -0
  57. data/spec/arbre/unit/html/comment_spec.rb +42 -0
  58. data/spec/arbre/unit/html/querying_spec.rb +32 -0
  59. data/spec/arbre/unit/html/tag_spec.rb +300 -0
  60. data/spec/arbre/unit/rails/layouts_spec.rb +127 -0
  61. data/spec/arbre/unit/text_node_spec.rb +40 -0
  62. data/spec/rails/app/controllers/example_controller.rb +18 -0
  63. data/spec/rails/app/views/example/_arbre_partial.html.arb +7 -0
  64. data/spec/rails/app/views/example/_erb_partial.html.erb +1 -0
  65. data/spec/rails/app/views/example/arbre.html.arb +1 -0
  66. data/spec/rails/app/views/example/arbre_partial_result.html.arb +3 -0
  67. data/spec/rails/app/views/example/erb.html.erb +5 -0
  68. data/spec/rails/app/views/example/erb_partial_result.html.arb +3 -0
  69. data/spec/rails/app/views/example/partials.html.arb +11 -0
  70. data/spec/rails/app/views/layouts/empty.html.arb +1 -0
  71. data/spec/rails/app/views/layouts/with_title.html.arb +5 -0
  72. data/spec/rails/config/routes.rb +4 -0
  73. data/spec/rails_spec_helper.rb +13 -0
  74. data/spec/spec_helper.rb +20 -0
  75. data/spec/support/arbre_example_group.rb +19 -0
  76. metadata +254 -0
@@ -0,0 +1,151 @@
1
+ module Arbre
2
+ class Element
3
+
4
+ # Dynamic module onto which builder methods can be defined.
5
+ module BuilderMethods
6
+ end
7
+
8
+ # Element building concern. Contains methods pertaining to building and inserting child
9
+ # elements.
10
+ module Building
11
+
12
+ ######
13
+ # Builder methods
14
+
15
+ def self.included(klass)
16
+ klass.send :include, BuilderMethods
17
+ klass.send :extend, BuilderMethod
18
+ end
19
+
20
+ ######
21
+ # Builder method DSL
22
+
23
+ module BuilderMethod
24
+
25
+ def builder_method(method_name)
26
+ BuilderMethods.class_eval <<-EOF, __FILE__, __LINE__
27
+ def #{method_name}(*args, &block)
28
+ insert ::#{self.name}, *args, &block
29
+ end
30
+ EOF
31
+ end
32
+
33
+ end
34
+
35
+ ######
36
+ # Building & inserting elements
37
+
38
+ # Builds an element of the given class using the given arguments and block, in the
39
+ # same arbre context as this element.
40
+ def build(klass, *args, &block)
41
+ element = klass.new(arbre_context)
42
+ within(element) { element.build! *args, &block }
43
+ element
44
+ end
45
+
46
+ # Builds an element of the given class using the given arguments and block, in the
47
+ # same arbre context as this element, and adds it to the current arbre element's
48
+ # children.
49
+ def insert(klass, *args, &block)
50
+ element = klass.new(arbre_context)
51
+ current_element.insert_child element
52
+ within(element) { element.build! *args, &block }
53
+ element
54
+ end
55
+
56
+ ######
57
+ # Flow
58
+
59
+ # Executes a block within the context of the given element, or DOM query.
60
+ def append_within(element, &block)
61
+ element = find(element).first if element.is_a?(String)
62
+ arbre_context.with_current element: element, flow: :append, &block
63
+ end
64
+ alias_method :within, :append_within
65
+
66
+ # Executes a block within the context of the given element, or DOM query. All elements
67
+ # are prepended.
68
+ def prepend_within(element, &block)
69
+ element = find(element).first if element.is_a?(String)
70
+ arbre_context.with_current element: element, flow: :prepend, &block
71
+ end
72
+
73
+ %w(append prepend).each do |flow|
74
+ class_eval <<-RUBY, __FILE__, __LINE__+1
75
+ def #{flow}(klass = nil, *args, &block)
76
+ arbre_context.with_current element: current_element, flow: :#{flow} do
77
+ insert_or_call_block klass, *args, &block
78
+ end
79
+ end
80
+ RUBY
81
+ end
82
+
83
+ %w(after before).each do |flow|
84
+ class_eval <<-RUBY, __FILE__, __LINE__+1
85
+ def #{flow}(element, klass = nil, *args, &block)
86
+ element = find(element).first if element.is_a?(String)
87
+
88
+ arbre_context.with_current element: element.parent, flow: [ :#{flow}, element ] do
89
+ insert_or_call_block klass, *args, &block
90
+ end
91
+ end
92
+ RUBY
93
+ end
94
+
95
+ def insert_or_call_block(klass, *args, &block)
96
+ if klass
97
+ insert klass, *args, &block
98
+ else
99
+ yield
100
+ end
101
+ end
102
+ private :insert_or_call_block
103
+
104
+ # Inserts a child element at the right place in the child array, taking the current
105
+ # flow into account.
106
+ def insert_child(child)
107
+ case current_flow
108
+ when :append
109
+ children << child
110
+
111
+ when :prepend
112
+ children.insert_at 0, child
113
+
114
+ # Update the flow - the next element should be added after this one, not be
115
+ # prepended.
116
+ arbre_context.replace_current_flow [:after, child]
117
+
118
+ else
119
+ # flow: [ :before, element ] or [ :after, element ]
120
+ operation, element = current_flow
121
+ children.send :"insert_#{operation}", element, child
122
+
123
+ if operation == :after
124
+ # Now that we've inserted something after the element, we need to
125
+ # make sure that the next element we insert will be after this one.
126
+ arbre_context.replace_current_flow [:after, child]
127
+ end
128
+ end
129
+ end
130
+
131
+ ######
132
+ # Support methods
133
+
134
+ # Builds a temporary container using the given block, but doesn't add it to the tree.
135
+ # The block is executed within the current context.
136
+ def temporary(&block)
137
+ build Element, &block
138
+ end
139
+
140
+ def current_element
141
+ arbre_context.current_element
142
+ end
143
+
144
+ def current_flow
145
+ arbre_context.current_flow
146
+ end
147
+
148
+ end
149
+
150
+ end
151
+ end
@@ -0,0 +1,194 @@
1
+ require 'arbre/element/building'
2
+ require 'arbre/html/querying'
3
+
4
+ module Arbre
5
+
6
+ # Base class for all Arbre elements. Rendering is not implemented, and should be implemented
7
+ # by subclasses.
8
+ class Element
9
+ include Building
10
+ include Html::Querying
11
+
12
+ ######
13
+ # Initialization
14
+
15
+ # Initializes a new Arbre element. Pass an existing Arbre context to re-use it.
16
+ def initialize(arbre_context = Arbre::Context.new)
17
+ @_arbre_context = arbre_context
18
+ @_children = ChildElementCollection.new(self)
19
+
20
+ expose_assigns
21
+ end
22
+
23
+ ######
24
+ # Context
25
+
26
+ def arbre_context
27
+ @_arbre_context
28
+ end
29
+
30
+ def assigns
31
+ arbre_context.assigns
32
+ end
33
+
34
+ def helpers
35
+ arbre_context.helpers
36
+ end
37
+
38
+ ######
39
+ # Hierarchy
40
+
41
+ def parent
42
+ @_parent
43
+ end
44
+ def parent=(parent)
45
+ @_parent = parent
46
+ end
47
+
48
+ def children
49
+ @_children
50
+ end
51
+
52
+ # Removes this element from its parent.
53
+ def remove!
54
+ parent.children.remove self if parent
55
+ end
56
+
57
+ def <<(child)
58
+ children << child
59
+ end
60
+
61
+ def has_children?
62
+ children.any?
63
+ end
64
+
65
+ def empty?
66
+ !has_children?
67
+ end
68
+
69
+ def orphan?
70
+ !parent
71
+ end
72
+
73
+ # Retrieves all ancestors (ordered from near to far) for this element.
74
+ # @return [ElementCollection]
75
+ def ancestors
76
+ ancestors = ElementCollection.new
77
+
78
+ unless orphan?
79
+ ancestors << parent
80
+ ancestors.concat parent.ancestors
81
+ end
82
+
83
+ ancestors
84
+ end
85
+
86
+ # Retrieves all descendants (in prefix form) for this element.
87
+ # @return [ElementCollection]
88
+ def descendants
89
+ descendants = ElementCollection.new
90
+ children.each do |child|
91
+ descendants << child
92
+ descendants.concat child.descendants
93
+ end
94
+
95
+ descendants
96
+ end
97
+
98
+ ######
99
+ # Content
100
+
101
+ def content=(content)
102
+ children.clear
103
+ case content
104
+ when Element
105
+ children << content
106
+ when ElementCollection
107
+ children.concat content
108
+ else
109
+ children << TextNode.from_string(content.to_s)
110
+ end
111
+ end
112
+
113
+ def content
114
+ children.to_s
115
+ end
116
+
117
+ ######
118
+ # Set operations
119
+
120
+ def +(element)
121
+ if element.is_a?(Enumerable)
122
+ ElementCollection.new([self] + element)
123
+ else
124
+ ElementCollection.new([ self, element])
125
+ end
126
+ end
127
+
128
+ ######
129
+ # Building & rendering
130
+
131
+ # Override this method to build your element.
132
+ def build!
133
+ yield self if block_given?
134
+
135
+ self
136
+ end
137
+
138
+ def indent_level
139
+ if parent
140
+ parent.indent_level + 1
141
+ else
142
+ 0
143
+ end
144
+ end
145
+
146
+ def to_s
147
+ content
148
+ end
149
+
150
+ def to_html
151
+ to_s
152
+ end
153
+
154
+ # Provide a clean element description when inspect is used.
155
+ def inspect
156
+ "#<#{self.class.name}:0x#{object_id.to_s(16)}>"
157
+ end
158
+
159
+ ######
160
+ # Helpers & assigns accessing
161
+
162
+ def respond_to?(method, include_private = false)
163
+ super || (helpers && helpers.respond_to?(method))
164
+ end
165
+
166
+ private
167
+
168
+ # Exposes the assigns from the context as instance variables to the given target.
169
+ def expose_assigns
170
+ assigns.each do |key, value|
171
+ instance_variable_set "@#{key}", value
172
+ end
173
+ end
174
+
175
+ # Access helper methods from any Arbre element through its context.
176
+ def method_missing(name, *args, &block)
177
+ if helpers && helpers.respond_to?(name)
178
+ define_helper_method name
179
+ send name, *args, &block
180
+ else
181
+ super
182
+ end
183
+ end
184
+
185
+ def define_helper_method(name)
186
+ self.class.class_eval <<-RUBY, __FILE__, __LINE__+1
187
+ def #{name}(*args, &block)
188
+ helpers.send :#{name}, *args, &block
189
+ end
190
+ RUBY
191
+ end
192
+
193
+ end
194
+ end
@@ -0,0 +1,93 @@
1
+ module Arbre
2
+
3
+ class ElementCollection
4
+
5
+ ######
6
+ # Initialization
7
+
8
+ def initialize(elements = [])
9
+ @elements = elements.to_a
10
+ end
11
+
12
+ ######
13
+ # Array proxy
14
+
15
+ include Enumerable
16
+ delegate :each, :empty?, :length, :size, :count, :last, :to => :@elements
17
+ delegate :clear, :to => :@elements
18
+
19
+ def [](*args)
20
+ result = @elements[*args]
21
+ result = self.class.new(result) if result.is_a?(Enumerable)
22
+ result
23
+ end
24
+
25
+ def add(element)
26
+ @elements << element unless include?(element)
27
+ self
28
+ end
29
+ def <<(element)
30
+ add element
31
+ end
32
+
33
+ def concat(elements)
34
+ elements.each do |element|
35
+ self << element
36
+ end
37
+ end
38
+
39
+ def remove(element)
40
+ @elements.delete element
41
+ end
42
+
43
+ def to_ary
44
+ @elements
45
+ end
46
+ def to_a
47
+ @elements.dup
48
+ end
49
+
50
+ def ==(other)
51
+ to_a == other.to_a
52
+ end
53
+
54
+ def eql?(other)
55
+ other.is_a?(ElementCollection) && self == other
56
+ end
57
+
58
+ ######
59
+ # Set operations
60
+
61
+ def +(other)
62
+ self.class.new((@elements + other).uniq)
63
+ end
64
+
65
+ def -(other)
66
+ self.class.new(@elements - other)
67
+ end
68
+
69
+ def &(other)
70
+ self.class.new(@elements & other)
71
+ end
72
+
73
+ ######
74
+ # String conversion
75
+
76
+ def to_s
77
+ html_safe_join(map(&:to_s), "\n")
78
+ end
79
+
80
+ private
81
+
82
+ def html_safe_join(array, delimiter = '')
83
+ ActiveSupport::SafeBuffer.new.tap do |str|
84
+ array.each_with_index do |element, i|
85
+ str << delimiter if i > 0
86
+ str << element
87
+ end
88
+ end
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -0,0 +1,91 @@
1
+ module Arbre
2
+ module Html
3
+
4
+ # HTML attributes hash. Behaves like a hash with some minor differences:
5
+ #
6
+ # - Indifferent access: everything is stored as strings, but also values.
7
+ # - Setting an attribute to +true+ sets it to the name of the attribute, as per the HTML
8
+ # standard.
9
+ # - Setting an attribute to +false+ or +nil+ will remove it.
10
+ class Attributes
11
+
12
+ def initialize(attributes = {})
13
+ @attributes = {}
14
+ update attributes
15
+ end
16
+
17
+ def self.[](*args)
18
+ Attributes.new(Hash[*args])
19
+ end
20
+
21
+ def [](attribute)
22
+ if attribute.to_s == 'class'
23
+ @attributes['class'] ||= ClassList.new
24
+ else
25
+ @attributes[attribute.to_s]
26
+ end
27
+ end
28
+ def []=(attribute, value)
29
+ if attribute.to_s == 'class'
30
+ if value.present?
31
+ @attributes['class'] = ClassList.new(value)
32
+ else
33
+ remove 'class'
34
+ end
35
+ elsif value == true
36
+ @attributes[attribute.to_s] = attribute.to_s
37
+ elsif value
38
+ @attributes[attribute.to_s] = value.to_s
39
+ else
40
+ remove attribute
41
+ end
42
+ end
43
+
44
+ def remove(attribute)
45
+ @attributes.delete attribute.to_s
46
+ end
47
+
48
+ def update(attributes)
49
+ attributes.each { |k, v| self[k] = v }
50
+ end
51
+
52
+ def ==(other)
53
+ to_hash == other.to_hash
54
+ end
55
+
56
+ def eql?(other)
57
+ other.is_a?(Attributes) && self == other
58
+ end
59
+
60
+ def has_key?(key)
61
+ @attributes.has_key?(key.to_s)
62
+ end
63
+
64
+ include Enumerable
65
+ delegate :each, :empty?, :length, :size, :count, :to => :@attributes
66
+
67
+ def pairs
68
+ map do |name, value|
69
+ next if name == 'class' && value.blank?
70
+ "#{html_escape(name)}=\"#{html_escape(value)}\""
71
+ end
72
+ end
73
+
74
+ def to_hash
75
+ @attributes
76
+ end
77
+
78
+ def to_s
79
+ pairs.join(' ').html_safe
80
+ end
81
+
82
+ protected
83
+
84
+ def html_escape(s)
85
+ ERB::Util.html_escape(s)
86
+ end
87
+
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,53 @@
1
+ require 'set'
2
+
3
+ module Arbre
4
+ module Html
5
+
6
+ # Holds a set of classes
7
+ class ClassList < Set
8
+
9
+ def initialize(classes = [])
10
+ super()
11
+ [*classes].each do |cls|
12
+ add cls
13
+ end
14
+ end
15
+
16
+ # Alias to the list itself.
17
+ def classes
18
+ self
19
+ end
20
+
21
+ # Adds one ore more classes to the list. You can pass in a string which is split by space, or
22
+ # an array of some kind.
23
+ def add(classes)
24
+ classes = classes.to_s.split(' ')
25
+ classes.each { |cls| super cls }
26
+ self
27
+ end
28
+ alias << add
29
+
30
+ def remove(classes)
31
+ classes = classes.split(' ')
32
+ classes.each { |cls| delete cls }
33
+ self
34
+ end
35
+ private :delete
36
+
37
+ def concat(classes)
38
+ classes.each { |cls| add cls }
39
+ self
40
+ end
41
+
42
+ def ==(other)
43
+ to_a.sort == other.to_a.sort
44
+ end
45
+
46
+ def to_s
47
+ to_a.join(' ')
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,47 @@
1
+ module Arbre
2
+ module Html
3
+
4
+ class Comment < Element
5
+ builder_method :comment
6
+
7
+ attr_accessor :comment
8
+
9
+ def build!(comment = nil)
10
+ @comment = comment
11
+ end
12
+
13
+ def to_s
14
+ spaces = (' ' * indent_level * Tag::INDENT_SIZE)
15
+
16
+ out = ActiveSupport::SafeBuffer.new
17
+
18
+ if !comment
19
+ out << '<!-- -->'.html_safe
20
+ elsif comment.include?("\n")
21
+ out << spaces << '<!--'.html_safe
22
+ out << "\n"
23
+ out << indent_comment
24
+ out << "\n"
25
+ out << spaces << '-->'.html_safe
26
+ else
27
+ out << '<!-- '.html_safe << comment << ' -->'.html_safe
28
+ end
29
+
30
+ out
31
+ end
32
+
33
+ private
34
+
35
+ def indent_comment
36
+ ActiveSupport::SafeBuffer.new.tap do |out|
37
+ comment.split("\n").each_with_index.map do |line, index|
38
+ out << "\n" unless index == 0
39
+ out << (' ' * (indent_level+1) * Tag::INDENT_SIZE) << line
40
+ end
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,93 @@
1
+ module Arbre
2
+ module Html
3
+
4
+ # Root tag for any Html document.
5
+ #
6
+ # Represents the combination of a doctype, a +head+ tag and a +body+ tag.
7
+ class Document < Tag
8
+
9
+ ######
10
+ # Initialization
11
+
12
+ def initialize(*)
13
+ super
14
+ end
15
+
16
+ ######
17
+ # Building
18
+
19
+ def build!
20
+ append_head
21
+ append_body
22
+
23
+ within body do
24
+ yield self if block_given?
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # Builds up a default head tag.
31
+ def append_head
32
+ @_head = append(Head) do
33
+ meta :"http-equiv" => "Content-Type", :content => "text/html; charset=utf-8"
34
+ end
35
+ end
36
+
37
+ # Builds up a default body tag.
38
+ def append_body
39
+ @_body = append(Body)
40
+ end
41
+
42
+ public
43
+
44
+ ######
45
+ # Head & body accessors
46
+
47
+ def title
48
+ @title ||= title_tag.content
49
+ end
50
+
51
+ def title=(title)
52
+ @title = title_tag.content = title
53
+ end
54
+
55
+ def title_tag
56
+ head.find_first('> title') || within(head) { prepend Title }
57
+ end
58
+ private :title_tag
59
+
60
+ # Adds content to the head tag and/or returns it.
61
+ def head(&block)
62
+ within @_head, &block if block_given?
63
+ @_head
64
+ end
65
+
66
+ # Adds content to the body tag and/or returns it.
67
+ def body(&block)
68
+ within @_body, &block if block_given?
69
+ @_body
70
+ end
71
+
72
+ ######
73
+ # Rendering
74
+
75
+ def tag_name
76
+ 'html'
77
+ end
78
+
79
+ def doctype
80
+ '<!DOCTYPE html>'.html_safe
81
+ end
82
+
83
+ def to_s
84
+ out = ActiveSupport::SafeBuffer.new
85
+ out << doctype
86
+ out << "\n\n"
87
+ out << super
88
+ end
89
+
90
+ end
91
+
92
+ end
93
+ end