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.
- checksums.yaml +4 -4
- data/lib/padrino-helpers.rb +4 -1
- data/lib/padrino-helpers/asset_tag_helpers.rb +17 -14
- data/lib/padrino-helpers/breadcrumb_helpers.rb +6 -6
- data/lib/padrino-helpers/form_builder/abstract_form_builder.rb +119 -163
- data/lib/padrino-helpers/form_builder/deprecated_builder_methods.rb +92 -0
- data/lib/padrino-helpers/form_helpers.rb +66 -347
- data/lib/padrino-helpers/form_helpers/errors.rb +138 -0
- data/lib/padrino-helpers/form_helpers/options.rb +97 -0
- data/lib/padrino-helpers/form_helpers/security.rb +70 -0
- data/lib/padrino-helpers/output_helpers.rb +1 -1
- data/lib/padrino-helpers/output_helpers/abstract_handler.rb +1 -1
- data/lib/padrino-helpers/render_helpers.rb +10 -9
- data/lib/padrino-helpers/tag_helpers.rb +2 -1
- data/lib/padrino/rendering.rb +378 -0
- data/lib/padrino/rendering/extensions/erubis.rb +74 -0
- data/lib/padrino/rendering/extensions/haml.rb +29 -0
- data/lib/padrino/rendering/extensions/slim.rb +21 -0
- data/padrino-helpers.gemspec +2 -1
- data/test/fixtures/apps/.components +6 -0
- data/test/fixtures/apps/.gitignore +7 -0
- data/test/fixtures/apps/render.rb +25 -0
- data/test/fixtures/apps/views/article/comment/show.slim +1 -0
- data/test/fixtures/apps/views/blog/post.erb +1 -0
- data/test/fixtures/apps/views/layouts/specific.erb +1 -0
- data/test/fixtures/apps/views/test/post.erb +1 -0
- data/test/fixtures/layouts/layout.erb +1 -0
- data/test/fixtures/markup_app/app.rb +0 -1
- data/test/fixtures/render_app/app.rb +25 -1
- data/test/fixtures/render_app/views/_unsafe.html.builder +2 -0
- data/test/fixtures/render_app/views/_unsafe_object.html.builder +2 -0
- data/test/fixtures/render_app/views/ruby_block_capture_erb.erb +1 -0
- data/test/fixtures/render_app/views/ruby_block_capture_haml.haml +1 -0
- data/test/fixtures/render_app/views/ruby_block_capture_slim.slim +1 -0
- data/test/helper.rb +65 -1
- data/test/test_asset_tag_helpers.rb +83 -79
- data/test/test_breadcrumb_helpers.rb +20 -20
- data/test/test_form_builder.rb +196 -196
- data/test/test_form_helpers.rb +163 -163
- data/test/test_format_helpers.rb +65 -65
- data/test/test_locale.rb +1 -1
- data/test/test_number_helpers.rb +10 -11
- data/test/test_output_helpers.rb +28 -28
- data/test/test_render_helpers.rb +89 -35
- data/test/test_rendering.rb +683 -0
- data/test/test_rendering_extensions.rb +14 -0
- data/test/test_tag_helpers.rb +23 -23
- 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
|
-
|
178
|
+
blocks.inject(ActiveSupport::SafeBuffer.new){ |all,content| all << capture_html(*args, &content) }
|
179
179
|
end
|
180
180
|
|
181
181
|
protected
|
@@ -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 =
|
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.
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
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'
|