erector 0.2.83 → 0.3.105
Sign up to get free protection for your applications and to get access to all the features.
- data/README.txt +17 -219
- data/lib/erector.rb +2 -1
- data/lib/erector/erect.rb +3 -1
- data/lib/erector/extensions/action_controller.rb +21 -19
- data/lib/erector/extensions/action_view_template_handler.rb +37 -12
- data/lib/erector/extensions/object.rb +14 -1
- data/lib/erector/helpers.rb +57 -14
- data/lib/erector/html_parts.rb +2 -0
- data/lib/erector/indenting.rb +1 -1
- data/lib/erector/raw_string.rb +8 -0
- data/lib/erector/widget.rb +225 -109
- data/lib/erector/widgets/table.rb +1 -1
- data/spec/erect/erect_spec.rb +4 -1
- data/spec/erect/rhtml_parser_spec.rb +6 -6
- data/spec/erector/extensions/template_handler_spec.rb +29 -0
- data/spec/erector/rails_helpers_spec.rb +147 -0
- data/spec/erector/widget_spec.rb +71 -0
- data/spec/rails/standard_helpers_spec.rb +105 -0
- data/spec/rails/view_spec.rb +33 -0
- data/spec/spec_helper.rb +8 -0
- data/test/rails_root/app/views/template_handler_spec/test_page.rb +8 -0
- metadata +11 -5
data/lib/erector/html_parts.rb
CHANGED
data/lib/erector/indenting.rb
CHANGED
data/lib/erector/widget.rb
CHANGED
@@ -1,20 +1,42 @@
|
|
1
1
|
require 'cgi'
|
2
2
|
|
3
3
|
module Erector
|
4
|
+
|
5
|
+
# A Widget is the center of the Erector universe.
|
6
|
+
#
|
7
|
+
# To create a widget, extend Erector::Widget and implement
|
8
|
+
# the +render+ method. Inside this method you may call any of the tag methods like +span+ or +p+ to emit HTML/XML
|
9
|
+
# tags.
|
10
|
+
#
|
11
|
+
# You can also define a widget on the fly by passing a block to +new+. This block will get executed when the widget's
|
12
|
+
# +render+ method is called.
|
13
|
+
#
|
14
|
+
# To render a widget from the outside, instantiate it and call its +to_s+ method.
|
15
|
+
#
|
16
|
+
# To call one widget from another, inside the parent widget's render method, instantiate the child widget and call
|
17
|
+
# its +render_to+ method, passing in +self+ (or self.doc if you prefer). This assures that the same HtmlParts stream
|
18
|
+
# is used, which gives better performance than using +capture+ or +to_s+.
|
19
|
+
#
|
20
|
+
# In this documentation we've tried to keep the distinction clear between methods that *emit* text and those that
|
21
|
+
# *return* text. "Emit" means that it writes HtmlParts to the doc stream; "return" means that it returns a string
|
22
|
+
# like a normal method and leaves it up to the caller to emit that string if it wants.
|
4
23
|
class Widget
|
5
24
|
class << self
|
6
25
|
def all_tags
|
7
26
|
Erector::Widget.full_tags + Erector::Widget.empty_tags
|
8
27
|
end
|
9
28
|
|
29
|
+
# tags which are always self-closing
|
10
30
|
def empty_tags
|
11
31
|
['area', 'base', 'br', 'hr', 'img', 'input', 'link', 'meta']
|
12
32
|
end
|
13
33
|
|
34
|
+
# tags which can contain other stuff
|
14
35
|
def full_tags
|
15
36
|
[
|
16
37
|
'a', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'body',
|
17
|
-
'button', 'caption', '
|
38
|
+
'button', 'caption', 'center', 'cite', 'code',
|
39
|
+
'dd', 'del', 'div', 'dl', 'dt', 'em',
|
18
40
|
'fieldset', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html', 'i',
|
19
41
|
'iframe', 'ins', 'kbd', 'label', 'legend', 'li', 'map',
|
20
42
|
'noframes', 'noscript', 'ol', 'optgroup', 'option', 'p', 'param', 'pre',
|
@@ -22,6 +44,7 @@ module Erector
|
|
22
44
|
'table', 'tbody', 'td', 'textarea', 'th', 'thead', 'title', 'tr', 'tt', 'u', 'ul', 'var'
|
23
45
|
]
|
24
46
|
end
|
47
|
+
|
25
48
|
end
|
26
49
|
|
27
50
|
include ActionController::UrlWriter
|
@@ -32,72 +55,151 @@ module Erector
|
|
32
55
|
attr_reader :block
|
33
56
|
attr_reader :parent
|
34
57
|
|
35
|
-
# Each item in @doc is an array containing three values: type, value, attributes
|
36
58
|
def initialize(helpers=nil, assigns={}, doc = HtmlParts.new, &block)
|
37
59
|
@assigns = assigns
|
38
|
-
assigns
|
60
|
+
assign_locals(assigns)
|
61
|
+
@helpers = helpers
|
62
|
+
@parent = block ? eval("self", block.binding) : nil
|
63
|
+
@doc = doc
|
64
|
+
@block = block
|
65
|
+
end
|
66
|
+
|
67
|
+
#-- methods for other classes to call, left public for ease of testing and documentation
|
68
|
+
#++
|
69
|
+
|
70
|
+
def assign_locals(local_assigns)
|
71
|
+
local_assigns.each do |name, value|
|
39
72
|
instance_variable_set("@#{name}", value)
|
40
73
|
metaclass.module_eval do
|
41
74
|
attr_reader name
|
42
75
|
end
|
43
76
|
end
|
44
|
-
@helpers = helpers
|
45
|
-
@parent = block ? eval("self", block.binding) : nil
|
46
|
-
@doc = doc
|
47
|
-
@block = block
|
48
77
|
end
|
49
78
|
|
79
|
+
# Entry point for rendering a widget (and all its children). This method creates a new HtmlParts doc stream,
|
80
|
+
# calls this widget's #render method, converts the HtmlParts to a string, and returns the string.
|
81
|
+
#
|
82
|
+
# If it's called again later
|
83
|
+
# then it returns the earlier rendered string, which leads to higher performance, but may have confusing
|
84
|
+
# effects if some underlying state has changed. In general we recommend you create a new instance of every
|
85
|
+
# widget for each render, unless you know what you're doing.
|
86
|
+
def to_s(&blk)
|
87
|
+
# The @__to_s variable is used as a cache.
|
88
|
+
# If it's useful we should add a test for it. -ac
|
89
|
+
return @__to_s if @__to_s
|
90
|
+
render(&blk)
|
91
|
+
@__to_s = @doc.to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
alias_method :inspect, :to_s
|
95
|
+
|
96
|
+
# Template method which must be overridden by all widget subclasses. Inside this method you call the magic
|
97
|
+
# #element methods which emit HTML and text to the HtmlParts stream.
|
50
98
|
def render
|
51
99
|
if @block
|
52
100
|
instance_eval(&@block)
|
53
101
|
end
|
54
102
|
end
|
55
103
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
104
|
+
# To call one widget from another, inside the parent widget's render method, instantiate the child widget and call
|
105
|
+
# its +render_to+ method, passing in +self+ (or self.doc if you prefer). This assures that the same HtmlParts stream
|
106
|
+
# is used, which gives better performance than using +capture+ or +to_s+.
|
107
|
+
def render_to(doc_or_widget)
|
108
|
+
if doc_or_widget.is_a?(Widget)
|
109
|
+
@parent = doc_or_widget
|
110
|
+
@doc = @parent.doc
|
111
|
+
else
|
112
|
+
@doc = doc_or_widget
|
113
|
+
end
|
64
114
|
render
|
65
115
|
end
|
66
116
|
|
117
|
+
# Convenience method for on-the-fly widgets. (Should we make this hidden? How is it used?)
|
67
118
|
def widget(widget_class, assigns={}, &block)
|
68
119
|
child = widget_class.new(helpers, assigns, doc, &block)
|
69
120
|
child.render
|
70
121
|
end
|
71
122
|
|
123
|
+
# (Should we make this hidden?)
|
124
|
+
def html_escape
|
125
|
+
return to_s
|
126
|
+
end
|
127
|
+
|
128
|
+
#-- methods for subclasses to call
|
129
|
+
#++
|
130
|
+
|
131
|
+
# Internal method used to emit an HTML/XML element, including an open tag, attributes (optional, via the default hash),
|
132
|
+
# contents (also optional), and close tag.
|
133
|
+
#
|
134
|
+
# Using the arcane powers of Ruby, there are magic methods that call +element+ for all the standard
|
135
|
+
# HTML tags, like +a+, +body+, +p+, and so forth. Look at the source of #full_tags for the full list.
|
136
|
+
# Unfortunately, this big mojo confuses rdoc, so we can't see each method in this rdoc page, but trust
|
137
|
+
# us, they're there.
|
138
|
+
#
|
139
|
+
# When calling one of these magic methods, put attributes in the default hash. If there is a string parameter,
|
140
|
+
# then it is used as the contents. If there is a block, then it is executed (yielded), and the string parameter is ignored.
|
141
|
+
# The block will usually be in the scope of the child widget, which means it has access to all the
|
142
|
+
# methods of Widget, which will eventually end up appending text to the +doc+ HtmlParts stream. See how
|
143
|
+
# elegant it is? Not confusing at all if you don't think about it.
|
144
|
+
#
|
145
|
+
def element(*args, &block)
|
146
|
+
__element__(*args, &block)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Internal method used to emit a self-closing HTML/XML element, including a tag name and optional attributes
|
150
|
+
# (passed in via the default hash).
|
151
|
+
#
|
152
|
+
# Using the arcane powers of Ruby, there are magic methods that call +empty_element+ for all the standard
|
153
|
+
# HTML tags, like +img+, +br+, and so forth. Look at the source of #empty_tags for the full list.
|
154
|
+
# Unfortunately, this big mojo confuses rdoc, so we can't see each method in this rdoc page, but trust
|
155
|
+
# us, they're there.
|
156
|
+
#
|
157
|
+
def empty_element(*args, &block)
|
158
|
+
__empty_element__(*args, &block)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Returns an HTML-escaped version of its parameter. Leaves the HtmlParts stream untouched. Note that
|
162
|
+
# the #text method automatically HTML-escapes its parameter, so be careful *not* to do something like
|
163
|
+
# text(h("2<4")) since that will double-escape the less-than sign (you'll get "2&lt;4" instead of
|
164
|
+
# "2<4").
|
72
165
|
def h(content)
|
73
166
|
content.html_escape
|
74
167
|
end
|
75
168
|
|
169
|
+
# Emits an open tag, comprising '<', tag name, optional attributes, and '>'
|
76
170
|
def open_tag(tag_name, attributes={})
|
77
171
|
@doc << {:type => :open, :tagName => tag_name, :attributes => attributes}
|
78
172
|
end
|
79
173
|
|
174
|
+
# Emits text which will be HTML-escaped.
|
80
175
|
def text(value)
|
81
176
|
@doc << {:type => :text, :value => value}
|
82
177
|
nil
|
83
178
|
end
|
84
179
|
|
180
|
+
# Returns text which will *not* be HTML-escaped.
|
85
181
|
def raw(value)
|
86
182
|
RawString.new(value.to_s)
|
87
183
|
end
|
88
184
|
|
185
|
+
# Returns text which will *not* be HTML-escaped. Same effect as text(raw(s))
|
89
186
|
def rawtext(value)
|
90
187
|
text raw(value)
|
91
188
|
end
|
92
189
|
|
190
|
+
# Returns a copy of value with spaces replaced by non-breaking space characters.
|
191
|
+
# The output uses the entity-escaping format ' ' since that works
|
192
|
+
# in both HTML and XML (as opposed to ' ' which only works in HTML).
|
93
193
|
def nbsp(value)
|
94
194
|
raw(value.html_escape.gsub(/ /,' '))
|
95
195
|
end
|
96
196
|
|
197
|
+
# Emits a close tag, consisting of '<', tag name, and '>'
|
97
198
|
def close_tag(tag_name)
|
98
199
|
@doc << {:type => :close, :tagName => tag_name}
|
99
200
|
end
|
100
201
|
|
202
|
+
# Emits an XML instruction, which looks like this: <?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
101
203
|
def instruct(attributes={:version => "1.0", :encoding => "UTF-8"})
|
102
204
|
@doc << {:type => :instruct, :attributes => attributes}
|
103
205
|
end
|
@@ -107,6 +209,41 @@ module Erector
|
|
107
209
|
@doc << {:type => :instruct, :attributes => attributes}
|
108
210
|
end
|
109
211
|
|
212
|
+
# Creates a whole new doc stream, executes the block, then converts the doc stream to a string and
|
213
|
+
# emits it as raw text. If at all possible you should avoid this method since it hurts performance,
|
214
|
+
# and use #render_to instead.
|
215
|
+
def capture(&block)
|
216
|
+
begin
|
217
|
+
original_doc = @doc
|
218
|
+
@doc = HtmlParts.new
|
219
|
+
yield
|
220
|
+
raw(@doc.to_s)
|
221
|
+
ensure
|
222
|
+
@doc = original_doc
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
full_tags.each do |tag_name|
|
227
|
+
self.class_eval(
|
228
|
+
"def #{tag_name}(*args, &block)\n" <<
|
229
|
+
" __element__('#{tag_name}', *args, &block)\n" <<
|
230
|
+
"end",
|
231
|
+
__FILE__,
|
232
|
+
__LINE__ - 4
|
233
|
+
)
|
234
|
+
end
|
235
|
+
|
236
|
+
empty_tags.each do |tag_name|
|
237
|
+
self.class_eval(
|
238
|
+
"def #{tag_name}(*args, &block)\n" <<
|
239
|
+
" __empty_element__('#{tag_name}', *args, &block)\n" <<
|
240
|
+
"end",
|
241
|
+
__FILE__,
|
242
|
+
__LINE__ - 4
|
243
|
+
)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Emits a javascript block inside a +script+ tag, wrapped in CDATA doohickeys like all the cool JS kids do.
|
110
247
|
def javascript(*args, &block)
|
111
248
|
if args.length > 2
|
112
249
|
raise ArgumentError, "Cannot accept more than two arguments"
|
@@ -142,8 +279,79 @@ module Erector
|
|
142
279
|
rawtext "\n// ]]>\n"
|
143
280
|
|
144
281
|
close_tag 'script'
|
145
|
-
|
282
|
+
rawtext "\n"
|
146
283
|
end
|
284
|
+
|
285
|
+
# Convenience method to emit a css file link, which looks like this: <link href="erector.css" rel="stylesheet" type="text/css" />
|
286
|
+
# The parameter is the full contents of the href attribute, including any ".css" extension.
|
287
|
+
#
|
288
|
+
# If you want to emit raw CSS inline, use the #script method instead.
|
289
|
+
def css(href)
|
290
|
+
link :rel => 'stylesheet', :type => 'text/css', :href => href
|
291
|
+
end
|
292
|
+
|
293
|
+
# Convenience method to emit an anchor tag whose href and text are the same, e.g. <a href="http://example.com">http://example.com</a>
|
294
|
+
def url(href)
|
295
|
+
a href, :href => href
|
296
|
+
end
|
297
|
+
|
298
|
+
### internal utility methods
|
299
|
+
|
300
|
+
protected
|
301
|
+
|
302
|
+
def method_missing(name, *args, &block)
|
303
|
+
block ||= lambda {} # captures self HERE
|
304
|
+
if @parent
|
305
|
+
@parent.send(name, *args, &block)
|
306
|
+
else
|
307
|
+
super
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def fake_erbout(&blk)
|
312
|
+
# override concat on the helpers object (which is usually a Rails view object)
|
313
|
+
unless @helpers.respond_to?(:concat_without_erector)
|
314
|
+
@helpers.metaclass.class_eval do
|
315
|
+
# alias_method :capture_without_erector, :capture
|
316
|
+
# define_method :capture do |*args|
|
317
|
+
# result = nil
|
318
|
+
# widget = @erector_widget_stack.first
|
319
|
+
# begin
|
320
|
+
# original_doc = widget.doc
|
321
|
+
# widget.doc = HtmlParts.new
|
322
|
+
# capture_without_erector(*args)
|
323
|
+
# result = raw(widget.doc.to_s)
|
324
|
+
# ensure
|
325
|
+
# widget.doc = original_doc
|
326
|
+
# end
|
327
|
+
# result
|
328
|
+
# end
|
329
|
+
|
330
|
+
alias_method :concat_without_erector, :concat
|
331
|
+
define_method :concat do |*args|
|
332
|
+
some_text, binding = args
|
333
|
+
raise "widget stack too big" if @erector_widget_stack.size > 10
|
334
|
+
if @erector_widget_stack.empty?
|
335
|
+
concat_without_erector(some_text, binding)
|
336
|
+
else
|
337
|
+
@erector_widget_stack.first.rawtext(some_text)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
define_method :erector_widget_stack do
|
341
|
+
@erector_widget_stack ||= []
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
@helpers.erector_widget_stack.unshift(self)
|
347
|
+
yield
|
348
|
+
ensure
|
349
|
+
if @helpers.respond_to?(:concat_without_erector)
|
350
|
+
@helpers.erector_widget_stack.shift
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
private
|
147
355
|
|
148
356
|
def __element__(tag_name, *args, &block)
|
149
357
|
if args.length > 2
|
@@ -169,102 +377,10 @@ module Erector
|
|
169
377
|
end
|
170
378
|
close_tag tag_name
|
171
379
|
end
|
172
|
-
alias_method :element, :__element__
|
173
380
|
|
174
381
|
def __empty_element__(tag_name, attributes={})
|
175
382
|
@doc << {:type => :empty, :tagName => tag_name, :attributes => attributes}
|
176
383
|
end
|
177
|
-
|
178
|
-
|
179
|
-
def capture(&block)
|
180
|
-
begin
|
181
|
-
original_doc = @doc
|
182
|
-
@doc = HtmlParts.new
|
183
|
-
yield
|
184
|
-
raw(@doc.to_s)
|
185
|
-
ensure
|
186
|
-
@doc = original_doc
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
def to_s(&blk)
|
191
|
-
return @__to_s if @__to_s
|
192
|
-
render(&blk)
|
193
|
-
@__to_s = @doc.to_s
|
194
|
-
end
|
195
|
-
|
196
|
-
def html_escape
|
197
|
-
return to_s
|
198
|
-
end
|
199
|
-
|
200
|
-
alias_method :inspect, :to_s
|
201
|
-
|
202
|
-
full_tags.each do |tag_name|
|
203
|
-
self.class_eval(
|
204
|
-
"def #{tag_name}(*args, &block)\n" <<
|
205
|
-
" __element__('#{tag_name}', *args, &block)\n" <<
|
206
|
-
"end",
|
207
|
-
__FILE__,
|
208
|
-
__LINE__ - 4
|
209
|
-
)
|
210
|
-
end
|
211
|
-
|
212
|
-
empty_tags.each do |tag_name|
|
213
|
-
self.class_eval(
|
214
|
-
"def #{tag_name}(*args, &block)\n" <<
|
215
|
-
" __empty_element__('#{tag_name}', *args, &block)\n" <<
|
216
|
-
"end",
|
217
|
-
__FILE__,
|
218
|
-
__LINE__ - 4
|
219
|
-
)
|
220
|
-
end
|
221
|
-
|
222
|
-
protected
|
223
|
-
def method_missing(name, *args, &block)
|
224
|
-
block ||= lambda {} # captures self HERE
|
225
|
-
if @parent
|
226
|
-
@parent.send(name, *args, &block)
|
227
|
-
else
|
228
|
-
super
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
|
-
def fake_erbout(&blk)
|
233
|
-
widget = self
|
234
|
-
@helpers.metaclass.class_eval do
|
235
|
-
raise "Cannot nest fake_erbout" if instance_methods.include?('concat_without_erector')
|
236
|
-
alias_method :concat_without_erector, :concat
|
237
|
-
define_method :concat do |some_text, binding|
|
238
|
-
widget.rawtext(some_text)
|
239
|
-
end
|
240
|
-
end
|
241
|
-
yield
|
242
|
-
ensure
|
243
|
-
@helpers.metaclass.class_eval do
|
244
|
-
alias_method :concat, :concat_without_erector
|
245
|
-
remove_method :concat_without_erector
|
246
|
-
end
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
class RawString < String
|
252
|
-
def html_escape
|
253
|
-
self
|
254
|
-
end
|
255
|
-
end
|
256
|
-
|
257
|
-
class Object
|
258
|
-
def html_escape
|
259
|
-
return CGI.escapeHTML(to_s)
|
260
|
-
end
|
261
|
-
|
262
|
-
def html_unescape
|
263
|
-
CGI.unescapeHTML(to_s)
|
264
|
-
end
|
265
|
-
|
266
|
-
# OMGWTF
|
267
|
-
def escape_single_quotes
|
268
|
-
self.gsub(/[']/, '\\\\\'')
|
384
|
+
|
269
385
|
end
|
270
386
|
end
|