actionpack 1.9.1 → 1.10.1

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 (123) hide show
  1. data/CHANGELOG +237 -0
  2. data/README +12 -12
  3. data/lib/action_controller.rb +17 -12
  4. data/lib/action_controller/assertions.rb +119 -67
  5. data/lib/action_controller/base.rb +184 -102
  6. data/lib/action_controller/benchmarking.rb +35 -6
  7. data/lib/action_controller/caching.rb +115 -58
  8. data/lib/action_controller/cgi_ext/cgi_methods.rb +54 -21
  9. data/lib/action_controller/cgi_ext/cookie_performance_fix.rb +39 -35
  10. data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +34 -21
  11. data/lib/action_controller/cgi_process.rb +23 -20
  12. data/lib/action_controller/components.rb +11 -2
  13. data/lib/action_controller/dependencies.rb +0 -5
  14. data/lib/action_controller/deprecated_redirects.rb +17 -0
  15. data/lib/action_controller/filters.rb +13 -9
  16. data/lib/action_controller/flash.rb +7 -7
  17. data/lib/action_controller/helpers.rb +1 -14
  18. data/lib/action_controller/layout.rb +40 -29
  19. data/lib/action_controller/macros/auto_complete.rb +52 -0
  20. data/lib/action_controller/macros/in_place_editing.rb +32 -0
  21. data/lib/action_controller/pagination.rb +44 -28
  22. data/lib/action_controller/request.rb +54 -40
  23. data/lib/action_controller/rescue.rb +8 -6
  24. data/lib/action_controller/routing.rb +77 -28
  25. data/lib/action_controller/scaffolding.rb +10 -14
  26. data/lib/action_controller/session/active_record_store.rb +36 -7
  27. data/lib/action_controller/session_management.rb +126 -0
  28. data/lib/action_controller/streaming.rb +14 -5
  29. data/lib/action_controller/templates/rescues/_request_and_response.rhtml +1 -1
  30. data/lib/action_controller/templates/rescues/_trace.rhtml +24 -0
  31. data/lib/action_controller/templates/rescues/diagnostics.rhtml +2 -13
  32. data/lib/action_controller/templates/rescues/template_error.rhtml +4 -2
  33. data/lib/action_controller/templates/scaffolds/list.rhtml +1 -1
  34. data/lib/action_controller/test_process.rb +35 -17
  35. data/lib/action_controller/upload_progress.rb +52 -0
  36. data/lib/action_controller/url_rewriter.rb +21 -16
  37. data/lib/action_controller/vendor/html-scanner/html/document.rb +2 -2
  38. data/lib/action_controller/vendor/html-scanner/html/node.rb +30 -3
  39. data/lib/action_pack/version.rb +9 -0
  40. data/lib/action_view.rb +1 -1
  41. data/lib/action_view/base.rb +204 -60
  42. data/lib/action_view/compiled_templates.rb +70 -0
  43. data/lib/action_view/helpers/active_record_helper.rb +7 -3
  44. data/lib/action_view/helpers/asset_tag_helper.rb +22 -12
  45. data/lib/action_view/helpers/capture_helper.rb +2 -10
  46. data/lib/action_view/helpers/date_helper.rb +21 -13
  47. data/lib/action_view/helpers/form_helper.rb +14 -10
  48. data/lib/action_view/helpers/form_options_helper.rb +4 -4
  49. data/lib/action_view/helpers/form_tag_helper.rb +59 -25
  50. data/lib/action_view/helpers/java_script_macros_helper.rb +188 -0
  51. data/lib/action_view/helpers/javascript_helper.rb +68 -133
  52. data/lib/action_view/helpers/javascripts/controls.js +427 -165
  53. data/lib/action_view/helpers/javascripts/dragdrop.js +256 -277
  54. data/lib/action_view/helpers/javascripts/effects.js +766 -277
  55. data/lib/action_view/helpers/javascripts/prototype.js +906 -218
  56. data/lib/action_view/helpers/javascripts/slider.js +258 -0
  57. data/lib/action_view/helpers/number_helper.rb +4 -3
  58. data/lib/action_view/helpers/pagination_helper.rb +42 -27
  59. data/lib/action_view/helpers/tag_helper.rb +25 -11
  60. data/lib/action_view/helpers/text_helper.rb +119 -13
  61. data/lib/action_view/helpers/upload_progress_helper.rb +2 -2
  62. data/lib/action_view/helpers/url_helper.rb +68 -21
  63. data/lib/action_view/partials.rb +17 -6
  64. data/lib/action_view/template_error.rb +19 -24
  65. data/rakefile +4 -3
  66. data/test/abstract_unit.rb +2 -1
  67. data/test/controller/action_pack_assertions_test.rb +62 -2
  68. data/test/controller/active_record_assertions_test.rb +5 -6
  69. data/test/controller/active_record_store_test.rb +23 -1
  70. data/test/controller/addresses_render_test.rb +4 -0
  71. data/test/controller/{base_tests.rb → base_test.rb} +4 -3
  72. data/test/controller/benchmark_test.rb +36 -0
  73. data/test/controller/caching_filestore.rb +22 -40
  74. data/test/controller/capture_test.rb +10 -1
  75. data/test/controller/cgi_test.rb +145 -23
  76. data/test/controller/components_test.rb +50 -0
  77. data/test/controller/custom_handler_test.rb +3 -3
  78. data/test/controller/fake_controllers.rb +24 -0
  79. data/test/controller/filters_test.rb +6 -6
  80. data/test/controller/flash_test.rb +6 -6
  81. data/test/controller/fragment_store_setting_test.rb +45 -0
  82. data/test/controller/helper_test.rb +1 -3
  83. data/test/controller/new_render_test.rb +119 -7
  84. data/test/controller/redirect_test.rb +11 -1
  85. data/test/controller/render_test.rb +34 -1
  86. data/test/controller/request_test.rb +14 -5
  87. data/test/controller/routing_test.rb +238 -42
  88. data/test/controller/send_file_test.rb +11 -10
  89. data/test/controller/session_management_test.rb +94 -0
  90. data/test/controller/test_test.rb +194 -5
  91. data/test/controller/url_rewriter_test.rb +46 -0
  92. data/test/fixtures/layouts/talk_from_action.rhtml +2 -0
  93. data/test/fixtures/layouts/yield.rhtml +2 -0
  94. data/test/fixtures/multipart/binary_file +0 -0
  95. data/test/fixtures/multipart/large_text_file +10 -0
  96. data/test/fixtures/multipart/mixed_files +0 -0
  97. data/test/fixtures/multipart/single_parameter +5 -0
  98. data/test/fixtures/multipart/text_file +10 -0
  99. data/test/fixtures/test/_customer_greeting.rhtml +1 -0
  100. data/test/fixtures/test/_hash_object.rhtml +1 -0
  101. data/test/fixtures/test/_person.rhtml +2 -0
  102. data/test/fixtures/test/action_talk_to_layout.rhtml +2 -0
  103. data/test/fixtures/test/content_for.rhtml +2 -0
  104. data/test/fixtures/test/potential_conflicts.rhtml +4 -0
  105. data/test/template/active_record_helper_test.rb +15 -8
  106. data/test/template/asset_tag_helper_test.rb +40 -16
  107. data/test/template/compiled_templates_tests.rb +63 -0
  108. data/test/template/date_helper_test.rb +80 -4
  109. data/test/template/form_helper_test.rb +48 -42
  110. data/test/template/form_options_helper_test.rb +40 -40
  111. data/test/template/form_tag_helper_test.rb +21 -15
  112. data/test/template/java_script_macros_helper_test.rb +56 -0
  113. data/test/template/javascript_helper_test.rb +70 -47
  114. data/test/template/number_helper_test.rb +2 -0
  115. data/test/template/tag_helper_test.rb +9 -0
  116. data/test/template/text_helper_test.rb +146 -1
  117. data/test/template/upload_progress_helper_testx.rb +11 -147
  118. data/test/template/url_helper_test.rb +90 -22
  119. data/test/testing_sandbox.rb +26 -0
  120. metadata +37 -7
  121. data/lib/action_controller/auto_complete.rb +0 -47
  122. data/lib/action_controller/deprecated_renders_and_redirects.rb +0 -76
  123. data/lib/action_controller/session.rb +0 -14
