hammer_builder 0.1.0

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