hammer_builder 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,205 @@
1
+ require 'cgi'
2
+ require 'active_support/core_ext/class/attribute'
3
+ require 'active_support/core_ext/string/inflections'
4
+
5
+ require 'hammer_builder/dynamic_classes'
6
+ require 'hammer_builder/strings'
7
+ require 'hammer_builder/data'
8
+ require 'hammer_builder/data/html5'
9
+ require "hammer_builder/pool"
10
+ require "hammer_builder/helper"
11
+
12
+ module HammerBuilder
13
+
14
+ # Abstract implementation of Builder
15
+ class Abstract
16
+ extend DynamicClasses
17
+
18
+ require "hammer_builder/abstract/abstract_tag"
19
+ require "hammer_builder/abstract/abstract_single_tag"
20
+ require "hammer_builder/abstract/abstract_double_tag"
21
+
22
+ # << faster then +
23
+ # yield faster then block.call
24
+ # accessing ivar and constant is faster then accesing hash or cvar
25
+ # class_eval faster then define_method
26
+ # beware of strings in methods -> creates a lot of garbage
27
+
28
+
29
+ class_attribute :tags, :instance_writer => false
30
+ self.tags = []
31
+
32
+ protected
33
+
34
+ # defines instance method for +tag+ in builder
35
+ def self.define_tag(tag)
36
+ tag = tag.to_s
37
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
38
+ def #{tag}(*args, &block)
39
+ flush
40
+ @_#{tag}.open(*args, &block)
41
+ end
42
+ RUBY
43
+ self.tags += [tag]
44
+ end
45
+
46
+ public
47
+
48
+
49
+ # current tag being builded
50
+ attr_accessor :_current
51
+ alias_method :current, :_current
52
+ alias_method :current=, :_current=
53
+
54
+
55
+ # creates a new builder
56
+ # This is quite expensive, HammerBuilder::Pool should be used
57
+ def initialize()
58
+ @_output = ""
59
+ @_stack = []
60
+ @_current = nil
61
+ # tag classes initialization
62
+ tags.each do |klass|
63
+ instance_variable_set(:"@_#{klass}", self.class.dynamic_classes[klass.camelize.to_sym].new(self))
64
+ end
65
+ end
66
+
67
+ # escapes +text+ to output
68
+ def text(text)
69
+ flush
70
+ @_output << CGI.escapeHTML(text.to_s)
71
+ end
72
+
73
+ # unescaped +text+ to output
74
+ def raw(text)
75
+ flush
76
+ @_output << text.to_s
77
+ end
78
+
79
+ # inserts +comment+
80
+ def comment(comment)
81
+ flush
82
+ @_output << Strings::COMMENT_START << comment.to_s << Strings::COMMENT_END
83
+ end
84
+
85
+ # insersts CDATA with +content+
86
+ def cdata(content)
87
+ flush
88
+ @_output << Strings::CDATA_START << content.to_s << Strings::CDATA_END
89
+ end
90
+
91
+ # renders html5 doc type
92
+ # @example
93
+ # html5 # => <!DOCTYPE html>
94
+ def html5
95
+ raw "<!DOCTYPE html>\n"
96
+ end
97
+
98
+ # resets the builder to the state after creation - much faster then creating a new one
99
+ def reset
100
+ flush
101
+ @_output.clear
102
+ @_stack.clear
103
+ self
104
+ end
105
+
106
+ #def capture
107
+ # flush
108
+ # _output = @_output.clone
109
+ # _stack = @_stack.clone
110
+ # @_output.clear
111
+ # @_stack.clear
112
+ # yield
113
+ # to_html
114
+ #ensure
115
+ # @_output.replace _output
116
+ # @_stack.replace _stack
117
+ #end
118
+
119
+ # enables you to evaluate +block+ inside the builder with +variables+
120
+ # @example
121
+ # HammerBuilder::Formatted.new.go_in('asd') do |string|
122
+ # div string
123
+ # end.to_html! #=> "<div>asd</div>"
124
+ #
125
+ def go_in(*variables, &block)
126
+ instance_exec *variables, &block
127
+ self
128
+ end
129
+
130
+ alias_method :dive, :go_in
131
+
132
+ # sets instance variables when block is yielded
133
+ # @param [Hash{String => Object}] instance_variables hash of names and values to set
134
+ # @yield block when variables are set, variables are cleaned up afterwards
135
+ def set_variables(instance_variables)
136
+ instance_variables.each { |name, value| instance_variable_set("@#{name}", value) }
137
+ yield(self)
138
+ instance_variables.each { |name, _| remove_instance_variable("@#{name}") }
139
+ self
140
+ end
141
+
142
+ # @return [String] output
143
+ def to_html()
144
+ flush
145
+ @_output.clone
146
+ end
147
+
148
+ # flushes open tag
149
+ # @api private
150
+ def flush
151
+ if @_current
152
+ @_current.flush
153
+ @_current = nil
154
+ end
155
+ end
156
+
157
+ # renders +object+ with +method+
158
+ # @param [Object] object an object to render
159
+ # @param [Symbol] method a method name which is used for rendering
160
+ # @param args arguments passed to rendering method
161
+ # @yield block passed to rendering method
162
+ def render(object, method, *args, &block)
163
+ object.__send__ method, self, *args, &block
164
+ self
165
+ end
166
+
167
+ alias_method :r, :render
168
+
169
+ # renders js
170
+ # @option options [Boolean] :cdata (false) should cdata be used?
171
+ # @example
172
+ # js 'a_js_function();' #=> <script type="text/javascript">a_js_function();</script>
173
+ def js(js, options = { })
174
+ use_cdata = options.delete(:cdata) || false
175
+ script({ :type => "text/javascript" }.merge(options)) { use_cdata ? cdata(js) : text(js) }
176
+ end
177
+
178
+ # joins and renders +collection+ with +glue+
179
+ # @param [Array<Proc, Object>] collection of objects or lambdas
180
+ # @param [Proc, String] glue can be String which is rendered with #text or block to render
181
+ # @yield how to render objects from +collection+, Proc in collection does not use this block
182
+ # @example
183
+ # join([1, 1.2], lambda { text ', ' }) {|o| text o } # => "1, 1.2"
184
+ # join([1, 1.2], ', ') {|o| text o } # => "1, 1.2"
185
+ # join([->{ text 1 }, 1.2], ', ') {|o| text o } # => "1, 1.2"
186
+ def join(collection, glue = nil, &it)
187
+ # TODO as helper? two block method call #join(collection, &item).with(&glue)
188
+ glue_block = case glue
189
+ when String
190
+ lambda { text glue }
191
+ when Proc
192
+ glue
193
+ else
194
+ lambda { }
195
+ end
196
+
197
+ collection.each_with_index do |obj, i|
198
+ glue_block.call() if i > 0
199
+ obj.is_a?(Proc) ? obj.call : it.call(obj)
200
+ end
201
+ end
202
+
203
+
204
+ end
205
+ end
@@ -0,0 +1,148 @@
1
+ module HammerBuilder
2
+ class Abstract
3
+ dynamic_classes do
4
+
5
+ def_class :AbstractDoubleTag, :AbstractTag do ###import
6
+ nil
7
+
8
+ # defined by class_eval because there is a error cased by super
9
+ # super from singleton method that is defined to multiple classes is not supported;
10
+ # this will be fixed in 1.9.3 or later (NotImplementedError)
11
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
12
+ # @api private
13
+ def initialize(builder)
14
+ super
15
+ @content = nil
16
+ end
17
+
18
+ # allows data-* attributes and id, classes by method_missing
19
+ def method_missing(method, *args, &block)
20
+ method = method.to_s
21
+ if method =~ METHOD_MISSING_REGEXP
22
+ if $1
23
+ self.rclass.add_attributes Data::Attribute.new(method.to_sym, :string)
24
+ self.send method, *args, &block
25
+ else
26
+ self.content(args[0]) if args[0]
27
+ self.__send__($3 == '!' ? :id : :class, $2, &block)
28
+ end
29
+ else
30
+ super(method, *args, &block)
31
+ end
32
+ end
33
+
34
+ # @api private
35
+ def open(*args, &block)
36
+ attributes = if args.last.is_a?(Hash)
37
+ args.pop
38
+ end
39
+ content args[0]
40
+ super attributes
41
+ @stack << @tag_name
42
+ if block
43
+ with &block
44
+ else
45
+ self
46
+ end
47
+ end
48
+ RUBY
49
+
50
+ # @api private
51
+ # closes the tag
52
+ def flush
53
+ flush_classes
54
+ @output << Strings::GT
55
+ @output << CGI.escapeHTML(@content) if @content
56
+ @output << Strings::SLASH_LT << @stack.pop << Strings::GT
57
+ @content = nil
58
+ end
59
+
60
+ # sets content of the double tag
61
+ # @example
62
+ # div 'content' # => <div>content</div>
63
+ # div.content 'content' # => <div>content</div>
64
+ # div :content => 'content' # => <div>content</div>
65
+ def content(content)
66
+ @content = content.to_s
67
+ self
68
+ end
69
+
70
+ # renders content of the double tag with block
71
+ # @yield content of the tag
72
+ # @example
73
+ # div { text 'content' } # => <div>content</div>
74
+ # div :id => 'id' do
75
+ # text 'content'
76
+ # end # => <div id="id">content</div>
77
+ def with
78
+ flush_classes
79
+ @output << Strings::GT
80
+ @content = nil
81
+ @builder.current = nil
82
+ yield
83
+ #if (content = yield).is_a?(String)
84
+ # @output << EscapeUtils.escape_html(content)
85
+ #end
86
+ @builder.flush
87
+ @output << Strings::SLASH_LT << @stack.pop << Strings::GT
88
+ nil
89
+ end
90
+
91
+ alias_method :w, :with
92
+
93
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
94
+ def mimic(obj, &block)
95
+ super(obj, &nil)
96
+ return with(&block) if block
97
+ self
98
+ end
99
+
100
+ def data(hash, &block)
101
+ super(hash, &nil)
102
+ return with(&block) if block
103
+ self
104
+ end
105
+
106
+ def attribute(name, value, &block)
107
+ super(name, value, &nil)
108
+ return with(&block) if block
109
+ self
110
+ end
111
+
112
+ def attributes(attrs, &block)
113
+ super(attrs, &nil)
114
+ return with(&block) if block
115
+ self
116
+ end
117
+ RUBY
118
+
119
+ protected
120
+
121
+ # @api private
122
+ def self.define_attribute_method(attribute)
123
+ return if instance_methods(false).include?(attribute.name)
124
+ name = attribute.name.to_s
125
+
126
+ if instance_methods.include?(attribute.name)
127
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
128
+ def #{name}(*args, &block)
129
+ super(*args, &nil)
130
+ return with(&block) if block
131
+ self
132
+ end
133
+ RUBY
134
+ else
135
+ content_rendering = attribute_content_rendering(attribute)
136
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
137
+ def #{name}(content#{' = true' if attribute.type == :boolean}, &block)
138
+ #{content_rendering}
139
+ return with(&block) if block
140
+ self
141
+ end
142
+ RUBY
143
+ end
144
+ end
145
+ end ###import
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,18 @@
1
+ module HammerBuilder
2
+ class Abstract
3
+ dynamic_classes do
4
+ def_class :AbstractSingleTag, :AbstractTag do ###import
5
+ nil
6
+
7
+ # @api private
8
+ # closes the tag
9
+ def flush
10
+ flush_classes
11
+ @output << Strings::SLASH_GT
12
+ nil
13
+ end
14
+ end ###import
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,233 @@
1
+ module HammerBuilder
2
+ class Abstract
3
+ dynamic_classes do
4
+ def_class :AbstractTag do ###import
5
+
6
+ class_attribute :_attributes, :instance_writer => false, :instance_reader => false
7
+ self._attributes = []
8
+
9
+ # @return [Array<String>] array of available attributes for the tag
10
+ def self.attributes
11
+ _attributes
12
+ end
13
+
14
+ # @return [String] tag's name
15
+ def self.tag_name
16
+ @tag || superclass.tag_name
17
+ end
18
+
19
+ protected
20
+
21
+ # sets the tag's name
22
+ # @api private
23
+ def self.set_tag(tag)
24
+ @tag = tag.to_s.freeze
25
+ end
26
+
27
+ set_tag 'abstract'
28
+
29
+ # defines dynamically methods for attributes
30
+ # @api private
31
+ def self.define_attribute_methods
32
+ attributes.each { |attr| define_attribute_method(attr) }
33
+ end
34
+
35
+ def self.inherited(base)
36
+ base.define_attribute_methods
37
+ end
38
+
39
+ # defines dynamically method for +attribute+
40
+ # @param [Data::Attribute] attribute
41
+ # @api private
42
+ def self.define_attribute_method(attribute)
43
+ return if instance_methods.include?(attribute.name)
44
+ name = attribute.name.to_s
45
+ content_rendering = attribute_content_rendering(attribute)
46
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
47
+ def #{name}(content#{' = true' if attribute.type == :boolean})
48
+ #{content_rendering}
49
+ self
50
+ end
51
+ RUBY
52
+ end
53
+
54
+ # @api private
55
+ # @param [Data::Attribute] attribute
56
+ # @returns Ruby code as string
57
+ def self.attribute_content_rendering(attribute)
58
+ name = attribute.name.to_s
59
+ case attribute.type
60
+ when :string
61
+ Strings.add "attr_#{name}", " #{name.gsub('_', '-')}=\""
62
+ "@output << Strings::ATTR_#{name.upcase} << CGI.escapeHTML(content.to_s) << Strings::QUOTE"
63
+ when :boolean
64
+ Strings.add "attr_#{name}", " #{name.gsub('_', '-')}=\"#{name}\""
65
+ "@output << Strings::ATTR_#{name.upcase} if content"
66
+ end
67
+ end
68
+
69
+ # adds attribute to class, triggers dynamical creation of needed instance methods etc.
70
+ # @param [Array<Data::Attribute>] attributes
71
+ # @api private
72
+ def self.add_attributes(attributes)
73
+ attributes = [attributes] unless attributes.is_a? Array
74
+ raise ArgumentError, attributes.inspect unless attributes.all? { |v| v.is_a? Data::Attribute }
75
+ self.send :_attributes=, _attributes + attributes
76
+ define_attribute_methods
77
+ end
78
+
79
+ public
80
+
81
+ attr_reader :builder
82
+
83
+ # @api private
84
+ def initialize(builder)
85
+ @builder = builder
86
+ @output = builder.instance_eval { @_output }
87
+ @stack = builder.instance_eval { @_stack }
88
+ @classes = []
89
+ @tag_name = self.rclass.tag_name
90
+ end
91
+
92
+ # @api private
93
+ def open(attributes = nil)
94
+ @output << Strings::LT << @tag_name
95
+ @builder.current = self
96
+ attributes(attributes)
97
+ default
98
+ self
99
+ end
100
+
101
+ # it renders attribute using defined attribute method or by rendering attribute directly
102
+ # @param [String, Symbol] name
103
+ # @param [#to_s] value
104
+ def attribute(name, value)
105
+ return __send__(name, value) if respond_to?(name)
106
+ @output << Strings::SPACE << name.to_s << Strings::EQL_QUOTE << CGI.escapeHTML(value.to_s) << Strings::QUOTE
107
+ self
108
+ end
109
+
110
+ # @example
111
+ # div.attributes :id => 'id' # => <div id="id"></div>
112
+ # div :id => 'id', :class => %w{left right} # => <div id="id" class="left right"></div>
113
+ # img :src => 'path' # => <img src="path"></div>
114
+ # attribute`s methods are called on background (in this case #id is called)
115
+ def attributes(attrs)
116
+ return self unless attrs
117
+ attrs.each do |attr, value|
118
+ if value.kind_of?(Array)
119
+ __send__(attr, *value)
120
+ else
121
+ __send__(attr, value)
122
+ end
123
+ end
124
+ self
125
+ end
126
+
127
+ # original Ruby method for class, class is used for html classes
128
+ alias_method(:rclass, :class)
129
+
130
+ id_class = /^([\w]+)(!|)$/
131
+ data_attribute = /^data_([a-z_]+)$/
132
+ METHOD_MISSING_REGEXP = /#{data_attribute}|#{id_class}/ unless defined? METHOD_MISSING_REGEXP
133
+
134
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
135
+ # allows data-* attributes and id, classes by method_missing
136
+ def method_missing(method, *args, &block)
137
+ method = method.to_s
138
+ if method =~ METHOD_MISSING_REGEXP
139
+ if $1
140
+ self.rclass.add_attributes Data::Attribute.new(method, :string)
141
+ self.send method, *args
142
+ else
143
+ self.__send__($3 == '!' ? :id : :class, $2)
144
+ end
145
+ else
146
+ super(method, *args, &block)
147
+ end
148
+ end
149
+
150
+ #def respond_to?(symbol, include_private = false)
151
+ # symbol.to_s =~ METHOD_MISSING_REGEXP || super(symbol, include_private)
152
+ #end
153
+ RUBY
154
+
155
+ Strings.add "attr_class", " class=\""
156
+ # adds classes to the tag by joining +classes+ with ' ' and skipping non-true classes
157
+ # @param [Array<#to_s>] classes
158
+ # @example
159
+ # class(!visible? && 'hidden', 'left') #=> class="hidden left" or class="left"
160
+ def class(*classes)
161
+ @classes.push(*classes.select { |c| c })
162
+ self
163
+ end
164
+
165
+ Strings.add "attr_id", " id=\""
166
+ # adds id to the tag by joining +values+ with '_'
167
+ # @param [Array<#to_s>] values
168
+ # @example
169
+ # id('user', 12) #=> id="user_15"
170
+ def id(*values)
171
+ @output << Strings::ATTR_ID << CGI.escapeHTML(values.select { |v| v }.join(Strings::UNDERSCORE)) <<
172
+ Strings::QUOTE
173
+ self
174
+ end
175
+
176
+ # adds id and class to a tag by an object
177
+ # @param [Object] obj
178
+ # To determine the class it looks for .hammer_builder_ref or
179
+ # it uses class.to_s.underscore.tr('/', '-').
180
+ # To determine id it looks for #hammer_builder_ref or it takes class and #id or #object_id.
181
+ # @example
182
+ # div[AUser.new].with { text 'a' } # => <div id="a_user_1" class="a_user">a</div>
183
+ def mimic(obj)
184
+ klass = if obj.class.respond_to? :hammer_builder_ref
185
+ obj.class.hammer_builder_ref
186
+ else
187
+ ActiveSupport::Inflector.underscore(obj.class.to_s).tr('/', '-')
188
+ end
189
+
190
+ id = case
191
+ when obj.respond_to?(:hammer_builder_ref)
192
+ obj.hammer_builder_ref
193
+ when obj.respond_to?(:id)
194
+ [klass, obj.id]
195
+ else
196
+ [klass, obj.object_id]
197
+ end
198
+ #noinspection RubyArgCount
199
+ self.class(klass).id(id)
200
+ end
201
+
202
+ alias_method :[], :mimic
203
+
204
+ # renders data-* attributes by +hash+
205
+ # @param [Hash] hash
206
+ # @example
207
+ # div.data(:remote => true, :id => 'an_id') # => <div data-remote="true" data-id="an_id"></div>
208
+ def data(hash)
209
+ hash.each { |k, v| __send__ "data_#{k}", v }
210
+ self
211
+ end
212
+
213
+ protected
214
+
215
+ # this method is called on each tag opening, useful for default attributes
216
+ # @example html tag uses this to add xmlns attr.
217
+ # html # => <html xmlns="http://www.w3.org/1999/xhtml"></html>
218
+ def default
219
+ end
220
+
221
+ # flushes classes to output
222
+ # @api private
223
+ def flush_classes
224
+ unless @classes.empty?
225
+ @output << Strings::ATTR_CLASS << CGI.escapeHTML(@classes.join(Strings::SPACE)) << Strings::QUOTE
226
+ @classes.clear
227
+ end
228
+ end
229
+ end ###import
230
+
231
+ end
232
+ end
233
+ end