@@ -0,0 +1,258 @@
1
+ // Copyright (c) 2005 Marty Haught
2
+ //
3
+ // See scriptaculous.js for full license.
4
+
5
+ if(!Control) var Control = {};
6
+ Control.Slider = Class.create();
7
+
8
+ // options:
9
+ // axis: 'vertical', or 'horizontal' (default)
10
+ // increment: (default: 1)
11
+ // step: (default: 1)
12
+ //
13
+ // callbacks:
14
+ // onChange(value)
15
+ // onSlide(value)
16
+ Control.Slider.prototype = {
17
+ initialize: function(handle, track, options) {
18
+ this.handle = $(handle);
19
+ this.track = $(track);
20
+
21
+ this.options = options || {};
22
+
23
+ this.axis = this.options.axis || 'horizontal';
24
+ this.increment = this.options.increment || 1;
25
+ this.step = parseInt(this.options.step) || 1;
26
+ this.value = 0;
27
+
28
+ var defaultMaximum = Math.round(this.track.offsetWidth / this.increment);
29
+ if(this.isVertical()) defaultMaximum = Math.round(this.track.offsetHeight / this.increment);
30
+
31
+ this.maximum = this.options.maximum || defaultMaximum;
32
+ this.minimum = this.options.minimum || 0;
33
+
34
+ // Will be used to align the handle onto the track, if necessary
35
+ this.alignX = parseInt (this.options.alignX) || 0;
36
+ this.alignY = parseInt (this.options.alignY) || 0;
37
+
38
+ // Zero out the slider position
39
+ this.setCurrentLeft(Position.cumulativeOffset(this.track)[0] - Position.cumulativeOffset(this.handle)[0] + this.alignX);
40
+ this.setCurrentTop(this.trackTop() - Position.cumulativeOffset(this.handle)[1] + this.alignY);
41
+
42
+ this.offsetX = 0;
43
+ this.offsetY = 0;
44
+
45
+ this.originalLeft = this.currentLeft();
46
+ this.originalTop = this.currentTop();
47
+ this.originalZ = parseInt(this.handle.style.zIndex || "0");
48
+
49
+ // Prepopulate Slider value
50
+ this.setSliderValue(parseInt(this.options.sliderValue) || 0);
51
+
52
+ this.active = false;
53
+ this.dragging = false;
54
+ this.disabled = false;
55
+
56
+ // FIXME: use css
57
+ this.handleImage = $(this.options.handleImage) || false;
58
+ this.handleDisabled = this.options.handleDisabled || false;
59
+ this.handleEnabled = false;
60
+ if(this.handleImage)
61
+ this.handleEnabled = this.handleImage.src || false;
62
+
63
+ if(this.options.disabled)
64
+ this.setDisabled();
65
+
66
+ // Value Array
67
+ this.values = this.options.values || false; // Add method to validate and sort??
68
+
69
+ Element.makePositioned(this.handle); // fix IE
70
+
71
+ this.eventMouseDown = this.startDrag.bindAsEventListener(this);
72
+ this.eventMouseUp = this.endDrag.bindAsEventListener(this);
73
+ this.eventMouseMove = this.update.bindAsEventListener(this);
74
+ this.eventKeypress = this.keyPress.bindAsEventListener(this);
75
+
76
+ Event.observe(this.handle, "mousedown", this.eventMouseDown);
77
+ Event.observe(document, "mouseup", this.eventMouseUp);
78
+ Event.observe(document, "mousemove", this.eventMouseMove);
79
+ Event.observe(document, "keypress", this.eventKeypress);
80
+ },
81
+ dispose: function() {
82
+ Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
83
+ Event.stopObserving(document, "mouseup", this.eventMouseUp);
84
+ Event.stopObserving(document, "mousemove", this.eventMouseMove);
85
+ Event.stopObserving(document, "keypress", this.eventKeypress);
86
+ },
87
+ setDisabled: function(){
88
+ this.disabled = true;
89
+ if(this.handleDisabled)
90
+ this.handleImage.src = this.handleDisabled;
91
+ },
92
+ setEnabled: function(){
93
+ this.disabled = false;
94
+ if(this.handleEnabled)
95
+ this.handleImage.src = this.handleEnabled;
96
+ },
97
+ currentLeft: function() {
98
+ return parseInt(this.handle.style.left || '0');
99
+ },
100
+ currentTop: function() {
101
+ return parseInt(this.handle.style.top || '0');
102
+ },
103
+ setCurrentLeft: function(left) {
104
+ this.handle.style.left = left +"px";
105
+ },
106
+ setCurrentTop: function(top) {
107
+ this.handle.style.top = top +"px";
108
+ },
109
+ trackLeft: function(){
110
+ return Position.cumulativeOffset(this.track)[0];
111
+ },
112
+ trackTop: function(){
113
+ return Position.cumulativeOffset(this.track)[1];
114
+ },
115
+ getNearestValue: function(value){
116
+ if(this.values){
117
+ var i = 0;
118
+ var offset = Math.abs(this.values[0] - value);
119
+ var newValue = this.values[0];
120
+
121
+ for(i=0; i < this.values.length; i++){
122
+ var currentOffset = Math.abs(this.values[i] - value);
123
+ if(currentOffset < offset){
124
+ newValue = this.values[i];
125
+ offset = currentOffset;
126
+ }
127
+ }
128
+ return newValue;
129
+ }
130
+ return value;
131
+ },
132
+ setSliderValue: function(sliderValue){
133
+ // First check our max and minimum and nearest values
134
+ sliderValue = this.getNearestValue(sliderValue);
135
+ if(sliderValue > this.maximum) sliderValue = this.maximum;
136
+ if(sliderValue < this.minimum) sliderValue = this.minimum;
137
+ var offsetDiff = (sliderValue - (this.value||this.minimum)) * this.increment;
138
+
139
+ if(this.isVertical()){
140
+ this.setCurrentTop(offsetDiff + this.currentTop());
141
+ } else {
142
+ this.setCurrentLeft(offsetDiff + this.currentLeft());
143
+ }
144
+ this.value = sliderValue;
145
+ this.updateFinished();
146
+ },
147
+ minimumOffset: function(){
148
+ return(this.isVertical() ?
149
+ this.trackTop() + this.alignY :
150
+ this.trackLeft() + this.alignX);
151
+ },
152
+ maximumOffset: function(){
153
+ return(this.isVertical() ?
154
+ this.trackTop() + this.alignY + (this.maximum - this.minimum) * this.increment :
155
+ this.trackLeft() + this.alignX + (this.maximum - this.minimum) * this.increment);
156
+ },
157
+ isVertical: function(){
158
+ return (this.axis == 'vertical');
159
+ },
160
+ startDrag: function(event) {
161
+ if(Event.isLeftClick(event)) {
162
+ if(!this.disabled){
163
+ this.active = true;
164
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
165
+ var offsets = Position.cumulativeOffset(this.handle);
166
+ this.offsetX = (pointer[0] - offsets[0]);
167
+ this.offsetY = (pointer[1] - offsets[1]);
168
+ this.originalLeft = this.currentLeft();
169
+ this.originalTop = this.currentTop();
170
+ }
171
+ Event.stop(event);
172
+ }
173
+ },
174
+ update: function(event) {
175
+ if(this.active) {
176
+ if(!this.dragging) {
177
+ var style = this.handle.style;
178
+ this.dragging = true;
179
+ if(style.position=="") style.position = "relative";
180
+ style.zIndex = this.options.zindex;
181
+ }
182
+ this.draw(event);
183
+ // fix AppleWebKit rendering
184
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
185
+ Event.stop(event);
186
+ }
187
+ },
188
+ draw: function(event) {
189
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
190
+ var offsets = Position.cumulativeOffset(this.handle);
191
+
192
+ offsets[0] -= this.currentLeft();
193
+ offsets[1] -= this.currentTop();
194
+
195
+ // Adjust for the pointer's position on the handle
196
+ pointer[0] -= this.offsetX;
197
+ pointer[1] -= this.offsetY;
198
+ var style = this.handle.style;
199
+
200
+ if(this.isVertical()){
201
+ if(pointer[1] > this.maximumOffset())
202
+ pointer[1] = this.maximumOffset();
203
+ if(pointer[1] < this.minimumOffset())
204
+ pointer[1] = this.minimumOffset();
205
+
206
+ // Increment by values
207
+ if(this.values){
208
+ this.value = this.getNearestValue(Math.round((pointer[1] - this.minimumOffset()) / this.increment) + this.minimum);
209
+ pointer[1] = this.trackTop() + this.alignY + (this.value - this.minimum) * this.increment;
210
+ } else {
211
+ this.value = Math.round((pointer[1] - this.minimumOffset()) / this.increment) + this.minimum;
212
+ }
213
+ style.top = pointer[1] - offsets[1] + "px";
214
+ } else {
215
+ if(pointer[0] > this.maximumOffset()) pointer[0] = this.maximumOffset();
216
+ if(pointer[0] < this.minimumOffset()) pointer[0] = this.minimumOffset();
217
+ // Increment by values
218
+ if(this.values){
219
+ this.value = this.getNearestValue(Math.round((pointer[0] - this.minimumOffset()) / this.increment) + this.minimum);
220
+ pointer[0] = this.trackLeft() + this.alignX + (this.value - this.minimum) * this.increment;
221
+ } else {
222
+ this.value = Math.round((pointer[0] - this.minimumOffset()) / this.increment) + this.minimum;
223
+ }
224
+ style.left = (pointer[0] - offsets[0]) + "px";
225
+ }
226
+ if(this.options.onSlide) this.options.onSlide(this.value);
227
+ },
228
+ endDrag: function(event) {
229
+ if(this.active && this.dragging) {
230
+ this.finishDrag(event, true);
231
+ Event.stop(event);
232
+ }
233
+ this.active = false;
234
+ this.dragging = false;
235
+ },
236
+ finishDrag: function(event, success) {
237
+ this.active = false;
238
+ this.dragging = false;
239
+ this.handle.style.zIndex = this.originalZ;
240
+ this.originalLeft = this.currentLeft();
241
+ this.originalTop = this.currentTop();
242
+ this.updateFinished();
243
+ },
244
+ updateFinished: function() {
245
+ if(this.options.onChange) this.options.onChange(this.value);
246
+ },
247
+ keyPress: function(event) {
248
+ if(this.active && !this.disabled) {
249
+ switch(event.keyCode) {
250
+ case Event.KEY_ESC:
251
+ this.finishDrag(event, false);
252
+ Event.stop(event);
253
+ break;
254
+ }
255
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
256
+ }
257
+ }
258
+ }
@@ -4,8 +4,8 @@ module ActionView
4
4
  # one of the following forms: phone number, percentage, money, or precision level.
