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
@@ -1,84 +1,126 @@
1
1
  module Erector
2
2
  module Rails
3
- # These controller instance variables should not be passed to the
4
- # widget if it does not +need+ them.
5
- NON_NEEDED_CONTROLLER_INSTANCE_VARIABLES = [:@template, :@_request]
6
-
7
- def self.render(widget, controller, assigns = nil)
8
- if widget.is_a?(Class)
9
- unless assigns
10
- needs = widget.get_needed_variables
11
- assigns = {}
12
- variables = controller.instance_variable_names
13
- variables -= controller.protected_instance_variables
14
- variables.each do |name|
15
- assign = name.sub('@', '').to_sym
16
- next if !needs.empty? && !needs.include?(assign) && NON_NEEDED_CONTROLLER_INSTANCE_VARIABLES.include?(name.to_sym)
17
- assigns[assign] = controller.instance_variable_get(name)
18
- end
19
- end
3
+ def self.assigns_for(widget_class, view, local_assigns, is_partial)
4
+ assigns = {}
5
+
6
+ instance_variables = view.instance_variables_for_widget_assignment
7
+ if is_partial || widget_class.ignore_extra_controller_assigns
8
+ instance_variables = remove_unneeded_assigns(widget_class, instance_variables)
9
+ end
10
+
11
+ assigns.merge!(instance_variables) unless is_partial && (! widget_class.controller_assigns_propagate_to_partials)
12
+
13
+ if is_partial
14
+ assigns.merge!(filter_local_assigns_for_partial(widget_class, local_assigns || { }))
15
+ end
16
+
17
+ assigns
18
+ end
20
19
 
20
+ def self.remove_unneeded_assigns(widget_class, assigns)
21
+ needs = widget_class.needed_variables
22
+ if needs.empty?
23
+ assigns
24
+ else
25
+ assigns.reject { |key, value| ! needs.include?(key) }
26
+ end
27
+ end
28
+
29
+ def self.filter_local_assigns_for_partial(widget_class, local_assigns)
30
+ widget_class_variable_name = widget_class.name.underscore
31
+ widget_class_variable_name = $1 if widget_class_variable_name =~ %r{.*/(.*?)$}
32
+
33
+ local_assigns.reject do |name, value|
34
+ name == :object || name == widget_class_variable_name.to_sym
35
+ end
36
+ end
37
+
38
+ def self.render(widget, view, assigns = nil, options = {})
39
+ if widget.is_a?(Class)
40
+ assigns ||= assigns_for(widget, view, nil, false)
21
41
  widget = widget.new(assigns)
22
42
  end
23
43
 
24
- view = controller.response.template
25
44
  view.send(:_evaluate_assigns_and_ivars)
26
45
 
27
46
  view.with_output_buffer do
28
- widget.to_s(
29
- :output => view.output_buffer,
30
- :parent => view,
31
- :helpers => view
32
- )
47
+ # Set parent to the view and use Rails's output buffer.
48
+ widget.to_html(options.merge(:parent => view,
49
+ :output => Output.new { view.output_buffer }))
33
50
  end
34
51
  end
35
52
 
36
53
  module WidgetExtensions
54
+ module ClassMethods
55
+ def ignore_extra_controller_assigns
56
+ out = @ignore_extra_controller_assigns
57
+ out ||= (superclass.ignore_extra_controller_assigns ? :true : :false) if superclass.respond_to?(:ignore_extra_controller_assigns)
58
+ out ||= :true
59
+ out == :true
60
+ end
61
+
62
+ # Often, large Rails applications will assign many controller instance variables.
63
+ # Sometimes these aren't used by a view: ApplicationController might assign
64
+ # variables that are used by many, but not all, views; and various other things
65
+ # may accumulate, especially if you've been using templating systems that are
66
+ # more forgiving than Erector. If you then migrate to Erector, you're stuck using
67
+ # no "needs" declaration at all, because it needs to contain every assigned
68
+ # variable, or Erector will raise an exception.
69
+ #
70
+ # If you set this to true (and it's inherited through to subclasses), however,
71
+ # then "needs" declarations on the widget will cause excess controller variables
72
+ # to be ignored -- they'll be unavailable to the widget (so 'needs' still means
73
+ # something), but they won't cause widget instantiation to fail, either. This
74
+ # can let a large Rails project transition to Erector more smoothly.
75
+ def ignore_extra_controller_assigns=(new_value)
76
+ @ignore_extra_controller_assigns = (new_value ? :true : :false)
77
+ end
78
+
79
+ def controller_assigns_propagate_to_partials
80
+ out = @controller_assigns_propagate_to_partials
81
+ out ||= (superclass.controller_assigns_propagate_to_partials ? :true : :false) if superclass.respond_to?(:controller_assigns_propagate_to_partials)
82
+ out ||= :true
83
+ out == :true
84
+ end
85
+
86
+ # In ERb templates, controller instance variables are available to any partial
87
+ # that gets rendered by the view, no matter how deeply-nested. This effectively
88
+ # makes controller instance variables "globals". In small view hierarchies this
89
+ # probably isn't an issue, but in large ones it can make debugging and
90
+ # reasoning about the code very difficult.
91
+ #
92
+ # If you set this to true (and it's inherited through to subclasses), then any
93
+ # widget that's getting rendered as a partial will only have access to locals
94
+ # explicitly passed to it (render :partial => ..., :locals => ...). (This
95
+ # doesn't change the behavior of widgets that are explicitly rendered, as they
96
+ # don't have this issue.) This can allow for cleaner encapsulation of partials,
97
+ # as they must be passed everything they use and can't rely on controller
98
+ # instance variables.
99
+ def controller_assigns_propagate_to_partials=(new_value)
100
+ @controller_assigns_propagate_to_partials = (new_value ? :true : :false)
101
+ end
102
+ end
103
+
37
104
  def self.included(base)
