erector 0.2.83 → 0.3.105
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/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
|