5
5
  module NumberHelper
6
6
 
7
- # Formats a +number+ into a US phone number string. The +options+ can be hash used to customize the format of the output.
8
- # The area code can be surrounded by parenthesis by setting +:area_code+ to true; default is false
7
+ # Formats a +number+ into a US phone number string. The +options+ can be a hash used to customize the format of the output.
8
+ # The area code can be surrounded by parentheses by setting +:area_code+ to true; default is false
9
9
  # The delimiter can be set using +:delimiter+; default is "-"
10
10
  # Examples:
11
11
  # number_to_phone(1235551234) => 123-555-1234
@@ -25,7 +25,7 @@ module ActionView
25
25
  end
26
26
  end
27
27
 
28
- # Formates a +number+ into a currency string. The +options+ hash can be used to customize the format of the output.
28
+ # Formats a +number+ into a currency string. The +options+ hash can be used to customize the format of the output.
29
29
  # The +number+ can contain a level of precision using the +precision+ key; default is 2
30
30
  # The currency type can be set using the +unit+ key; default is "$"
31
31
  # The unit separator can be set using the +separator+ key; default is "."
@@ -37,6 +37,7 @@ module ActionView
37
37
  def number_to_currency(number, options = {})
38
38
  options = options.stringify_keys
39
39
  precision, unit, separator, delimiter = options.delete("precision") { 2 }, options.delete("unit") { "$" }, options.delete("separator") { "." }, options.delete("delimiter") { "," }
