erector 0.7.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/README.txt +17 -3
  2. data/VERSION.yml +2 -2
  3. data/bin/erector +1 -1
  4. data/lib/erector.rb +22 -2
  5. data/lib/erector/after_initialize.rb +34 -0
  6. data/lib/erector/caching.rb +93 -0
  7. data/lib/erector/convenience.rb +58 -0
  8. data/lib/erector/dependencies.rb +24 -0
  9. data/lib/erector/dependency.rb +21 -0
  10. data/lib/erector/{erect.rb → erect/erect.rb} +14 -4
  11. data/lib/erector/{erected.rb → erect/erected.rb} +6 -4
  12. data/lib/erector/{indenting.rb → erect/indenting.rb} +0 -0
  13. data/lib/erector/{rhtml.treetop → erect/rhtml.treetop} +51 -11
  14. data/lib/erector/errors.rb +12 -0
  15. data/lib/erector/extensions/hash.rb +21 -0
  16. data/lib/erector/externals.rb +88 -24
  17. data/lib/erector/html.rb +352 -0
  18. data/lib/erector/inline.rb +5 -5
  19. data/lib/erector/jquery.rb +36 -0
  20. data/lib/erector/mixin.rb +3 -5
  21. data/lib/erector/needs.rb +94 -0
  22. data/lib/erector/output.rb +117 -0
  23. data/lib/erector/rails.rb +2 -2
  24. data/lib/erector/rails/extensions/action_controller.rb +5 -3
  25. data/lib/erector/rails/extensions/rails_helpers.rb +159 -0
  26. data/lib/erector/rails/extensions/rails_widget.rb +98 -56
  27. data/lib/erector/rails/rails_form_builder.rb +8 -4
  28. data/lib/erector/rails/rails_version.rb +2 -2
  29. data/lib/erector/rails/template_handlers/ert_handler.rb +1 -1
  30. data/lib/erector/rails/template_handlers/rb_handler.rb +42 -1
  31. data/lib/erector/raw_string.rb +2 -2
  32. data/lib/erector/sass.rb +22 -0
  33. data/lib/erector/widget.rb +100 -653
  34. data/lib/erector/widgets.rb +1 -0
  35. data/lib/erector/widgets/external_renderer.rb +51 -0
  36. data/lib/erector/widgets/page.rb +45 -63
  37. data/lib/erector/widgets/table.rb +9 -1
  38. data/spec/erect/erect_rails_spec.rb +19 -17
  39. data/spec/erect/erect_spec.rb +11 -1
  40. data/spec/erect/erected_spec.rb +76 -5
  41. data/spec/erect/rhtml_parser_spec.rb +11 -1
  42. data/spec/erector/caching_spec.rb +267 -0
  43. data/spec/erector/convenience_spec.rb +258 -0
  44. data/spec/erector/dependency_spec.rb +46 -0
  45. data/spec/erector/externals_spec.rb +233 -0
  46. data/spec/erector/html_spec.rb +508 -0
  47. data/spec/erector/indentation_spec.rb +84 -24
  48. data/spec/erector/inline_spec.rb +19 -8
  49. data/spec/erector/jquery_spec.rb +35 -0
  50. data/spec/erector/mixin_spec.rb +1 -1
  51. data/spec/erector/needs_spec.rb +120 -0
  52. data/spec/erector/output_spec.rb +199 -0
  53. data/spec/erector/sample-file.txt +1 -0
  54. data/spec/erector/sass_spec.rb +33 -0
  55. data/spec/erector/widget_spec.rb +113 -932
  56. data/spec/erector/widgets/field_table_spec.rb +6 -6
  57. data/spec/erector/widgets/form_spec.rb +3 -3
  58. data/spec/erector/widgets/page_spec.rb +52 -6
  59. data/spec/erector/widgets/table_spec.rb +4 -4
  60. data/spec/spec_helper.rb +70 -29
  61. metadata +56 -19
  62. data/lib/erector/rails/extensions/rails_widget/rails_helpers.rb +0 -137
  63. data/spec/core_spec_suite.rb +0 -3
  64. data/spec/erector/external_spec.rb +0 -110
  65. data/spec/rails_spec_suite.rb +0 -3
  66. data/spec/spec.opts +0 -1
  67. data/spec/spec_suite.rb +0 -40