38
- base.alias_method_chain :output, :parent
39
- base.alias_method_chain :capture, :parent
105
+ base.extend(ClassMethods)
40
106
  end
41
107
 
42
- def output_with_parent
43
- if parent.respond_to?(:output_buffer)
44
- parent.output_buffer.is_a?(String) ? parent.output_buffer : handle_rjs_buffer
108
+ # We need to delegate #capture to parent.capture, so that when
109
+ # the captured block is executed, both erector and Rails output
110
+ # from within the block go to the appropriate buffer.
111
+ def capture(&block)
112
+ if parent.respond_to?(:capture)
113
+ raw(parent.capture(&block).to_s)
45
114
  else
46
- output_without_parent
115
+ super
47
116
  end
48
117
  end
49
118
 
50
- def capture_with_parent(&block)
51
- parent ? raw(parent.capture(&block).to_s) : capture_without_parent(&block)
52
- end
53
-
54
119
  # This is here to force #parent.capture to return the output
55
120
  def __in_erb_template;
56
121
  end
57
-
58
- private
59
-
60
- def handle_rjs_buffer
61
- returning buffer = parent.output_buffer.dup.to_s do
62
- parent.output_buffer.clear
63
- parent.with_output_buffer(buffer) do
64
- buffer << parent.output_buffer.to_s
65
- end
66
- end
67
- end
68
122
  end
69
123
 
70
124
  Erector::Widget.send :include, WidgetExtensions
71
125
  end
72
-
73
- # RailsWidget and InlineRailsWidget are for backward compatibility.
74
- # New code should use Widget, InlineWidget, or Erector.inline.
75
- class RailsWidget < Widget
76
- def self.inline(*args, &block)
77
- InlineRailsWidget.new(*args, &block)
78
- end
79
- end
80
-
81
- class InlineRailsWidget < RailsWidget
82
- include Inline
83
- end
84
126
  end
@@ -4,17 +4,21 @@ module Erector
4
4
 
5
5
  def initialize(object_name, object, template, options, proc)
6
6
  @template = template
7
- @parent = ActionView::Helpers::FormBuilder.new(object_name, object, template, options, proc)
7
+ @parent = ActionView::Base.default_form_builder.new(object_name, object, template, options, proc)
8
8
  end
9
9
 
10
10
  def method_missing(method_name, *args, &block)
11
11
  if parent.respond_to?(method_name)
12
12
  return_value = parent.send(method_name, *args, &block)
13
- template.concat(return_value) if return_value.is_a?(String)
14
- return_value
13
+ if return_value.is_a?(String)
14
+ template.concat(return_value)
15
+ nil
16
+ else
17
+ return_value
18
+ end
15
19
  else
16
20
  super
17
21
  end
18
22
  end
19
23
  end
20
- end
24
+ end
@@ -1,6 +1,6 @@
1
1
  module Erector
2
2
  module Rails
3
- RAILS_VERSION = "2.3.4"
4
- RAILS_VERSION_TAG = "v2.3.4"
3
+ RAILS_VERSION = "2.3.8"
4
+ RAILS_VERSION_TAG = "v2.3.8"
5
5
  end
6
6
  end
@@ -18,7 +18,7 @@ module ActionView #:nodoc:
18
18
  " all[instance_variable] = instance_variable_get(instance_variable)",
19
19
  " all",
20
20
  "end",
21
- "r =::Erector.inline do",
21
+ "r = (controller.ert_template_base_class || ::Erector).inline do",
22
22
  " memoized_instance_variables.each do |instance_variable, value|",
23
23
  " instance_variable_set(instance_variable, value)",
24
24
  " end",
@@ -1,9 +1,50 @@
1
+ class ActionView::Base
2
+ def instance_variables_for_widget_assignment
3
+ instance_variables_for_widget_assignment_for(controller)
4
+ end
5
+
6
+ def instance_variables_for_widget_assignment_for(target)
7
+ assigns = { }
8
+ variables = target.instance_variable_names
9
+ variables -= target.protected_instance_variables if target.respond_to?(:protected_instance_variables)
10
+ variables -= %w{@real_format @request @template @_request}
11
+ variables.each do |name|
12
+ assign = name.sub('@', '').to_sym
13
+ assigns[assign] = target.instance_variable_get(name)
14
+ end
15
+ assigns
16
+ end
17
+ end
18
+
19
+ # Out of the box, the Cells plugin for Rails (http://cells.rubyforge.org/)
20
+ # does not work with Erector, because Erector tries to grab instance variables
21
+ # off the controller, rather than the cell itself.
22
+ #
23
+ # This code patches up Cell::View to make it work, but only if the Cells plugin
24
+ # is installed. (That's the bare "Cell::View" at the top, and rescue NameError
25
+ # at the bottom.) While you'd imagine that unilaterally opening Cell::View
26
+ # and adding the method would work, it doesn't; Cell::View is normally
27
+ # autoloaded, and since we'd end up defining it here, we'd keep it from getting
28
+ # loaded at all.
29
+ begin
30
+ Cell::View
31
+
32
+ class Cell::View < ActionView::Base
33
+ def instance_variables_for_widget_assignment
34
+ instance_variables_for_widget_assignment_for(cell)
35
+ end
36
+ end
37
+ rescue NameError
38
+ end
39
+
1
40
  module Erector
2
41
  class RbHandler < ActionView::TemplateHandler
3
42
  def render(template, local_assigns)
