arbre2 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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