40
+ separator = "" unless precision > 0
40
41
  begin
41
42
  parts = number_with_precision(number, precision).split('.')
42
43
  unit + number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s
@@ -18,7 +18,8 @@ module ActionView
18
18
  }
19
19
  end
20
20
 
21
- # Creates a basic HTML link bar for the given +paginator+.
21
+ # Creates a basic HTML link bar for the given +paginator+.
22
+ # +html_options+ are passed to +link_to+.
22
23
  #
23
24
  # +options+ are:
24
25
  # <tt>:name</tt>:: the routing name for this paginator
@@ -33,39 +34,53 @@ module ActionView
33
34
  # +false+)
34
35
  # <tt>:params</tt>:: any additional routing parameters
35
36
  # for page URLs
36
- def pagination_links(paginator, options={})
37
- options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
37
+ def pagination_links(paginator, options={}, html_options={})
38
+ name = options[:name] || DEFAULT_OPTIONS[:name]
39
+ params = (options[:params] || DEFAULT_OPTIONS[:params]).clone
38
40
 
39
- window_pages = paginator.current.window(options[:window_size]).pages
41
+ pagination_links_each(paginator, options) do |n|
42
+ params[name] = n
43
+ link_to(n.to_s, params, html_options)
44
+ end
45
+ end
46
+
47
+ # Iterate through the pages of a given +paginator+, invoking a
48
+ # block for each page number that needs to be rendered as a link.
49
+ def pagination_links_each(paginator, options)
50
+ options = DEFAULT_OPTIONS.merge(options)
51
+ link_to_current_page = options[:link_to_current_page]
52
+ always_show_anchors = options[:always_show_anchors]
40
53
 