4
43
  require_dependency File.expand_path(template.filename)
5
44
  widget_class = "views/#{template.path_without_format_and_extension}".camelize.constantize
6
- Erector::Rails.render(widget_class, @view.controller)
45
+ is_partial = (File.basename(template.path_without_format_and_extension) =~ /^_/)
46
+ assigns = Erector::Rails.assigns_for(widget_class, @view, local_assigns, is_partial)
47
+ Erector::Rails.render(widget_class, @view, assigns)
7
48
  end
8
49
  end
9
50
  end
@@ -1,8 +1,8 @@
1
1
  module Erector
2
2
  # A string that has a special type so Erector knows to render it directly, not HTML-escaped
3
3
  class RawString < String
4
- def html_escape
5
- self
4
+ def html_safe?
5
+ true
6
6
  end
7
7
  end
8
8
  end
@@ -0,0 +1,22 @@
1
+
2
+ if Object.const_defined?(:Sass)
3
+ module Erector
4
+ # Adds sass support to Erector widgets.
5
+ # Note that sass is provided inside the gem named "haml",
6
+ # not the gem named "sass".
7
+ # To get sass support into your Erector project, install the
8
+ # haml gem -- see http://sass-lang.com/download.html -- and
9
+ # then do something like this:
10
+ # require 'rubygems'
11
+ # require 'sass'
12
+ #
13
+ # Current support is barebones. Please offer suggestions (or better
14
+ # yet, patches) for whether and how to support, e.g., caching,
15
+ # loading from files, precompilation, etc.
16
+ module Sass
17
+ def sass(sass_text)
18
+ style ::Sass::Engine.new(sass_text, :cache => false).render
19
+ end
20
+ end
21
+ end
22
+ end
@@ -7,14 +7,15 @@ module Erector
7
7
  # or +p+ to emit HTML/XML tags.
8
8
  #
9
9
  # You can also define a widget on the fly by passing a block to +new+. This
10
- # block will get executed when the widget's +content+ method is called.
10
+ # block will get executed when the widget's +content+ method is called. See
11
+ # the userguide for important details about the scope of this block when run --
12
+ # http://erector.rubyforge.org/userguide.html#blocks
11
13
  #
12
- # To render a widget from the outside, instantiate it and call its +to_s+
14
+ # To render a widget from the outside, instantiate it and call its +to_html+
13
15
  # method.
14
16
  #
15
17
  # A widget's +new+ method optionally accepts an options hash. Entries in
16
- # this hash are converted to instance variables, and +attr_reader+ accessors
17
- # are defined for each.
18
+ # this hash are converted to instance variables.
18
19
  #
19
20
  # You can add runtime input checking via the +needs+ macro. See #needs.
20
21
  # This mechanism is meant to ameliorate development-time confusion about
@@ -24,7 +25,7 @@ module Erector
24
25
  # To call one widget from another, inside the parent widget's +content+
25
26
  # method, instantiate the child widget and call the +widget+ method. This
26
27
  # assures that the same output stream is used, which gives better
27
- # performance than using +capture+ or +to_s+. It also preserves the
28
+ # performance than using +capture+ or +to_html+. It also preserves the
28
29
  # indentation and helpers of the enclosing class.
29
30
  #
30
31
  # In this documentation we've tried to keep the distinction clear between
@@ -32,252 +33,50 @@ module Erector
32
33
  # it writes to the output stream; "return" means that it returns a string
33
34
  # like a normal method and leaves it up to the caller to emit that string if
34
35
  # it wants.
