erector 0.7.2 → 0.8.0

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.
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