41
- return if window_pages.length <= 1 unless
42
- options[:link_to_current_page]
54
+ current_page = paginator.current_page
55
+ window_pages = current_page.window(options[:window_size]).pages
56
+ return if window_pages.length <= 1 unless link_to_current_page
43
57
 
44
58
  first, last = paginator.first, paginator.last
45
59
 
46
- returning html = '' do
47
- if options[:always_show_anchors] and not window_pages[0].first?
48
- html << link_to(first.number, { options[:name] => first }.update( options[:params] ))
49
- html << ' ... ' if window_pages[0].number - first.number > 1
50
- html << ' '
51
- end
52
-
53
- window_pages.each do |page|
54
- if paginator.current == page && !options[:link_to_current_page]
55
- html << page.number.to_s
56
- else
57
- html << link_to(page.number, { options[:name] => page }.update( options[:params] ))
58
- end
59
- html << ' '
60
- end
60
+ html = ''
61
+ if always_show_anchors and not (wp_first = window_pages[0]).first?
62
+ html << yield(first.number)
63
+ html << ' ... ' if wp_first.number - first.number > 1
64
+ html << ' '
65
+ end
61
66
 
62
- if options[:always_show_anchors] && !window_pages.last.last?
63
- html << ' ... ' if last.number - window_pages[-1].number > 1
64
- html << link_to(paginator.last.number, { options[:name] => last }.update( options[:params]))
67
+ window_pages.each do |page|
68
+ if current_page == page && !link_to_current_page
69
+ html << page.number.to_s
70
+ else
71
+ html << yield(page.number)
65
72
  end