35
- class Widget
36
- extend Erector::Externals # 'extend'ing since they're class methods, not instance methods
37
-
38
- class << self
39
- def all_tags
40
- Erector::Widget.full_tags + Erector::Widget.empty_tags
41
- end
42
-
43
- # Tags which are always self-closing. Click "[Source]" to see the full list.
44
- def empty_tags
45
- ['area', 'base', 'br', 'col', 'frame',
46
- 'hr', 'img', 'input', 'link', 'meta']
47
- end
48
-
49
- # Tags which can contain other stuff. Click "[Source]" to see the full list.
50
- def full_tags
51
- [
52
- 'a', 'abbr', 'acronym', 'address', 'article', 'aside', 'audio',
53
- 'b', 'bdo', 'big', 'blockquote', 'body', 'button',
54
- 'canvas', 'caption', 'center', 'cite', 'code', 'colgroup', 'command',
55
- 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt',
56
- 'em', 'embed',
57
- 'fieldset', 'figure', 'footer', 'form', 'frameset',
58
- 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'html', 'i',
59
- 'iframe', 'ins', 'keygen', 'kbd', 'label', 'legend', 'li',
60
- 'map', 'mark', 'meter',
61
- 'nav', 'noframes', 'noscript',
62
- 'object', 'ol', 'optgroup', 'option',
63
- 'p', 'param', 'pre', 'progress',
64
- 'q', 'ruby', 'rt', 'rp', 's',
65
- 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strike',
66
- 'strong', 'style', 'sub', 'sup',
67
- 'table', 'tbody', 'td', 'textarea', 'tfoot',
68
- 'th', 'thead', 'time', 'title', 'tr', 'tt', 'u', 'ul',
69
- 'var', 'video'
70
- ]
71
- end
72
-
73
- def def_empty_tag_method(tag_name)
74
- self.class_eval(<<-SRC, __FILE__, __LINE__)
75
- def #{tag_name}(*args, &block)
76
- __empty_element__('#{tag_name}', *args, &block)
77
- end
78
- SRC
79
- end
80
-
81
- def def_full_tag_method(tag_name)
82
- self.class_eval(<<-SRC, __FILE__, __LINE__)
83
- def #{tag_name}(*args, &block)
84
- __element__(false, '#{tag_name}', *args, &block)
85
- end
86
-
87
- def #{tag_name}!(*args, &block)
88
- __element__(true, '#{tag_name}', *args, &block)
89
- end
90
- SRC
91
- end
92
-
93
- def after_initialize(instance=nil, &blk)
94
- if blk
95
- after_initialize_parts << blk
96
- elsif instance
97
- if superclass.respond_to?(:after_initialize)
98
- superclass.after_initialize instance
99
- end
100
- after_initialize_parts.each do |part|
101
- instance.instance_eval &part
102
- end
103
- else
104
- raise ArgumentError, "You must provide either an instance or a block"
105
- end
106
- end
107
-
108
- protected
109
- def after_initialize_parts
110
- @after_initialize_parts ||= []
111
- end
112
-
113
- end
114
-
115
- # Class method by which widget classes can declare that they need certain
116
- # parameters. If needed parameters are not passed in to #new, then an
117
- # exception will be thrown (with a hopefully useful message about which
118
- # parameters are missing). This is intended to catch silly bugs like
119
- # passing in a parameter called 'name' to a widget that expects a
120
- # parameter called 'title'. Every variable declared in 'needs' will get an
121
- # attr_reader accessor declared for it.
122
- #
123
- # You can also declare default values for parameters using hash syntax.
124
- # You can put #needs declarations on multiple lines or on the same line;
125
- # the only caveat is that if there are default values, they all have to be
126
- # at the end of the line (so they go into the magic hash parameter).
127
- #
128
- # If a widget has no #needs declaration then it will accept any
129
- # combination of parameters (and make accessors for them) just like
130
- # normal. In that case there will be no 'attr_reader's declared. If a
131
- # widget wants to declare that it takes no parameters, use the special
132
- # incantation "needs nil" (and don't declare any other needs, or kittens
133
- # will cry).
134
- #
135
- # Usage:
136
- # class FancyForm < Erector::Widget
137
- # needs :title, :show_okay => true, :show_cancel => false
138
- # ...
139
- # end
140
- #
141
- # That means that
142
- # FancyForm.new(:title => 'Login')
143
- # will succeed, as will
144
- # FancyForm.new(:title => 'Login', :show_cancel => true)
145
- # but
146
- # FancyForm.new(:name => 'Login')
147
- # will fail.
148
- #
149
- def self.needs(*args)
150
- args.each do |arg|
151
- (@needs ||= []) << (arg.nil? ? nil : (arg.is_a? Hash) ? arg : arg.to_sym)
152
- end
153
- end
154
-
155
- protected
156
- def self.get_needs
157
- @needs ||= []
158
-
159
- ancestors[1..-1].inject(@needs.dup) do |needs, ancestor|
160
- needs.push(*ancestor.get_needs) if ancestor.respond_to?(:get_needs)
161
- needs
162
- end
163
- end
164
-
165
- def self.get_needed_variables
166
- get_needs.map{|need| need.is_a?(Hash) ? need.keys : need}.flatten
167
- end
168
-
169
- def self.get_needed_defaults
170
- get_needs.select{|need| need.is_a? Hash}
171
- end
172
-
173
- public
36
+ #
37
+ # Now, seriously, after playing around a bit, go read the user guide. It's
38
+ # fun!
39
+ class AbstractWidget
174
40
  @@prettyprint_default = false
175
41
  def prettyprint_default
176
42
  @@prettyprint_default
177
43
  end
178
44
 
45
+ def self.prettyprint_default
46
+ @@prettyprint_default
47
+ end
48
+
179
49
  def self.prettyprint_default=(enabled)
180
50
  @@prettyprint_default = enabled
181
51
  end
182
52
 
183
- NON_NEWLINEY = {'i' => true, 'b' => true, 'small' => true,
184
- 'img' => true, 'span' => true, 'a' => true,
185
- 'input' => true, 'textarea' => true, 'button' => true, 'select' => true
186
- }
187
-
188
- SPACES_PER_INDENT = 2
189
-
190
- RESERVED_INSTANCE_VARS = [:helpers, :assigns, :block, :output, :prettyprint, :indentation, :at_start_of_line]
191
-
192
- attr_reader *RESERVED_INSTANCE_VARS
193
- attr_reader :parent
194
- attr_writer :block
195
-
196
- def initialize(assigns={}, &block)
197
- unless assigns.is_a? Hash
198
- raise "Erector's API has changed. Now you should pass only an options hash into Widget.new; the rest come in via to_s, or by using #widget."
199
- end
200
- @assigns = assigns
201
- assign_instance_variables(assigns)
202
- unless @parent
203
- @parent = block ? eval("self", block.binding) : nil
204
- end
205
- @block = block
206
- self.class.after_initialize self
53
+ def self.inline(*args, &block)
54
+ Class.new(self) do
55
+ include Erector::Inline
56
+ end.new(*args, &block)
207
57
  end
208
58
 
209
- #-- methods for other classes to call, left public for ease of testing and documentation
210
- #++
211
-
212
- protected
213
- def context(parent, output, prettyprint = false, indentation = 0, helpers = nil)
214
- #TODO: pass in options hash, maybe, instead of parameters
215
- original_parent = @parent
216
- original_output = @output
217
- original_indendation = @indentation
218
- original_helpers = @helpers
219
- original_prettyprint = @prettyprint
220
- @parent = parent
221
- @output = output
222
- @at_start_of_line = true
223
- raise "indentation must be a number, not #{indentation.inspect}" unless indentation.is_a? Fixnum
224
- @indentation = indentation
225
- @helpers = helpers
226
- @prettyprint = prettyprint
227
- yield
228
- ensure
229
- @parent = original_parent
230
- @output = original_output
231
- @indentation = original_indendation
232
- @helpers = original_helpers
233
- @prettyprint = original_prettyprint
59
+ [:helpers, :assigns, :output, :parent, :block].each do |attr|
60
+ class_eval(<<-SRC, __FILE__, __LINE__ + 1)
61
+ def #{attr}
62
+ @_#{attr}
63
+ end
64
+ SRC
234
65
  end
