hammer_builder 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +4 -0
- data/README.md +24 -41
- data/lib/hammer_builder.rb +3 -699
- data/lib/hammer_builder/abstract.rb +205 -0
- data/lib/hammer_builder/abstract/abstract_double_tag.rb +148 -0
- data/lib/hammer_builder/abstract/abstract_single_tag.rb +18 -0
- data/lib/hammer_builder/abstract/abstract_tag.rb +233 -0
- data/lib/hammer_builder/data.rb +11 -0
- data/lib/hammer_builder/data/html5.rb +179 -0
- data/lib/hammer_builder/doc.rb +385 -0
- data/lib/hammer_builder/dynamic_classes.rb +136 -130
- data/lib/hammer_builder/formatted.rb +43 -0
- data/lib/hammer_builder/helper.rb +24 -0
- data/lib/hammer_builder/pool.rb +72 -0
- data/lib/hammer_builder/rails.rb +40 -0
- data/lib/hammer_builder/standard.rb +48 -0
- data/lib/hammer_builder/strings.rb +29 -0
- data/spec/hammer_builder_spec.rb +239 -146
- data/spec/spec_helper.rb +3 -1
- metadata +264 -96
data/CHANGELOG.md
CHANGED
@@ -1,16 +1,20 @@
|
|
1
1
|
## 0.1.2
|
2
|
+
|
2
3
|
* 2011-05-17: Fixed gemspec and changelog fix #11
|
3
4
|
* 2011-05-16: Rails bench corrected
|
5
|
+
|
4
6
|
## 0.1.1
|
5
7
|
* 2011-05-16: Dynamic classes rewriten
|
6
8
|
* 2011-05-11: doc update
|
7
9
|
* 2011-05-11: doc update
|
10
|
+
|
8
11
|
## 0.1.0
|
9
12
|
* 2011-05-11: doc update
|
10
13
|
* 2011-05-11: doc update
|
11
14
|
* 2011-05-11: Specs, Yardoc, minor updates
|
12
15
|
* 2011-05-09: YARD added
|
13
16
|
* 2011-05-09: repository clean up
|
17
|
+
|
14
18
|
## 0.0.0
|
15
19
|
* 2011-05-09: better way for js #3
|
16
20
|
* 2011-05-09: cdata added
|
data/README.md
CHANGED
@@ -1,37 +1,32 @@
|
|
1
1
|
# HammerBuilder
|
2
2
|
|
3
|
-
|
4
|
-
is a xhtml5 builder written in and for 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
|
3
|
+
Fast Ruby xhtml5 renderer
|
11
4
|
|
12
5
|
## Links
|
13
6
|
|
14
|
-
*
|
15
|
-
|
16
|
-
*
|
17
|
-
*
|
18
|
-
*
|
19
|
-
*
|
7
|
+
* **Presentation**: <http://hammer.pitr.ch/hammer_builder/presentation/presentation.html>
|
8
|
+
* Gemcutter: <https://rubygems.org/gems/hammer_builder>
|
9
|
+
* Github: <https://github.com/ruby-hammer/hammer-builder>
|
10
|
+
* Yardoc: <http://rubydoc.info/github/ruby-hammer/hammer-builder/frames>
|
11
|
+
* Issues: <https://github.com/ruby-hammer/hammer-builder/issues>
|
12
|
+
* Changelog: <http://hammer.pitr.ch/hammer-builder/file.CHANGELOG.html>
|
13
|
+
* Gem: [https://rubygems.org/gems/hammer_builder](https://rubygems.org/gems/hammer_builder)
|
14
|
+
* Blog: <http://hammer.pitr.ch/>
|
20
15
|
|
21
16
|
## Syntax
|
22
17
|
|
23
|
-
HammerBuilder::Formated.
|
18
|
+
HammerBuilder::Formated.new.go_in do
|
24
19
|
xhtml5!
|
25
20
|
html do
|
26
21
|
head { title 'a title' }
|
27
22
|
body do
|
28
|
-
div.
|
23
|
+
div.menu!.left do
|
29
24
|
ul do
|
30
25
|
li 'home'
|
31
26
|
li 'contacts', :class => 'active'
|
32
27
|
end
|
33
28
|
end
|
34
|
-
div.
|
29
|
+
div.content! do
|
35
30
|
article.id 'article1' do
|
36
31
|
h1 'header'
|
37
32
|
p('some text').class('centered')
|
@@ -75,20 +70,18 @@ has been written with three objectives:
|
|
75
70
|
|
76
71
|
### Synthetic
|
77
72
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
erubis
|
85
|
-
fasterubis
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
markaby 20.750000 0.030000 20.780000 ( 21.371292)
|
91
|
-
tagz 73.200000 0.140000 73.340000 ( 73.306450)
|
73
|
+
user system total real
|
74
|
+
tenjin-reuse 2.040000 0.000000 2.040000 ( 2.055140)
|
75
|
+
HammerBuilder::Standard 2.520000 0.000000 2.520000 ( 2.519284)
|
76
|
+
fasterubis-reuse 2.580000 0.000000 2.580000 ( 2.581407)
|
77
|
+
erubis-reuse 2.680000 0.000000 2.680000 ( 2.690176)
|
78
|
+
HammerBuilder::Formatted 2.780000 0.000000 2.780000 ( 2.794307)
|
79
|
+
erubis 5.180000 0.000000 5.180000 ( 5.183333)
|
80
|
+
fasterubis 5.210000 0.000000 5.210000 ( 5.219176)
|
81
|
+
tenjin 7.650000 0.160000 7.810000 ( 7.820490)
|
82
|
+
erector 9.450000 0.010000 9.460000 ( 9.471654)
|
83
|
+
markaby 14.300000 0.000000 14.300000 ( 14.318844)
|
84
|
+
tagz 33.430000 0.000000 33.430000 ( 33.483693)
|
92
85
|
|
93
86
|
### In Rails 3
|
94
87
|
|
@@ -122,13 +115,3 @@ has been written with three objectives:
|
|
122
115
|
objects: 0
|
123
116
|
gc_runs: 6
|
124
117
|
gc_time: 0.20 ms
|
125
|
-
|
126
|
-
### Conclusion
|
127
|
-
|
128
|
-
Template engines are slightly faster than [`HammerBuilder`](https://github.com/ruby-hammer/hammer-builder)
|
129
|
-
when template does not content a lot of inserting or partials.
|
130
|
-
On the other hand when partials are used, [`HammerBuilder`](https://github.com/ruby-hammer/hammer-builder)
|
131
|
-
beats template engines.
|
132
|
-
There is no overhead for partials in [`HammerBuilder`](https://github.com/ruby-hammer/hammer-builder)
|
133
|
-
compared to using partials in template engine. The difference is significant for `Erubis`, `Tenjin` is
|
134
|
-
not so bad, but I did not find any easy way to use `Tenjin` in Rails 3 (I did some hacking).
|
data/lib/hammer_builder.rb
CHANGED
@@ -1,702 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'active_support/core_ext/string/inflections'
|
4
|
-
require 'hammer_builder/dynamic_classes'
|
1
|
+
path = File.expand_path(File.dirname(__FILE__))
|
2
|
+
$: << path unless $:.include? path
|
5
3
|
|
6
|
-
|
7
|
-
EXTRA_ATTRIBUTES = {
|
8
|
-
"a" => ["href", "target", "ping", "rel", "media", "hreflang", "type"],
|
9
|
-
"abbr" => [],
|
10
|
-
"address" => [],
|
11
|
-
"area" => ["alt", "coords", "shape", "href", "target", "ping", "rel", "media", "hreflang", "type"],
|
12
|
-
"article" => [],
|
13
|
-
"aside" => [],
|
14
|
-
"audio" => ["src", "preload", "autoplay", "mediagroup", "loop", "controls"],
|
15
|
-
"b" => [],
|
16
|
-
"base" => ["href", "target"],
|
17
|
-
"bdi" => [],
|
18
|
-
"bdo" => [],
|
19
|
-
"blockquote" => ["cite"],
|
20
|
-
"body" => ["onafterprint", "onbeforeprint", "onbeforeunload", "onblur", "onerror", "onfocus", "onhashchange",
|
21
|
-
"onload", "onmessage", "onoffline", "ononline", "onpagehide", "onpageshow", "onpopstate", "onredo", "onresize",
|
22
|
-
"onscroll", "onstorage", "onundo", "onunload"],
|
23
|
-
"br" => [],
|
24
|
-
"button" => ["autofocus", "disabled", "form", "formaction", "formenctype", "formmethod", "formnovalidate",
|
25
|
-
"formtarget", "name", "type", "value"],
|
26
|
-
"canvas" => ["width", "height"],
|
27
|
-
"caption" => [],
|
28
|
-
"cite" => [],
|
29
|
-
"code" => [],
|
30
|
-
"col" => ["span"],
|
31
|
-
"colgroup" => ["span"],
|
32
|
-
"command" => ["type", "label", "icon", "disabled", "checked", "radiogroup"],
|
33
|
-
"datalist" => ["option"],
|
34
|
-
"dd" => [],
|
35
|
-
"del" => ["cite", "datetime"],
|
36
|
-
"details" => ["open"],
|
37
|
-
"dfn" => [],
|
38
|
-
"div" => [],
|
39
|
-
"dl" => [],
|
40
|
-
"dt" => [],
|
41
|
-
"em" => [],
|
42
|
-
"embed" => ["src", "type", "width", "height"],
|
43
|
-
"fieldset" => ["disabled", "form", "name"],
|
44
|
-
"figcaption" => [],
|
45
|
-
"figure" => [],
|
46
|
-
"footer" => [],
|
47
|
-
"form" => ["action", "autocomplete", "enctype", "method", "name", "novalidate", "target", 'accept_charset'],
|
48
|
-
"h1" => [],
|
49
|
-
"h2" => [],
|
50
|
-
"h3" => [],
|
51
|
-
"h4" => [],
|
52
|
-
"h5" => [],
|
53
|
-
"h6" => [],
|
54
|
-
"head" => [],
|
55
|
-
"header" => [],
|
56
|
-
"hgroup" => [],
|
57
|
-
"hr" => [],
|
58
|
-
"html" => ["manifest"],
|
59
|
-
"i" => [],
|
60
|
-
"iframe" => ["src", "srcdoc", "name", "sandbox", "seamless", "width", "height"],
|
61
|
-
"img" => ["alt", "src", "usemap", "ismap", "width", "height"],
|
62
|
-
"input" => ["accept", "alt", "autocomplete", "autofocus", "checked", "dirname", "disabled", "form", "formaction",
|
63
|
-
"formenctype", "formmethod", "formnovalidate", "formtarget", "height", "list", "max", "maxlength", "min",
|
64
|
-
"multiple", "name", "pattern", "placeholder", "readonly", "required", "size", "src", "step", "type", "value",
|
65
|
-
"width"],
|
66
|
-
"ins" => ["cite", "datetime"],
|
67
|
-
"kbd" => [],
|
68
|
-
"keygen" => ["autofocus", "challenge", "disabled", "form", "keytype", "name"],
|
69
|
-
"label" => ["form", "for"],
|
70
|
-
"legend" => [],
|
71
|
-
"li" => ["value"],
|
72
|
-
"link" => ["href", "rel", "media", "hreflang", "type", "sizes"],
|
73
|
-
"map" => ["name"],
|
74
|
-
"mark" => [],
|
75
|
-
"menu" => ["type", "label"],
|
76
|
-
"meta" => ["name", "content", "charset", "http_equiv"],
|
77
|
-
"meter" => ["value", "min", "max", "low", "high", "optimum", "form"],
|
78
|
-
"nav" => [],
|
79
|
-
"noscript" => [],
|
80
|
-
"object" => ["data", "type", "name", "usemap", "form", "width", "height"],
|
81
|
-
"ol" => ["reversed", "start"],
|
82
|
-
"optgroup" => ["disabled", "label"],
|
83
|
-
"option" => ["disabled", "label", "selected", "value"],
|
84
|
-
"output" => ["for", "form", "name"],
|
85
|
-
"p" => [],
|
86
|
-
"param" => ["name", "value"],
|
87
|
-
"pre" => [],
|
88
|
-
"progress" => ["value", "max", "form"],
|
89
|
-
"q" => ["cite"],
|
90
|
-
"rp" => [],
|
91
|
-
"rt" => [],
|
92
|
-
"ruby" => [],
|
93
|
-
"s" => [],
|
94
|
-
"samp" => [],
|
95
|
-
"script" => ["src", "async", "defer", "type", "charset"],
|
96
|
-
"section" => [],
|
97
|
-
"select" => ["autofocus", "disabled", "form", "multiple", "name", "required", "size"],
|
98
|
-
"small" => [],
|
99
|
-
"source" => ["src", "type", "media"],
|
100
|
-
"span" => [],
|
101
|
-
"strong" => [],
|
102
|
-
"style" => ["media", "type", "scoped"],
|
103
|
-
"sub" => [],
|
104
|
-
"summary" => [],
|
105
|
-
"sup" => [],
|
106
|
-
"table" => ["border"],
|
107
|
-
"tbody" => [],
|
108
|
-
"td" => ["colspan", "rowspan", "headers"],
|
109
|
-
"textarea" => ["autofocus", "cols", "disabled", "form", "maxlength", "name", "placeholder", "readonly",
|
110
|
-
"required", "rows", "wrap"],
|
111
|
-
"tfoot" => [],
|
112
|
-
"th" => ["colspan", "rowspan", "headers", "scope"],
|
113
|
-
"thead" => [],
|
114
|
-
"time" => ["datetime", "pubdate"],
|
115
|
-
"title" => [],
|
116
|
-
"tr" => [],
|
117
|
-
"track" => ["default", "kind", "label", "src", "srclang"],
|
118
|
-
"u" => [],
|
119
|
-
"ul" => [],
|
120
|
-
"var" => [],
|
121
|
-
"video" => ["src", "poster", "preload", "autoplay", "mediagroup", "loop", "controls", "width", "height"],
|
122
|
-
"wbr" => []
|
123
|
-
}
|
124
|
-
|
125
|
-
GLOBAL_ATTRIBUTES = [
|
126
|
-
'accesskey','class','contenteditable','contextmenu','dir','draggable','dropzone','hidden','id','lang',
|
127
|
-
'spellcheck','style','tabindex','title','onabort','onblur','oncanplay','oncanplaythrough','onchange',
|
128
|
-
'onclick','oncontextmenu','oncuechange','ondblclick','ondrag','ondragend','ondragenter','ondragleave',
|
129
|
-
'ondragover','ondragstart','ondrop','ondurationchange','onemptied','onended','onerror','onfocus','oninput',
|
130
|
-
'oninvalid','onkeydown','onkeypress','onkeyup','onload','onloadeddata','onloadedmetadata','onloadstart',
|
131
|
-
'onmousedown','onmousemove','onmouseout','onmouseover','onmouseup','onmousewheel','onpause','onplay',
|
132
|
-
'onplaying','onprogress','onratechange','onreadystatechange','onreset','onscroll','onseeked','onseeking',
|
133
|
-
'onselect','onshow','onstalled','onsubmit','onsuspend','ontimeupdate','onvolumechange','onwaiting'
|
134
|
-
]
|
4
|
+
require "hammer_builder/formatted"
|
135
5
|
|
136
|
-
DOUBLE_TAGS = [
|
137
|
-
'a', 'abbr', 'article', 'aside', 'audio', 'address',
|
138
|
-
'b', 'bdo', 'blockquote', 'body', 'button',
|
139
|
-
'canvas', 'caption', 'cite', 'code', 'colgroup', 'command',
|
140
|
-
'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt',
|
141
|
-
'em',
|
142
|
-
'fieldset', 'figure', 'footer', 'form',
|
143
|
-
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'html', 'i',
|
144
|
-
'iframe', 'ins', 'keygen', 'kbd', 'label', 'legend', 'li',
|
145
|
-
'map', 'mark', 'meter',
|
146
|
-
'nav', 'noscript',
|
147
|
-
'object', 'ol', 'optgroup', 'option',
|
148
|
-
'p', 'pre', 'progress',
|
149
|
-
'q', 'ruby', 'rt', 'rp', 's',
|
150
|
-
'samp', 'script', 'section', 'select', 'small', 'source', 'span',
|
151
|
-
'strong', 'style', 'sub', 'sup',
|
152
|
-
'table', 'tbody', 'td', 'textarea', 'tfoot',
|
153
|
-
'th', 'thead', 'time', 'title', 'tr',
|
154
|
-
'u', 'ul',
|
155
|
-
'var', 'video'
|
156
|
-
]
|
157
|
-
|
158
|
-
EMPTY_TAGS = [
|
159
|
-
'area', 'base', 'br', 'col', 'embed',
|
160
|
-
'hr', 'img', 'input', 'link', 'meta', 'param'
|
161
|
-
]
|
162
|
-
|
163
|
-
LT = '<'.freeze
|
164
|
-
GT = '>'.freeze
|
165
|
-
SLASH_LT = '</'.freeze
|
166
|
-
SLASH_GT = ' />'.freeze
|
167
|
-
SPACE = ' '.freeze
|
168
|
-
MAX_LEVELS = 300
|
169
|
-
SPACES = Array.new(MAX_LEVELS) {|i| (' ' * i).freeze }
|
170
|
-
NEWLINE = "\n".freeze
|
171
|
-
QUOTE = '"'.freeze
|
172
|
-
EQL = '='.freeze
|
173
|
-
EQL_QUOTE = EQL + QUOTE
|
174
|
-
COMMENT_START = '<!--'.freeze
|
175
|
-
COMMENT_END = '-->'.freeze
|
176
|
-
CDATA_START = '<![CDATA['.freeze
|
177
|
-
CDATA_END = ']]>'.freeze
|
178
|
-
|
179
|
-
module Helper
|
180
|
-
def self.included(base)
|
181
|
-
super
|
182
|
-
base.extend ClassMethods
|
183
|
-
base.class_inheritable_array :builder_methods, :instance_writer => false, :instance_reader => false
|
184
|
-
end
|
185
|
-
|
186
|
-
module ClassMethods
|
187
|
-
|
188
|
-
# adds instance method to the class. Method accepts any instance of builder and returns it after rendering.
|
189
|
-
# @param [Symbol] method_name
|
190
|
-
# @yield [self] builder_block is evaluated inside builder and accepts instance of a rendered object as parameter
|
191
|
-
# @example
|
192
|
-
# class User
|
193
|
-
# # ...
|
194
|
-
# include HammerBuilder::Helper
|
195
|
-
#
|
196
|
-
# builder :menu do |user|
|
197
|
-
# li user.name
|
198
|
-
# end
|
199
|
-
# end
|
200
|
-
#
|
201
|
-
# User.new.menu(HammerBuilder::Standard.get).to_xhtml! #=> "<li>Name</li>"
|
202
|
-
def builder(method_name, &builder_block)
|
203
|
-
self.builder_methods = [method_name.to_sym]
|
204
|
-
define_method(method_name) do |builder, *args|
|
205
|
-
builder.go_in(self, *args, &builder_block)
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
# Creating builder instances is expensive, therefore you can use Pool to go around that
|
212
|
-
module Pool
|
213
|
-
def self.included(base)
|
214
|
-
super
|
215
|
-
base.extend ClassMethods
|
216
|
-
end
|
217
|
-
|
218
|
-
module ClassMethods
|
219
|
-
# This the preferred way of getting new Builder. If you forget to release it, it does not matter -
|
220
|
-
# builder gets GCed after you lose reference
|
221
|
-
# @return [Standard, Formated]
|
222
|
-
def get
|
223
|
-
mutex.synchronize do
|
224
|
-
if free_builders.empty?
|
225
|
-
new
|
226
|
-
else
|
227
|
-
free_builders.pop
|
228
|
-
end
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
|
-
# returns +builder+ back into pool *DONT* forget to lose the reference to the +builder+
|
233
|
-
# @param [Standard, Formated]
|
234
|
-
def release(builder)
|
235
|
-
builder.reset
|
236
|
-
mutex.synchronize do
|
237
|
-
free_builders.push builder
|
238
|
-
end
|
239
|
-
nil
|
240
|
-
end
|
241
|
-
|
242
|
-
# @return [Fixnum] size of free builders
|
243
|
-
def pool_size
|
244
|
-
free_builders.size
|
245
|
-
end
|
246
|
-
|
247
|
-
private
|
248
|
-
|
249
|
-
def mutex
|
250
|
-
@mutex ||= Mutex.new
|
251
|
-
end
|
252
|
-
|
253
|
-
def free_builders
|
254
|
-
@free_builders ||= []
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
# instance version of ClassMethods.release
|
259
|
-
# @see ClassMethods.release
|
260
|
-
def release!
|
261
|
-
self.class.release(self)
|
262
|
-
end
|
263
|
-
end
|
264
|
-
|
265
|
-
# Abstract implementation of Builder
|
266
|
-
class Abstract
|
267
|
-
extend DynamicClasses
|
268
|
-
include Pool
|
269
|
-
|
270
|
-
# << faster then +
|
271
|
-
# yield faster then block.call
|
272
|
-
# accessing ivar and constant is faster then accesing hash or cvar
|
273
|
-
# class_eval faster then define_method
|
274
|
-
# beware of strings in methods -> creates a lot of garbage
|
275
|
-
|
276
|
-
dc do
|
277
|
-
|
278
|
-
define :AbstractTag do
|
279
|
-
def initialize(builder)
|
280
|
-
@builder = builder
|
281
|
-
@output = builder.instance_eval { @output }
|
282
|
-
@stack = builder.instance_eval { @stack }
|
283
|
-
@classes = []
|
284
|
-
set_tag
|
285
|
-
end
|
286
|
-
|
287
|
-
def open(attributes = nil)
|
288
|
-
@output << LT << @tag
|
289
|
-
@builder.current = self
|
290
|
-
attributes(attributes)
|
291
|
-
default
|
292
|
-
self
|
293
|
-
end
|
294
|
-
|
295
|
-
# @example
|
296
|
-
# div.attributes :id => 'id' # => <div id="id"></div>
|
297
|
-
def attributes(attrs)
|
298
|
-
return self unless attrs
|
299
|
-
attrs.each do |attr, value|
|
300
|
-
__send__(attr, *value)
|
301
|
-
end
|
302
|
-
self
|
303
|
-
end
|
304
|
-
|
305
|
-
# @example
|
306
|
-
# div.attribute :id, 'id' # => <div id="id"></div>
|
307
|
-
# @deprecated Please use {#attributes} instead
|
308
|
-
def attribute(attribute, content) # TODO lose the method in 0.2
|
309
|
-
warn ("method #attribute is deprecated use #attributes instead, called from:#{caller[0]}" )
|
310
|
-
@output << SPACE << attribute.to_s << EQL_QUOTE << CGI.escapeHTML(content.to_s) << QUOTE
|
311
|
-
end
|
312
|
-
|
313
|
-
alias_method(:rclass, :class)
|
314
|
-
|
315
|
-
class_inheritable_array :_attributes, :instance_writer => false, :instance_reader => false
|
316
|
-
|
317
|
-
def self.attributes
|
318
|
-
self._attributes
|
319
|
-
end
|
320
|
-
|
321
|
-
class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
|
322
|
-
# allows data-* attributes
|
323
|
-
def method_missing(method, *args, &block)
|
324
|
-
if method.to_s =~ /data_([a-z_]+)/
|
325
|
-
self.rclass.attributes = [method.to_s]
|
326
|
-
self.send method, *args, &block
|
327
|
-
else
|
328
|
-
super
|
329
|
-
end
|
330
|
-
end
|
331
|
-
RUBYCODE
|
332
|
-
|
333
|
-
protected
|
334
|
-
|
335
|
-
# sets the right tag in descendants
|
336
|
-
def self.set_tag(tag)
|
337
|
-
class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
|
338
|
-
def set_tag
|
339
|
-
@tag = '#{tag}'.freeze
|
340
|
-
end
|
341
|
-
RUBYCODE
|
342
|
-
end
|
343
|
-
|
344
|
-
set_tag 'abstract'
|
345
|
-
|
346
|
-
# this method is called on each tag opening, useful for default attributes
|
347
|
-
# @example html tag uses this to add xmlns attr.
|
348
|
-
# html # => <html xmlns="http://www.w3.org/1999/xhtml"></html>
|
349
|
-
def default
|
350
|
-
end
|
351
|
-
|
352
|
-
# defines dynamically methods for attributes
|
353
|
-
def self.define_attributes
|
354
|
-
attributes.each do |attr|
|
355
|
-
next if instance_methods.include?(attr.to_sym)
|
356
|
-
class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
|
357
|
-
def #{attr}(content)
|
358
|
-
@output << ATTR_#{attr.upcase} << CGI.escapeHTML(content.to_s) << QUOTE
|
359
|
-
self
|
360
|
-
end
|
361
|
-
RUBYCODE
|
362
|
-
end
|
363
|
-
define_attribute_constants
|
364
|
-
end
|
365
|
-
|
366
|
-
# defines constant strings not to make garbage
|
367
|
-
def self.define_attribute_constants
|
368
|
-
attributes.each do |attr|
|
369
|
-
const = "attr_#{attr}".upcase
|
370
|
-
HammerBuilder.const_set const, " #{attr.gsub('_', '-')}=\"".freeze unless HammerBuilder.const_defined?(const)
|
371
|
-
end
|
372
|
-
end
|
373
|
-
|
374
|
-
# adds attribute to class, triggers dynamical creation of needed instance methods etc.
|
375
|
-
def self.attributes=(attributes)
|
376
|
-
self._attributes = attributes
|
377
|
-
define_attributes
|
378
|
-
end
|
379
|
-
|
380
|
-
# flushes classes to output
|
381
|
-
def flush_classes
|
382
|
-
unless @classes.empty?
|
383
|
-
@output << ATTR_CLASS << CGI.escapeHTML(@classes.join(SPACE)) << QUOTE
|
384
|
-
@classes.clear
|
385
|
-
end
|
386
|
-
end
|
387
|
-
|
388
|
-
public
|
389
|
-
|
390
|
-
# global HTML5 attributes
|
391
|
-
self.attributes = GLOBAL_ATTRIBUTES
|
392
|
-
|
393
|
-
alias :[] :id
|
394
|
-
|
395
|
-
class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
|
396
|
-
def class(*classes)
|
397
|
-
@classes.push(*classes)
|
398
|
-
self
|
399
|
-
end
|
400
|
-
RUBYCODE
|
401
|
-
end
|
402
|
-
|
403
|
-
define :AbstractEmptyTag, :AbstractTag do
|
404
|
-
def flush
|
405
|
-
flush_classes
|
406
|
-
@output << SLASH_GT
|
407
|
-
nil
|
408
|
-
end
|
409
|
-
end
|
410
|
-
|
411
|
-
define :AbstractDoubleTag, :AbstractTag do
|
412
|
-
# defined by class_eval because there is a super calling, causing error:
|
413
|
-
# super from singleton method that is defined to multiple classes is not supported;
|
414
|
-
# this will be fixed in 1.9.3 or later (NotImplementedError)
|
415
|
-
class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
|
416
|
-
def initialize(builder)
|
417
|
-
super
|
418
|
-
@content = nil
|
419
|
-
end
|
420
|
-
|
421
|
-
def open(*args, &block)
|
422
|
-
attributes = if args.last.is_a?(Hash)
|
423
|
-
args.pop
|
424
|
-
end
|
425
|
-
content args[0]
|
426
|
-
super attributes
|
427
|
-
@stack << @tag
|
428
|
-
if block
|
429
|
-
with &block
|
430
|
-
else
|
431
|
-
self
|
432
|
-
end
|
433
|
-
end
|
434
|
-
RUBYCODE
|
435
|
-
|
436
|
-
def flush
|
437
|
-
flush_classes
|
438
|
-
@output << GT
|
439
|
-
@output << CGI.escapeHTML(@content) if @content
|
440
|
-
@output << SLASH_LT << @stack.pop << GT
|
441
|
-
@content = nil
|
442
|
-
end
|
443
|
-
|
444
|
-
# sets content of the double tag
|
445
|
-
def content(content)
|
446
|
-
@content = content.to_s
|
447
|
-
self
|
448
|
-
end
|
449
|
-
|
450
|
-
# renders content of the double tag with block
|
451
|
-
def with
|
452
|
-
flush_classes
|
453
|
-
@output << GT
|
454
|
-
@content = nil
|
455
|
-
@builder.current = nil
|
456
|
-
yield
|
457
|
-
# if (content = yield).is_a?(String)
|
458
|
-
# @output << CGI.escapeHTML(content)
|
459
|
-
# end
|
460
|
-
@builder.flush
|
461
|
-
@output << SLASH_LT << @stack.pop << GT
|
462
|
-
nil
|
463
|
-
end
|
464
|
-
|
465
|
-
protected
|
466
|
-
|
467
|
-
def self.define_attributes
|
468
|
-
attributes.each do |attr|
|
469
|
-
next if instance_methods(false).include?(attr.to_sym)
|
470
|
-
if instance_methods.include?(attr.to_sym)
|
471
|
-
class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
|
472
|
-
def #{attr}(*args, &block)
|
473
|
-
super(*args, &nil)
|
474
|
-
return with(&block) if block
|
475
|
-
self
|
476
|
-
end
|
477
|
-
RUBYCODE
|
478
|
-
else
|
479
|
-
class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
|
480
|
-
def #{attr}(content, &block)
|
481
|
-
@output << ATTR_#{attr.upcase} << CGI.escapeHTML(content.to_s) << QUOTE
|
482
|
-
return with(&block) if block
|
483
|
-
self
|
484
|
-
end
|
485
|
-
RUBYCODE
|
486
|
-
end
|
487
|
-
end
|
488
|
-
define_attribute_constants
|
489
|
-
end
|
490
|
-
end
|
491
|
-
end
|
492
|
-
|
493
|
-
class_inheritable_accessor :tags, :instance_writer => false
|
494
|
-
self.tags = {}
|
495
|
-
|
496
|
-
protected
|
497
|
-
|
498
|
-
# defines instance method for +tag+ in builder
|
499
|
-
def self.define_tag(tag)
|
500
|
-
tag = tag.to_s
|
501
|
-
class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
|
502
|
-
def #{tag}(*args, &block)
|
503
|
-
flush
|
504
|
-
@#{tag}.open(*args, &block)
|
505
|
-
end
|
506
|
-
RUBYCODE
|
507
|
-
self.tags[tag] = tag
|
508
|
-
end
|
509
|
-
|
510
|
-
public
|
511
|
-
|
512
|
-
attr_accessor :current
|
513
|
-
|
514
|
-
def initialize()
|
515
|
-
@output = ""
|
516
|
-
@stack = []
|
517
|
-
@current = nil
|
518
|
-
# tag classes initialization
|
519
|
-
tags.values.each do |klass|
|
520
|
-
instance_variable_set(:"@#{klass}", self.class.dc[klass.camelize.to_sym].new(self))
|
521
|
-
end
|
522
|
-
end
|
523
|
-
|
524
|
-
# escapes +text+ to output
|
525
|
-
def text(text)
|
526
|
-
flush
|
527
|
-
@output << CGI.escapeHTML(text.to_s)
|
528
|
-
end
|
529
|
-
|
530
|
-
# unescaped +text+ to output
|
531
|
-
def raw(text)
|
532
|
-
flush
|
533
|
-
@output << text.to_s
|
534
|
-
end
|
535
|
-
|
536
|
-
# inserts +comment+
|
537
|
-
def comment(comment)
|
538
|
-
flush
|
539
|
-
@output << COMMENT_START << comment.to_s << COMMENT_END
|
540
|
-
end
|
541
|
-
|
542
|
-
# insersts CDATA with +content+
|
543
|
-
def cdata(content)
|
544
|
-
flush
|
545
|
-
@output << CDATA_START << content.to_s << CDATA_END
|
546
|
-
end
|
547
|
-
|
548
|
-
def xml_version(version = '1.0', encoding = 'UTF-8')
|
549
|
-
flush
|
550
|
-
@output << "<?xml version=\"#{version}\" encoding=\"#{encoding}\"?>"
|
551
|
-
end
|
552
|
-
|
553
|
-
def doctype
|
554
|
-
flush
|
555
|
-
@output << "<!DOCTYPE html>"
|
556
|
-
end
|
557
|
-
|
558
|
-
# inserts xhtml5 header
|
559
|
-
def xhtml5!
|
560
|
-
xml_version
|
561
|
-
doctype
|
562
|
-
end
|
563
|
-
|
564
|
-
# resets the builder to the state after creation - much faster then creating a new one
|
565
|
-
def reset
|
566
|
-
flush
|
567
|
-
@output.clear
|
568
|
-
@stack.clear
|
569
|
-
self
|
570
|
-
end
|
571
|
-
|
572
|
-
# enables you to evaluate +block+ inside the builder with +variables+
|
573
|
-
# @example
|
574
|
-
# HammerBuilder::Formated.get.freeze.go_in('asd') do |string|
|
575
|
-
# div string
|
576
|
-
# end.to_html! #=> "<div>asd</div>"
|
577
|
-
#
|
578
|
-
def go_in(*variables, &block)
|
579
|
-
instance_exec *variables, &block
|
580
|
-
self
|
581
|
-
end
|
582
|
-
|
583
|
-
def set_variables(instance_variables)
|
584
|
-
instance_variables.each {|name,value| instance_variable_set("@#{name}", value) }
|
585
|
-
yield
|
586
|
-
instance_variables.each {|name,_| remove_instance_variable("@#{name}") }
|
587
|
-
self
|
588
|
-
end
|
589
|
-
|
590
|
-
# @return [String] output
|
591
|
-
def to_xhtml()
|
592
|
-
flush
|
593
|
-
@output.clone
|
594
|
-
end
|
595
|
-
|
596
|
-
# @return [String] output and releases the builder to pool
|
597
|
-
def to_xhtml!
|
598
|
-
r = to_xhtml
|
599
|
-
release!
|
600
|
-
r
|
601
|
-
end
|
602
|
-
|
603
|
-
def flush
|
604
|
-
if @current
|
605
|
-
@current.flush
|
606
|
-
@current = nil
|
607
|
-
end
|
608
|
-
end
|
609
|
-
end
|
610
|
-
|
611
|
-
# Builder implementation without formating (one line)
|
612
|
-
class Standard < Abstract
|
613
|
-
|
614
|
-
dc do
|
615
|
-
(DOUBLE_TAGS - ['html']).each do |tag|
|
616
|
-
define tag.camelize.to_sym , :AbstractDoubleTag do
|
617
|
-
set_tag tag
|
618
|
-
self.attributes = EXTRA_ATTRIBUTES[tag]
|
619
|
-
end
|
620
|
-
|
621
|
-
base.define_tag(tag)
|
622
|
-
end
|
623
|
-
|
624
|
-
define :Html, :AbstractDoubleTag do
|
625
|
-
set_tag 'html'
|
626
|
-
self.attributes = ['xmlns'] + EXTRA_ATTRIBUTES['html']
|
627
|
-
|
628
|
-
def default
|
629
|
-
xmlns('http://www.w3.org/1999/xhtml')
|
630
|
-
end
|
631
|
-
end
|
632
|
-
base.define_tag('html')
|
633
|
-
|
634
|
-
EMPTY_TAGS.each do |tag|
|
635
|
-
define tag.camelize.to_sym, :AbstractEmptyTag do
|
636
|
-
set_tag tag
|
637
|
-
self.attributes = EXTRA_ATTRIBUTES[tag]
|
638
|
-
end
|
639
|
-
|
640
|
-
base.define_tag(tag)
|
641
|
-
end
|
642
|
-
end
|
643
|
-
|
644
|
-
def js(js , options = {})
|
645
|
-
flush
|
646
|
-
script({:type => "text/javascript"}.merge(options)) { cdata js }
|
647
|
-
end
|
648
|
-
|
649
|
-
def join(collection, glue, &it)
|
650
|
-
flush
|
651
|
-
glue_block = if glue.is_a? String
|
652
|
-
lambda { text glue }
|
653
|
-
else
|
654
|
-
glue
|
655
|
-
end
|
656
|
-
|
657
|
-
collection.each_with_index do |obj, i|
|
658
|
-
glue_block.call() if i > 0
|
659
|
-
it.call(obj)
|
660
|
-
end
|
661
|
-
end
|
662
|
-
|
663
|
-
end
|
664
|
-
|
665
|
-
# Builder implementation with formating (indented by ' ')
|
666
|
-
# Slow down is less then 1%
|
667
|
-
class Formated < Standard
|
668
|
-
|
669
|
-
dc do
|
670
|
-
extend :AbstractTag do
|
671
|
-
def open(attributes = nil)
|
672
|
-
@output << NEWLINE << SPACES.fetch(@stack.size, SPACE) << LT << @tag
|
673
|
-
@builder.current = self
|
674
|
-
attributes(attributes)
|
675
|
-
default
|
676
|
-
self
|
677
|
-
end
|
678
|
-
end
|
679
|
-
|
680
|
-
extend :AbstractDoubleTag do
|
681
|
-
def with
|
682
|
-
flush_classes
|
683
|
-
@output << GT
|
684
|
-
@content = nil
|
685
|
-
@builder.current = nil
|
686
|
-
yield
|
687
|
-
# if (content = yield).is_a?(String)
|
688
|
-
# @output << CGI.escapeHTML(content)
|
689
|
-
# end
|
690
|
-
@builder.flush
|
691
|
-
@output << NEWLINE << SPACES.fetch(@stack.size-1, SPACE) << SLASH_LT << @stack.pop << GT
|
692
|
-
nil
|
693
|
-
end
|
694
|
-
end
|
695
|
-
end
|
696
|
-
|
697
|
-
def comment(comment)
|
698
|
-
@output << NEWLINE << SPACES.fetch(@stack.size, SPACE) << COMMENT_START << comment.to_s << COMMENT_END
|
699
|
-
end
|
700
|
-
end
|
701
|
-
end
|
702
6
|
|