actionpack 0.9.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (100) hide show
  1. data/CHANGELOG +604 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +418 -0
  4. data/RUNNING_UNIT_TESTS +14 -0
  5. data/examples/.htaccess +24 -0
  6. data/examples/address_book/index.rhtml +33 -0
  7. data/examples/address_book/layout.rhtml +8 -0
  8. data/examples/address_book_controller.cgi +9 -0
  9. data/examples/address_book_controller.fcgi +6 -0
  10. data/examples/address_book_controller.rb +52 -0
  11. data/examples/address_book_controller.rbx +4 -0
  12. data/examples/benchmark.rb +52 -0
  13. data/examples/benchmark_with_ar.fcgi +89 -0
  14. data/examples/blog_controller.cgi +53 -0
  15. data/examples/debate/index.rhtml +14 -0
  16. data/examples/debate/new_topic.rhtml +22 -0
  17. data/examples/debate/topic.rhtml +32 -0
  18. data/examples/debate_controller.cgi +57 -0
  19. data/install.rb +93 -0
  20. data/lib/action_controller.rb +47 -0
  21. data/lib/action_controller/assertions/action_pack_assertions.rb +166 -0
  22. data/lib/action_controller/assertions/active_record_assertions.rb +65 -0
  23. data/lib/action_controller/base.rb +626 -0
  24. data/lib/action_controller/benchmarking.rb +49 -0
  25. data/lib/action_controller/cgi_ext/cgi_ext.rb +43 -0
  26. data/lib/action_controller/cgi_ext/cgi_methods.rb +91 -0
  27. data/lib/action_controller/cgi_process.rb +123 -0
  28. data/lib/action_controller/filters.rb +279 -0
  29. data/lib/action_controller/flash.rb +65 -0
  30. data/lib/action_controller/layout.rb +143 -0
  31. data/lib/action_controller/request.rb +92 -0
  32. data/lib/action_controller/rescue.rb +94 -0
  33. data/lib/action_controller/response.rb +15 -0
  34. data/lib/action_controller/scaffolding.rb +183 -0
  35. data/lib/action_controller/session/active_record_store.rb +72 -0
  36. data/lib/action_controller/session/drb_server.rb +9 -0
  37. data/lib/action_controller/session/drb_store.rb +31 -0
  38. data/lib/action_controller/support/class_attribute_accessors.rb +57 -0
  39. data/lib/action_controller/support/class_inheritable_attributes.rb +37 -0
  40. data/lib/action_controller/support/clean_logger.rb +10 -0
  41. data/lib/action_controller/support/cookie_performance_fix.rb +121 -0
  42. data/lib/action_controller/support/inflector.rb +70 -0
  43. data/lib/action_controller/templates/rescues/_request_and_response.rhtml +28 -0
  44. data/lib/action_controller/templates/rescues/diagnostics.rhtml +22 -0
  45. data/lib/action_controller/templates/rescues/layout.rhtml +29 -0
  46. data/lib/action_controller/templates/rescues/missing_template.rhtml +2 -0
  47. data/lib/action_controller/templates/rescues/template_error.rhtml +26 -0
  48. data/lib/action_controller/templates/rescues/unknown_action.rhtml +2 -0
  49. data/lib/action_controller/templates/scaffolds/edit.rhtml +6 -0
  50. data/lib/action_controller/templates/scaffolds/layout.rhtml +29 -0
  51. data/lib/action_controller/templates/scaffolds/list.rhtml +24 -0
  52. data/lib/action_controller/templates/scaffolds/new.rhtml +5 -0
  53. data/lib/action_controller/templates/scaffolds/show.rhtml +9 -0
  54. data/lib/action_controller/test_process.rb +194 -0
  55. data/lib/action_controller/url_rewriter.rb +153 -0
  56. data/lib/action_view.rb +40 -0
  57. data/lib/action_view/base.rb +253 -0
  58. data/lib/action_view/helpers/active_record_helper.rb +171 -0
  59. data/lib/action_view/helpers/date_helper.rb +223 -0
  60. data/lib/action_view/helpers/debug_helper.rb +17 -0
  61. data/lib/action_view/helpers/form_helper.rb +176 -0
  62. data/lib/action_view/helpers/form_options_helper.rb +169 -0
  63. data/lib/action_view/helpers/tag_helper.rb +59 -0
  64. data/lib/action_view/helpers/text_helper.rb +129 -0
  65. data/lib/action_view/helpers/url_helper.rb +72 -0
  66. data/lib/action_view/partials.rb +61 -0
  67. data/lib/action_view/template_error.rb +84 -0
  68. data/lib/action_view/vendor/builder.rb +13 -0
  69. data/lib/action_view/vendor/builder/blankslate.rb +21 -0
  70. data/lib/action_view/vendor/builder/xmlbase.rb +143 -0
  71. data/lib/action_view/vendor/builder/xmlevents.rb +63 -0
  72. data/lib/action_view/vendor/builder/xmlmarkup.rb +288 -0
  73. data/rakefile +105 -0
  74. data/test/abstract_unit.rb +9 -0
  75. data/test/controller/action_pack_assertions_test.rb +295 -0
  76. data/test/controller/active_record_assertions_test.rb +118 -0
  77. data/test/controller/cgi_test.rb +142 -0
  78. data/test/controller/cookie_test.rb +38 -0
  79. data/test/controller/filters_test.rb +159 -0
  80. data/test/controller/flash_test.rb +69 -0
  81. data/test/controller/layout_test.rb +49 -0
  82. data/test/controller/redirect_test.rb +44 -0
  83. data/test/controller/render_test.rb +169 -0
  84. data/test/controller/url_test.rb +318 -0
  85. data/test/fixtures/layouts/builder.rxml +3 -0
  86. data/test/fixtures/layouts/standard.rhtml +1 -0
  87. data/test/fixtures/test/_customer.rhtml +1 -0
  88. data/test/fixtures/test/greeting.rhtml +1 -0
  89. data/test/fixtures/test/hello.rxml +4 -0
  90. data/test/fixtures/test/hello_world.rhtml +1 -0
  91. data/test/fixtures/test/hello_xml_world.rxml +11 -0
  92. data/test/fixtures/test/list.rhtml +1 -0
  93. data/test/template/active_record_helper_test.rb +76 -0
  94. data/test/template/date_helper_test.rb +103 -0
  95. data/test/template/form_helper_test.rb +115 -0
  96. data/test/template/form_options_helper_test.rb +174 -0
  97. data/test/template/tag_helper_test.rb +18 -0
  98. data/test/template/text_helper_test.rb +62 -0
  99. data/test/template/url_helper_test.rb +35 -0
  100. metadata +154 -0