73
+ html << ' '
66
74
  end
75
+
76
+ if always_show_anchors and not (wp_last = window_pages[-1]).last?
77
+ html << ' ... ' if last.number - wp_last.number > 1
78
+ html << yield(last.number)
79
+ end
80
+
81
+ html
67
82
  end
68
83
 
69
- end
70
- end
71
- end
84
+ end # PaginationHelper
85
+ end # Helpers
86
+ end # ActionView
@@ -10,27 +10,41 @@ module ActionView
10
10
  # Examples:
11
11
  # * <tt>tag("br") => <br /></tt>
12
12
  # * <tt>tag("input", { "type" => "text"}) => <input type="text" /></tt>
13
- def tag(name, options = {}, open = false)
14
- "<#{name}#{tag_options(options)}" + (open ? ">" : " />")
13
+ def tag(name, options = nil, open = false)
14
+ "<#{name}#{tag_options(options.stringify_keys) if options}" + (open ? ">" : " />")
15
15
  end
16
16
 
17
17
  # Examples:
18
18
  # * <tt>content_tag("p", "Hello world!") => <p>Hello world!</p></tt>
19
19
  # * <tt>content_tag("div", content_tag("p", "Hello world!"), "class" => "strong") => </tt>
20
20
  # <tt><div class="strong"><p>Hello world!</p></div></tt>
21
- def content_tag(name, content, options = {})
22
- "<#{name}#{tag_options(options)}>#{content}</#{name}>"
21
+ def content_tag(name, content, options = nil)
22
+ "<#{name}#{tag_options(options.stringify_keys) if options}>#{content}</#{name}>"
23
+ end
24
+
25
+ # Returns a CDATA section for the given +content+. CDATA sections
26
+ # are used to escape blocks of text containing characters which would
27
+ # otherwise be recognized as markup. CDATA sections begin with the string
28
+ # <tt>&lt;![CDATA[</tt> and end with (and may not contain) the string
29
+ # <tt>]]></tt>.
30
+ def cdata_section(content)
31
+ "<![CDATA[#{content}]]>"
23
32
  end
24
33
 
25
34
  private
26
35
  def tag_options(options)
