padrino-helpers 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/lib/padrino-helpers.rb +4 -1
  3. data/lib/padrino-helpers/asset_tag_helpers.rb +17 -14
  4. data/lib/padrino-helpers/breadcrumb_helpers.rb +6 -6
  5. data/lib/padrino-helpers/form_builder/abstract_form_builder.rb +119 -163
  6. data/lib/padrino-helpers/form_builder/deprecated_builder_methods.rb +92 -0
  7. data/lib/padrino-helpers/form_helpers.rb +66 -347
  8. data/lib/padrino-helpers/form_helpers/errors.rb +138 -0
  9. data/lib/padrino-helpers/form_helpers/options.rb +97 -0
  10. data/lib/padrino-helpers/form_helpers/security.rb +70 -0
  11. data/lib/padrino-helpers/output_helpers.rb +1 -1
  12. data/lib/padrino-helpers/output_helpers/abstract_handler.rb +1 -1
  13. data/lib/padrino-helpers/render_helpers.rb +10 -9
  14. data/lib/padrino-helpers/tag_helpers.rb +2 -1
  15. data/lib/padrino/rendering.rb +378 -0
  16. data/lib/padrino/rendering/extensions/erubis.rb +74 -0
  17. data/lib/padrino/rendering/extensions/haml.rb +29 -0
  18. data/lib/padrino/rendering/extensions/slim.rb +21 -0
  19. data/padrino-helpers.gemspec +2 -1
  20. data/test/fixtures/apps/.components +6 -0
  21. data/test/fixtures/apps/.gitignore +7 -0
  22. data/test/fixtures/apps/render.rb +25 -0
  23. data/test/fixtures/apps/views/article/comment/show.slim +1 -0
  24. data/test/fixtures/apps/views/blog/post.erb +1 -0
  25. data/test/fixtures/apps/views/layouts/specific.erb +1 -0
  26. data/test/fixtures/apps/views/test/post.erb +1 -0
  27. data/test/fixtures/layouts/layout.erb +1 -0
  28. data/test/fixtures/markup_app/app.rb +0 -1
  29. data/test/fixtures/render_app/app.rb +25 -1
  30. data/test/fixtures/render_app/views/_unsafe.html.builder +2 -0
  31. data/test/fixtures/render_app/views/_unsafe_object.html.builder +2 -0
  32. data/test/fixtures/render_app/views/ruby_block_capture_erb.erb +1 -0
  33. data/test/fixtures/render_app/views/ruby_block_capture_haml.haml +1 -0
  34. data/test/fixtures/render_app/views/ruby_block_capture_slim.slim +1 -0
  35. data/test/helper.rb +65 -1
  36. data/test/test_asset_tag_helpers.rb +83 -79
  37. data/test/test_breadcrumb_helpers.rb +20 -20
  38. data/test/test_form_builder.rb +196 -196
  39. data/test/test_form_helpers.rb +163 -163
  40. data/test/test_format_helpers.rb +65 -65
  41. data/test/test_locale.rb +1 -1
  42. data/test/test_number_helpers.rb +10 -11
  43. data/test/test_output_helpers.rb +28 -28
  44. data/test/test_render_helpers.rb +89 -35
  45. data/test/test_rendering.rb +683 -0
  46. data/test/test_rendering_extensions.rb +14 -0
  47. data/test/test_tag_helpers.rb +23 -23
  48. metadata +57 -5
