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 +19 -0
- data/README.md +133 -0
- data/lib/hammer_builder.rb +726 -0
- data/spec/hammer_builder_spec.rb +185 -0
- data/spec/spec_helper.rb +5 -0
- metadata +143 -0
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<>&cc</div>
|
139
|
+
<meta http-equiv="cc<>&cc" />
|
140
|
+
<p>cc<>&cc</p>
|
141
|
+
<div id="cc<>&cc"></div>
|
142
|
+
<div data-id="cc<>&cc"></div>
|
143
|
+
<div id="cc<>&cc">cc<>&cc</div>
|
144
|
+
<div id="cc<>&cc">cc<>&cc</div>
|
145
|
+
<div newone="cc<>&cc"></div>
|
146
|
+
<div>cc<>&cc
|
147
|
+
</div>
|
148
|
+
<div id="cc<>&cc">
|
149
|
+
<article>cc<>&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
|
data/spec/spec_helper.rb
ADDED
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
|