lib/erector/widget.rb in pivotal-erector-0.6.3 vs lib/erector/widget.rb in pivotal-erector-0.6.4

- old
+ new

@@ -1,48 +1,52 @@ module Erector # A Widget is the center of the Erector universe. # - # To create a widget, extend Erector::Widget and implement - # the +content+ method. Inside this method you may call any of the tag methods like +span+ or +p+ to emit HTML/XML - # tags. - # - # You can also define a widget on the fly by passing a block to +new+. This block will get executed when the widget's - # +content+ method is called. + # To create a widget, extend Erector::Widget and implement the +content+ + # method. Inside this method you may call any of the tag methods like +span+ + # or +p+ to emit HTML/XML tags. + # + # You can also define a widget on the fly by passing a block to +new+. This + # block will get executed when the widget's +content+ method is called. # - # To render a widget from the outside, instantiate it and call its +to_s+ method. + # To render a widget from the outside, instantiate it and call its +to_s+ + # method. # - # A widget's +new+ method optionally accepts an options hash. Entries in this hash are converted to instance - # variables, and +attr_reader+ accessors are defined for each. + # A widget's +new+ method optionally accepts an options hash. Entries in + # this hash are converted to instance variables, and +attr_reader+ accessors + # are defined for each. # - # TODO: You can add runtime input checking via the +needs+ macro. If any of the variables named via - # +needs+ are absent, an exception is thrown. Optional variables are specified with +wants+. If a variable appears - # in the options hash that is in neither the +needs+ nor +wants+ lists, then that too provokes an exception. - # This mechanism is meant to ameliorate development-time confusion about exactly what parameters are supported - # by a given widget, avoiding confusing runtime NilClass errors. - # - # To call one widget from another, inside the parent widget's +content+ method, instantiate the child widget and call - # the +widget+ method. This assures that the same output stream - # is used, which gives better performance than using +capture+ or +to_s+. It also preserves the indentation and - # helpers of the enclosing class. - # - # In this documentation we've tried to keep the distinction clear between methods that *emit* text and those that - # *return* text. "Emit" means that it writes to the output stream; "return" means that it returns a string - # like a normal method and leaves it up to the caller to emit that string if it wants. + # You can add runtime input checking via the +needs+ macro. See #needs. + # This mechanism is meant to ameliorate development-time confusion about + # exactly what parameters are supported by a given widget, avoiding + # confusing runtime NilClass errors. + # + # To call one widget from another, inside the parent widget's +content+ + # method, instantiate the child widget and call the +widget+ method. This + # assures that the same output stream is used, which gives better + # performance than using +capture+ or +to_s+. It also preserves the + # indentation and helpers of the enclosing class. + # + # In this documentation we've tried to keep the distinction clear between + # methods that *emit* text and those that *return* text. "Emit" means that + # it writes to the output stream; "return" means that it returns a string + # like a normal method and leaves it up to the caller to emit that string if + # it wants. class Widget class << self def all_tags Erector::Widget.full_tags + Erector::Widget.empty_tags end - # tags which are always self-closing + # Tags which are always self-closing. Click "[Source]" to see the full list. def empty_tags ['area', 'base', 'br', 'col', 'frame', 'hr', 'img', 'input', 'link', 'meta'] end - # tags which can contain other stuff + # Tags which can contain other stuff. Click "[Source]" to see the full list. def full_tags [ 'a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'body', 'button', 'caption', 'center', 'cite', 'code', 'colgroup', @@ -79,40 +83,41 @@ def after_initialize_parts @after_initialize_parts ||= [] end end - # Class method by which widget classes can declare that they need certain parameters. - # If needed parameters are not passed in to #new, then an exception will be thrown - # (with a hopefully useful message about which parameters are missing). This is intended - # to catch silly bugs like passing in a parameter called 'name' to a widget that expects - # a parameter called 'title'. Every variable declared in 'needs' will get an attr_reader - # accessor declared for it. + # Class method by which widget classes can declare that they need certain + # parameters. If needed parameters are not passed in to #new, then an + # exception will be thrown (with a hopefully useful message about which + # parameters are missing). This is intended to catch silly bugs like + # passing in a parameter called 'name' to a widget that expects a + # parameter called 'title'. Every variable declared in 'needs' will get an + # attr_reader accessor declared for it. # - # You can also declare default values for parameters using hash syntax. You can put #needs - # declarations on multiple lines or on the same line; the only caveat is that if there are - # default values, they all have to be at the end of the line (so they go into the magic - # hash parameter). + # You can also declare default values for parameters using hash syntax. + # You can put #needs declarations on multiple lines or on the same line; + # the only caveat is that if there are default values, they all have to be + # at the end of the line (so they go into the magic hash parameter). # - # If a widget has no #needs declaration then it will accept any combination of parameters - # (and make accessors for them) just like normal. In that case there will be no 'attr_reader's - # declared. - # If a widget wants to declare that it - # takes no parameters, use the special incantation "needs nil" (and don't declare any other - # needs, or kittens will cry). + # If a widget has no #needs declaration then it will accept any + # combination of parameters (and make accessors for them) just like + # normal. In that case there will be no 'attr_reader's declared. If a + # widget wants to declare that it takes no parameters, use the special + # incantation "needs nil" (and don't declare any other needs, or kittens + # will cry). # # Usage: # class FancyForm < Erector::Widget # needs :title, :show_okay => true, :show_cancel => false # ... # end - # - # That means that + # + # That means that # FancyForm.new(:title => 'Login') - # will succeed, as will + # will succeed, as will # FancyForm.new(:title => 'Login', :show_cancel => true) - # but + # but # FancyForm.new(:name => 'Login') # will fail. # def self.needs(*args) args.each do |arg| @@ -235,68 +240,87 @@ # so you can pass in other rendering options as well. def to_pretty to_s(:prettyprint => true) end - # Entry point for rendering a widget (and all its children). This method creates a new output string (if necessary), - # calls this widget's #content method and returns the string. + # Entry point for rendering a widget (and all its children). This method + # creates a new output string (if necessary), calls this widget's #content + # method and returns the string. # # Options: # output:: the string to output to. Default: a new empty string - # prettyprint:: whether Erector should add newlines and indentation. Default: the value of prettyprint_default (which is false by default). - # indentation:: the amount of spaces to indent. Ignored unless prettyprint is true. - # helpers:: a helpers object containing utility methods. Usually this is a Rails view object. - # content_method_name:: in case you want to call a method other than #content, pass its name in here. - # - # Note: Prettyprinting is an experimental feature and is subject to change - # (either in terms of how it is enabled, or in terms of - # what decisions Erector makes about where to add whitespace). + # prettyprint:: whether Erector should add newlines and indentation. + # Default: the value of prettyprint_default (which is false + # by default). + # indentation:: the amount of spaces to indent. Ignored unless prettyprint + # is true. + # helpers:: a helpers object containing utility methods. Usually this is a + # Rails view object. + # content_method_name:: in case you want to call a method other than + # #content, pass its name in here. def to_s(options = {}, &blk) - 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 - + _render(options, &blk).to_s + end + + # Entry point for rendering a widget (and all its children). Same as #to_s + # only it returns an array, for theoretical performance improvements when using a + # Rack server (like Sinatra or Rails Metal). + # + # # Options: see #to_s + def to_a(options = {}, &blk) + _render(options, &blk).to_a + end + + def _render(options = {}, &blk) options = { - :output => "", + :output => "", # "" is apparently faster than [] in a long-running process :prettyprint => prettyprint_default, :indentation => 0, :helpers => nil, :content_method_name => :content, }.merge(options) context(options[:output], options[:prettyprint], options[:indentation], options[:helpers]) do send(options[:content_method_name], &blk) - output.to_s + output end end alias_method :inspect, :to_s - # Template method which must be overridden by all widget subclasses. Inside this method you call the magic - # #element methods which emit HTML and text to the output string. + # Template method which must be overridden by all widget subclasses. + # Inside this method you call the magic #element methods which emit HTML + # and text to the output string. If you call "super" (or don't override + # +content+) then your widget will render any block that was passed into + # its constructor (in the current instance context so it can get access + # to parent widget methods via method_missing). def content if @block instance_eval(&@block) end end - # To call one widget from another, inside the parent widget's +content+ method, instantiate the child widget and call - # its +write_via+ method, passing in +self+. This assures that the same output string - # is used, which gives better performance than using +capture+ or +to_s+. - # You can also use the +widget+ method. + # To call one widget from another, inside the parent widget's +content+ + # method, instantiate the child widget and call its +write_via+ method, + # passing in +self+. This assures that the same output string is used, + # which gives better performance than using +capture+ or +to_s+. You can + # also use the +widget+ method. def write_via(parent) @parent = parent context(parent.output, parent.prettyprint, parent.indentation, parent.helpers) do content end end - # Emits a (nested) widget onto the current widget's output stream. Accepts either - # a class or an instance. If the first argument is a class, then the second argument - # is a hash used to populate its instance variables. If the first argument is an - # instance then the hash must be unspecified (or empty). + # Emits a (nested) widget onto the current widget's output stream. Accepts + # either a class or an instance. If the first argument is a class, then + # the second argument is a hash used to populate its instance variables. + # If the first argument is an instance then the hash must be unspecified + # (or empty). # - # The sub-widget will have access to the methods of the parent class, via some method_missing - # magic and a "parent" pointer. + # The sub-widget will have access to the methods of the parent class, via + # some method_missing magic and a "parent" pointer. def widget(target, assigns={}, &block) child = if target.is_a? Class target.new(assigns, &block) else unless assigns.empty? @@ -313,67 +337,73 @@ end #-- methods for subclasses to call #++ - # Internal method used to emit an HTML/XML element, including an open tag, attributes (optional, via the default hash), - # contents (also optional), and close tag. + # Internal method used to emit an HTML/XML element, including an open tag, + # attributes (optional, via the default hash), contents (also optional), + # and close tag. # - # Using the arcane powers of Ruby, there are magic methods that call +element+ for all the standard - # HTML tags, like +a+, +body+, +p+, and so forth. Look at the source of #full_tags for the full list. - # Unfortunately, this big mojo confuses rdoc, so we can't see each method in this rdoc page, but trust - # us, they're there. + # Using the arcane powers of Ruby, there are magic methods that call + # +element+ for all the standard HTML tags, like +a+, +body+, +p+, and so + # forth. Look at the source of #full_tags for the full list. + # Unfortunately, this big mojo confuses rdoc, so we can't see each method + # in this rdoc page, but trust us, they're there. # - # When calling one of these magic methods, put attributes in the default hash. If there is a string parameter, - # then it is used as the contents. If there is a block, then it is executed (yielded), and the string parameter is ignored. - # The block will usually be in the scope of the child widget, which means it has access to all the - # methods of Widget, which will eventually end up appending text to the +output+ string. See how - # elegant it is? Not confusing at all if you don't think about it. + # When calling one of these magic methods, put attributes in the default + # hash. If there is a string parameter, then it is used as the contents. + # If there is a block, then it is executed (yielded), and the string + # parameter is ignored. The block will usually be in the scope of the + # child widget, which means it has access to all the methods of Widget, + # which will eventually end up appending text to the +output+ string. See + # how elegant it is? Not confusing at all if you don't think about it. # def element(*args, &block) __element__(*args, &block) end - # Internal method used to emit a self-closing HTML/XML element, including a tag name and optional attributes - # (passed in via the default hash). - # - # Using the arcane powers of Ruby, there are magic methods that call +empty_element+ for all the standard - # HTML tags, like +img+, +br+, and so forth. Look at the source of #empty_tags for the full list. - # Unfortunately, this big mojo confuses rdoc, so we can't see each method in this rdoc page, but trust - # us, they're there. + # Internal method used to emit a self-closing HTML/XML element, including + # a tag name and optional attributes (passed in via the default hash). # + # Using the arcane powers of Ruby, there are magic methods that call + # +empty_element+ for all the standard HTML tags, like +img+, +br+, and so + # forth. Look at the source of #empty_tags for the full list. + # Unfortunately, this big mojo confuses rdoc, so we can't see each method + # in this rdoc page, but trust us, they're there. + # def empty_element(*args, &block) __empty_element__(*args, &block) end - # Returns an HTML-escaped version of its parameter. Leaves the output string untouched. Note that - # the #text method automatically HTML-escapes its parameter, so be careful *not* to do something like - # text(h("2<4")) since that will double-escape the less-than sign (you'll get "2&amp;lt;4" instead of - # "2&lt;4"). + # Returns an HTML-escaped version of its parameter. Leaves the output + # string untouched. Note that the #text method automatically HTML-escapes + # its parameter, so be careful *not* to do something like text(h("2<4")) + # since that will double-escape the less-than sign (you'll get + # "2&amp;lt;4" instead of "2&lt;4"). def h(content) content.html_escape end # Emits an open tag, comprising '<', tag name, optional attributes, and '>' def open_tag(tag_name, attributes={}) indent_for_open_tag(tag_name) @indentation += SPACES_PER_INDENT - output.concat "<#{tag_name}#{format_attributes(attributes)}>" + output << "<#{tag_name}#{format_attributes(attributes)}>" @at_start_of_line = false end - # Emits text. If a string is passed in, it will be HTML-escaped. - # If a widget or the result of calling methods such as raw - # is passed in, the HTML will not be HTML-escaped again. - # If another kind of object is passed in, the result of calling - # its to_s method will be treated as a string would be. + # Emits text. If a string is passed in, it will be HTML-escaped. If a + # widget or the result of calling methods such as raw is passed in, the + # HTML will not be HTML-escaped again. If another kind of object is passed + # in, the result of calling its to_s method will be treated as a string + # would be. def text(value) if value.is_a? Widget widget value else - output.concat(value.html_escape) + output <<(value.html_escape) end @at_start_of_line = false nil end @@ -408,16 +438,16 @@ else raise "Unrecognized argument to character: #{code_point_or_name}" end end - # Emits a close tag, consisting of '<', tag name, and '>' + # Emits a close tag, consisting of '<', '/', tag name, and '>' def close_tag(tag_name) @indentation -= SPACES_PER_INDENT indent() - output.concat("</#{tag_name}>") + output <<("</#{tag_name}>") if newliney(tag_name) _newline end end @@ -436,16 +466,17 @@ end end # Emits an XML instruction, which looks like this: <?xml version=\"1.0\" encoding=\"UTF-8\"?> def instruct(attributes={:version => "1.0", :encoding => "UTF-8"}) - output.concat "<?xml#{format_sorted(sort_for_xml_declaration(attributes))}?>" + output << "<?xml#{format_sorted(sort_for_xml_declaration(attributes))}?>" end - # Creates a whole new output string, executes the block, then converts the output string to a string and - # emits it as raw text. If at all possible you should avoid this method since it hurts performance, - # and use +content+ or +write_via+ instead. + # Creates a whole new output string, executes the block, then converts the + # output string to a string and emits it as raw text. If at all possible + # you should avoid this method since it hurts performance, and use + # +widget+ or +write_via+ instead. def capture(&block) begin original_output = output @output = "" yield @@ -473,11 +504,12 @@ __FILE__, __LINE__ - 4 ) end - # Emits a javascript block inside a +script+ tag, wrapped in CDATA doohickeys like all the cool JS kids do. + # Emits a javascript block inside a +script+ tag, wrapped in CDATA + # doohickeys like all the cool JS kids do. def javascript(*args, &block) if args.length > 2 raise ArgumentError, "Cannot accept more than two arguments" end attributes, value = nil, nil @@ -512,20 +544,21 @@ close_tag 'script' rawtext "\n" end - # Convenience method to emit a css file link, which looks like this: + # Convenience method to emit a css file link, which looks like this: # <link href="erector.css" rel="stylesheet" type="text/css" /> - # The parameter is the full contents of the href attribute, including any ".css" extension. + # The parameter is the full contents of the href attribute, including any ".css" extension. # # If you want to emit raw CSS inline, use the #style method instead. def css(href) link :rel => 'stylesheet', :type => 'text/css', :href => href end - # 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> + # 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> def url(href) a href, :href => href end def newliney(tag_name) @@ -579,20 +612,20 @@ end def __empty_element__(tag_name, attributes={}) indent_for_open_tag(tag_name) - output.concat "<#{tag_name}#{format_attributes(attributes)} />" + output << "<#{tag_name}#{format_attributes(attributes)} />" if newliney(tag_name) _newline end end def _newline return unless @prettyprint - output.concat "\n" + output << "\n" @at_start_of_line = true end def indent_for_open_tag(tag_name) return unless @prettyprint @@ -602,10 +635,10 @@ indent() end def indent() if @at_start_of_line - output.concat " " * @indentation + output << " " * [@indentation, 0].max end end def format_attributes(attributes) if !attributes || attributes.empty?