235
66
 
236
- public
237
- def assign_instance_variables (instance_variables)
238
- needed = self.class.get_needed_variables
239
- assigned = []
240
- instance_variables.each do |name, value|
241
- unless needed.empty? || needed.include?(name)
242
- raise "Unknown parameter '#{name}'. #{self.class.name} only accepts #{needed.join(', ')}"
243
- end
244
- assign_instance_variable(name, value)
245
- assigned << name
67
+ def initialize(assigns = {}, &block)
68
+ unless assigns.is_a? Hash
69
+ raise "Erector widgets are initialized with only a parameter hash. (Other parameters are passed to to_html, or the #widget method.)"
246
70
  end
247
71
 
248
- # set variables with default values
249
- self.class.get_needed_defaults.each do |hash|
250
- hash.each_pair do |name, value|
251
- unless assigned.include?(name)
252
- assign_instance_variable(name, value)
253
- assigned << name
254
- end
255
- end
256
- end
72
+ @_assigns = assigns
257
73
 
258
- missing = needed - assigned
259
- unless missing.empty? || missing == [nil]
260
- raise "Missing parameter#{missing.size == 1 ? '' : 's'}: #{missing.join(', ')}"
74
+ assigns.each do |name, value|
75
+ instance_variable_set(name.to_s[0..0] == '@' ? name : "@#{name}", value)
261
76
  end
262
- end
263
-
264
- def assign_instance_variable (name, value)
265
- raise ArgumentError, "Sorry, #{name} is a reserved variable name for Erector. Please choose a different name." if RESERVED_INSTANCE_VARS.include?(name)
266
- name = name.to_s
267
- ivar_name = (name[0..0] == '@' ? name : "@#{name}")
268
- instance_variable_set(ivar_name, value)
269
- end
270
-
271
- # Render (like to_s) but adding newlines and indentation.
272
- # This is a convenience method; you may just want to call to_s(:prettyprint => true)
273
- # so you can pass in other rendering options as well.
274
- def to_pretty
275
- to_s(:prettyprint => true)
276
- end
277
-
278
- # Render (like to_s) but stripping all tags.
279
- def to_text
280
- CGI.unescapeHTML(to_s(:prettyprint => false).gsub(/<[^>]*>/, ''))
77
+
78
+ @_parent = eval("self", block.binding) if block
79
+ @_block = block
281
80
  end
282
81
 
283
82
  # Entry point for rendering a widget (and all its children). This method
@@ -291,39 +90,40 @@ module Erector
291
90
  # by default).
292
91
  # indentation:: the amount of spaces to indent. Ignored unless prettyprint
293
92
  # is true.
93
+ # max_length:: preferred maximum length of a line. Line wraps will only
94
+ # occur at space characters, so a long word may end up creating
95
+ # a line longer than this. If nil (default), then there is no
96
+ # arbitrary limit to line lengths, and only internal newline
97
+ # characters and prettyprinting will determine newlines in the
98
+ # output.
294
99
  # helpers:: a helpers object containing utility methods. Usually this is a
295
100
  # Rails view object.
296
101
  # content_method_name:: in case you want to call a method other than
297
102
  # #content, pass its name in here.
298
- def to_s(options = {}, &blk)
299
- raise "Erector::Widget#to_s now takes an options hash, not a symbol. Try calling \"to_s(:content_method_name=> :#{options})\"" if options.is_a? Symbol
300
- _render(options, &blk).to_s
103
+ def to_html(options = {})
104
+ raise "Erector::Widget#to_html takes an options hash, not a symbol. Try calling \"to_html(:content_method_name=> :#{options})\"" if options.is_a? Symbol
105
+ _render(options).to_s
301
106
  end
302
-
303
- # Entry point for rendering a widget (and all its children). Same as #to_s
107
+
108
+ # alias for #to_html
109
+ # @deprecated Please use {#to_html} instead
110
+ def to_s(*args)
111
+ unless defined? @@already_warned_to_s
112
+ $stderr.puts "Erector::Widget#to_s is deprecated. Please use #to_html instead. Called from #{caller.first}"
113
+ @@already_warned_to_s = true
114
+ end
115
+ to_html(*args)
116
+ end
117
+
118
+ # Entry point for rendering a widget (and all its children). Same as #to_html
304
119
  # only it returns an array, for theoretical performance improvements when using a
305
120
  # Rack server (like Sinatra or Rails Metal).
306
121
  #
307
- # # Options: see #to_s
308
- def to_a(options = {}, &blk)
309
- _render({:output => []}.merge(options), &blk).to_a
310
- end
311
-
312
- def _render(options = {}, &blk)
313
- options = {
314
- :output => "", # "" is apparently faster than [] in a long-running process
315
- :prettyprint => prettyprint_default,
316
- :indentation => 0,
317
- :helpers => nil,
318
- :parent => @parent,
319
- :content_method_name => :content,
320
- }.merge(options)
321
- context(options[:parent], options[:output], options[:prettyprint], options[:indentation], options[:helpers]) do
322
- send(options[:content_method_name], &blk)
323
- output
324
- end
122
+ # # Options: see #to_html
123
+ def to_a(options = {})
124
+ _render(options).to_a
325
125
  end