@@ -0,0 +1,59 @@
1
+ require 'cgi'
2
+
3
+ module ActionView
4
+ module Helpers
5
+ # This is poor man's Builder for the rare cases where you need to programmatically make tags but can't use Builder.
6
+ module TagHelper
7
+ include ERB::Util
8
+
9
+ # Examples:
10
+ # * tag("br") => <br />
11
+ # * tag("input", { "type" => "text"}) => <input type="text" />
12
+ def tag(name, options = {}, open = false)
13
+ "<#{name + tag_options(options)}" + (open ? ">" : " />")
14
+ end
15
+
16
+ # Examples:
17
+ # * content_tag("p", "Hello world!") => <p>Hello world!</p>
18
+ # * content_tag("div", content_tag("p", "Hello world!"), "class" => "strong") =>
19
+ # <div class="strong"><p>Hello world!</p></div>
20
+ def content_tag(name, content, options = {})
21
+ "<#{name + tag_options(options)}>#{content}</#{name}>"
22
+ end
23
+
24
+ # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like
25
+ # ActionController::Base#url_for.
26
+ def form_tag(url_for_options, options = {}, *parameters_for_url)
27
+ html_options = { "method" => "POST" }.merge(options)
28
+
29
+ if html_options[:multipart]
30
+ html_options["enctype"] = "multipart/form-data"
31
+ html_options.delete(:multipart)
32
+ end
33
+
34
+ html_options["action"] = url_for(url_for_options, *parameters_for_url)
35
+
36
+ tag("form", html_options, true)
37
+ end
38
+
39
+ alias_method :start_form_tag, :form_tag
40
+
41
+ # Outputs "</form>"
42
+ def end_form_tag
43
+ "</form>"
44
+ end
45
+
46
+
47
+ private
48
+ def tag_options(options)
49
+ if options.empty?
50
+ ""
51
+ else
52
+ " " + options.collect { |pair|
53
+ "#{pair.first}=\"#{html_escape(pair.last)}\""
54
+ }.sort.join(" ")
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,129 @@
1
+ module ActionView
2
+ # The template helpers serves to relieve the templates from including the same inline code again and again. It's a
3
+ # set of standardized methods for working with forms (FormHelper), dates (DateHelper), texts (TextHelper), and
4
+ # Active Records (ActiveRecordHelper) that's available to all templates by default.
5
+ #
6
+ # It's also really easy to make your own helpers and it's much encouraged to keep the template files free
7
+ # from complicated logic. It's even encouraged to bundle common compositions of methods from other helpers
8
+ # (often the common helpers) as they're used by the specific application.
9
+ #
10
+ # Defining a helper requires you to include a specialized +append_features+ method that makes them capable
11
+ # of configuring their integration upon inclusion in a controller, like this:
12
+ #
13
+ # module MyHelper
14
+ # def self.append_features(controller)
15
+ # controller.ancestors.include?(ActionController::Base) ?
16
+ # controller.add_template_helper(self) : super
17
+ # end
18
+ #
19
+ # def hello_world() "hello world" end
20
+ # end
21
+ #
22
+ # MyHelper can now be included in a controller, like this:
23
+ #
24
+ # require 'my_helper'
25
+ # class MyController < ActionController::Base
26
+ # include MyHelper
27
+ # end
28
+ #
29
+ # ...and, same as above, used in any template rendered from MyController, like this:
30
+ #
31
+ # Let's hear what the helper has to say: <tt><%= hello_world %></tt>
32
+ module Helpers
33
+ # Provides a set of methods for working with text strings that can help unburden the level of inline Ruby code in the
34
+ # templates. In the example below we iterate over a collection of posts provided to the template and prints each title
35
+ # after making sure it doesn't run longer than 20 characters:
36
+ # <% for post in @posts %>
37
+ # Title: <%= truncate(post.title, 20) %>
38
+ # <% end %>
39
+ module TextHelper
40
+ # The regular puts and print are outlawed in eRuby. It's recommended to use the <%= "hello" %> form instead of print "hello".
41
+ # If you absolutely must use a method-based output, you can use concat. It's use like this <% concat "hello", binding %>. Notice that
42
+ # it doesn't have an equal sign in front. Using <%= concat "hello" %> would result in a double hello.
43
+ def concat(string, binding)
44
+ eval("_erbout", binding).concat(string)
45
+ end
46
+
47
+ # Truncates +text+ to the length of +length+ and replaces the last three characters with the +truncate_string+
48
+ # if the +text+ is longer than +length+.
49
+ def truncate(text, length = 30, truncate_string = "...")
50
+ if text.nil? then return end
51
+ if text.length > length then text[0..(length - 3)] + truncate_string else text end
52
+ end
53
+
54
+ # Highlights the +phrase+ where it is found in the +text+ by surrounding it like
55
+ # <strong class="highlight">I'm a highlight phrase</strong>. The highlighter can be specialized by
56
+ # passing +highlighter+ as single-quoted string with \1 where the phrase is supposed to be inserted.
57
+ # N.B.: The +phrase+ is sanitized to include only letters, digits, and spaces before use.
58
+ def highlight(text, phrase, highlighter = '<strong class="highlight">\1</strong>')
59
+ if text.nil? || phrase.nil? then return end
60
+ text.gsub(/(#{escape_regexp(phrase)})/i, highlighter) unless text.nil?
61
+ end
62
+
63
+ # Extracts an excerpt from the +text+ surrounding the +phrase+ with a number of characters on each side determined
64
+ # by +radius+. If the phrase isn't found, nil is returned. Ex:
65
+ # excerpt("hello my world", "my", 3) => "...lo my wo..."
66
+ def excerpt(text, phrase, radius = 100, excerpt_string = "...")
67
+ if text.nil? || phrase.nil? then return end
68
+ phrase = escape_regexp(phrase)
69
+
70
+ if found_pos = text =~ /(#{phrase})/i
71
+ start_pos = [ found_pos - radius, 0 ].max
72
+ end_pos = [ found_pos + phrase.length + radius, text.length ].min
73
+
74
+ prefix = start_pos > 0 ? excerpt_string : ""
75
+ postfix = end_pos < text.length ? excerpt_string : ""
76
+
77
+ prefix + text[start_pos..end_pos].strip + postfix
78
+ else
79
+ nil
80
+ end
81
+ end
82
+
83
+ # Attempts to pluralize the +singular+ word unless +count+ is 1. See source for pluralization rules.
84
+ def pluralize(count, singular, plural = nil)
85
+ "#{count} " + if count == 1
86
+ singular
87
+ elsif plural
88
+ plural
89
+ elsif Object.const_defined?("Inflector")
90
+ Inflector.pluralize(singular)
91
+ else
92
+ singular + "s"
93
+ end
94
+ end
95
+
96
+ begin
97
+ require "redcloth"
98
+
99
+ # Returns the text with all the Textile codes turned into HTML-tags.
100
+ # <i>This method is only available if RedCloth can be required</i>.
101
+ def textilize(text)
102
+ RedCloth.new(text).to_html
103
+ end
104
+
105
+ # Returns the text with all the Textile codes turned into HTML-tags, but without the regular bounding <p> tag.
106
+ # <i>This method is only available if RedCloth can be required</i>.
107
+ def textilize_without_paragraph(text)
108
+ textiled = textilize(text)
109
+ if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end
110
+ if textiled[-4..-1] == "</p>" then textiled = textiled[0..-5] end
111
+ return textiled
112
+ end
113
+ rescue LoadError
114
+ # We can't really help what's not there
115
+ end
116
+
117
+ # Turns all links into words, like "<a href="something">else</a>" to "else".
118
+ def strip_links(text)
119
+ text.gsub(/<a.*>(.*)<\/a>/m, '\1')
120
+ end
121
+
122
+ private
123
+ # Returns a version of the text that's safe to use in a regular expression without triggering engine features.
124
+ def escape_regexp(text)
125
+ text.gsub(/([\\|?+*\/\)\(])/) { |m| "\\#{$1}" }
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,72 @@
1
+ module ActionView
2
+ module Helpers
3
+ # Provides a set of methods for making easy links and getting urls that depend on the controller and action. This means that
4
+ # you can use the same format for links in the views that you do in the controller. The different methods are even named
5
+ # synchronously, so link_to uses that same url as is generated by url_for, which again is the same url used for
6
+ # redirection in redirect_to.
7
+ module UrlHelper
8
+ # Returns the URL for the set of +options+ provided. See the valid options in link:classes/ActionController/Base.html#M000021
9
+ def url_for(options = {}, *parameters_for_method_reference)
10
+ if Hash === options then options = { :only_path => true }.merge(options) end
11
+ @controller.send(:url_for, options, *parameters_for_method_reference).gsub("&", "&amp;")
12
+ end
13
+
14
+ # Creates a link tag of the given +name+ using an URL created by the set of +options+. See the valid options in
15
+ # link:classes/ActionController/Base.html#M000021. It's also possible to pass a string instead of an options hash to
16
+ # get a link tag that just points without consideration. The html_options have a special feature for creating javascript
17
+ # confirm alerts where if you pass :confirm => 'Are you sure?', the link will be guarded with a JS popup asking that question.
18
+ # If the user accepts, the link is processed, otherwise not.
19
+ def link_to(name, options = {}, html_options = {}, *parameters_for_method_reference)
20
+ if options.is_a?(String)
21
+ content_tag "a", name, "href" => options
22
+ else
23
+ convert_confirm_option_to_javascript!(html_options)
24
+ content_tag("a", name, html_options.merge({ "href" => url_for(options, *parameters_for_method_reference) }))
25
+ end
26
+ end
27
+
28
+ # Creates a link tag of the given +name+ using an URL created by the set of +options+, unless the current
29
+ # controller, action, and id are the same as the link's, in which case only the name is returned (or the
30
+ # given block is yielded, if one exists). This is useful for creating link bars where you don't want to link
31
+ # to the page currently being viewed.
32
+ def link_to_unless_current(name, options = {}, html_options = {}, *parameters_for_method_reference)
33
+ assume_current_url_options!(options)
34
+
35
+ if destination_equal_to_current(options)
36
+ block_given? ?
37
+ yield(name, options, html_options, *parameters_for_method_reference) :
38
+ html_escape(name)
39
+ else
40
+ link_to name, options, html_options, *parameters_for_method_reference
41
+ end
42
+ end
43
+
44
+ private
45
+ def destination_equal_to_current(options)
46
+ params_without_location = @params.reject { |key, value| %w( controller action id ).include?(key) }
47
+
48
+ options[:action] == @params['action'] &&
49
+ options[:id] == @params['id'] &&
50
+ options[:controller] == @params['controller'] &&
51
+ (options.has_key?(:params) ? params_without_location == options[:params] : true)
52
+ end
53
+
54
+ def assume_current_url_options!(options)
55
+ if options[:controller].nil?
56
+ options[:controller] = @params['controller']
57
+ if options[:action].nil?
58
+ options[:action] = @params['action']
59
+ if options[:id].nil? then options[:id] ||= @params['id'] end
60
+ end
61
+ end
62
+ end
63
+
64
+ def convert_confirm_option_to_javascript!(html_options)
65
+ if html_options.include?(:confirm)
66
+ html_options["onClick"] = "return confirm('#{html_options[:confirm]}');"
67
+ html_options.delete(:confirm)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,61 @@
1
+ module ActionView
2
+ # There's also a convenience method for rendering sub templates within the current controller that depends on a single object
3
+ # (we call this kind of sub templates for partials). It relies on the fact that partials should follow the naming convention of being
4
+ # prefixed with an underscore -- as to separate them from regular templates that could be rendered on their own. In the template for
5
+ # Advertiser#buy, we could have:
6
+ #
7
+ # <% for ad in @advertisements %>
8
+ # <%= render_partial "ad", ad %>
9
+ # <% end %>
10
+ #
11
+ # This would render "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display.
12
+ #
13
+ # == Rendering a collection of partials
14
+ #
15
+ # The example of partial use describes a familar pattern where a template needs to iterate over an array and render a sub
16
+ # template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders
17
+ # a partial by the same name as the elements contained within. So the three-lined example in "Using partials" can be rewritten
18
+ # with a single line:
19
+ #
20
+ # <%= render_collection_of_partials "ad", @advertisements %>
21
+ #
22
+ # This will render "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display. An iteration counter
23
+ # will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the
24
+ # example above, the template would be fed +ad_counter+.
25
+ #
26
+ # == Rendering shared partials
27
+ #
28
+ # Two controllers can share a set of partials and render them like this:
29
+ #
30
+ # <%= render_partial "advertisement/ad", ad %>
31
+ #
32
+ # This will render the partial "advertisement/_ad.rhtml" regardless of which controller this is being called from.
33
+ module Partials
34
+ def render_partial(partial_path, object = nil, local_assigns = {})
35
+ path, partial_name = partial_pieces(partial_path)
36
+ object ||= controller.instance_variable_get("@#{partial_name}")
37
+ render("#{path}/_#{partial_name}", { partial_name => object }.merge(local_assigns))
38
+ end
39
+
40
+ def render_collection_of_partials(partial_name, collection, partial_spacer_template = nil)
41
+ collection_of_partials = Array.new
42
+ collection.each_with_index do |element, counter|
43
+ collection_of_partials.push(render_partial(partial_name, element, "#{partial_name.split("/").last}_counter" => counter))
44
+ end
45
+
46
+ return nil if collection_of_partials.empty?
47
+ partial_spacer_template ?
48
+ collection_of_partials.join(render("#{controller.send(:controller_name)}/_#{partial_spacer_template}")) :
49
+ collection_of_partials
50
+ end
51
+
52
+ private
53
+ def partial_pieces(partial_path)
54
+ if partial_path.include?('/')
55
+ return File.dirname(partial_path), File.basename(partial_path)
56
+ else
57
+ return controller.send(:controller_name), partial_path
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,84 @@
1
+ module ActionView
2
+ # The TemplateError exception is raised when the compilation of the template fails. This exception then gathers a
3
+ # bunch of intimate details and uses it to report a very precise exception message.
4
+ class TemplateError < ActionViewError #:nodoc:
5
+ SOURCE_CODE_RADIUS = 3
6
+
7
+ attr_reader :original_exception
8
+
9
+ def initialize(base_path, file_name, assigns, source, original_exception)
10
+ @base_path, @file_name, @assigns, @source, @original_exception =
11
+ base_path, file_name, assigns, source, original_exception
12
+ end
13
+
14
+ def message
15
+ if original_exception.message.include?("(eval):")
16
+ original_exception.message.scan(/\(eval\):(?:[0-9]*):in `.*'(.*)/).first.first
17
+ else
18
+ original_exception.message
19
+ end
20
+ end
21
+
22
+ def sub_template_message
23
+ if @sub_templates
24
+ "Trace of template inclusion: " +
25
+ @sub_templates.collect { |template| strip_base_path(template) }.join(", ")
26
+ else
27
+ ""
28
+ end
29
+ end
30
+
31
+ def source_extract
32
+ source_code = IO.readlines(@file_name)
33
+
34
+ start_on_line = [ line_number - SOURCE_CODE_RADIUS - 1, 0 ].max
35
+ end_on_line = [ line_number + SOURCE_CODE_RADIUS - 1, source_code.length].min
36
+
37
+ line_counter = start_on_line
38
+ extract = source_code[start_on_line..end_on_line].collect do |line|
39
+ line_counter += 1
40
+ "#{line_counter}: " + line
41
+ end
42
+
43
+ extract.join
44
+ end
45
+
46
+ def sub_template_of(file_name)
47
+ @sub_templates ||= []
48
+ @sub_templates << file_name
49
+ end
50
+
51
+ def line_number
52
+ begin
53
+ @original_exception.backtrace.join.scan(/\((?:erb)\):([0-9]*)/).first.first.to_i
54
+ rescue
55
+ begin
56
+ original_exception.message.scan(/\((?:eval)\):([0-9]*)/).first.first.to_i
57
+ rescue
58
+ 1
59
+ end
60
+ end
61
+ end
62
+
63
+ def file_name
64
+ strip_base_path(@file_name)
65
+ end
66
+
67
+ def to_s
68
+ "\n\n#{self.class} (#{message}) on line ##{line_number} of #{file_name}:\n" +
69
+ source_extract + "\n " +
70
+ clean_backtrace(original_exception).join("\n ") +
71
+ "\n\n"
72
+ end
73
+
74
+ private
75
+ def strip_base_path(file_name)
76
+ file_name.gsub(@base_path, "")
77
+ end
78
+
79
+ def clean_backtrace(exception)
80
+ base_dir = File.expand_path(File.dirname(__FILE__) + "/../../../../")
81
+ exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/public/../config/environments/../../", "").gsub("/public/../", "") }
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #--
4
+ # Copyright 2004 by Jim Weirich (jim@weirichhouse.org).
5
+ # All rights reserved.
6
+
7
+ # Permission is granted for use, copying, modification, distribution,
8
+ # and distribution of modified versions of this work as long as the
9
+ # above copyright notice is included.
10
+ #++
11
+
12
+ require 'builder/xmlmarkup'
13
+ require 'builder/xmlevents'
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ #--
3
+ # Copyright 2004 by Jim Weirich (jim@weirichhouse.org).
4
+ # All rights reserved.
5
+
6
+ # Permission is granted for use, copying, modification, distribution,
7
+ # and distribution of modified versions of this work as long as the
8
+ # above copyright notice is included.
9
+ #++
10
+
11
+ module Builder #:nodoc:
12
+
13
+ # BlankSlate provides an abstract base class with no predefined
14
+ # methods (except for <tt>\_\_send__</tt> and <tt>\_\_id__</tt>).
15
+ # BlankSlate is useful as a base class when writing classes that
16
+ # depend upon <tt>method_missing</tt> (e.g. dynamic proxies).
17
+ class BlankSlate #:nodoc:
18
+ instance_methods.each { |m| undef_method m unless m =~ /^(__|instance_eval)/ }
19
+ end
20
+
21
+ end
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'builder/blankslate'
4
+
5
+ module Builder #:nodoc:
6
+
7
+ # Generic error for builder
8
+ class IllegalBlockError < RuntimeError #:nodoc:
9
+ end
10
+
11
+ # XmlBase is a base class for building XML builders. See
12
+ # Builder::XmlMarkup and Builder::XmlEvents for examples.
13
+ class XmlBase < BlankSlate #:nodoc:
14
+
15
+ # Create an XML markup builder.
16
+ #
17
+ # out:: Object receiving the markup.1 +out+ must respond to
18
+ # <tt><<</tt>.
19
+ # indent:: Number of spaces used for indentation (0 implies no
20
+ # indentation and no line breaks).
21
+ # initial:: Level of initial indentation.
22
+ #
23
+ def initialize(indent=0, initial=0)
24
+ @indent = indent
25
+ @level = initial
26
+ end
27
+
28
+ # Create a tag named +sym+. Other than the first argument which
29
+ # is the tag name, the arguements are the same as the tags
30
+ # implemented via <tt>method_missing</tt>.
31
+ def tag!(sym, *args, &block)
32
+ self.__send__(sym, *args, &block)
33
+ end
34
+
35
+ # Create XML markup based on the name of the method. This method
36
+ # is never invoked directly, but is called for each markup method
37
+ # in the markup block.
38
+ def method_missing(sym, *args, &block)
39
+ text = nil
40
+ attrs = nil
41
+ sym = "#{sym}:#{args.shift}" if args.first.kind_of?(Symbol)
42
+ args.each do |arg|
43
+ case arg
44
+ when Hash
45
+ attrs ||= {}
46
+ attrs.merge!(arg)
47
+ else
48
+ text ||= ''
49
+ text << arg.to_s
50
+ end
51
+ end
52
+ if block
53
+ unless text.nil?
54
+ raise ArgumentError, "XmlMarkup cannot mix a text argument with a block"
55
+ end
56
+ _capture_outer_self(block) if @self.nil?
57
+ _indent
58
+ _start_tag(sym, attrs)
59
+ _newline
60
+ _nested_structures(block)
61
+ _indent
62
+ _end_tag(sym)
63
+ _newline
64
+ elsif text.nil?
65
+ _indent
66
+ _start_tag(sym, attrs, true)
67
+ _newline
68
+ else
69
+ _indent
70
+ _start_tag(sym, attrs)
71
+ text! text
72
+ _end_tag(sym)
73
+ _newline
74
+ end
75
+ @target
76
+ end
77
+
78
+ # Append text to the output target. Escape any markup. May be
79
+ # used within the markup brakets as:
80
+ #
81
+ # builder.p { br; text! "HI" } #=> <p><br/>HI</p>
82
+ def text!(text)
83
+ _text(_escape(text))
84
+ end
85
+
86
+ # Append text to the output target without escaping any markup.
87
+ # May be used within the markup brakets as:
88
+ #
89
+ # builder.p { |x| x << "<br/>HI" } #=> <p><br/>HI</p>
90
+ #
91
+ # This is useful when using non-builder enabled software that
92
+ # generates strings. Just insert the string directly into the
93
+ # builder without changing the inserted markup.
94
+ #
95
+ # It is also useful for stacking builder objects. Builders only
96
+ # use <tt><<</tt> to append to the target, so by supporting this
97
+ # method/operation builders can use oother builders as their
98
+ # targets.
99
+ def <<(text)
100
+ _text(text)
101
+ end
102
+
103
+ # For some reason, nil? is sent to the XmlMarkup object. If nil?
104
+ # is not defined and method_missing is invoked, some strange kind
105
+ # of recursion happens. Since nil? won't ever be an XML tag, it
106
+ # is pretty safe to define it here. (Note: this is an example of
107
+ # cargo cult programming,
108
+ # cf. http://fishbowl.pastiche.org/2004/10/13/cargo_cult_programming).
109
+ def nil?
110
+ false
111
+ end
112
+
113
+ private
114
+
115
+ def _escape(text)
116
+ text.
117
+ gsub(%r{&}, '&amp;').
118
+ gsub(%r{<}, '&lt;').
119
+ gsub(%r{>}, '&gt;')
120
+ end
121
+
122
+ def _capture_outer_self(block)
123
+ @self = eval("self", block)
124
+ end
125
+
126
+ def _newline
127
+ return if @indent == 0
128
+ text! "\n"
129
+ end
130
+
131
+ def _indent
132
+ return if @indent == 0 || @level == 0
133
+ text!(" " * (@level * @indent))
134
+ end
135
+
136
+ def _nested_structures(block)
137
+ @level += 1
138
+ block.call(self)
139
+ ensure
140
+ @level -= 1
141
+ end
142
+ end
143
+ end