@@ -0,0 +1,12 @@
1
+ module Erector
2
+ module Errors
3
+ class RubyVersionNotSupported < RuntimeError
4
+ def initialize(version_identifier, explanation=nil)
5
+ super [
6
+ "Erector does not support Ruby version(s) #{version_identifier}.",
7
+ explanation ? "The reason(s) are:\n#{explanation}" : nil
8
+ ].compact.join("\n")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ class Hash
2
+ module CorrectlyHashedHash
3
+ def hash
4
+ out = 0
5
+ # This sort_by is all kinds of weird...basically, we need a deterministic order here,
6
+ # but we can't just use "sort", because keys aren't necessarily sortable (they don't
7
+ # necessarily respond to <=>). Sorting by their hash codes works just as well, and
8
+ # is guaranteed to work, since everything hashes.
9
+ keys.sort_by { |k| k.hash }.each { |k| out ^= k.hash; out ^= self[k].hash }
10
+ out
11
+ end
12
+
13
+ def eql?(o)
14
+ self == o
15
+ end
16
+ end
17
+
18
+ def correctly_hashed
19
+ extend(CorrectlyHashedHash)
20
+ end
21
+ end
@@ -1,32 +1,96 @@
1
1
  module Erector
2
- class External < Struct.new(:type, :klass, :text, :options)
3
- def initialize(type, klass, text, options = {})
4
- super(type.to_sym, klass, text, options)
5
- end
6
-
7
- def ==(other)
8
- (self.type == other.type and
9
- self.text == other.text and
10
- self.options == other.options) ? true : false
11
- end
12
- end
13
-
14
2
  module Externals
15
- def externals(type, klass = nil)
16
- type = type.to_sym
17
- (@@externals ||= []).select do |x|
18
- x.type == type &&
19
- (klass.nil? || x.klass == klass)
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+
9
+ # Express a dependency of this widget
10
+ # Multiple forms:
11
+ # depends_on(type, text, options = {})
12
+ # for example
13
+ # depends_on(:js, '/foo.js', :embed=>true)
14
+ #
15
+ # Other variants:
16
+ # depends_on(type, an_io, ... # file to be read
17
+ # depends_on('blah.js' ... infer :js
18
+ # depends_on('blah.css' ... infer :css
19
+ # depends on :js, 'file1.js', 'file2.js'... [options]
20
+ # depends_on :js => ["foo.js", "bar.js"], :css=>['file.css']
21
+ # depends_on :js => ["foo.js", "bar.js"], other_option=>:blah
22
+ def depends_on(*args)
23
+ x = interpret_args(*args)
24
+ my_dependencies.push(x)
25
+ end
26
+
27
+ # deprecated in favor of #depends_on
28
+ # todo: warning
29
+ def external(type, value, options = {})
30
+ type = type.to_sym
31
+ x = Dependency.new(type, value, options)
32
+ my_dependencies << x unless my_dependencies.include?(x)
33
+ end
34
+
35
+ # returns all dependencies of the given type from this class and all its
36
+ # superclasses
37
+ def dependencies(type)
38
+ type = type.to_sym
39
+ deps = Dependencies.new
40
+ deps.push(*superclass.dependencies(type)) if superclass.respond_to?(:dependencies)
41
+ deps.push(*my_dependencies.select { |x| x.type == type })
42
+ deps.uniq
43
+ end
44
+
45
+ def my_dependencies
46
+ @my_dependencies ||= Dependencies.new
47
+ end
48
+
49
+ private
50
+ INFERABLE_TYPES = [:css, :js]
51
+
52
+ def interpret_args(*args)
53
+ options = {}
54
+ options = args.pop if args.last.is_a?(::Hash)
55
+ if args.empty? && options.any?
56
+ deps = []
57
+ texts_hash = {}
58
+ INFERABLE_TYPES.each do |t|
59
+ texts_hash[t] = options.delete(t) if options.has_key? t
60
+ end
61
+ texts_hash.each do |t, texts|
62
+ texts.each do |text|
63
+ deps << interpret_args(t, text, options)
64
+ end
65
+ end
66
+ return deps
67
+ elsif args[0].class == Symbol
68
+ type = args.shift
69
+ else
70
+ type = /.+\.js/.match(args[0]) ? :js : :css
71
+ end
72
+
73
+ deps = args.map do |text|
74
+ Dependency.new(type, text, options)
75
+ end
76
+ deps.size == 1 ? deps.first : deps
20
77
  end