326
-
126
+
327
127
  # Template method which must be overridden by all widget subclasses.
328
128
  # Inside this method you call the magic #element methods which emit HTML
329
129
  # and text to the output string. If you call "super" (or don't override
@@ -347,18 +147,7 @@ module Erector
347
147
  # If you want this block to have access to Erector methods then use
348
148
  # Erector::Inline#content or Erector#inline.
349
149
  def call_block
350
- @block.call(self) if @block
351
- end
352
-
353
- # To call one widget from another, inside the parent widget's +content+
354
- # method, instantiate the child widget and call its +write_via+ method,
355
- # passing in +self+. This assures that the same output string is used,
356
- # which gives better performance than using +capture+ or +to_s+. You can
357
- # also use the +widget+ method.
358
- def write_via(parent)
359
- context(parent, parent.output, parent.prettyprint, parent.indentation, parent.helpers) do
360
- content
361
- end
150
+ @_block.call(self) if @_block
362
151
  end
363
152
 
364
153
  # Emits a (nested) widget onto the current widget's output stream. Accepts
@@ -367,404 +156,62 @@ module Erector
367
156
  # If the first argument is an instance then the hash must be unspecified
368
157
  # (or empty). If a block is passed to this method, then it gets set as the
369
158
  # rendered widget's block.
370
- def widget(target, assigns={}, &block)
371
- child = if target.is_a? Class
372
- target.new(assigns, &block)
373
- else
374
- unless assigns.empty?
375
- raise "Unexpected second parameter. Did you mean to pass in variables when you instantiated the #{target.class.to_s}?"
376
- end
377
- target.block = block unless block.nil?
378
- target
379
- end
380
- child.write_via(self)
381
- end
382
-
383
- # (Should we make this hidden?)
384
- def html_escape
385
- return to_s
386
- end
387
-
388
- #-- methods for subclasses to call
389
- #++
390
-
391
- # Internal method used to emit an HTML/XML element, including an open tag,
392
- # attributes (optional, via the default hash), contents (also optional),
393
- # and close tag.
394
- #
395
- # Using the arcane powers of Ruby, there are magic methods that call
396
- # +element+ for all the standard HTML tags, like +a+, +body+, +p+, and so
397
- # forth. Look at the source of #full_tags for the full list.
398
- # Unfortunately, this big mojo confuses rdoc, so we can't see each method
399
- # in this rdoc page, but trust us, they're there.
400
- #
401
- # When calling one of these magic methods, put attributes in the default
402
- # hash. If there is a string parameter, then it is used as the contents.
403
- # If there is a block, then it is executed (yielded), and the string
404
- # parameter is ignored. The block will usually be in the scope of the
405
- # child widget, which means it has access to all the methods of Widget,
406
- # which will eventually end up appending text to the +output+ string. See
407
- # how elegant it is? Not confusing at all if you don't think about it.
408
- #
409
- def element(*args, &block)
410
- __element__(false, *args, &block)
411
- end
412
-
413
- # Like +element+, but string parameters are not escaped.
414
- def element!(*args, &block)
415
- __element__(true, *args, &block)
416
- end
417
-
418
- # Internal method used to emit a self-closing HTML/XML element, including
419
- # a tag name and optional attributes (passed in via the default hash).
420
159
  #
421
- # Using the arcane powers of Ruby, there are magic methods that call
422
- # +empty_element+ for all the standard HTML tags, like +img+, +br+, and so
423
- # forth. Look at the source of #empty_tags for the full list.
424
- # Unfortunately, this big mojo confuses rdoc, so we can't see each method
425
- # in this rdoc page, but trust us, they're there.
426
- #
427
- def empty_element(*args, &block)
428
- __empty_element__(*args, &block)
429
- end
430
-
431
- # Returns an HTML-escaped version of its parameter. Leaves the output
432
- # string untouched. Note that the #text method automatically HTML-escapes
433
- # its parameter, so be careful *not* to do something like text(h("2<4"))
434
- # since that will double-escape the less-than sign (you'll get
435
- # "2&amp;lt;4" instead of "2&lt;4").
436
- def h(content)
437
- content.html_escape
438
- end
439
-
440
- # Emits an open tag, comprising '<', tag name, optional attributes, and '>'
441
- def open_tag(tag_name, attributes={})
442
- indent_for_open_tag(tag_name)
443
- @indentation += SPACES_PER_INDENT
444
-
445
- output << "<#{tag_name}#{format_attributes(attributes)}>"
446
- @at_start_of_line = false
447
- end
448
-
449
- # Emits text. If a string is passed in, it will be HTML-escaped. If a
450
- # widget or the result of calling methods such as raw is passed in, the
451
- # HTML will not be HTML-escaped again. If another kind of object is passed
452
- # in, the result of calling its to_s method will be treated as a string
453
- # would be.
454
- def text(value)
455
- if value.is_a? Widget
456
- widget value
160
+ # This is the preferred way to call one widget from inside another. This
161
+ # method assures that the same output string is used, which gives better
162
+ # performance than using +capture+ or +to_html+.
163
+ def widget(target, assigns = {}, options = {}, &block)
164
+ if target.is_a? Class
165
+ target.new(assigns, &block)._render_via(self, options)
457
166
  else
