hammer_builder 0.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.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Petr Chalupa
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # HammerBuilder
2
+
3
+ [`HammerBuilder`](https://github.com/ruby-hammer/hammer-builder)
4
+ is a xhtml5 builder written in Ruby 1.9.2. It does not introduce anything special, you just
5
+ use Ruby to get your xhtml. [`HammerBuilder`](https://github.com/ruby-hammer/hammer-builder)
6
+ has been written with three objectives:
7
+
8
+ * Speed
9
+ * Rich API
10
+ * Extensibility
11
+
12
+ ## Links
13
+
14
+ * Introduction:
15
+ [http://hammer.pitr.ch/2011/05/11/HammerBuilder-introduction/](http://hammer.pitr.ch/2011/05/11/HammerBuilder-introduction/)
16
+ * Yardoc: [http://hammer.pitr.ch/hammer-builder/](http://hammer.pitr.ch/hammer-builder/)
17
+ * Issues: [https://github.com/ruby-hammer/hammer-builder/issues](https://github.com/ruby-hammer/hammer-builder/issues)
18
+ * Changelog: [http://hammer.pitr.ch/hammer-builder/file.CHANGELOG.html](http://hammer.pitr.ch/hammer-builder/file.CHANGELOG.html)
19
+
20
+ ## Syntax
21
+
22
+ HammerBuilder::Formated.get.go_in do
23
+ xhtml5!
24
+ html do
25
+ head { title 'a title' }
26
+ body do
27
+ div.id('menu').class('left') do
28
+ ul do
29
+ li 'home'
30
+ li 'contacts', :class => 'active'
31
+ end
32
+ end
33
+ div.id('content') do
34
+ article.id 'article1' do
35
+ h1 'header'
36
+ p('some text').class('centered')
37
+ div(:class => 'like').class('hide').with do
38
+ text 'like on '
39
+ strong 'Facebook'
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end.to_xhtml!
46
+
47
+ #=>
48
+ #<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html>
49
+ #<html xmlns="http://www.w3.org/1999/xhtml">
50
+ # <head>
51
+ # <title>a title</title>
52
+ # </head>
53
+ # <body>
54
+ # <div id="menu" class="left">
55
+ # <ul>
56
+ # <li>home</li>
57
+ # <li class="active">contacts</li>
58
+ # </ul>
59
+ # </div>
60
+ # <div id="content">
61
+ # <article id="article1">
62
+ # <h1>header</h1>
63
+ # <p class="centered">some text</p>
64
+ # <div class="like hide">like on
65
+ # <strong>Facebook</strong>
66
+ # </div>
67
+ # </article>
68
+ # </div>
69
+ # </body>
70
+ #</html>
71
+
72
+
73
+ ## Benchmark
74
+
75
+ ### Synthetic
76
+
77
+ user system total real
78
+ render 4.380000 0.000000 4.380000 ( 4.394127)
79
+ render3 4.990000 0.000000 4.990000 ( 5.017267)
80
+ HammerBuilder::Standard 5.590000 0.000000 5.590000 ( 5.929775)
81
+ HammerBuilder::Formated 5.520000 0.000000 5.520000 ( 5.511297)
82
+ erubis 7.340000 0.000000 7.340000 ( 7.345410)
83
+ erubis-reuse 4.670000 0.000000 4.670000 ( 4.666334)
84
+ fasterubis 7.700000 0.000000 7.700000 ( 7.689792)
85
+ fasterubis-reuse 4.650000 0.000000 4.650000 ( 4.648017)
86
+ tenjin 11.810000 0.280000 12.090000 ( 12.084124)
87
+ tenjin-reuse 3.170000 0.010000 3.180000 ( 3.183110)
88
+ erector 12.100000 0.000000 12.100000 ( 12.103520)
89
+ markaby 20.750000 0.030000 20.780000 ( 21.371292)
90
+ tagz 73.200000 0.140000 73.340000 ( 73.306450)
91
+
92
+ ### In Rails 3
93
+
94
+ BenchTest#test_erubis_partials (3.34 sec warmup)
95
+ wall_time: 3.56 sec
96
+ memory: 0.00 KB
97
+ objects: 0
98
+ gc_runs: 15
99
+ gc_time: 0.53 ms
100
+ BenchTest#test_erubis_single (552 ms warmup)
101
+ wall_time: 544 ms
102
+ memory: 0.00 KB
103
+ objects: 0
104
+ gc_runs: 4
105
+ gc_time: 0.12 ms
106
+ BenchTest#test_hammer_builder (2.33 sec warmup)
107
+ wall_time: 847 ms
108
+ memory: 0.00 KB
109
+ objects: 0
110
+ gc_runs: 5
111
+ gc_time: 0.17 ms
112
+ BenchTest#test_tenjin_partial (942 ms warmup)
113
+ wall_time: 1.21 sec
114
+ memory: 0.00 KB
115
+ objects: 0
116
+ gc_runs: 7
117
+ gc_time: 0.25 ms
118
+ BenchTest#test_tenjin_single (531 ms warmup)
119
+ wall_time: 532 ms
120
+ memory: 0.00 KB
121
+ objects: 0
122
+ gc_runs: 6
123
+ gc_time: 0.20 ms
124
+
125
+ ### Conclusion
126
+
127
+ Template engines are slightly faster than [`HammerBuilder`](https://github.com/ruby-hammer/hammer-builder)
128
+ when template does not content a lot of inserting or partials.
129
+ On the other hand when partials are used, [`HammerBuilder`](https://github.com/ruby-hammer/hammer-builder)
130
+ beats template engines.
131
+ There is no overhead for partials in [`HammerBuilder`](https://github.com/ruby-hammer/hammer-builder)
132
+ compared to using partials in template engine. The difference is significant for `Erubis`, `Tenjin` is
133
+ not so bad, but I did not find any easy way to use `Tenjin` in Rails 3 (I did some hacking).
@@ -0,0 +1,726 @@
1
+ require 'cgi'
2
+ require 'active_support/core_ext/class/inheritable_attributes'
3
+ require 'active_support/core_ext/string/inflections'
4
+
5
+ module HammerBuilder
6
+ EXTRA_ATTRIBUTES = {
7
+ "a" => ["href", "target", "ping", "rel", "media", "hreflang", "type"],
8
+ "abbr" => [],
9
+ "address" => [],
10
+ "area" => ["alt", "coords", "shape", "href", "target", "ping", "rel", "media", "hreflang", "type"],
11
+ "article" => [],
12
+ "aside" => [],
13
+ "audio" => ["src", "preload", "autoplay", "mediagroup", "loop", "controls"],
14
+ "b" => [],
15
+ "base" => ["href", "target"],
16
+ "bdi" => [],
17
+ "bdo" => [],
18
+ "blockquote" => ["cite"],
19
+ "body" => ["onafterprint", "onbeforeprint", "onbeforeunload", "onblur", "onerror", "onfocus", "onhashchange",
20
+ "onload", "onmessage", "onoffline", "ononline", "onpagehide", "onpageshow", "onpopstate", "onredo", "onresize",
21
+ "onscroll", "onstorage", "onundo", "onunload"],
22
+ "br" => [],
23
+ "button" => ["autofocus", "disabled", "form", "formaction", "formenctype", "formmethod", "formnovalidate",
24
+ "formtarget", "name", "type", "value"],
25
+ "canvas" => ["width", "height"],
26
+ "caption" => [],
27
+ "cite" => [],
28
+ "code" => [],
29
+ "col" => ["span"],
30
+ "colgroup" => ["span"],
31
+ "command" => ["type", "label", "icon", "disabled", "checked", "radiogroup"],
32
+ "datalist" => ["option"],
33
+ "dd" => [],
34
+ "del" => ["cite", "datetime"],
35
+ "details" => ["open"],
36
+ "dfn" => [],
37
+ "div" => [],
38
+ "dl" => [],
39
+ "dt" => [],
40
+ "em" => [],
41
+ "embed" => ["src", "type", "width", "height"],
42
+ "fieldset" => ["disabled", "form", "name"],
43
+ "figcaption" => [],
44
+ "figure" => [],
45
+ "footer" => [],
46
+ "form" => ["action", "autocomplete", "enctype", "method", "name", "novalidate", "target", 'accept_charset'],
47
+ "h1" => [],
48
+ "h2" => [],
49
+ "h3" => [],
50
+ "h4" => [],
51
+ "h5" => [],
52
+ "h6" => [],
53
+ "head" => [],
54
+ "header" => [],
55
+ "hgroup" => [],
56
+ "hr" => [],
57
+ "html" => ["manifest"],
58
+ "i" => [],
59
+ "iframe" => ["src", "srcdoc", "name", "sandbox", "seamless", "width", "height"],
60
+ "img" => ["alt", "src", "usemap", "ismap", "width", "height"],
61
+ "input" => ["accept", "alt", "autocomplete", "autofocus", "checked", "dirname", "disabled", "form", "formaction",
62
+ "formenctype", "formmethod", "formnovalidate", "formtarget", "height", "list", "max", "maxlength", "min",
63
+ "multiple", "name", "pattern", "placeholder", "readonly", "required", "size", "src", "step", "type", "value",
64
+ "width"],
65
+ "ins" => ["cite", "datetime"],
66
+ "kbd" => [],
67
+ "keygen" => ["autofocus", "challenge", "disabled", "form", "keytype", "name"],
68
+ "label" => ["form", "for"],
69
+ "legend" => [],
70
+ "li" => ["value"],
71
+ "link" => ["href", "rel", "media", "hreflang", "type", "sizes"],
72
+ "map" => ["name"],
73
+ "mark" => [],
74
+ "menu" => ["type", "label"],
75
+ "meta" => ["name", "content", "charset", "http_equiv"],
76
+ "meter" => ["value", "min", "max", "low", "high", "optimum", "form"],
77
+ "nav" => [],
78
+ "noscript" => [],
79
+ "object" => ["data", "type", "name", "usemap", "form", "width", "height"],
80
+ "ol" => ["reversed", "start"],
81
+ "optgroup" => ["disabled", "label"],
82
+ "option" => ["disabled", "label", "selected", "value"],
83
+ "output" => ["for", "form", "name"],
84
+ "p" => [],
85
+ "param" => ["name", "value"],
86
+ "pre" => [],
87
+ "progress" => ["value", "max", "form"],
88
+ "q" => ["cite"],
89
+ "rp" => [],
90
+ "rt" => [],
91
+ "ruby" => [],
92
+ "s" => [],
93
+ "samp" => [],
94
+ "script" => ["src", "async", "defer", "type", "charset"],
95
+ "section" => [],
96
+ "select" => ["autofocus", "disabled", "form", "multiple", "name", "required", "size"],
97
+ "small" => [],
98
+ "source" => ["src", "type", "media"],
99
+ "span" => [],
100
+ "strong" => [],
101
+ "style" => ["media", "type", "scoped"],
102
+ "sub" => [],
103
+ "summary" => [],
104
+ "sup" => [],
105
+ "table" => ["border"],
106
+ "tbody" => [],
107
+ "td" => ["colspan", "rowspan", "headers"],
108
+ "textarea" => ["autofocus", "cols", "disabled", "form", "maxlength", "name", "placeholder", "readonly",
109
+ "required", "rows", "wrap"],
110
+ "tfoot" => [],
111
+ "th" => ["colspan", "rowspan", "headers", "scope"],
112
+ "thead" => [],
113
+ "time" => ["datetime", "pubdate"],
114
+ "title" => [],
115
+ "tr" => [],
116
+ "track" => ["default", "kind", "label", "src", "srclang"],
117
+ "u" => [],
118
+ "ul" => [],
119
+ "var" => [],
120
+ "video" => ["src", "poster", "preload", "autoplay", "mediagroup", "loop", "controls", "width", "height"],
121
+ "wbr" => []
122
+ }
123
+
124
+ GLOBAL_ATTRIBUTES = [
125
+ 'accesskey','class','contenteditable','contextmenu','dir','draggable','dropzone','hidden','id','lang',
126
+ 'spellcheck','style','tabindex','title','onabort','onblur','oncanplay','oncanplaythrough','onchange',
127
+ 'onclick','oncontextmenu','oncuechange','ondblclick','ondrag','ondragend','ondragenter','ondragleave',
128
+ 'ondragover','ondragstart','ondrop','ondurationchange','onemptied','onended','onerror','onfocus','oninput',
129
+ 'oninvalid','onkeydown','onkeypress','onkeyup','onload','onloadeddata','onloadedmetadata','onloadstart',
130
+ 'onmousedown','onmousemove','onmouseout','onmouseover','onmouseup','onmousewheel','onpause','onplay',
131
+ 'onplaying','onprogress','onratechange','onreadystatechange','onreset','onscroll','onseeked','onseeking',
132
+ 'onselect','onshow','onstalled','onsubmit','onsuspend','ontimeupdate','onvolumechange','onwaiting'
133
+ ]
134
+
135
+ DOUBLE_TAGS = [
136
+ 'a', 'abbr', 'article', 'aside', 'audio', 'address',
137
+ 'b', 'bdo', 'blockquote', 'body', 'button',
138
+ 'canvas', 'caption', 'cite', 'code', 'colgroup', 'command',
139
+ 'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt',
140
+ 'em',
141
+ 'fieldset', 'figure', 'footer', 'form',
142
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'html', 'i',
143
+ 'iframe', 'ins', 'keygen', 'kbd', 'label', 'legend', 'li',
144
+ 'map', 'mark', 'meter',
145
+ 'nav', 'noscript',
146
+ 'object', 'ol', 'optgroup', 'option',
147
+ 'p', 'pre', 'progress',
148
+ 'q', 'ruby', 'rt', 'rp', 's',
149
+ 'samp', 'script', 'section', 'select', 'small', 'source', 'span',
150
+ 'strong', 'style', 'sub', 'sup',
151
+ 'table', 'tbody', 'td', 'textarea', 'tfoot',
152
+ 'th', 'thead', 'time', 'title', 'tr',
153
+ 'u', 'ul',
154
+ 'var', 'video'
155
+ ]
156
+
157
+ EMPTY_TAGS = [
158
+ 'area', 'base', 'br', 'col', 'embed',
159
+ 'hr', 'img', 'input', 'link', 'meta', 'param'
160
+ ]
161
+
162
+ LT = '<'.freeze
163
+ GT = '>'.freeze
164
+ SLASH_LT = '</'.freeze
165
+ SLASH_GT = ' />'.freeze
166
+ SPACE = ' '.freeze
167
+ MAX_LEVELS = 300
168
+ SPACES = Array.new(MAX_LEVELS) {|i| (' ' * i).freeze }
169
+ NEWLINE = "\n".freeze
170
+ QUOTE = '"'.freeze
171
+ EQL = '='.freeze
172
+ EQL_QUOTE = EQL + QUOTE
173
+ COMMENT_START = '<!--'.freeze
174
+ COMMENT_END = '-->'.freeze
175
+ CDATA_START = '<![CDATA['.freeze
176
+ CDATA_END = ']]>'.freeze
177
+
178
+ module Helper
179
+ def self.included(base)
180
+ super
181
+ base.extend ClassMethods
182
+ base.class_inheritable_array :builder_methods, :instance_writer => false, :instance_reader => false
183
+ end
184
+
185
+ module ClassMethods
186
+
187
+ # adds instance method to the class. Method accepts any instance of builder and returns it after rendering.
188
+ # @param [Symbol] method_name
189
+ # @yield [self] builder_block is evaluated inside builder and accepts instance of a rendered object as parameter
190
+ # @example
191
+ # class User
192
+ # # ...
193
+ # include HammerBuilder::Helper
194
+ #
195
+ # builder :menu do |user|
196
+ # li user.name
197
+ # end
198
+ # end
199
+ #
200
+ # User.new.menu(HammerBuilder::Standard.get).to_xhtml! #=> "<li>Name</li>"
201
+ def builder(method_name, &builder_block)
202
+ self.builder_methods = [method_name.to_sym]
203
+ define_method(method_name) do |builder, *args|
204
+ builder.go_in(self, *args, &builder_block)
205
+ end
206
+ end
207
+ end
208
+ end
209
+
210
+ module RedefinableClassTree
211
+ # defines new class
212
+ # @param [Symbol] class_name
213
+ # @param [Symbol] superclass_name e.g. :AbstractEmptyTag
214
+ # @yield definition block which is evaluated inside the new class, doing so defining the class's methods etc.
215
+ def define_class(class_name, superclass_name = nil, &definition)
216
+ class_name = class_name(class_name)
217
+ superclass_name = class_name(superclass_name) if superclass_name
218
+
219
+ raise "class: '#{class_name}' already defined" if respond_to? method_class(class_name)
220
+
221
+ define_singleton_method method_class(class_name) do |builder|
222
+ builder.instance_variable_get("@#{method_class(class_name)}") || begin
223
+ klass = builder.send(method_class_definition(class_name), builder)
224
+ builder.const_set class_name, klass
225
+ builder.instance_variable_set("@#{method_class(class_name)}", klass)
226
+ end
227
+ end
228
+
229
+ define_singleton_method method_class_definition(class_name) do |builder|
230
+ superclass = if superclass_name
231
+ builder.send method_class(superclass_name), builder
232
+ else
233
+ Object
234
+ end
235
+ Class.new(superclass, &definition)
236
+ end
237
+ end
238
+
239
+ # extends existing class
240
+ # @param [Symbol] class_name
241
+ # @yield definition block which is evaluated inside the new class, doing so extending the class's methods etc.
242
+ def extend_class(class_name, &definition)
243
+ raise "class: '#{class_name}' not defined" unless respond_to? method_class(class_name)
244
+
245
+ define_singleton_method method_class_definition(class_name) do |builder|
246
+ ancestor = super(builder)
247
+ count = 1; count += 1 while builder.const_defined? "#{class_name}Super#{count}"
248
+ builder.const_set "#{class_name}Super#{count}", ancestor
249
+ Class.new(ancestor, &definition)
250
+ end
251
+ end
252
+
253
+ private
254
+
255
+ def class_name(klass)
256
+ klass.to_s.camelize
257
+ end
258
+
259
+ def method_class(klass)
260
+ "#{klass.to_s.underscore}_class"
261
+ end
262
+
263
+ def method_class_definition(klass)
264
+ "#{method_class(klass)}_definition"
265
+ end
266
+ end
267
+
268
+ # Creating builder instances is expensive, therefore you can use Pool to go around that
269
+ module Pool
270
+ def self.included(base)
271
+ super
272
+ base.extend ClassMethods
273
+ end
274
+
275
+ module ClassMethods
276
+ # This the preferred way of getting new Builder. If you forget to release it, it does not matter -
277
+ # builder gets GCed after you lose reference
278
+ # @return [Standard, Formated]
279
+ def get
280
+ mutex.synchronize do
281
+ if free_builders.empty?
282
+ new
283
+ else
284
+ free_builders.pop
285
+ end
286
+ end
287
+ end
288
+
289
+ # returns +builder+ back into pool *DONT* forget to lose the reference to the +builder+
290
+ # @param [Standard, Formated]
291
+ def release(builder)
292
+ builder.reset
293
+ mutex.synchronize do
294
+ free_builders.push builder
295
+ end
296
+ nil
297
+ end
298
+
299
+ # @return [Fixnum] size of free builders
300
+ def pool_size
301
+ free_builders.size
302
+ end
303
+
304
+ private
305
+
306
+ def mutex
307
+ @mutex ||= Mutex.new
308
+ end
309
+
310
+ def free_builders
311
+ @free_builders ||= []
312
+ end
313
+ end
314
+
315
+ # instance version of ClassMethods.release
316
+ # @see ClassMethods.release
317
+ def release!
318
+ self.class.release(self)
319
+ end
320
+ end
321
+
322
+ # Abstract implementation of Builder
323
+ class Abstract
324
+ extend RedefinableClassTree
325
+ include Pool
326
+
327
+ # << faster then +
328
+ # yield faster then block.call
329
+ # accessing ivar and constant is faster then accesing hash or cvar
330
+ # class_eval faster then define_method
331
+ # beware of strings in methods -> creates a lot of garbage
332
+
333
+ define_class :AbstractTag do
334
+ def initialize(builder)
335
+ @builder = builder
336
+ @output = builder.instance_eval { @output }
337
+ @stack = builder.instance_eval { @stack }
338
+ @classes = []
339
+ set_tag
340
+ end
341
+
342
+ def open(attributes = nil)
343
+ @output << LT << @tag
344
+ @builder.current = self
345
+ attributes(attributes)
346
+ default
347
+ self
348
+ end
349
+
350
+ # @example
351
+ # div.attributes :id => 'id' # => <div id="id"></div>
352
+ def attributes(attrs)
353
+ return self unless attrs
354
+ attrs.each do |attr, value|
355
+ __send__(attr, *value)
356
+ end
357
+ self
358
+ end
359
+
360
+ # @example
361
+ # div.attribute :id, 'id' # => <div id="id"></div>
362
+ def attribute(attribute, content)
363
+ @output << SPACE << attribute.to_s << EQL_QUOTE << CGI.escapeHTML(content.to_s) << QUOTE
364
+ end
365
+
366
+ alias_method(:rclass, :class)
367
+
368
+ class_inheritable_array :_attributes, :instance_writer => false, :instance_reader => false
369
+
370
+ def self.attributes
371
+ self._attributes
372
+ end
373
+
374
+ # allows data-* attributes
375
+ def method_missing(method, *args, &block)
376
+ if method.to_s =~ /data_([a-z_]+)/
377
+ self.rclass.attributes = [method.to_s]
378
+ self.send method, *args, &block
379
+ else
380
+ super
381
+ end
382
+ end
383
+
384
+ protected
385
+
386
+ # sets the right tag in descendants
387
+ def self.set_tag(tag)
388
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
389
+ def set_tag
390
+ @tag = '#{tag}'.freeze
391
+ end
392
+ RUBYCODE
393
+ end
394
+
395
+ # this method is called on each tag opening, useful for default attributes
396
+ # @example html tag uses this to add xmlns attr.
397
+ # html # => <html xmlns="http://www.w3.org/1999/xhtml"></html>
398
+ def default
399
+ end
400
+
401
+ # defines dynamically methods for attributes
402
+ def self.define_attributes
403
+ attributes.each do |attr|
404
+ next if instance_methods.include?(attr.to_sym)
405
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
406
+ def #{attr}(content)
407
+ @output << ATTR_#{attr.upcase} << CGI.escapeHTML(content.to_s) << QUOTE
408
+ self
409
+ end
410
+ RUBYCODE
411
+ end
412
+ define_attribute_constants
413
+ end
414
+
415
+ # defines constant strings not to make garbage
416
+ def self.define_attribute_constants
417
+ attributes.each do |attr|
418
+ const = "attr_#{attr}".upcase
419
+ HammerBuilder.const_set const, " #{attr.gsub('_', '-')}=\"".freeze unless HammerBuilder.const_defined?(const)
420
+ end
421
+ end
422
+
423
+ # adds attribute to class, triggers dynamical creation of needed instance methods etc.
424
+ def self.attributes=(attributes)
425
+ self._attributes = attributes
426
+ define_attributes
427
+ end
428
+
429
+ # flushes classes to output
430
+ def flush_classes
431
+ unless @classes.empty?
432
+ @output << ATTR_CLASS << CGI.escapeHTML(@classes.join(SPACE)) << QUOTE
433
+ @classes.clear
434
+ end
435
+ end
436
+
437
+ def set_tag
438
+ @tag = 'abstract'
439
+ end
440
+
441
+ public
442
+
443
+ # global HTML5 attributes
444
+ self.attributes = GLOBAL_ATTRIBUTES
445
+
446
+ alias :[] :id
447
+
448
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
449
+ def class(*classes)
450
+ @classes.push(*classes)
451
+ self
452
+ end
453
+ RUBYCODE
454
+ end
455
+
456
+ define_class :AbstractEmptyTag, :AbstractTag do
457
+ def flush
458
+ flush_classes
459
+ @output << SLASH_GT
460
+ nil
461
+ end
462
+ end
463
+
464
+ define_class :AbstractDoubleTag, :AbstractTag do
465
+ # defined by class_eval because there is a super calling, causing error:
466
+ # super from singleton method that is defined to multiple classes is not supported;
467
+ # this will be fixed in 1.9.3 or later (NotImplementedError)
468
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
469
+ def initialize(builder)
470
+ super
471
+ @content = nil
472
+ end
473
+
474
+ def open(*args, &block)
475
+ attributes = if args.last.is_a?(Hash)
476
+ args.pop
477
+ end
478
+ content args[0]
479
+ super attributes
480
+ @stack << @tag
481
+ if block
482
+ with &block
483
+ else
484
+ self
485
+ end
486
+ end
487
+ RUBYCODE
488
+
489
+ def flush
490
+ flush_classes
491
+ @output << GT
492
+ @output << CGI.escapeHTML(@content) if @content
493
+ @output << SLASH_LT << @stack.pop << GT
494
+ @content = nil
495
+ end
496
+
497
+ # sets content of the double tag
498
+ def content(content)
499
+ @content = content.to_s
500
+ self
501
+ end
502
+
503
+ # renders content of the double tag with block
504
+ def with
505
+ flush_classes
506
+ @output << GT
507
+ @content = nil
508
+ @builder.current = nil
509
+ yield
510
+ # if (content = yield).is_a?(String)
511
+ # @output << CGI.escapeHTML(content)
512
+ # end
513
+ @builder.flush
514
+ @output << SLASH_LT << @stack.pop << GT
515
+ nil
516
+ end
517
+
518
+ protected
519
+
520
+ def self.define_attributes
521
+ attributes.each do |attr|
522
+ next if instance_methods(false).include?(attr.to_sym)
523
+ if instance_methods.include?(attr.to_sym)
524
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
525
+ def #{attr}(*args, &block)
526
+ super(*args, &nil)
527
+ return with(&block) if block
528
+ self
529
+ end
530
+ RUBYCODE
531
+ else
532
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
533
+ def #{attr}(content, &block)
534
+ @output << ATTR_#{attr.upcase} << CGI.escapeHTML(content.to_s) << QUOTE
535
+ return with(&block) if block
536
+ self
537
+ end
538
+ RUBYCODE
539
+ end
540
+ end
541
+ define_attribute_constants
542
+ end
543
+ end
544
+
545
+ class_inheritable_accessor :tags, :instance_writer => false
546
+ self.tags = {}
547
+
548
+ protected
549
+
550
+ # defines instance method for +tag+ in builder
551
+ def self.define_tag(tag)
552
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
553
+ def #{tag}(*args, &block)
554
+ flush
555
+ @#{tag}.open(*args, &block)
556
+ end
557
+ RUBYCODE
558
+ self.tags[tag] = tag
559
+ end
560
+
561
+ public
562
+
563
+ attr_accessor :current
564
+
565
+ def initialize()
566
+ @output = ""
567
+ @stack = []
568
+ @current = nil
569
+ # tag classes initialization
570
+ tags.values.each do |klass|
571
+ instance_variable_set(:"@#{klass}", self.class.send("#{klass}_class", self.class).new(self))
572
+ end
573
+ end
574
+
575
+ # escapes +text+ to output
576
+ def text(text)
577
+ flush
578
+ @output << CGI.escapeHTML(text.to_s)
579
+ end
580
+
581
+ # unescaped +text+ to output
582
+ def raw(text)
583
+ flush
584
+ @output << text.to_s
585
+ end
586
+
587
+ # inserts +comment+
588
+ def comment(comment)
589
+ flush
590
+ @output << COMMENT_START << comment.to_s << COMMENT_END
591
+ end
592
+
593
+ # insersts CDATA with +content+
594
+ def cdata(content)
595
+ flush
596
+ @output << CDATA_START << content.to_s << CDATA_END
597
+ end
598
+
599
+ def xml_version(version = '1.0', encoding = 'UTF-8')
600
+ flush
601
+ @output << "<?xml version=\"#{version}\" encoding=\"#{encoding}\"?>"
602
+ end
603
+
604
+ def doctype
605
+ flush
606
+ @output << "<!DOCTYPE html>"
607
+ end
608
+
609
+ # inserts xhtml5 header
610
+ def xhtml5!
611
+ xml_version
612
+ doctype
613
+ end
614
+
615
+ # resets the builder to the state after creation - much faster then creating a new one
616
+ def reset
617
+ flush
618
+ @output.clear
619
+ @stack.clear
620
+ self
621
+ end
622
+
623
+ # enables you to evaluate +block+ inside the builder with +variables+
624
+ # @example
625
+ # HammerBuilder::Formated.get.freeze.go_in('asd') do |string|
626
+ # div string
627
+ # end.to_html! #=> "<div>asd</div>"
628
+ #
629
+ def go_in(*variables, &block)
630
+ instance_exec *variables, &block
631
+ self
632
+ end
633
+
634
+ # @return [String] output
635
+ def to_xhtml()
636
+ flush
637
+ @output.clone
638
+ end
639
+
640
+ # @return [String] output and releases the builder to pool
641
+ def to_xhtml!
642
+ r = to_xhtml
643
+ release!
644
+ r
645
+ end
646
+
647
+ def flush
648
+ if @current
649
+ @current.flush
650
+ @current = nil
651
+ end
652
+ end
653
+ end
654
+
655
+ # Builder implementation without formating (one line)
656
+ class Standard < Abstract
657
+
658
+ (DOUBLE_TAGS - ['html']).each do |tag|
659
+ define_class tag.camelize , :AbstractDoubleTag do
660
+ set_tag tag
661
+ self.attributes = EXTRA_ATTRIBUTES[tag]
662
+ end
663
+
664
+ define_tag(tag)
665
+ end
666
+
667
+ define_class :Html, :AbstractDoubleTag do
668
+ set_tag 'html'
669
+ self.attributes = ['xmlns'] + EXTRA_ATTRIBUTES['html']
670
+
671
+ def default
672
+ xmlns('http://www.w3.org/1999/xhtml')
673
+ end
674
+ end
675
+
676
+ define_tag('html')
677
+
678
+ def js(js , options = {})
679
+ script({:type => "text/javascript"}.merge(options)) { cdata js }
680
+ end
681
+
682
+ EMPTY_TAGS.each do |tag|
683
+ define_class tag.camelize, :AbstractEmptyTag do
684
+ set_tag tag
685
+ self.attributes = EXTRA_ATTRIBUTES[tag]
686
+ end
687
+
688
+ define_tag(tag)
689
+ end
690
+ end
691
+
692
+ # Builder implementation with formating (indented by ' ')
693
+ # Slow down is less then 1%
694
+ class Formated < Standard
695
+ extend_class :AbstractTag do
696
+ def open(attributes = nil)
697
+ @output << NEWLINE << SPACES.fetch(@stack.size, SPACE) << LT << @tag
698
+ @builder.current = self
699
+ attributes(attributes)
700
+ default
701
+ self
702
+ end
703
+ end
704
+
705
+ extend_class :AbstractDoubleTag do
706
+ def with
707
+ flush_classes
708
+ @output << GT
709
+ @content = nil
710
+ @builder.current = nil
711
+ yield
712
+ # if (content = yield).is_a?(String)
713
+ # @output << CGI.escapeHTML(content)
714
+ # end
715
+ @builder.flush
716
+ @output << NEWLINE << SPACES.fetch(@stack.size-1, SPACE) << SLASH_LT << @stack.pop << GT
717
+ nil
718
+ end
719
+ end
720
+
721
+ def comment(comment)
722
+ @output << NEWLINE << SPACES.fetch(@stack.size, SPACE) << COMMENT_START << comment.to_s << COMMENT_END
723
+ end
724
+ end
725
+ end
726
+
@@ -0,0 +1,185 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+
4
+ describe HammerBuilder do
5
+ def super_subject
6
+ self.class.ancestors[1].allocate.subject
7
+ end
8
+
9
+ describe 'object with HammerBuilder::Helper' do
10
+ class AObject
11
+ include HammerBuilder::Helper
12
+
13
+ builder :render do |obj|
14
+ @obj = obj
15
+ div 'a'
16
+ end
17
+ end
18
+
19
+ subject { AObject.new }
20
+
21
+ describe 'methods' do
22
+ subject { super_subject.class.instance_methods }
23
+ it { should include(:render) }
24
+ end
25
+
26
+ describe '#render(builder)' do
27
+ subject { super_subject.render(HammerBuilder::Standard.get).to_xhtml! }
28
+ it { should == "<div>a</div>"}
29
+ end
30
+ end
31
+
32
+ pending 'RedefinableClassTree'
33
+
34
+ # describe 'RedefinableClassTree' do
35
+ # let :klass do
36
+ # Class.new do
37
+ # extend HammerBuilder::RedefinableClassTree
38
+ # end
39
+ # end
40
+ #
41
+ # describe '.define_class' do
42
+ # before do
43
+ # klass.define_class(:AClass) do
44
+ # def a_method
45
+ # end
46
+ # end
47
+ # end
48
+ #
49
+ # it { }
50
+ # end
51
+ # end
52
+
53
+ describe HammerBuilder::Standard do
54
+ describe 'Pool methods' do
55
+ describe '.get' do
56
+ it { HammerBuilder::Standard.get.should be_an_instance_of(HammerBuilder::Standard) }
57
+ it { HammerBuilder::Formated.get.should be_an_instance_of(HammerBuilder::Formated) }
58
+ end
59
+
60
+ describe '#release!' do
61
+ before do
62
+ (@builder = HammerBuilder::Standard.get).release!
63
+ end
64
+
65
+ it 'should be same object' do
66
+ @builder.should == HammerBuilder::Standard.get
67
+ end
68
+ end
69
+
70
+ describe 'pools does not mix' do
71
+ before { HammerBuilder::Standard.get.release! }
72
+ it { HammerBuilder::Standard.pool_size.should == 1 }
73
+ it { HammerBuilder::Formated.get.should be_an_instance_of(HammerBuilder::Formated) }
74
+ end
75
+ end
76
+
77
+ describe 'available methods' do
78
+ subject { HammerBuilder::Standard.instance_methods }
79
+
80
+ (HammerBuilder::DOUBLE_TAGS + HammerBuilder::EMPTY_TAGS).each do |tag|
81
+ it "should have method #{tag}" do
82
+ should include(tag.to_sym)
83
+ end
84
+
85
+ describe tag do
86
+ before { @builder = HammerBuilder::Standard.get }
87
+ after { @builder.release! }
88
+ subject { @builder.send(tag).methods }
89
+ it "should include its attribute methods" do
90
+ attrs = (HammerBuilder::GLOBAL_ATTRIBUTES + HammerBuilder::EXTRA_ATTRIBUTES[tag]).
91
+ map {|attr| attr.to_sym}
92
+ should include(*attrs)
93
+ end
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ CONTENT = :'cc<>&cc'
100
+
101
+ describe 'rendering' do
102
+ describe '1' do
103
+ subject do
104
+ HammerBuilder::Formated.get.go_in do
105
+ xhtml5!
106
+ html do
107
+ head { title }
108
+ body do
109
+ div CONTENT
110
+ meta.http_equiv CONTENT
111
+ p.content CONTENT
112
+ div.id CONTENT
113
+ div.data_id CONTENT
114
+ div :id => CONTENT, :content => CONTENT
115
+ div.attributes :id => CONTENT, :content => CONTENT
116
+ div.attribute :newone, CONTENT
117
+ div { text CONTENT }
118
+ div[CONTENT].with { article CONTENT }
119
+ js 'var < 1;'
120
+ div do
121
+ strong :content
122
+ text :content
123
+ end
124
+ end
125
+ end
126
+ end.to_xhtml!.strip
127
+ end
128
+
129
+ it { should_not match(/cc<>&cc/) }
130
+ it 'should render corectly' do
131
+ should == (<<STR).strip
132
+ <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html>
133
+ <html xmlns="http://www.w3.org/1999/xhtml">
134
+ <head>
135
+ <title></title>
136
+ </head>
137
+ <body>
138
+ <div>cc&lt;&gt;&amp;cc</div>
139
+ <meta http-equiv="cc&lt;&gt;&amp;cc" />
140
+ <p>cc&lt;&gt;&amp;cc</p>
141
+ <div id="cc&lt;&gt;&amp;cc"></div>
142
+ <div data-id="cc&lt;&gt;&amp;cc"></div>
143
+ <div id="cc&lt;&gt;&amp;cc">cc&lt;&gt;&amp;cc</div>
144
+ <div id="cc&lt;&gt;&amp;cc">cc&lt;&gt;&amp;cc</div>
145
+ <div newone="cc&lt;&gt;&amp;cc"></div>
146
+ <div>cc&lt;&gt;&amp;cc
147
+ </div>
148
+ <div id="cc&lt;&gt;&amp;cc">
149
+ <article>cc&lt;&gt;&amp;cc</article>
150
+ </div>
151
+ <script type="text/javascript"><![CDATA[var < 1;]]>
152
+ </script>
153
+ <div>
154
+ <strong>content</strong>content
155
+ </div>
156
+ </body>
157
+ </html>
158
+ STR
159
+ end
160
+ end
161
+ describe '2' do
162
+ subject do
163
+ HammerBuilder::Formated.get.go_in do
164
+ html do
165
+ body do
166
+ comment CONTENT
167
+ cdata CONTENT
168
+ end
169
+ end
170
+ end.to_xhtml!.strip
171
+ end
172
+
173
+ it 'should render corectly' do
174
+ should == (<<STR).strip
175
+ <html xmlns="http://www.w3.org/1999/xhtml">
176
+ <body>
177
+ <!--cc<>&cc--><![CDATA[cc<>&cc]]>
178
+ </body>
179
+ </html>
180
+ STR
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,5 @@
1
+ require "#{File.dirname(__FILE__)}/../lib/hammer_builder"
2
+
3
+ #RSpec.configure do |config|
4
+ # config.mock_with :rspec
5
+ #end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hammer_builder
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Petr Chalupa
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-05-11 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activesupport
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 3
30
+ - 0
31
+ - 0
32
+ version: 3.0.0
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rspec
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 2
45
+ - 5
46
+ - 0
47
+ version: 2.5.0
48
+ type: :development
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: yard
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ~>
57
+ - !ruby/object:Gem::Version
58
+ segments:
59
+ - 0
60
+ - 6
61
+ version: "0.6"
62
+ type: :development
63
+ version_requirements: *id003
64
+ - !ruby/object:Gem::Dependency
65
+ name: bluecloth
66
+ prerelease: false
67
+ requirement: &id004 !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ~>
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 2
74
+ - 0
75
+ version: "2.0"
76
+ type: :development
77
+ version_requirements: *id004
78
+ - !ruby/object:Gem::Dependency
79
+ name: jeweler
80
+ prerelease: false
81
+ requirement: &id005 !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ~>
85
+ - !ruby/object:Gem::Version
86
+ segments:
87
+ - 1
88
+ - 6
89
+ version: "1.6"
90
+ type: :development
91
+ version_requirements: *id005
92
+ description: |-
93
+ is a xhtml5 builder written in Ruby. It does not introduce anything special, you just
94
+ use Ruby to get your xhtml. HammerBuilder has been written with three objectives: Speed, Rich API, Extensibility
95
+ email: email@pitr.ch
96
+ executables: []
97
+
98
+ extensions: []
99
+
100
+ extra_rdoc_files:
101
+ - LICENSE
102
+ - README.md
103
+ files:
104
+ - lib/hammer_builder.rb
105
+ - LICENSE
106
+ - README.md
107
+ - spec/hammer_builder_spec.rb
108
+ - spec/spec_helper.rb
109
+ has_rdoc: true
110
+ homepage: https://github.com/ruby-hammer/hammer-builder
111
+ licenses:
112
+ - MIT
113
+ post_install_message:
114
+ rdoc_options: []
115
+
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ segments:
124
+ - 0
125
+ version: "0"
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ segments:
132
+ - 0
133
+ version: "0"
134
+ requirements:
135
+ - Ruby 1.9.2
136
+ rubyforge_project:
137
+ rubygems_version: 1.3.7
138
+ signing_key:
139
+ specification_version: 3
140
+ summary: fast ruby xhtml5 builder
141
+ test_files:
142
+ - spec/hammer_builder_spec.rb
143
+ - spec/spec_helper.rb