hammer_builder 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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