/
abstract_renderer.rb
186 lines (150 loc) · 5.7 KB
/
abstract_renderer.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# frozen_string_literal: true
require "concurrent/map"
module ActionView
# This class defines the interface for a renderer. Each class that
# subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
# render a specific type of object.
#
# The base +Renderer+ class uses its +render+ method to delegate to the
# renderers. These currently consist of
#
# PartialRenderer - Used for rendering partials
# TemplateRenderer - Used for rendering other types of templates
# StreamingTemplateRenderer - Used for streaming
#
# Whenever the +render+ method is called on the base +Renderer+ class, a new
# renderer object of the correct type is created, and the +render+ method on
# that new object is called in turn. This abstracts the set up and rendering
# into a separate classes for partials and templates.
class AbstractRenderer # :nodoc:
delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
def initialize(lookup_context)
@lookup_context = lookup_context
end
def render
raise NotImplementedError
end
module ObjectRendering # :nodoc:
PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
h.compute_if_absent(k) { Concurrent::Map.new }
end
def initialize(lookup_context, options)
super
@context_prefix = lookup_context.prefixes.first
end
private
def local_variable(path)
if as = @options[:as]
raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
as.to_sym
else
base = path.end_with?("/") ? "" : File.basename(path)
raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
$1.to_sym
end
end
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
"make sure your partial name starts with underscore."
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
"make sure it starts with lowercase letter, " \
"and is followed by any combination of letters, numbers and underscores."
def raise_invalid_identifier(path)
raise ArgumentError, IDENTIFIER_ERROR_MESSAGE % path
end
def raise_invalid_option_as(as)
raise ArgumentError, OPTION_AS_ERROR_MESSAGE % as
end
# Obtains the path to where the object's partial is located. If the object
# responds to +to_partial_path+, then +to_partial_path+ will be called and
# will provide the path. If the object does not respond to +to_partial_path+,
# then an +ArgumentError+ is raised.
#
# If +prefix_partial_path_with_controller_namespace+ is true, then this
# method will prefix the partial paths with a namespace.
def partial_path(object, view)
object = object.to_model if object.respond_to?(:to_model)
path = if object.respond_to?(:to_partial_path)
object.to_partial_path
else
raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement #to_partial_path.")
end
if view.prefix_partial_path_with_controller_namespace
PREFIXED_PARTIAL_NAMES[@context_prefix][path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
else
path
end
end
def merge_prefix_into_object_path(prefix, object_path)
if prefix.include?(?/) && object_path.include?(?/)
prefixes = []
prefix_array = File.dirname(prefix).split("/")
object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
prefix_array.each_with_index do |dir, index|
break if dir == object_path_array[index]
prefixes << dir
end
(prefixes << object_path).join("/")
else
object_path
end
end
end
class RenderedCollection # :nodoc:
def self.empty(format)
EmptyCollection.new format
end
attr_reader :rendered_templates
def initialize(rendered_templates, spacer)
@rendered_templates = rendered_templates
@spacer = spacer
end
def body
@rendered_templates.map(&:body).join(@spacer.body).html_safe
end
def format
rendered_templates.first.format
end
class EmptyCollection
attr_reader :format
def initialize(format)
@format = format
end
def body; nil; end
end
end
class RenderedTemplate # :nodoc:
attr_reader :body, :template
def initialize(body, template)
@body = body
@template = template
end
def format
template.format
end
EMPTY_SPACER = Struct.new(:body).new
end
private
NO_DETAILS = {}.freeze
def extract_details(options) # :doc:
details = nil
LookupContext.registered_details.each do |key|
value = options[key]
if value
(details ||= {})[key] = Array(value)
end
end
details || NO_DETAILS
end
def prepend_formats(formats) # :doc:
formats = Array(formats)
return if formats.empty? || @lookup_context.html_fallback_for_js
@lookup_context.formats = formats | @lookup_context.formats
end
def build_rendered_template(content, template)
RenderedTemplate.new content, template
end
def build_rendered_collection(templates, spacer)
RenderedCollection.new templates, spacer
end
end
end