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