27
- cleaned_options = options.reject { |key, value| value.nil? }
28
- unless cleaned_options.empty?
29
- " " + cleaned_options.symbolize_keys.map { |key, value|
30
- %(#{key}="#{html_escape(value.to_s)}")
31
- }.sort.join(" ")
32
- end
36
+ cleaned_options = convert_booleans(options.stringify_keys.reject {|key, value| value.nil?})
37
+ ' ' + cleaned_options.map {|key, value| %(#{key}="#{html_escape(value.to_s)}")}.sort * ' ' unless cleaned_options.empty?
38
+ end
39
+
40
+ def convert_booleans(options)
41
+ %w( disabled readonly multiple ).each { |a| boolean_attribute(options, a) }
42
+ options
43
+ end
44
+
45
+ def boolean_attribute(options, attribute)
46
+ options[attribute] ? options[attribute] = attribute : options.delete(attribute)
33
47
  end
34
48
  end
35
49
  end
36
- end
50
+ end
@@ -3,14 +3,14 @@ require File.dirname(__FILE__) + '/tag_helper'
3
3
  module ActionView
4
4
  module Helpers #:nodoc:
5
5
  # Provides a set of methods for working with text strings that can help unburden the level of inline Ruby code in the
6
- # templates. In the example below we iterate over a collection of posts provided to the template and prints each title
6
+ # templates. In the example below we iterate over a collection of posts provided to the template and print each title
7
7
  # after making sure it doesn't run longer than 20 characters:
8
8
  # <% for post in @posts %>
9
9
  # Title: <%= truncate(post.title, 20) %>
10
10
  # <% end %>
11
- module TextHelper
11
+ module TextHelper
12
12
  # The regular puts and print are outlawed in eRuby. It's recommended to use the <%= "hello" %> form instead of print "hello".
13
- # If you absolutely must use a method-based output, you can use concat. It's use like this <% concat "hello", binding %>. Notice that
13
+ # If you absolutely must use a method-based output, you can use concat. It's used like this: <% concat "hello", binding %>. Notice that
14
14
  # it doesn't have an equal sign in front. Using <%= concat "hello" %> would result in a double hello.
15
15
  def concat(string, binding)
16
16
  eval("_erbout", binding).concat(string)
@@ -20,7 +20,13 @@ module ActionView
20
20
  # if the +text+ is longer than +length+.
21
21
  def truncate(text, length = 30, truncate_string = "...")
22
22
  if text.nil? then return end
23
- if text.length > length then text[0..(length - 3)] + truncate_string else text end
23
+
24
+ if $KCODE == "NONE"
25
+ text.length > length ? text[0..(length - 3)] + truncate_string : text
26
+ else
27
+ chars = text.split(//)
28
+ chars.length > length ? chars[0..(length-3)].join + truncate_string : text
29
+ end
24
30
  end
25
31
 
26
32
  # Highlights the +phrase+ where it is found in the +text+ by surrounding it like
@@ -29,7 +35,7 @@ module ActionView
29
35
  # N.B.: The +phrase+ is sanitized to include only letters, digits, and spaces before use.
30
36
  def highlight(text, phrase, highlighter = '<strong class="highlight">\1</strong>')
31
37
  if phrase.blank? then return text end
32
- text.gsub(/(#{escape_regexp(phrase)})/i, highlighter) unless text.nil?
38
+ text.gsub(/(#{Regexp.escape(phrase)})/i, highlighter) unless text.nil?
33
39
  end
34
40
 
35
41
  # Extracts an excerpt from the +text+ surrounding the +phrase+ with a number of characters on each side determined
@@ -37,7 +43,7 @@ module ActionView
37
43
  # excerpt("hello my world", "my", 3) => "...lo my wo..."
38
44
  def excerpt(text, phrase, radius = 100, excerpt_string = "...")
39
45
  if text.nil? || phrase.nil? then return end
40
- phrase = escape_regexp(phrase)
46
+ phrase = Regexp.escape(phrase)
41
47
 
42
48
  if found_pos = text =~ /(#{phrase})/i
43
49
  start_pos = [ found_pos - radius, 0 ].max
@@ -76,7 +82,13 @@ module ActionView
76
82
  # Returns the text with all the Textile codes turned into HTML-tags.
77
83
  # <i>This method is only available if RedCloth can be required</i>.
78
84
  def textilize(text)
79
- text.blank? ? "" : RedCloth.new(text, [ :hard_breaks ]).to_html
85
+ if text.blank?
86
+ ""
87
+ else
88
+ textilized = RedCloth.new(text, [ :hard_breaks ])
89
+ textilized.hard_breaks = true if textilized.respond_to?("hard_breaks=")
90
+ textilized.to_html
91
+ end
80
92
  end
81
93
 
82
94
  # Returns the text with all the Textile codes turned into HTML-tags, but without the regular bounding <p> tag.
@@ -103,7 +115,7 @@ module ActionView
103
115
  # We can't really help what's not there
104
116
  end
105
117
 
106
- # Returns +text+ transformed into html using very simple formatting rules
118
+ # Returns +text+ transformed into HTML using very simple formatting rules
107
119
  # Surrounds paragraphs with <tt>&lt;p&gt;</tt> tags, and converts line breaks into <tt>&lt;br /&gt;</tt>
108
120
  # Two consecutive newlines(<tt>\n\n</tt>) are considered as a paragraph, one newline (<tt>\n</tt>) is
109
121
  # considered a linebreak, three or more consecutive newlines are turned into two newlines
@@ -189,18 +201,112 @@ module ActionView
189
201
 
190
202
  html
191
203
  end
204
+
205
+ # Returns a Cycle object whose to_s value cycles through items of an
206
+ # array every time it is called. This can be used to alternate classes
207
+ # for table rows:
208
+ #
209
+ # <%- for item in @items do -%>
210
+ # <tr class="<%= cycle("even", "odd") %>">
211
+ # ... use item ...
212
+ # </tr>
213
+ # <%- end -%>
214
+ #
215
+ # You can use named cycles to prevent clashes in nested loops. You'll
216
+ # have to reset the inner cycle, manually:
217
+ #
218
+ # <%- for item in @items do -%>
219
+ # <tr class="<%= cycle("even", "odd", :name => "row_class")
220
+ # <td>
221
+ # <%- for value in item.values do -%>
222
+ # <span style="color:'<%= cycle("red", "green", "blue"
223
+ # :name => "colors") %>'">
224
+ # item
225
+ # </span>
226
+ # <%- end -%>
227
+ # <%- reset_cycle("colors") -%>
228
+ # </td>
229
+ # </tr>
230
+ # <%- end -%>
231
+ def cycle(first_value, *values)
232
+ if (values.last.instance_of? Hash)
233
+ params = values.pop
234
+ name = params[:name]
235
+ else
236
+ name = "default"
237
+ end
238
+ values.unshift(first_value)
192
239
 
240
+ cycle = get_cycle(name)
241
+ if (cycle.nil? || cycle.values != values)
242
+ cycle = set_cycle(name, Cycle.new(*values))
243
+ end
244
+ return cycle.to_s
245
+ end
246
+
247
+ # Resets a cycle so that it starts from the first element in the array
248
+ # the next time it is used.
249
+ def reset_cycle(name = "default")
250
+ cycle = get_cycle(name)
251
+ return if cycle.nil?
252
+ cycle.reset
253
+ end
254
+
255
+ class Cycle #:nodoc:
256
+ attr_reader :values
257
+
258
+ def initialize(first_value, *values)
259
+ @values = values.unshift(first_value)
260
+ reset
261
+ end
262
+
263
+ def reset
264
+ @index = 0
265
+ end
266
+
267
+ def to_s
268
+ value = @values[@index].to_s
269
+ @index = (@index + 1) % @values.size
270
+ return value
271
+ end
272
+ end
193
273
 
194
274
  private
195
- # Returns a version of the text that's safe to use in a regular expression without triggering engine features.
196
- def escape_regexp(text)
197
- text.gsub(/([\\|?+*\/\)\(])/) { |m| "\\#{$1}" }
275
+ # The cycle helpers need to store the cycles in a place that is
276
+ # guaranteed to be reset every time a page is rendered, so it
277
+ # uses an instance variable of ActionView::Base.
278
+ def get_cycle(name)
279
+ @_cycles = Hash.new if @_cycles.nil?
280
+ return @_cycles[name]
198
281
  end
282
+
283
+ def set_cycle(name, cycle_object)
284
+ @_cycles = Hash.new if @_cycles.nil?
285
+ @_cycles[name] = cycle_object
286
+ end
287
+
288
+ AUTO_LINK_RE = /
289
+ ( # leading text
290
+ <\w+.*?>| # leading HTML tag, or
291
+ [^=!:'"\/]| # leading punctuation, or
292
+ ^ # beginning of line
293
+ )
294
+ (
295
+ (?:http[s]?:\/\/)| # protocol spec, or
296
+ (?:www\.) # www.*
297
+ )
298
+ (
299
+ ([\w]+[=?&\/.-]?)* # url segment
300
+ \w+[\/]? # url tail
301
+ (?:\#\w*)? # trailing anchor
302
+ )
303
+ ([[:punct:]]|\s|<|$) # trailing text
304
+ /x unless const_defined?(:AUTO_LINK_RE)
199
305
 
200
306
  # Turns all urls into clickable links.
201
307
  def auto_link_urls(text, href_options = {})
202
- text.gsub(/(<\w+.*?>|[^=!:'"\/]|^)((?:http[s]?:\/\/)|(?:www\.))([^\s<]+\/?)([[:punct:]]|\s|<|$)/) do
203
- all, a, b, c, d = $&, $1, $2, $3, $4
308
+ text.gsub(AUTO_LINK_RE) do
309
+ all, a, b, c, d = $&, $1, $2, $3, $5
204
310
  if a =~ /<a\s/i # don't replace URL's that are already linked
205
311
  all
206
312
  else