@@ -0,0 +1,138 @@
1
+ module Padrino
2
+ module Helpers
3
+ module FormHelpers
4
+ ##
5
+ # Helpers to generate form errors.
6
+ #
7
+ module Errors
8
+ ##
9
+ # Constructs list HTML for the errors for a given symbol.
10
+ #
11
+ # @overload error_messages_for(*objects, options = {})
12
+ # @param [Array<Object>] object Splat of objects to display errors for.
13
+ # @param [Hash] options Error message display options.
14
+ # @option options [String] :header_tag ("h2")
15
+ # Used for the header of the error div.
16
+ # @option options [String] :id ("field-errors")
17
+ # The id of the error div.
18
+ # @option options [String] :class ("field-errors")
19
+ # The class of the error div.
20
+ # @option options [Array<Object>] :object
21
+ # The object (or array of objects) for which to display errors,
22
+ # if you need to escape the instance variable convention.
23
+ # @option options [String] :object_name
24
+ # The object name to use in the header, or any text that you prefer.
25
+ # If +:object_name+ is not set, the name of the first object will be used.
26
+ # @option options [String] :header_message ("X errors prohibited this object from being saved")
27
+ # The message in the header of the error div. Pass +nil+ or an empty string
28
+ # to avoid the header message altogether.
29
+ # @option options [String] :message ("There were problems with the following fields:")
30
+ # The explanation message after the header message and before
31
+ # the error list. Pass +nil+ or an empty string to avoid the explanation message
32
+ # altogether.
33
+ #
34
+ # @return [String] The html section with all errors for the specified +objects+
35
+ #
36
+ # @example
37
+ # error_messages_for :user
38
+ #
39
+ def error_messages_for(*objects)
40
+ options = objects.extract_options!.symbolize_keys
41
+ objects = objects.map{ |obj| resolve_object(obj) }.compact
42
+ count = objects.inject(0){ |sum, object| sum + object.errors.count }
43
+ return ActiveSupport::SafeBuffer.new if count.zero?
44
+
45
+ content_tag(:div, error_contents(objects, count, options), error_html_attributes(options))
46
+ end
47
+
48
+ ##
49
+ # Returns a string containing the error message attached to the
50
+ # +method+ on the +object+ if one exists.
51
+ #
52
+ # @param [Object] object
53
+ # The object to display the error for.
54
+ # @param [Symbol] field
55
+ # The field on the +object+ to display the error for.
56
+ # @param [Hash] options
57
+ # The options to control the error display.
58
+ # @option options [String] :tag ("span")
59
+ # The tag that encloses the error.
60
+ # @option options [String] :prepend ("")
61
+ # The text to prepend before the field error.
62
+ # @option options [String] :append ("")
63
+ # The text to append after the field error.
64
+ #
65
+ # @example
66
+ # # => <span class="error">can't be blank</div>
67
+ # error_message_on :post, :title
68
+ # error_message_on @post, :title
69
+ #
70
+ # # => <div class="custom" style="border:1px solid red">can't be blank</div>
71
+ # error_message_on :post, :title, :tag => :id, :class => :custom, :style => "border:1px solid red"
72
+ #
73
+ # # => <div class="error">This title can't be blank (or it won't work)</div>
74
+ # error_message_on :post, :title, :prepend => "This title", :append => "(or it won't work)"
75
+ #
76
+ # @return [String] The html display of an error for a particular +object+ and +field+.
77
+ #
78
+ # @api public
79
+ def error_message_on(object, field, options={})
80
+ error = Array(resolve_object(object).errors[field]).first
81
+ return ActiveSupport::SafeBuffer.new unless error
82
+ options = { :tag => :span, :class => :error }.update(options)
83
+ tag = options.delete(:tag)
84
+ error = [options.delete(:prepend), error, options.delete(:append)].compact.join(" ")
85
+ content_tag(tag, error, options)
86
+ end
87
+
88
+ private
89
+
90
+ def error_contents(objects, count, options)
91
+ object_name = options[:object_name] || objects.first.class.to_s.underscore.gsub(/\//, ' ')
92
+
93
+ contents = ActiveSupport::SafeBuffer.new
94
+ contents << error_header_tag(options, object_name, count)
95
+ contents << error_body_tag(options)
96
+ contents << error_list_tag(objects, object_name)
97
+ end
98
+
99
+ def error_list_tag(objects, object_name)
100
+ errors = objects.inject({}){ |all,object| all.update(object.errors) }
101
+ error_messages = errors.inject(ActiveSupport::SafeBuffer.new) do |all, (field, message)|
102
+ field_name = I18n.t(field, :default => field.to_s.humanize, :scope => [:models, object_name, :attributes])
103
+ all << content_tag(:li, "#{field_name} #{message}")
104
+ end
105
+ content_tag(:ul, error_messages)
106
+ end
107
+
108
+ def error_header_tag(options, object_name, count)
109
+ header_message = options[:header_message] || begin
110
+ model_name = I18n.t(:name, :default => object_name.humanize, :scope => [:models, object_name], :count => 1)
111
+ I18n.t :header, :count => count, :model => model_name, :locale => options[:locale], :scope => [:models, :errors, :template]
112
+ end
113
+ content_tag(options[:header_tag] || :h2, header_message) if header_message.present?
114
+ end
115
+
116
+ def error_body_tag(options)
117
+ body_message = options[:message] || I18n.t(:body, :locale => options[:locale], :scope => [:models, :errors, :template])
118
+ content_tag(:p, body_message) if body_message.present?
119
+ end
120
+
121
+ def error_html_attributes(options)
122
+ [:id, :class, :style].each_with_object({}) do |key,all|
123
+ if options.include?(key)
124
+ value = options[key]
125
+ all[key] = value unless value.blank?
126
+ else
127
+ all[key] = 'field-errors' unless key == :style
128
+ end
129
+ end
130
+ end
131
+
132
+ def resolve_object(object)
133
+ object.is_a?(Symbol) ? instance_variable_get("@#{object}") : object
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,97 @@
1
+ module Padrino
2
+ module Helpers
3
+ module FormHelpers
4
+ ##
5
+ # Helpers to generate options list for select tag.
6
+ #
7
+ module Options
8
+ def extract_option_tags!(options)
9
+ state = extract_option_state!(options)
10
+ option_tags = if options[:grouped_options]
11
+ grouped_options_for_select(options.delete(:grouped_options), state)
12
+ else
13
+ options_for_select(extract_option_items!(options), state)
14
+ end
15
+ if prompt = options[:include_blank]
16
+ option_tags.unshift(blank_option(prompt))
17
+ end
18
+ option_tags
19
+ end
20
+
21
+ private
22
+
23
+ ##
24
+ # Returns the blank option serving as a prompt if passed.
25
+ #
26
+ def blank_option(prompt)
27
+ case prompt
28
+ when nil, false
29
+ nil
30
+ when String
31
+ content_tag(:option, prompt, :value => '')
32
+ when Array
33
+ content_tag(:option, prompt.first, :value => prompt.last)
34
+ else
35
+ content_tag(:option, '', :value => '')
36
+ end
37
+ end
38
+
39
+ ##
40
+ # Returns whether the option should be selected or not.
41
+ #
42
+ # @example
43
+ # option_is_selected?("red", "Red", ["red", "blue"]) => true
44
+ # option_is_selected?("red", "Red", ["green", "blue"]) => false
45
+ #
46
+ def option_is_selected?(value, caption, selected_values)
47
+ Array(selected_values).any? do |selected|
48
+ [value.to_s, caption.to_s].include?(selected.to_s)
49
+ end
50
+ end
51
+
52
+ ##
53
+ # Returns the options tags for a select based on the given option items.
54
+ #
55
+ def options_for_select(option_items, state = {})
56
+ return [] if option_items.blank?
57
+ option_items.map do |caption, value, attributes|
58
+ html_attributes = { :value => value || caption }.merge(attributes||{})
59
+ html_attributes[:selected] ||= option_is_selected?(value, caption, state[:selected])
60
+ html_attributes[:disabled] ||= option_is_selected?(value, caption, state[:disabled])
61
+ content_tag(:option, caption, html_attributes)
62
+ end
63
+ end
64
+
65
+ ##
66
+ # Returns the optgroups with options tags for a select based on the given :grouped_options items.
67
+ #
68
+ def grouped_options_for_select(collection, state = {})
69
+ collection.map do |item|
70
+ caption = item.shift
71
+ attributes = item.last.kind_of?(Hash) ? item.pop : {}
72
+ value = item.flatten(1)
73
+ attributes = value.pop if value.last.kind_of?(Hash)
74
+ html_attributes = { :label => caption }.merge(attributes||{})
75
+ content_tag(:optgroup, options_for_select(value, state), html_attributes)
76
+ end
77
+ end
78
+
79
+ def extract_option_state!(options)
80
+ {
81
+ :selected => Array(options.delete(:value))|Array(options.delete(:selected))|Array(options.delete(:selected_options)),
82
+ :disabled => Array(options.delete(:disabled_options))
83
+ }
84
+ end
85
+
86
+ def extract_option_items!(options)
87
+ if options[:collection]
88
+ collection, fields = options.delete(:collection), options.delete(:fields)
89
+ collection.map{ |item| [ item.send(fields.first), item.send(fields.last) ] }
90
+ else
91
+ options.delete(:options) || []
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,70 @@
1
+ require 'securerandom'
2
+
3
+ module Padrino
4
+ module Helpers
5
+ module FormHelpers
6
+ ##
7
+ # Helpers to generate form security tags for csrf protection.
8
+ #
9
+ module Security
10
+ ##
11
+ # Constructs a hidden field containing a CSRF token.
12
+ #
13
+ # @param [String] token
14
+ # The token to use. Will be read from the session by default.
15
+ #
16
+ # @return [String] The hidden field with CSRF token as value.
17
+ #
18
+ # @example
19
+ # csrf_token_field
20
+ #
21
+ def csrf_token_field
22
+ hidden_field_tag csrf_param, :value => csrf_token
23
+ end
24
+
25
+ ##
26
+ # Constructs meta tags `csrf-param` and `csrf-token` with the name of the
27
+ # cross-site request forgery protection parameter and token, respectively.
28
+ #
29
+ # @return [String] The meta tags with the CSRF token and the param your app expects it in.
30
+ #
31
+ # @example
32
+ # csrf_meta_tags
33
+ #
34
+ def csrf_meta_tags
35
+ if is_protected_from_csrf?
36
+ meta_tag(csrf_param, :name => 'csrf-param') <<
37
+ meta_tag(csrf_token, :name => 'csrf-token')
38
+ end
39
+ end
40
+
41
+ protected
42
+
43
+ ##
44
+ # Returns whether the application is being protected from CSRF. Defaults to true.
45
+ #
46
+ def is_protected_from_csrf?
47
+ defined?(settings) ? settings.protect_from_csrf : true
48
+ end
49
+
50
+ ##
51
+ # Returns the current CSRF token (based on the session). If it doesn't exist,
52
+ # it will create one and assign it to the session's `csrf` key.
53
+ #
54
+ def csrf_token
55
+ session[:csrf] ||= SecureRandom.hex(32) if defined?(session)
56
+ end
57
+
58
+ ##
59
+ # Returns the param/field name in which your CSRF token should be expected by your
60
+ # controllers. Defaults to `authenticity_token`.
61
+ #
62
+ # Set this in your application with `set :csrf_param, :something_else`.
63
+ #
64
+ def csrf_param
65
+ defined?(settings) && settings.respond_to?(:csrf_param) ? settings.csrf_param : :authenticity_token
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -175,7 +175,7 @@ module Padrino
175
175
  def yield_content(key, *args)
176
176
  blocks = content_blocks[key.to_sym]
177
177
  return nil if blocks.empty?
178
- mark_safe(blocks.map { |content| capture_html(*args, &content) }.join)
178
+ blocks.inject(ActiveSupport::SafeBuffer.new){ |all,content| all << capture_html(*args, &content) }
179
179
  end
180
180
 
181
181
  protected
@@ -33,7 +33,7 @@ module Padrino
33
33
  raw = block.call(*args)
34
34
  captured = template.instance_variable_get(:@_out_buf)
35
35
  self.output_buffer = _buf_was
36
- engine_matches?(block) ? captured : raw
36
+ engine_matches?(block) && !captured.empty? ? captured : raw.to_s
37
37
  end
38
38
 
39
39
  ##
@@ -31,12 +31,12 @@ module Padrino
31
31
  # @note If using this from Sinatra, pass explicit +:engine+ option
32
32
  #
33
33
  def partial(template, options={}, &block)
34
- options = options.reverse_merge(:locals => {}, :layout => false)
34
+ options = { :locals => {}, :layout => false }.update(options)
35
35
  explicit_engine = options.delete(:engine)
36
36
 
37
37
  path,_,name = template.to_s.rpartition(File::SEPARATOR)
38
38
  template_path = File.join(path,"_#{name}").to_sym
39
- object_name = name.to_sym
39
+ object_name = name.partition('.').first.to_sym
40
40
 
41
41
  objects, counter = if options[:collection].respond_to?(:inject)
42
42
  [options.delete(:collection), 0]
@@ -45,15 +45,16 @@ module Padrino
45
45
  end
46
46
 
47
47
  locals = options[:locals]
48
- objects.inject(''.html_safe) do |html,object|
48
+ objects.each_with_object(ActiveSupport::SafeBuffer.new) do |object,html|
49
49
  locals[object_name] = object if object
50
50
  locals["#{object_name}_counter".to_sym] = counter += 1 if counter
51
- if block_given?
52
- output = render(explicit_engine, template_path, options){ capture_html(&block) }.html_safe
53
- html << (block_is_template?(block) ? concat_content(output) : output)
54
- else
55
- html << render(explicit_engine, template_path, options).html_safe
56
- end
51
+ content =
52
+ if block_given?
53
+ concat_content render(explicit_engine, template_path, options){ capture_html(&block) }
54
+ else
55
+ render(explicit_engine, template_path, options)
56
+ end
57
+ html.safe_concat content if content
57
58
  end
58
59
  end
59
60
  alias :render_partial :partial
@@ -127,7 +127,7 @@ module Padrino
127
127
  output = ActiveSupport::SafeBuffer.new
128
128
  output.safe_concat "<#{name}#{attributes}>"
129
129
  if content.respond_to?(:each) && !content.is_a?(String)
130
- content.each { |c| output.concat c; output.safe_concat NEWLINE }
130
+ content.each{ |item| output.concat item; output.safe_concat NEWLINE }
131
131
  else
132
132
  output.concat content.to_s
133
133
  end
@@ -236,6 +236,7 @@ module Padrino
236
236
  end
237
237
 
238
238
  private
239
+
239
240
  ##
240
241
  # Returns a compiled list of HTML attributes.
241
242
  #
@@ -0,0 +1,378 @@
1
+ require 'padrino-support'
2
+ require 'pathname'
3
+
4
+ module Padrino
5
+ ##
6
+ # Padrino enhances the Sinatra 'render' method to have support for
7
+ # automatic template engine detection, enhanced layout functionality,
8
+ # locale enabled rendering, among other features.
9
+ #
10
+ module Rendering
11
+ ##
12
+ # A SafeTemplate assumes that its output is safe.
13
+ #
14
+ module SafeTemplate
15
+ def render(*)
16
+ super.html_safe
17
+ end
18
+ end
19
+
20
+ ##
21
+ # Exception responsible for when an expected template did not exist.
22
+ #
23
+ class TemplateNotFound < RuntimeError
24
+ end
25
+
26
+ ##
27
+ # This is an array of file patterns to ignore. If your editor add a
28
+ # suffix during editing to your files please add it like:
29
+ #
30
+ # @example
31
+ # Padrino::Rendering::IGNORE_FILE_PATTERN << /~$/
32
+ #
33
+ IGNORE_FILE_PATTERN = [
34
+ /~$/ # This is for Gedit
35
+ ] unless defined?(IGNORE_FILE_PATTERN)
36
+
37
+ ##
38
+ # Default options used in the resolve_template-method.
39
+ #
40
+ DEFAULT_RENDERING_OPTIONS = { :strict_format => false, :raise_exceptions => true } unless defined?(DEFAULT_RENDERING_OPTIONS)
41
+
42
+ class << self
43
+ ##
44
+ # Default engine configurations for Padrino::Rendering.
45
+ #
46
+ # @return {Hash<Symbol,Hash>}
47
+ # The configurations, keyed by engine.
48
+ def engine_configurations
49
+ @engine_configurations ||= {}
50
+ end
51
+
52
+ def registered(app)
53
+ if defined?(Padrino::Application) && app == Padrino::Application
54
+ # this fail can be removed later when jRuby is not bugged and MRI19 is dropped
55
+ fail 'Please, do not use `register` on Padrino::Application object, use `.dup` or subclassing'
56
+ end
57
+ included(app)
58
+ engine_configurations.each do |engine, configs|
59
+ app.set engine, configs
60
+ end
61
+ end
62
+
63
+ def included(base)
64
+ base.send(:include, InstanceMethods)
65
+ base.extend(ClassMethods)
66
+ end
67
+ end
68
+
69
+ ##
70
+ # Class methods responsible for rendering templates as part of a request.
71
+ #
72
+ module ClassMethods
73
+ ##
74
+ # Use layout like rails does or if a block given then like sinatra.
75
+ # If used without a block, sets the current layout for the route.
76
+ #
77
+ # By default, searches in your:
78
+ #
79
+ # +app+/+views+/+layouts+/+application+.(+haml+|+erb+|+xxx+)
80
+ # +app+/+views+/+layout_name+.(+haml+|+erb+|+xxx+)
81
+ #
82
+ # If you define +layout+ :+custom+ then searches for your layouts in
83
+ # +app+/+views+/+layouts+/+custom+.(+haml+|+erb+|+xxx+)
84
+ # +app+/+views+/+custom+.(+haml+|+erb+|+xxx+)
85
+ #
86
+ # @param [Symbol] name (:layout)
87
+ # The layout to use.
88
+ #
89
+ # @yield []
90
+ #
91
+ def layout(name=:layout, &block)
92
+ return super(name, &block) if block_given?
93
+ @layout = name
94
+ end
95
+
96
+ ##
97
+ # Returns the cached template file to render for a given url,
98
+ # content_type and locale. Deprecated since 0.12.1
99
+ #
100
+ # @param [Array<template_path, content_type, locale>] render_options
101
+ #
102
+ def fetch_template_file(render_options)
103
+ logger.warn "##{__method__} is deprecated"
104
+ (@_cached_templates ||= {})[render_options]
105
+ end
106
+
107
+ ##
108
+ # Caches the template file for the given rendering options. Deprecated since 0.12.1
109
+ #
110
+ # @param [String] template_file
111
+ # The path of the template file.
112
+ #
113
+ # @param [Array<template_path, content_type, locale>] render_options
114
+ #
115
+ def cache_template_file!(template_file, render_options)
116
+ logger.warn "##{__method__} is deprecated"
117
+ (@_cached_templates ||= {})[render_options] = template_file || []
118
+ end
119
+
120
+ ##
121
+ # Returns the cached layout path.
122
+ #
123
+ # @param [String, nil] given_layout
124
+ # The requested layout.
125
+ # @param [String, nil] layouts_path
126
+ # The directory where the layouts are located. Defaults to #views.
127
+ #
128
+ def fetch_layout_path(given_layout, layouts_path=views)
129
+ layout_name = (given_layout || @layout || :application).to_s
130
+ cache_layout_path(layout_name) do
131
+ if Pathname.new(layout_name).absolute? && Dir["#{layout_name}.*"].any? || Dir["#{layouts_path}/#{layout_name}.*"].any?
132
+ layout_name
133
+ else
134
+ File.join('layouts', layout_name)
135
+ end
136
+ end
137
+ end
138
+
139
+ def cache_layout_path(name)
140
+ @_cached_layout ||= {}
141
+ if !reload_templates? && path = @_cached_layout[name]
142
+ path
143
+ else
144
+ @_cached_layout[name] = yield(name)
145
+ end
146
+ end
147
+
148
+ def cache_template_path(options)
149
+ began_at = Time.now
150
+ @_cached_templates ||= {}
151
+ logging = defined?(settings) && settings.logging? && defined?(logger)
152
+ if !reload_templates? && path = @_cached_templates[options]
153
+ logger.debug :cached, began_at, path[0] if logging
154
+ else
155
+ path = @_cached_templates[options] = yield(name)
156
+ logger.debug :template, began_at, path[0] if path && logging
157
+ end
158
+ path
159
+ end
160
+ end
161
+
162
+ # Instance methods that allow enhanced rendering to function properly in Padrino.
163
+ module InstanceMethods
164
+ attr_reader :current_engine
165
+
166
+ ##
167
+ # Get/Set the content_type
168
+ #
169
+ # @param [String, nil] type
170
+ # The Content-Type to use.
171
+ #
172
+ # @param [Symbol, nil] type.
173
+ # Look and parse the given symbol to the matched Content-Type.
174
+ #
175
+ # @param [Hash] params
176
+ # Additional params to append to the Content-Type.
177
+ #
178
+ # @example
179
+ # case content_type
180
+ # when :js then do_some
181
+ # when :css then do_another
182
+ # end
183
+ #
184
+ # content_type :js
185
+ # # => set the response with 'application/javascript' Content-Type
186
+ # content_type 'text/html'
187
+ #
188
+ # # => set directly the Content-Type to 'text/html'
189
+ #
190
+ def content_type(type=nil, params={})
191
+ if type
192
+ super(type, params)
193
+ @_content_type = type
194
+ end
195
+ @_content_type
196
+ end
197
+
198
+ private
199
+
200
+ ##
201
+ # Enhancing Sinatra render functionality for:
202
+ #
203
+ # * Using layout similar to rails
204
+ # * Use render 'path/to/my/template' (without symbols)
205
+ # * Use render 'path/to/my/template' (with engine lookup)
206
+ # * Use render 'path/to/template.haml' (with explicit engine lookup)
207
+ # * Use render 'path/to/template', :layout => false
208
+ # * Use render 'path/to/template', :layout => false, :engine => 'haml'
209
+ #
210
+ def render(engine, data=nil, options={}, locals={}, &block)
211
+ # If engine is nil, ignore engine parameter and shift up all arguments
212
+ # render nil, "index", { :layout => true }, { :localvar => "foo" }
213
+ engine, data, options = data, options, locals if engine.nil? && data
214
+
215
+ # Data is a hash of options when no engine isn't explicit
216
+ # render "index", { :layout => true }, { :localvar => "foo" }
217
+ # Data is options, and options is locals in this case
218
+ data, options, locals = nil, data, options if data.is_a?(Hash)
219
+
220
+ # If data is unassigned then this is a likely a template to be resolved
221
+ # This means that no engine was explicitly defined
222
+ data, engine = resolve_template(engine, options) if data.nil?
223
+
224
+ # Cleanup the template.
225
+ @current_engine, engine_was = engine, @current_engine
226
+ @_out_buf, buf_was = ActiveSupport::SafeBuffer.new, @_out_buf
227
+
228
+ # Pass arguments to Sinatra render method.
229
+ super(engine, data, with_layout(options), locals, &block)
230
+ ensure
231
+ @current_engine = engine_was
232
+ @_out_buf = buf_was
233
+ end
234
+
235
+ ##
236
+ # Returns the located layout tuple to be used for the rendered template
237
+ # (if available). Deprecated since 0.12.1
238
+ #
239
+ # @example
240
+ # resolve_layout
241
+ # # => ["/layouts/custom", :erb]
242
+ # # => [nil, nil]
243
+ #
244
+ def resolved_layout
245
+ logger.warn "##{__method__} is deprecated"
246
+ resolve_template(settings.fetch_layout_path, :raise_exceptions => false, :strict_format => true) || [nil, nil]
247
+ end
248
+
249
+ ##
250
+ # Returns the template path and engine that match content_type (if present),
251
+ # I18n.locale.
252
+ #
253
+ # @param [String] template_path
254
+ # The path of the template.
255
+ #
256
+ # @param [Hash] options
257
+ # Additional options.
258
+ #
259
+ # @option options [Boolean] :strict_format (false)
260
+ # The resolved template must match the content_type of the request.
261
+ #
262
+ # @option options [Boolean] :raise_exceptions (false)
263
+ # Raises a {TemplateNotFound} exception if the template cannot be located.
264
+ #
265
+ # @return [Array<Symbol, Symbol>]
266
+ # The path and format of the template.
267
+ #
268
+ # @raise [TemplateNotFound]
269
+ # The template could not be found.
270
+ #
271
+ # @example
272
+ # get "/foo", :provides => [:html, :js] do; render 'path/to/foo'; end
273
+ # # If you request "/foo.js" with I18n.locale == :ru => [:"/path/to/foo.ru.js", :erb]
274
+ # # If you request "/foo" with I18n.locale == :de => [:"/path/to/foo.de.haml", :haml]
275
+ #
276
+ def resolve_template(template_path, options={})
277
+ template_path = template_path.to_s
278
+ controller_key = respond_to?(:request) && request.respond_to?(:controller) && request.controller
279
+ rendering_options = [template_path, content_type || :html, locale]
280
+
281
+ settings.cache_template_path(["#{controller_key}/#{template_path}", rendering_options[1], rendering_options[2]]) do
282
+ options = DEFAULT_RENDERING_OPTIONS.merge(options)
283
+ view_path = options[:views] || settings.views || "./views"
284
+
285
+ template_candidates = glob_templates(view_path, template_path)
286
+ selected_template = select_template(template_candidates, *rendering_options)
287
+ selected_template ||= template_candidates.first unless options[:strict_format]
288
+
289
+ fail TemplateNotFound, "Template '#{template_path}' not found in '#{view_path}'" if !selected_template && options[:raise_exceptions]
290
+ selected_template
291
+ end
292
+ end
293
+
294
+ ##
295
+ # Return the I18n.locale if I18n is defined.
296
+ #
297
+ def locale
298
+ I18n.locale if defined?(I18n)
299
+ end
300
+
301
+ LAYOUT_EXTENSIONS = %w[.slim .erb .haml].freeze
302
+
303
+ def resolve_layout(layout, options={})
304
+ layouts_path = options[:layout_options] && options[:layout_options][:views] || options[:views] || settings.views || "./views"
305
+ template_path = settings.fetch_layout_path(layout, layouts_path)
306
+ rendering_options = [template_path, content_type || :html, locale]
307
+
308
+ layout, engine =
309
+ settings.cache_template_path(rendering_options) do
310
+ template_candidates = glob_templates(layouts_path, template_path)
311
+ selected_template = select_template(template_candidates, *rendering_options)
312
+
313
+ fail TemplateNotFound, "Layout '#{template_path}' not found in '#{layouts_path}'" if !selected_template && layout.present?
314
+ selected_template
315
+ end
316
+
317
+ is_included_extension = LAYOUT_EXTENSIONS.include?(File.extname(template_path.to_s))
318
+ layout = false unless is_included_extension ? engine : engine == @current_engine
319
+
320
+ [layout, engine]
321
+ end
322
+
323
+ def with_layout(options)
324
+ options = options.dup
325
+ layout = options[:layout]
326
+ return options if layout == false
327
+
328
+ layout = @layout if !layout || layout == true
329
+ return options if settings.templates.has_key?(:layout) && layout.blank?
330
+
331
+ if layout.kind_of?(String) && Pathname.new(layout).absolute?
332
+ layout_path, _, layout = layout.rpartition('/')
333
+ options[:layout_options] ||= {}
334
+ options[:layout_options][:views] ||= layout_path
335
+ end
336
+ layout, layout_engine = resolve_layout(layout, options)
337
+ options.update(:layout => layout, :layout_engine => layout_engine)
338
+ end
339
+
340
+ def glob_templates(views_path, template_path)
341
+ parts = []
342
+ parts << views_path if views_path.present?
343
+ if respond_to?(:request) && request.respond_to?(:controller) && request.controller.present? && Pathname.new(template_path).relative?
344
+ parts << "{,#{request.controller}}"
345
+ end
346
+ parts << template_path.chomp(File.extname(template_path)) + '.*'
347
+ Dir.glob(File.join(parts)).sort.inject([]) do |all,file|
348
+ next all if IGNORE_FILE_PATTERN.any?{ |pattern| file.to_s =~ pattern }
349
+ all << path_and_engine(file, views_path)
350
+ end
351
+ end
352
+
353
+ def select_template(templates, template_path, content_type, _locale)
354
+ simple_content_type = [:html, :plain].include?(content_type)
355
+ target_path, target_engine = path_and_engine(template_path)
356
+
357
+ templates.find{ |file,_| file.to_s == "#{target_path}.#{locale}.#{content_type}" } ||
358
+ templates.find{ |file,_| file.to_s == "#{target_path}.#{locale}" && simple_content_type } ||
359
+ templates.find{ |file,engine| engine == target_engine || File.extname(file.to_s) == ".#{target_engine}" } ||
360
+ templates.find{ |file,_| file.to_s == "#{target_path}.#{content_type}" } ||
361
+ templates.find{ |file,_| file.to_s == "#{target_path}" && simple_content_type }
362
+ end
363
+
364
+ def path_and_engine(path, relative=nil)
365
+ extname = File.extname(path)
366
+ engine = (extname[1..-1]||'none').to_sym
367
+ path = path.chomp(extname)
368
+ path.insert(0, '/') unless Pathname.new(path).absolute?
369
+ path = path.squeeze('/').sub(relative, '') if relative
370
+ [path.to_sym, engine]
371
+ end
372
+ end
373
+ end
374
+ end
375
+
376
+ require 'padrino/rendering/extensions/haml'
377
+ require 'padrino/rendering/extensions/erubis'
378
+ require 'padrino/rendering/extensions/slim'