21
78
  end
22
79
 
23
- def external(type, value, options = {})
24
- type = type.to_sym
25
- klass = self # since it's a class method, self should be the class itself
26
- x = External.new(type, klass, value, options)
27
- @@externals ||= []
28
- @@externals << x unless @@externals.include?(x)
80
+ def render_with_externals(options_to_external_renderer = {})
81
+ output = Erector::Output.new
82
+ self.to_a(:output => output)
83
+ nested_widgets = output.widgets.to_a
84
+ externals = ExternalRenderer.new({:classes => nested_widgets}.merge(options_to_external_renderer)).to_html(:output => output)
85
+ output.to_a
29
86
  end
30
- end
31
87
 
88
+ def render_externals(options_to_external_renderer = {})
89
+ output_for_externals = Erector::Output.new
90
+ nested_widgets = output.widgets
91
+ externalizer = ExternalRenderer.new({:classes => nested_widgets}.merge(options_to_external_renderer))
92
+ externalizer._render(:output => output_for_externals)
93
+ output_for_externals.to_a
94
+ end
95
+ end
32
96
  end
@@ -0,0 +1,352 @@
1
+ module Erector
2
+ module HTML
3
+ module ClassMethods
4
+ # Tags which are always self-closing. Click "[Source]" to see the full list.
5
+ def empty_tags
6
+ ['area', 'base', 'br', 'col', 'embed', 'frame',
7
+ 'hr', 'img', 'input', 'link', 'meta', 'param']
8
+ end
9
+
10
+ # Tags which can contain other stuff. Click "[Source]" to see the full list.
11
+ def full_tags
12
+ [
13
+ 'a', 'abbr', 'acronym', 'address', 'article', 'aside', 'audio',
14
+ 'b', 'bdo', 'big', 'blockquote', 'body', 'button',
15
+ 'canvas', 'caption', 'center', 'cite', 'code', 'colgroup', 'command',
16
+ 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt',
17
+ 'em',
18
+ 'fieldset', 'figure', 'footer', 'form', 'frameset',
19
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'html', 'i',
20
+ 'iframe', 'ins', 'keygen', 'kbd', 'label', 'legend', 'li',
21
+ 'map', 'mark', 'meter',
22
+ 'nav', 'noframes', 'noscript',
23
+ 'object', 'ol', 'optgroup', 'option',
24
+ 'p', 'pre', 'progress',
25
+ 'q', 'ruby', 'rt', 'rp', 's',
26
+ 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strike',
27
+ 'strong', 'style', 'sub', 'sup',
28
+ 'table', 'tbody', 'td', 'textarea', 'tfoot',
29
+ 'th', 'thead', 'time', 'title', 'tr', 'tt',
30
+ 'u', 'ul',
31
+ 'var', 'video'
32
+ ]
33
+ end
34
+
35
+ def all_tags
36
+ full_tags + empty_tags
37
+ end
38
+
39
+ def def_empty_tag_method(tag_name)
40
+ self.class_eval(<<-SRC, __FILE__, __LINE__ + 1)
41
+ def #{tag_name}(*args, &block)
42
+ __empty_element__('#{tag_name}', *args, &block)
43
+ end
44
+ SRC
45
+ end
46
+
47
+ def def_full_tag_method(tag_name)
48
+ self.class_eval(<<-SRC, __FILE__, __LINE__ + 1)
49
+ def #{tag_name}(*args, &block)
50
+ __element__(false, '#{tag_name}', *args, &block)
51
+ end
52
+
53
+ def #{tag_name}!(*args, &block)
54
+ __element__(true, '#{tag_name}', *args, &block)
55
+ end
56
+ SRC
57
+ end
58
+ end
59
+
60
+ def self.included(base)
61
+ base.extend ClassMethods
62
+
63
+ base.full_tags.each do |tag_name|
64
+ base.def_full_tag_method(tag_name)
65
+ end
66
+
67
+ base.empty_tags.each do |tag_name|
68
+ base.def_empty_tag_method(tag_name)
69
+ end
70
+ end
71
+
72
+ # Internal method used to emit an HTML/XML element, including an open tag,
73
+ # attributes (optional, via the default hash), contents (also optional),
74
+ # and close tag.
75
+ #
76
+ # Using the arcane powers of Ruby, there are magic methods that call
77
+ # +element+ for all the standard HTML tags, like +a+, +body+, +p+, and so
78
+ # forth. Look at the source of #full_tags for the full list.
79
+ # Unfortunately, this big mojo confuses rdoc, so we can't see each method
80
+ # in this rdoc page, but trust us, they're there.
81
+ #
82
+ # When calling one of these magic methods, put attributes in the default
83
+ # hash. If there is a string parameter, then it is used as the contents.
84
+ # If there is a block, then it is executed (yielded), and the string
85
+ # parameter is ignored. The block will usually be in the scope of the
86
+ # child widget, which means it has access to all the methods of Widget,
87
+ # which will eventually end up appending text to the +output+ string. See
88
+ # how elegant it is? Not confusing at all if you don't think about it.
89
+ #
90
+ def element(*args, &block)
91
+ __element__(false, *args, &block)
92
+ end
93
+
94
+ # Like +element+, but string parameters are not escaped.
95
+ def element!(*args, &block)
96
+ __element__(true, *args, &block)
97
+ end
98
+
99
+ # Internal method used to emit a self-closing HTML/XML element, including
100
+ # a tag name and optional attributes (passed in via the default hash).
101
+ #
102
+ # Using the arcane powers of Ruby, there are magic methods that call
103
+ # +empty_element+ for all the standard HTML tags, like +img+, +br+, and so
104
+ # forth. Look at the source of #empty_tags for the full list.
105
+ # Unfortunately, this big mojo confuses rdoc, so we can't see each method
106
+ # in this rdoc page, but trust us, they're there.
107
+ #
108
+ def empty_element(*args, &block)
109
+ __empty_element__(*args, &block)
110
+ end
111
+
112
+ # Returns an HTML-escaped version of its parameter. Leaves the output
113
+ # string untouched. This method is idempotent: h(h(text)) will not
114
+ # double-escape text. This means that it is safe to do something like
115
+ # text(h("2<4")) -- it will produce "2&lt;4", not "2&amp;lt;4".
116
+ def h(content)
117
+ if content.respond_to?(:html_safe?) && content.html_safe?
118
+ content
119
+ else
120
+ raw(CGI.escapeHTML(content.to_s))
121
+ end
122
+ end
123
+
124
+ # Emits an open tag, comprising '<', tag name, optional attributes, and '>'
125
+ def open_tag(tag_name, attributes={})
126
+ output.newline if newliney?(tag_name) && !output.at_line_start?
127
+ output << raw("<#{tag_name}#{format_attributes(attributes)}>")
128
+ output.indent
129
+ end
130
+
131
+ # Emits a close tag, consisting of '<', '/', tag name, and '>'
132
+ def close_tag(tag_name)
133
+ output.undent
134
+ output << raw("</#{tag_name}>")
135
+ if newliney?(tag_name)
136
+ output.newline
137
+ end
138
+ end
139
+
140
+ # Returns text which will *not* be HTML-escaped.
141
+ def raw(value)
142
+ RawString.new(value.to_s)
143
+ end
144
+
145
+ # Emits text. If a string is passed in, it will be HTML-escaped. If the
146
+ # result of calling methods such as raw is passed in, the HTML will not be
147
+ # HTML-escaped again. If another kind of object is passed in, the result
148
+ # of calling its to_s method will be treated as a string would be.
149
+ #
150
+ # You shouldn't pass a widget in to this method, as that will cause
151
+ # performance problems (as well as being semantically goofy). Use the
152
+ # #widget method instead.
153
+ def text(value)
154
+ if value.is_a? Widget
155
+ widget value
156
+ else
157
+ output << h(value)
158
+ end
159
+ nil
160
+ end
161
+
162
+ # Emits text which will *not* be HTML-escaped. Same effect as text(raw(s))
163
+ def text!(value)
164
+ text raw(value)
165
+ end
166
+
167
+ alias rawtext text!
168
+
169
+ # Returns a copy of value with spaces replaced by non-breaking space characters.
170
+ # With no arguments, return a single non-breaking space.
171
+ # The output uses the escaping format '&#160;' since that works
172
+ # in both HTML and XML (as opposed to '&nbsp;' which only works in HTML).
173
+ def nbsp(value = " ")
174
+ raw(h(value).gsub(/ /,'&#160;'))
175
+ end
176
+
177
+ # Return a character given its unicode code point or unicode name.
178
+ def character(code_point_or_name)
179
+ if code_point_or_name.is_a?(Symbol)
180
+ found = Erector::CHARACTERS[code_point_or_name]
181
+ if found.nil?
182
+ raise "Unrecognized character #{code_point_or_name}"
183
+ end
184
+ raw("&#x#{sprintf '%x', found};")
185
+ elsif code_point_or_name.is_a?(Integer)
186
+ raw("&#x#{sprintf '%x', code_point_or_name};")
187
+ else
188
+ raise "Unrecognized argument to character: #{code_point_or_name}"
189
+ end
190
+ end
191
+
192
+ # Emits an XML instruction, which looks like this: <?xml version=\"1.0\" encoding=\"UTF-8\"?>
193
+ def instruct(attributes={:version => "1.0", :encoding => "UTF-8"})
194
+ output << raw("<?xml#{format_sorted(sort_for_xml_declaration(attributes))}?>")
195
+ end
196
+
197
+ # Emits an HTML comment (&lt;!-- ... --&gt;) surrounding +text+ and/or the output of +block+.
198
+ # see http://www.w3.org/TR/html4/intro/sgmltut.html#h-3.2.4
199
+ #
200
+ # If +text+ is an Internet Explorer conditional comment condition such as "[if IE]",
201
+ # the output includes the opening condition and closing "[endif]". See
202
+ # http://www.quirksmode.org/css/condcom.html
203
+ #
204
+ # Since "Authors should avoid putting two or more adjacent hyphens inside comments,"
205
+ # we emit a warning if you do that.
206
+ def comment(text = '', &block)
207
+ puts "Warning: Authors should avoid putting two or more adjacent hyphens inside comments." if text =~ /--/
208
+
209
+ conditional = text =~ /\[if .*\]/
210
+
211
+ rawtext "<!--"
212
+ rawtext text
213
+ rawtext ">" if conditional
214
+
215
+ if block
216
+ rawtext "\n"
217
+ block.call
218
+ rawtext "\n"
219
+ end
220
+
221
+ rawtext "<![endif]" if conditional
222
+ rawtext "-->\n"
223
+ end
224
+
225
+ # Emits a javascript block inside a +script+ tag, wrapped in CDATA
226
+ # doohickeys like all the cool JS kids do.
227
+ def javascript(*args, &block)
228
+ if args.length > 2
229
+ raise ArgumentError, "Cannot accept more than two arguments"
230
+ end
231
+ attributes, value = nil, nil
232
+ arg0 = args[0]
233
+ if arg0.is_a?(Hash)
234
+ attributes = arg0
235
+ else
236
+ value = arg0
237
+ arg1 = args[1]
238
+ if arg1.is_a?(Hash)
239
+ attributes = arg1
240
+ end
241
+ end
242
+ attributes ||= {}
243
+ attributes[:type] = "text/javascript"
244
+ open_tag 'script', attributes
245
+
246
+ # Shouldn't this be a "cdata" HtmlPart?
247
+ # (maybe, but the syntax is specific to javascript; it isn't
248
+ # really a generic XML CDATA section. Specifically,
249
+ # ]]> within value is not treated as ending the
250
+ # CDATA section by Firefox2 when parsing text/html,
251
+ # although I guess we could refuse to generate ]]>
252
+ # there, for the benefit of XML/XHTML parsers).
253
+ rawtext "\n// <![CDATA[\n"
254
+ if block
255
+ instance_eval(&block)
256
+ else
257
+ rawtext value
258
+ end
259
+ rawtext "\n// ]]>"
260
+ output.append_newline # this forces a newline even if we're not in pretty mode
261
+
262
+ close_tag 'script'
263
+ rawtext "\n"
264
+ end
265
+
266
+ protected
267
+ def __element__(raw, tag_name, *args, &block)
268
+ if args.length > 2
269
+ raise ArgumentError, "Cannot accept more than four arguments"
270
+ end
271
+ attributes, value = nil, nil
272
+ arg0 = args[0]
273
+ if arg0.is_a?(Hash)
274
+ attributes = arg0
275
+ else
276
+ value = arg0
277
+ arg1 = args[1]
278
+ if arg1.is_a?(Hash)
279
+ attributes = arg1
280
+ end
281
+ end
282
+ attributes ||= {}
283
+ open_tag tag_name, attributes
284
+ if block && value
285
+ raise ArgumentError, "You can't pass both a block and a value to #{tag_name} -- please choose one."
286
+ end
287
+ if block
288
+ block.call
289
+ elsif raw
290
+ text! value
291
+ else
292
+ text value
293
+ end
294
+ close_tag tag_name
295
+ end
296
+
297
+ def __empty_element__(tag_name, attributes={})
298
+ output << raw("<#{tag_name}#{format_attributes(attributes)} />")
299
+ output.newline if newliney?(tag_name)
300
+ end
301
+
302
+ def format_attributes(attributes)
303
+ if !attributes || attributes.empty?
304
+ ""
305
+ else
306
+ format_sorted(sorted(attributes))
307
+ end
308
+ end
309
+
310
+ def format_sorted(sorted)
311
+ results = ['']
312
+ sorted.each do |key, value|
313
+ if value
314
+ if value.is_a?(Array)
315
+ value = value.flatten
316
+ next if value.empty?
317
+ value = value.join(' ')
318
+ end
319
+ results << "#{key}=\"#{h(value)}\""
320
+ end
321
+ end
322
+ return results.join(' ')
323
+ end
324
+
325
+ def sorted(attributes)
326
+ stringized = []
327
+ attributes.each do |key, value|
328
+ stringized << [key.to_s, value]
329
+ end
330
+ return stringized.sort
331
+ end
332
+
333
+ def sort_for_xml_declaration(attributes)
334
+ # correct order is "version, encoding, standalone" (XML 1.0 section 2.8).
335
+ # But we only try to put version before encoding for now.
336
+ stringized = []
337
+ attributes.each do |key, value|
338
+ stringized << [key.to_s, value]
339
+ end
340
+ return stringized.sort{|a, b| b <=> a}
341
+ end
342
+
343
+ NON_NEWLINEY = {'i' => true, 'b' => true, 'small' => true,
344
+ 'img' => true, 'span' => true, 'a' => true,
345
+ 'input' => true, 'textarea' => true, 'button' => true, 'select' => true
346
+ }
347
+
348
+ def newliney?(tag_name)
349
+ !NON_NEWLINEY.include?(tag_name)
350
+ end
351
+ end
352
+ end