458
- output <<(value.html_escape)
459
- end
460
- @at_start_of_line = false
461
- nil
462
- end
463
-
464
- # Returns text which will *not* be HTML-escaped.
465
- def raw(value)
466
- RawString.new(value.to_s)
467
- end
468
-
469
- # Emits text which will *not* be HTML-escaped. Same effect as text(raw(s))
470
- def text!(value)
471
- text raw(value)
472
- end
473
-
474
- alias rawtext text!
475
-
476
- # Returns a copy of value with spaces replaced by non-breaking space characters.
477
- # With no arguments, return a single non-breaking space.
478
- # The output uses the escaping format '&#160;' since that works
479
- # in both HTML and XML (as opposed to '&nbsp;' which only works in HTML).
480
- def nbsp(value = " ")
481
- raw(value.html_escape.gsub(/ /,'&#160;'))
482
- end
483
-
484
- # Return a character given its unicode code point or unicode name.
485
- def character(code_point_or_name)
486
- if code_point_or_name.is_a?(Symbol)
487
- found = Erector::CHARACTERS[code_point_or_name]
488
- if found.nil?
489
- raise "Unrecognized character #{code_point_or_name}"
490
- end
491
- raw("&#x#{sprintf '%x', found};")
492
- elsif code_point_or_name.is_a?(Integer)
493
- raw("&#x#{sprintf '%x', code_point_or_name};")
494
- else
495
- raise "Unrecognized argument to character: #{code_point_or_name}"
496
- end
497
- end
498
-
499
- # Emits a close tag, consisting of '<', '/', tag name, and '>'
500
- def close_tag(tag_name)
501
- @indentation -= SPACES_PER_INDENT
502
- indent()
503
-
504
- output <<("</#{tag_name}>")
505
-
506
- if newliney?(tag_name)
507
- _newline
508
- end
509
- end
510
-
511
- # Emits the result of joining the elements in array with the separator.
512
- # The array elements and separator can be Erector::Widget objects,
513
- # which are rendered, or strings, which are html-escaped and output.
514
- def join(array, separator)
515
- first = true
516
- array.each do |widget_or_text|
517
- if !first
518
- text separator
167
+ unless assigns.empty?
168
+ raise "Unexpected second parameter. Did you mean to pass in assigns when you instantiated the #{target.class.to_s}?"
519
169
  end
520
- first = false
521
- text widget_or_text
170
+ target._render_via(self, options, &block)
522
171
  end
523
172
  end
524
173
 
525
- # Emits an XML instruction, which looks like this: <?xml version=\"1.0\" encoding=\"UTF-8\"?>
526
- def instruct(attributes={:version => "1.0", :encoding => "UTF-8"})
527
- output << "<?xml#{format_sorted(sort_for_xml_declaration(attributes))}?>"
528
- end
529
-
530
- # Emits an HTML comment (&lt;!-- ... --&gt;) surrounding +text+ and/or the output of +block+.
531
- # see http://www.w3.org/TR/html4/intro/sgmltut.html#h-3.2.4
532
- #
533
- # If +text+ is an Internet Explorer conditional comment condition such as "[if IE]",
534
- # the output includes the opening condition and closing "[endif]". See
535
- # http://www.quirksmode.org/css/condcom.html
536
- #
537
- # Since "Authors should avoid putting two or more adjacent hyphens inside comments,"
538
- # we emit a warning if you do that.
539
- def comment(text = '', &block)
540
- puts "Warning: Authors should avoid putting two or more adjacent hyphens inside comments." if text =~ /--/
541
-
542
- conditional = text =~ /\[if .*\]/
543
-
544
- rawtext "<!--"
545
- rawtext text
546
- rawtext ">" if conditional
547
-
548
- if block
549
- rawtext "\n"
550
- block.call
551
- rawtext "\n"
552
- end
553
-
554
- rawtext "<![endif]" if conditional
555
- rawtext "-->\n"
556
- end
557
-
558
174
  # Creates a whole new output string, executes the block, then converts the
559
175
  # output string to a string and returns it as raw text. If at all possible
560
- # you should avoid this method since it hurts performance, and use
561
- # +widget+ or +write_via+ instead.
562
- def capture(&block)
563
- begin
564
- original_output = output
565
- @output = ""
566
- yield
567
- raw(output.to_s)
568
- ensure
569
- @output = original_output
570
- end
571
- end
572
-
573
- full_tags.each do |tag_name|
574
- def_full_tag_method(tag_name)
575
- end
576
-
577
- empty_tags.each do |tag_name|
578
- def_empty_tag_method(tag_name)
579
- end
580
-
581
- # Emits a javascript block inside a +script+ tag, wrapped in CDATA
582
- # doohickeys like all the cool JS kids do.
583
- def javascript(*args, &block)
584
- if args.length > 2
585
- raise ArgumentError, "Cannot accept more than two arguments"
586
- end
587
- attributes, value = nil, nil
588
- arg0 = args[0]
589
- if arg0.is_a?(Hash)
590
- attributes = arg0
591
- else
592
- value = arg0
593
- arg1 = args[1]
594
- if arg1.is_a?(Hash)
595
- attributes = arg1
596
- end
597
- end
598
- attributes ||= {}
599
- attributes[:type] = "text/javascript"
600
- open_tag 'script', attributes
601
-
602
- # Shouldn't this be a "cdata" HtmlPart?
603
- # (maybe, but the syntax is specific to javascript; it isn't
604
- # really a generic XML CDATA section. Specifically,
605
- # ]]> within value is not treated as ending the
606
- # CDATA section by Firefox2 when parsing text/html,
607
- # although I guess we could refuse to generate ]]>
608
- # there, for the benefit of XML/XHTML parsers).
609
- rawtext "\n// <![CDATA[\n"
610
- if block
611
- instance_eval(&block)
612
- else
613
- rawtext value
614
- end
615
- rawtext "\n// ]]>\n"
616
-
617
- close_tag 'script'
618
- rawtext "\n"
619
- end
620
-
621
- # Convenience method to emit a css file link, which looks like this:
622
- # <link href="erector.css" rel="stylesheet" type="text/css" />
623
- # The parameter is the full contents of the href attribute, including any ".css" extension.
624
- #
625
- # If you want to emit raw CSS inline, use the #style method instead.
626
- def css(href, options = {})
627
- link({:rel => 'stylesheet', :type => 'text/css', :href => href}.merge(options))
628
- end
629
-
630
- # Convenience method to emit an anchor tag whose href and text are the same,
631
- # e.g. <a href="http://example.com">http://example.com</a>
632
- def url(href, options = {})
633
- a href, ({:href => href}.merge(options))
634
- end
635
-
636
- # makes a unique id based on the widget's class name and object id
637
- # that you can use as the HTML id of an emitted element
638
- def dom_id
639
- "#{self.class.name.gsub(/:+/,"_")}_#{self.object_id}"
640
- end
641
-
642
- # emits a jQuery script that is to be run on document ready
643
- def jquery(txt)
644
- javascript do
645
- jquery_ready txt
646
- end
176
+ # you should avoid this method since it hurts performance, and use +widget+
177
+ # instead.
178
+ def capture
179
+ original, @_output = output, Output.new
180
+ yield
181
+ original.widgets.concat(output.widgets) # todo: test!!!
182
+ output.to_s
183
+ ensure
184
+ @_output = original
647
185
  end
648
186
 
649
187
  protected
650
- def jquery_ready(txt)
651
- rawtext "\n"
652
- rawtext "jQuery(document).ready(function($){\n"
653
- rawtext txt
654
- rawtext "\n});"
655
- end
656
-
657
- ### internal utility methods
658
-
659
- protected
660
- def __element__(raw, tag_name, *args, &block)
661
- if args.length > 2
662
- raise ArgumentError, "Cannot accept more than four arguments"
663
- end
664
- attributes, value = nil, nil
665
- arg0 = args[0]
666
- if arg0.is_a?(Hash)
667
- attributes = arg0
668
- else
669
- value = arg0
670
- arg1 = args[1]
671
- if arg1.is_a?(Hash)
672
- attributes = arg1
673
- end
674
- end
675
- attributes ||= {}
676
- open_tag tag_name, attributes
677
- if block && value
678
- raise ArgumentError, "You can't pass both a block and a value to #{tag_name} -- please choose one."
679
- end
680
- if block
681
- block.call
682
- elsif raw
683
- text! value
684
- else
685
- text value
686
- end
687
- close_tag tag_name
688
- end
689
-
690
- def __empty_element__(tag_name, attributes={})
691
- indent_for_open_tag(tag_name)
692
-
693
- output << "<#{tag_name}#{format_attributes(attributes)} />"
694
-
695
- if newliney?(tag_name)
696
- _newline
697
- end
698
- end
699
-
700
- def _newline
701
- return unless @prettyprint
702
- output << "\n"
703
- @at_start_of_line = true
704
- end
705
-
706
- def indent_for_open_tag(tag_name)
707
- return unless @prettyprint
708
- if !@at_start_of_line && newliney?(tag_name)
709
- _newline
710
- end
711
- indent()
712
- end
713
-
714
- def indent()
715
- if @at_start_of_line
716
- output << " " * [@indentation, 0].max
717
- end
718
- end
719
-
720
- def format_attributes(attributes)
721
- if !attributes || attributes.empty?
722
- ""
723
- else
724
- format_sorted(sorted(attributes))
725
- end
726
- end
727
-
728
- def format_sorted(sorted)
729
- results = ['']
730
- sorted.each do |key, value|
731
- if value
732
- if value.is_a?(Array)
733
- value = value.flatten
734
- next if value.empty?
735
- value = value.join(' ')
736
- end
737
- results << "#{key}=\"#{value.html_escape}\""
738
- end
739
- end
740
- return results.join(' ')
741
- end
742
-
743
- def sorted(attributes)
744
- stringized = []
745
- attributes.each do |key, value|
746
- stringized << [key.to_s, value]
747
- end
748
- return stringized.sort
749
- end
188
+ def _render(options = {}, &block)
189
+ @_block = block if block
190
+ @_parent = options[:parent] || parent
191
+ @_helpers = options[:helpers] || parent
192
+ @_output = options[:output]
193
+ @_output = Output.new(options) unless output.is_a?(Output)
750
194
 
751
- def sort_for_xml_declaration(attributes)
752
- # correct order is "version, encoding, standalone" (XML 1.0 section 2.8).
753
- # But we only try to put version before encoding for now.
754
- stringized = []
755
- attributes.each do |key, value|
756
- stringized << [key.to_s, value]
757
- end
758
- return stringized.sort{|a, b| b <=> a}
195
+ output.widgets << self.class
196
+ send(options[:content_method_name] || :content)
197
+ output
759
198
  end
760
199
 
761
- def newliney?(tag_name)
762
- if @prettyprint
763
- !NON_NEWLINEY.include?(tag_name)
764
- else
765
- false
766
- end
200
+ def _render_via(parent, options = {}, &block)
201
+ _render(options.merge(:parent => parent,
202
+ :output => parent.output,
203
+ :helpers => parent.helpers), &block)
767
204
  end
205
+ end
768
206
 
207
+ class Widget < AbstractWidget
208
+ include Erector::HTML
209
+ include Erector::Needs
210
+ include Erector::Caching
211
+ include Erector::Externals
212
+ include Erector::Convenience
213
+ include Erector::JQuery
214
+ include Erector::AfterInitialize
215
+ include Erector::Sass if Object.const_defined?(:Sass)
769
216
  end
770
217
  end