phlex 1.11.1 → 2.0.0.beta1
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/README.md +21 -3
- data/lib/phlex/context.rb +0 -5
- data/lib/phlex/csv.rb +6 -12
- data/lib/phlex/element_clobbering_guard.rb +6 -2
- data/lib/phlex/elements.rb +52 -32
- data/lib/phlex/error.rb +4 -0
- data/lib/phlex/errors/argument_error.rb +5 -0
- data/lib/phlex/errors/double_render_error.rb +5 -0
- data/lib/phlex/errors/name_error.rb +5 -0
- data/lib/phlex/fifo.rb +47 -0
- data/lib/phlex/helpers.rb +27 -92
- data/lib/phlex/html/standard_elements.rb +1 -1
- data/lib/phlex/html/void_elements.rb +0 -6
- data/lib/phlex/html.rb +30 -49
- data/lib/phlex/kit.rb +4 -8
- data/lib/phlex/sgml/safe_object.rb +7 -0
- data/lib/phlex/sgml/safe_value.rb +11 -0
- data/lib/phlex/sgml.rb +520 -403
- data/lib/phlex/svg.rb +10 -12
- data/lib/phlex/testing/capybara.rb +28 -0
- data/lib/phlex/testing/nokogiri.rb +19 -0
- data/lib/phlex/testing/nokolexbor.rb +19 -0
- data/lib/phlex/testing/sgml.rb +9 -0
- data/lib/phlex/testing.rb +10 -0
- data/lib/phlex/version.rb +1 -1
- data/lib/phlex.rb +19 -35
- metadata +19 -7
- data/lib/phlex/callable.rb +0 -8
- data/lib/phlex/testing/view_helper.rb +0 -17
- data/lib/phlex/unbuffered.rb +0 -50
data/lib/phlex/sgml.rb
CHANGED
|
@@ -1,524 +1,641 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
REF_ATTRIBUTES = Set.new(%w[href src action formaction lowsrc dynsrc background ping xlinkhref]).freeze
|
|
8
|
-
NAMED_CHARACTER_REFERENCES = {
|
|
9
|
-
"colon" => ":",
|
|
10
|
-
"tab" => "\t",
|
|
11
|
-
"newline" => "\n",
|
|
12
|
-
}.freeze
|
|
13
|
-
UNSAFE_ATTRIBUTE_NAME_CHARS = %r([<>&"'/=\s\x00])
|
|
14
|
-
|
|
15
|
-
class << self
|
|
16
|
-
# Render the view to a String. Arguments are delegated to {.new}.
|
|
17
|
-
def call(...)
|
|
18
|
-
new(...).call
|
|
19
|
-
end
|
|
3
|
+
# **Standard Generalized Markup Language** for behaviour common to {HTML} and {SVG}.
|
|
4
|
+
class Phlex::SGML
|
|
5
|
+
autoload :SafeObject, "phlex/sgml/safe_object"
|
|
6
|
+
autoload :SafeValue, "phlex/sgml/safe_value"
|
|
20
7
|
|
|
21
|
-
|
|
22
|
-
# @note The block will not be delegated {#initialize}. Instead, it will be sent to {#template} when rendering.
|
|
23
|
-
def new(*args, **kwargs, &block)
|
|
24
|
-
if block
|
|
25
|
-
object = super(*args, **kwargs, &nil)
|
|
26
|
-
object.instance_variable_set(:@_content_block, block)
|
|
27
|
-
object
|
|
28
|
-
else
|
|
29
|
-
super
|
|
30
|
-
end
|
|
31
|
-
end
|
|
8
|
+
include Phlex::Helpers
|
|
32
9
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
10
|
+
class << self
|
|
11
|
+
# Render the view to a String. Arguments are delegated to {.new}.
|
|
12
|
+
def call(...)
|
|
13
|
+
new(...).call
|
|
14
|
+
end
|
|
38
15
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
16
|
+
# Create a new instance of the component.
|
|
17
|
+
# @note The block will not be delegated {#initialize}. Instead, it will be sent to {#template} when rendering.
|
|
18
|
+
def new(*, **, &block)
|
|
19
|
+
if block
|
|
20
|
+
object = super(*, **, &nil)
|
|
21
|
+
object.instance_exec { @_content_block = block }
|
|
22
|
+
object
|
|
23
|
+
else
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
42
27
|
|
|
28
|
+
# @api private
|
|
29
|
+
def __element_method__?(method_name)
|
|
30
|
+
if instance_methods.include?(method_name)
|
|
43
31
|
owner = instance_method(method_name).owner
|
|
44
32
|
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
if Phlex::Elements === owner && owner.registered_elements[method_name]
|
|
34
|
+
true
|
|
35
|
+
else
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
else
|
|
47
39
|
false
|
|
48
40
|
end
|
|
49
41
|
end
|
|
42
|
+
end
|
|
50
43
|
|
|
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
|
-
|
|
44
|
+
# @!method initialize
|
|
45
|
+
# @abstract Override to define an initializer for your component.
|
|
46
|
+
# @note Your initializer will not receive a block passed to {.new}. Instead, this block will be sent to {#template} when rendering.
|
|
47
|
+
# @example
|
|
48
|
+
# def initialize(articles:)
|
|
49
|
+
# @articles = articles
|
|
50
|
+
# end
|
|
51
|
+
|
|
52
|
+
# @abstract Override to define a template for your component.
|
|
53
|
+
# @example
|
|
54
|
+
# def view_template
|
|
55
|
+
# h1 { "👋 Hello World!" }
|
|
56
|
+
# end
|
|
57
|
+
# @example Your template may yield a content block.
|
|
58
|
+
# def view_template
|
|
59
|
+
# main {
|
|
60
|
+
# h1 { "Hello World" }
|
|
61
|
+
# yield
|
|
62
|
+
# }
|
|
63
|
+
# end
|
|
64
|
+
# @example Alternatively, you can delegate the content block to an element.
|
|
65
|
+
# def view_template(&block)
|
|
66
|
+
# article(class: "card", &block)
|
|
67
|
+
# end
|
|
68
|
+
def view_template
|
|
69
|
+
if block_given?
|
|
76
70
|
yield
|
|
77
71
|
end
|
|
72
|
+
end
|
|
78
73
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
74
|
+
def await(task)
|
|
75
|
+
case task
|
|
76
|
+
when defined?(Concurrent::IVar) && Concurrent::IVar
|
|
77
|
+
flush if task.pending?
|
|
78
|
+
task.wait.value
|
|
79
|
+
when defined?(Async::Task) && Async::Task
|
|
80
|
+
flush if task.running?
|
|
81
|
+
task.wait
|
|
82
|
+
else
|
|
83
|
+
raise Phlex::ArgumentError.new("Expected an asynchronous task / promise.")
|
|
87
84
|
end
|
|
85
|
+
end
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
flush if task.pending?
|
|
93
|
-
task.wait.value
|
|
94
|
-
when defined?(Async::Task) && Async::Task
|
|
95
|
-
flush if task.running?
|
|
96
|
-
task.wait
|
|
97
|
-
else
|
|
98
|
-
raise ArgumentError, "Expected an asynchronous task / promise."
|
|
99
|
-
end
|
|
100
|
-
end
|
|
87
|
+
def to_proc
|
|
88
|
+
proc { |c| c.render(self) }
|
|
89
|
+
end
|
|
101
90
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
# Renders the view and returns the buffer. The default buffer is a mutable String.
|
|
92
|
+
def call(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, fragments: nil, &block)
|
|
93
|
+
@_buffer = buffer
|
|
94
|
+
@_context = context
|
|
95
|
+
@_view_context = view_context
|
|
96
|
+
@_parent = parent
|
|
108
97
|
|
|
109
|
-
# @
|
|
110
|
-
|
|
111
|
-
@_buffer = buffer
|
|
112
|
-
@_context = context
|
|
113
|
-
@_view_context = view_context
|
|
114
|
-
@_parent = parent
|
|
115
|
-
if @_rendered
|
|
116
|
-
warn "⚠️ [WARNING] You are rendering a component #{self.class.name} twice. This is not supported in Phlex 2.0."
|
|
117
|
-
end
|
|
118
|
-
@_rendered = true
|
|
98
|
+
raise Phlex::DoubleRenderError.new("You can't render a #{self.class.name} more than once.") if @_rendered
|
|
99
|
+
@_rendered = true
|
|
119
100
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
end
|
|
101
|
+
if fragments
|
|
102
|
+
@_context.target_fragments(fragments)
|
|
103
|
+
end
|
|
124
104
|
|
|
125
|
-
|
|
105
|
+
block ||= @_content_block
|
|
126
106
|
|
|
127
|
-
|
|
107
|
+
return "" unless render?
|
|
128
108
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
109
|
+
if !parent && Phlex::SUPPORTS_FIBER_STORAGE
|
|
110
|
+
original_fiber_storage = Fiber[:__phlex_component__]
|
|
111
|
+
Fiber[:__phlex_component__] = self
|
|
112
|
+
end
|
|
133
113
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
end
|
|
114
|
+
@_context.around_render do
|
|
115
|
+
around_template do
|
|
116
|
+
if block
|
|
117
|
+
if Phlex::DeferredRender === self
|
|
118
|
+
vanish(self, &block)
|
|
119
|
+
view_template
|
|
120
|
+
else
|
|
121
|
+
view_template do |*args|
|
|
122
|
+
if args.length > 0
|
|
123
|
+
yield_content_with_args(*args, &block)
|
|
124
|
+
else
|
|
125
|
+
yield_content(&block)
|
|
147
126
|
end
|
|
148
127
|
end
|
|
149
|
-
else
|
|
150
|
-
view_template
|
|
151
128
|
end
|
|
129
|
+
else
|
|
130
|
+
view_template
|
|
152
131
|
end
|
|
153
132
|
end
|
|
133
|
+
end
|
|
154
134
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
end
|
|
159
|
-
buffer << context.buffer
|
|
135
|
+
unless parent
|
|
136
|
+
if Phlex::SUPPORTS_FIBER_STORAGE
|
|
137
|
+
Fiber[:__phlex_component__] = original_fiber_storage
|
|
160
138
|
end
|
|
139
|
+
buffer << context.buffer
|
|
161
140
|
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Access the current render context data
|
|
144
|
+
# @return the supplied context object, by default a Hash
|
|
145
|
+
def context
|
|
146
|
+
@_context.user_context
|
|
147
|
+
end
|
|
162
148
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
149
|
+
# Output text content. The text will be HTML-escaped.
|
|
150
|
+
# @param content [String, Symbol, Integer, void] the content to be output on the buffer. Strings, Symbols, and Integers are handled by `plain` directly, but any object can be handled by overriding `format_object`
|
|
151
|
+
# @return [nil]
|
|
152
|
+
# @see #format_object
|
|
153
|
+
def plain(content)
|
|
154
|
+
unless __text__(content)
|
|
155
|
+
raise Phlex::ArgumentError.new("You've passed an object to plain that is not handled by format_object. See https://rubydoc.info/gems/phlex/Phlex/SGML#format_object-instance_method for more information")
|
|
167
156
|
end
|
|
168
157
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
# @return [nil]
|
|
172
|
-
# @see #format_object
|
|
173
|
-
def plain(content)
|
|
174
|
-
unless __text__(content)
|
|
175
|
-
raise ArgumentError, "You've passed an object to plain that is not handled by format_object. See https://rubydoc.info/gems/phlex/Phlex/SGML#format_object-instance_method for more information"
|
|
176
|
-
end
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
177
160
|
|
|
178
|
-
|
|
179
|
-
|
|
161
|
+
# Output a whitespace character. This is useful for getting inline elements to wrap. If you pass a block, a whitespace will be output before and after yielding the block.
|
|
162
|
+
# @return [nil]
|
|
163
|
+
# @yield If a block is given, it yields the block with no arguments.
|
|
164
|
+
def whitespace(&)
|
|
165
|
+
context = @_context
|
|
166
|
+
return if context.fragments && !context.in_target_fragment
|
|
180
167
|
|
|
181
|
-
|
|
182
|
-
# @return [nil]
|
|
183
|
-
# @yield If a block is given, it yields the block with no arguments.
|
|
184
|
-
def whitespace(&block)
|
|
185
|
-
context = @_context
|
|
186
|
-
return if context.fragments && !context.in_target_fragment
|
|
168
|
+
buffer = context.buffer
|
|
187
169
|
|
|
188
|
-
|
|
170
|
+
buffer << " "
|
|
189
171
|
|
|
172
|
+
if block_given?
|
|
173
|
+
yield_content(&)
|
|
190
174
|
buffer << " "
|
|
175
|
+
end
|
|
191
176
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
buffer << " "
|
|
195
|
-
end
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
196
179
|
|
|
197
|
-
|
|
198
|
-
|
|
180
|
+
# Output an HTML comment.
|
|
181
|
+
# @return [nil]
|
|
182
|
+
def comment(&)
|
|
183
|
+
context = @_context
|
|
184
|
+
return if context.fragments && !context.in_target_fragment
|
|
185
|
+
|
|
186
|
+
buffer = context.buffer
|
|
187
|
+
|
|
188
|
+
buffer << "<!-- "
|
|
189
|
+
yield_content(&)
|
|
190
|
+
buffer << " -->"
|
|
191
|
+
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
199
194
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
195
|
+
# This method is very dangerous and should usually be avoided. It will output the given String without any HTML safety. You should never use this method to output unsafe user input.
|
|
196
|
+
# @param content [String|nil]
|
|
197
|
+
# @return [nil]
|
|
198
|
+
def raw(content)
|
|
199
|
+
case content
|
|
200
|
+
when Phlex::SGML::SafeObject
|
|
203
201
|
context = @_context
|
|
204
202
|
return if context.fragments && !context.in_target_fragment
|
|
205
203
|
|
|
206
|
-
buffer
|
|
204
|
+
context.buffer << content.to_s
|
|
205
|
+
when nil, "" # do nothing
|
|
206
|
+
else
|
|
207
|
+
raise Phlex::ArgumentError.new("You passed an unsafe object to `raw`.")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
207
212
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
# Capture a block of output as a String.
|
|
214
|
+
# @note This only works if the block's receiver is the current component or the block returns a String.
|
|
215
|
+
# @return [String]
|
|
216
|
+
def capture(*args, &block)
|
|
217
|
+
return "" unless block
|
|
211
218
|
|
|
212
|
-
|
|
219
|
+
if args.length > 0
|
|
220
|
+
@_context.capturing_into(+"") { yield_content_with_args(*args, &block) }
|
|
221
|
+
else
|
|
222
|
+
@_context.capturing_into(+"") { yield_content(&block) }
|
|
213
223
|
end
|
|
224
|
+
end
|
|
214
225
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
226
|
+
def tag(name, ...)
|
|
227
|
+
normalized_name = case name
|
|
228
|
+
when Symbol then name.name.downcase
|
|
229
|
+
when String then name.downcase
|
|
230
|
+
else raise Phlex::ArgumentError.new("Expected the tag name as a Symbol or String.")
|
|
231
|
+
end
|
|
220
232
|
|
|
221
|
-
|
|
222
|
-
|
|
233
|
+
if normalized_name == "script"
|
|
234
|
+
raise Phlex::ArgumentError.new("You can’t use the `<script>` tag from the `tag` method. Use `unsafe_tag` instead, but be careful if using user input.")
|
|
235
|
+
end
|
|
223
236
|
|
|
224
|
-
|
|
225
|
-
|
|
237
|
+
if registered_elements[normalized_name]
|
|
238
|
+
public_send(normalized_name, ...)
|
|
239
|
+
else
|
|
240
|
+
raise Phlex::ArgumentError.new("Unknown tag: #{normalized_name}")
|
|
226
241
|
end
|
|
242
|
+
end
|
|
227
243
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
244
|
+
def safe(value)
|
|
245
|
+
Phlex::SGML::SafeValue.new(value)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
alias_method :🦺, :safe
|
|
249
|
+
|
|
250
|
+
def flush
|
|
251
|
+
return if @_context.capturing
|
|
233
252
|
|
|
234
|
-
|
|
235
|
-
|
|
253
|
+
buffer = @_context.buffer
|
|
254
|
+
@_buffer << buffer.dup
|
|
255
|
+
buffer.clear
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def render(renderable = nil, &)
|
|
259
|
+
case renderable
|
|
260
|
+
when Phlex::SGML
|
|
261
|
+
renderable.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &)
|
|
262
|
+
when Class
|
|
263
|
+
if renderable < Phlex::SGML
|
|
264
|
+
renderable.new.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &)
|
|
265
|
+
end
|
|
266
|
+
when Enumerable
|
|
267
|
+
renderable.each { |r| render(r, &) }
|
|
268
|
+
when Proc, Method
|
|
269
|
+
if renderable.arity == 0
|
|
270
|
+
yield_content_with_no_args(&renderable)
|
|
236
271
|
else
|
|
237
|
-
|
|
272
|
+
yield_content(&renderable)
|
|
238
273
|
end
|
|
274
|
+
when String
|
|
275
|
+
plain(renderable)
|
|
276
|
+
when nil
|
|
277
|
+
yield_content(&) if block_given?
|
|
278
|
+
else
|
|
279
|
+
raise Phlex::ArgumentError.new("You can't render a #{renderable.inspect}.")
|
|
239
280
|
end
|
|
240
281
|
|
|
241
|
-
|
|
282
|
+
nil
|
|
283
|
+
end
|
|
242
284
|
|
|
243
|
-
|
|
244
|
-
def flush
|
|
245
|
-
return if @_context.capturing
|
|
285
|
+
private
|
|
246
286
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
287
|
+
# Like {#capture} but the output is vanished into a BlackHole buffer.
|
|
288
|
+
# Because the BlackHole does nothing with the output, this should be faster.
|
|
289
|
+
# @return [nil]
|
|
290
|
+
def vanish(*args)
|
|
291
|
+
return unless block_given?
|
|
251
292
|
|
|
252
|
-
|
|
253
|
-
# @return [nil]
|
|
254
|
-
# @overload render(component, &block)
|
|
255
|
-
# Renders the component.
|
|
256
|
-
# @param component [Phlex::SGML]
|
|
257
|
-
# @overload render(component_class, &block)
|
|
258
|
-
# Renders a new instance of the component class. This is useful for component classes that take no arguments.
|
|
259
|
-
# @param component_class [Class<Phlex::SGML>]
|
|
260
|
-
# @overload render(proc)
|
|
261
|
-
# Renders the proc with {#yield_content}.
|
|
262
|
-
# @param proc [Proc]
|
|
263
|
-
# @overload render(enumerable)
|
|
264
|
-
# Renders each item of the enumerable.
|
|
265
|
-
# @param enumerable [Enumerable]
|
|
266
|
-
# @example
|
|
267
|
-
# render @items
|
|
268
|
-
def render(renderable, &block)
|
|
269
|
-
case renderable
|
|
270
|
-
when Phlex::SGML
|
|
271
|
-
renderable.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &block)
|
|
272
|
-
when Class
|
|
273
|
-
if renderable < Phlex::SGML
|
|
274
|
-
renderable.new.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &block)
|
|
275
|
-
end
|
|
276
|
-
when Enumerable
|
|
277
|
-
renderable.each { |r| render(r, &block) }
|
|
278
|
-
when Proc, Method
|
|
279
|
-
if renderable.arity == 0
|
|
280
|
-
yield_content_with_no_args(&renderable)
|
|
281
|
-
else
|
|
282
|
-
yield_content(&renderable)
|
|
283
|
-
end
|
|
284
|
-
when String
|
|
285
|
-
plain(renderable)
|
|
286
|
-
else
|
|
287
|
-
raise ArgumentError, "You can't render a #{renderable.inspect}."
|
|
288
|
-
end
|
|
293
|
+
@_context.capturing_into(Phlex::BlackHole) { yield(*args) }
|
|
289
294
|
|
|
290
|
-
|
|
295
|
+
nil
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Determines if the component should render. By default, it returns `true`.
|
|
299
|
+
# @abstract Override to define your own predicate to prevent rendering.
|
|
300
|
+
# @return [Boolean]
|
|
301
|
+
def render?
|
|
302
|
+
true
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Format the object for output
|
|
306
|
+
# @abstract Override to define your own format handling for different object types. Please remember to call `super` in the case that the passed object doesn't match, so that object formatting can be added at different layers of the inheritance tree.
|
|
307
|
+
# @return [String]
|
|
308
|
+
def format_object(object)
|
|
309
|
+
case object
|
|
310
|
+
when Float, Integer
|
|
311
|
+
object.to_s
|
|
291
312
|
end
|
|
313
|
+
end
|
|
292
314
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
315
|
+
# @abstract Override this method to hook in around a template render. You can do things before and after calling `super` to render the template. You should always call `super` so that callbacks can be added at different layers of the inheritance tree.
|
|
316
|
+
# @return [nil]
|
|
317
|
+
def around_template
|
|
318
|
+
before_template
|
|
319
|
+
yield
|
|
320
|
+
after_template
|
|
299
321
|
|
|
300
|
-
|
|
322
|
+
nil
|
|
323
|
+
end
|
|
301
324
|
|
|
302
|
-
|
|
303
|
-
|
|
325
|
+
# @abstract Override this method to hook in right before a template is rendered. Please remember to call `super` so that callbacks can be added at different layers of the inheritance tree.
|
|
326
|
+
# @return [nil]
|
|
327
|
+
def before_template
|
|
328
|
+
nil
|
|
329
|
+
end
|
|
304
330
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
end
|
|
331
|
+
# @abstract Override this method to hook in right after a template is rendered. Please remember to call `super` so that callbacks can be added at different layers of the inheritance tree.
|
|
332
|
+
# @return [nil]
|
|
333
|
+
def after_template
|
|
334
|
+
nil
|
|
335
|
+
end
|
|
311
336
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
when Float, Integer
|
|
318
|
-
object.to_s
|
|
319
|
-
end
|
|
320
|
-
end
|
|
337
|
+
# Yields the block and checks if it buffered anything. If nothing was buffered, the return value is treated as text. The text is always HTML-escaped.
|
|
338
|
+
# @yieldparam component [self]
|
|
339
|
+
# @return [nil]
|
|
340
|
+
def yield_content
|
|
341
|
+
return unless block_given?
|
|
321
342
|
|
|
322
|
-
|
|
323
|
-
# @return [nil]
|
|
324
|
-
def around_template
|
|
325
|
-
before_template
|
|
326
|
-
yield
|
|
327
|
-
after_template
|
|
343
|
+
buffer = @_context.buffer
|
|
328
344
|
|
|
329
|
-
|
|
330
|
-
|
|
345
|
+
original_length = buffer.bytesize
|
|
346
|
+
content = yield(self)
|
|
347
|
+
__text__(content) if original_length == buffer.bytesize
|
|
331
348
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def before_template
|
|
335
|
-
nil
|
|
336
|
-
end
|
|
349
|
+
nil
|
|
350
|
+
end
|
|
337
351
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
end
|
|
352
|
+
# Same as {#yield_content} but yields no arguments.
|
|
353
|
+
# @yield Yields the block with no arguments.
|
|
354
|
+
def yield_content_with_no_args
|
|
355
|
+
return unless block_given?
|
|
343
356
|
|
|
344
|
-
|
|
345
|
-
# @yieldparam component [self]
|
|
346
|
-
# @return [nil]
|
|
347
|
-
def yield_content
|
|
348
|
-
return unless block_given?
|
|
357
|
+
buffer = @_context.buffer
|
|
349
358
|
|
|
350
|
-
|
|
359
|
+
original_length = buffer.bytesize
|
|
360
|
+
content = yield
|
|
361
|
+
__text__(content) if original_length == buffer.bytesize
|
|
351
362
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
__text__(content) if original_length == buffer.bytesize
|
|
363
|
+
nil
|
|
364
|
+
end
|
|
355
365
|
|
|
356
|
-
|
|
357
|
-
|
|
366
|
+
# Same as {#yield_content} but accepts a splat of arguments to yield. This is slightly slower than {#yield_content}.
|
|
367
|
+
# @yield [*args] Yields the given arguments.
|
|
368
|
+
# @return [nil]
|
|
369
|
+
def yield_content_with_args(*)
|
|
370
|
+
return unless block_given?
|
|
358
371
|
|
|
359
|
-
|
|
360
|
-
# @yield Yields the block with no arguments.
|
|
361
|
-
def yield_content_with_no_args
|
|
362
|
-
return unless block_given?
|
|
372
|
+
buffer = @_context.buffer
|
|
363
373
|
|
|
364
|
-
|
|
374
|
+
original_length = buffer.bytesize
|
|
375
|
+
content = yield(*)
|
|
376
|
+
__text__(content) if original_length == buffer.bytesize
|
|
365
377
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
__text__(content) if original_length == buffer.bytesize
|
|
378
|
+
nil
|
|
379
|
+
end
|
|
369
380
|
|
|
381
|
+
# Performs the same task as the public method #plain, but does not raise an error if an unformattable object is passed
|
|
382
|
+
# @api private
|
|
383
|
+
def __text__(content)
|
|
384
|
+
context = @_context
|
|
385
|
+
return true if context.fragments && !context.in_target_fragment
|
|
386
|
+
|
|
387
|
+
case content
|
|
388
|
+
when String
|
|
389
|
+
context.buffer << Phlex::Escape.html_escape(content)
|
|
390
|
+
when Symbol
|
|
391
|
+
context.buffer << Phlex::Escape.html_escape(content.name)
|
|
392
|
+
when nil
|
|
370
393
|
nil
|
|
394
|
+
else
|
|
395
|
+
if (formatted_object = format_object(content))
|
|
396
|
+
context.buffer << Phlex::Escape.html_escape(formatted_object)
|
|
397
|
+
else
|
|
398
|
+
return false
|
|
399
|
+
end
|
|
371
400
|
end
|
|
372
401
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
# @return [nil]
|
|
376
|
-
def yield_content_with_args(*args)
|
|
377
|
-
return unless block_given?
|
|
402
|
+
true
|
|
403
|
+
end
|
|
378
404
|
|
|
379
|
-
|
|
405
|
+
# @api private
|
|
406
|
+
def __attributes__(attributes, buffer = +"")
|
|
407
|
+
attributes.each do |k, v|
|
|
408
|
+
next unless v
|
|
380
409
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
410
|
+
name = case k
|
|
411
|
+
when String then k
|
|
412
|
+
when Symbol then k.name.tr("_", "-")
|
|
413
|
+
else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols.")
|
|
414
|
+
end
|
|
384
415
|
|
|
385
|
-
|
|
386
|
-
end
|
|
416
|
+
lower_name = name.downcase
|
|
387
417
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
418
|
+
unless Phlex::SGML::SafeObject === v
|
|
419
|
+
if lower_name == "href" && v.to_s.downcase.delete("^a-z:").start_with?("javascript:")
|
|
420
|
+
next
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Detect unsafe attribute names. Attribute names are considered unsafe if they match an event attribute or include unsafe characters.
|
|
424
|
+
if Phlex::HTML::UNSAFE_ATTRIBUTES.include?(lower_name.delete("^a-z-"))
|
|
425
|
+
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
if name.match?(/[<>&"']/)
|
|
430
|
+
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
|
431
|
+
end
|
|
393
432
|
|
|
394
|
-
|
|
433
|
+
if lower_name.to_sym == :id && k != :id
|
|
434
|
+
raise Phlex::ArgumentError.new(":id attribute should only be passed as a lowercase symbol.")
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
case v
|
|
438
|
+
when true
|
|
439
|
+
buffer << " " << name
|
|
395
440
|
when String
|
|
396
|
-
|
|
441
|
+
buffer << " " << name << '="' << v.gsub('"', """) << '"'
|
|
397
442
|
when Symbol
|
|
398
|
-
|
|
399
|
-
when
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
443
|
+
buffer << " " << name << '="' << v.name.tr("_", "-").gsub('"', """) << '"'
|
|
444
|
+
when Integer, Float
|
|
445
|
+
buffer << " " << name << '="' << v.to_s << '"'
|
|
446
|
+
when Hash
|
|
447
|
+
case k
|
|
448
|
+
when :class
|
|
449
|
+
buffer << " " << name << '="' << __classes__(v).gsub('"', """) << '"'
|
|
450
|
+
when :style
|
|
451
|
+
buffer << " " << name << '="' << __styles__(v).gsub('"', """) << '"'
|
|
404
452
|
else
|
|
405
|
-
|
|
453
|
+
__nested_attributes__(v, "#{name}-", buffer)
|
|
454
|
+
end
|
|
455
|
+
when Array
|
|
456
|
+
value = case k
|
|
457
|
+
when :class
|
|
458
|
+
__classes__(v)
|
|
459
|
+
when :style
|
|
460
|
+
__styles__(v)
|
|
461
|
+
else
|
|
462
|
+
__nested_tokens__(v)
|
|
406
463
|
end
|
|
407
|
-
end
|
|
408
464
|
|
|
409
|
-
|
|
410
|
-
|
|
465
|
+
buffer << " " << name << '="' << value.gsub('"', """) << '"'
|
|
466
|
+
when Set
|
|
467
|
+
buffer << " " << name << '="' << __nested_tokens__(v.to_a) << '"'
|
|
468
|
+
when Phlex::SGML::SafeObject
|
|
469
|
+
buffer << " " << name << '="' << v.to_s.gsub('"', """) << '"'
|
|
470
|
+
else
|
|
471
|
+
value = if v.respond_to?(:to_phlex_attribute_value)
|
|
472
|
+
v.to_phlex_attribute_value
|
|
473
|
+
elsif v.respond_to?(:to_str)
|
|
474
|
+
v.to_str
|
|
475
|
+
else
|
|
476
|
+
v.to_s
|
|
477
|
+
end
|
|
411
478
|
|
|
412
|
-
|
|
413
|
-
def __attributes__(**attributes)
|
|
414
|
-
__final_attributes__(**attributes).tap do |buffer|
|
|
415
|
-
Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] = buffer.freeze
|
|
479
|
+
buffer << " " << name << '="' << value.gsub('"', """) << '"'
|
|
416
480
|
end
|
|
417
481
|
end
|
|
418
482
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
483
|
+
buffer
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# @api private
|
|
487
|
+
#
|
|
488
|
+
# Provides the nested-attributes case for serializing out attributes.
|
|
489
|
+
# This allows us to skip many of the checks the `__attributes__` method must perform.
|
|
490
|
+
def __nested_attributes__(attributes, base_name, buffer = +"")
|
|
491
|
+
attributes.each do |k, v|
|
|
492
|
+
next unless v
|
|
493
|
+
|
|
494
|
+
name = case k
|
|
495
|
+
when String then k
|
|
496
|
+
when Symbol then k.name.tr("_", "-")
|
|
497
|
+
else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols")
|
|
423
498
|
end
|
|
424
499
|
|
|
425
|
-
|
|
426
|
-
|
|
500
|
+
case v
|
|
501
|
+
when true
|
|
502
|
+
buffer << " " << base_name << name
|
|
503
|
+
when String
|
|
504
|
+
buffer << " " << base_name << name << '="' << v.gsub('"', """) << '"'
|
|
505
|
+
when Symbol
|
|
506
|
+
buffer << " " << base_name << name << '="' << v.name.tr("_", "-").gsub('"', """) << '"'
|
|
507
|
+
when Integer, Float
|
|
508
|
+
buffer << " " << base_name << name << '="' << v.to_s << '"'
|
|
509
|
+
when Hash
|
|
510
|
+
__nested_attributes__(v, "#{base_name}#{name}-", buffer)
|
|
511
|
+
when Array
|
|
512
|
+
buffer << " " << base_name << name << '="' << __nested_tokens__(v) << '"'
|
|
513
|
+
when Set
|
|
514
|
+
buffer << " " << base_name << name << '="' << __nested_tokens__(v.to_a) << '"'
|
|
515
|
+
else
|
|
516
|
+
value = if v.respond_to?(:to_phlex_attribute_value)
|
|
517
|
+
v.to_phlex_attribute_value
|
|
518
|
+
elsif v.respond_to?(:to_str)
|
|
519
|
+
v.to_str
|
|
520
|
+
else
|
|
521
|
+
v.to_s
|
|
522
|
+
end
|
|
523
|
+
buffer << " " << base_name << name << '="' << value.gsub('"', """) << '"'
|
|
524
|
+
end
|
|
427
525
|
|
|
428
526
|
buffer
|
|
429
527
|
end
|
|
528
|
+
end
|
|
430
529
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
begin
|
|
435
|
-
[$1.to_i(16)].pack("U*")
|
|
436
|
-
rescue
|
|
437
|
-
""
|
|
438
|
-
end
|
|
439
|
-
}
|
|
440
|
-
.gsub(/&#(\d+);?/) {
|
|
441
|
-
begin
|
|
442
|
-
[$1.to_i].pack("U*")
|
|
443
|
-
rescue
|
|
444
|
-
""
|
|
445
|
-
end
|
|
446
|
-
}
|
|
447
|
-
.gsub(/&([a-z][a-z0-9]+);?/i) {
|
|
448
|
-
NAMED_CHARACTER_REFERENCES[$1.downcase] || ""
|
|
449
|
-
}
|
|
450
|
-
end
|
|
530
|
+
# @api private
|
|
531
|
+
def __nested_tokens__(tokens)
|
|
532
|
+
buffer = +""
|
|
451
533
|
|
|
452
|
-
|
|
453
|
-
def __build_attributes__(attributes, buffer:)
|
|
454
|
-
attributes.each do |k, v|
|
|
455
|
-
next unless v
|
|
534
|
+
i, length = 0, tokens.length
|
|
456
535
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
when Symbol then k.name.tr("_", "-")
|
|
460
|
-
else raise ArgumentError, "Attribute keys should be Strings or Symbols."
|
|
461
|
-
end
|
|
536
|
+
while i < length
|
|
537
|
+
token = tokens[i]
|
|
462
538
|
|
|
463
|
-
|
|
464
|
-
|
|
539
|
+
case token
|
|
540
|
+
when String
|
|
541
|
+
if i > 0
|
|
542
|
+
buffer << " " << token
|
|
543
|
+
else
|
|
544
|
+
buffer << token
|
|
545
|
+
end
|
|
546
|
+
when Symbol
|
|
547
|
+
if i > 0
|
|
548
|
+
buffer << " " << token.name.tr("_", "-")
|
|
549
|
+
else
|
|
550
|
+
buffer << token.name.tr("_", "-")
|
|
551
|
+
end
|
|
552
|
+
when nil
|
|
553
|
+
# Do nothing
|
|
554
|
+
else
|
|
555
|
+
if i > 0
|
|
556
|
+
buffer << " " << token.to_s
|
|
557
|
+
else
|
|
558
|
+
buffer << token.to_s
|
|
559
|
+
end
|
|
560
|
+
end
|
|
465
561
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
when String then decode_html_character_references(v)
|
|
469
|
-
when Symbol then decode_html_character_references(v.name)
|
|
470
|
-
end
|
|
562
|
+
i += 1
|
|
563
|
+
end
|
|
471
564
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
end
|
|
565
|
+
buffer.gsub!('"', """)
|
|
566
|
+
buffer
|
|
567
|
+
end
|
|
476
568
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
569
|
+
# @api private
|
|
570
|
+
def __classes__(c)
|
|
571
|
+
case c
|
|
572
|
+
when String
|
|
573
|
+
c
|
|
574
|
+
when Symbol
|
|
575
|
+
c.name.tr("_", "-")
|
|
576
|
+
when Array, Set
|
|
577
|
+
c.filter_map { |c| __classes__(c) }.join(" ")
|
|
578
|
+
when Hash
|
|
579
|
+
c.filter_map { |c, add|
|
|
580
|
+
next unless add
|
|
581
|
+
case c
|
|
582
|
+
when String then c
|
|
583
|
+
when Symbol then c.name.tr("_", "-").delete_suffix("?")
|
|
584
|
+
else raise Phlex::ArgumentError.new("Class keys should be Strings or Symbols.")
|
|
480
585
|
end
|
|
586
|
+
}.join(" ")
|
|
587
|
+
when nil, false
|
|
588
|
+
nil
|
|
589
|
+
else
|
|
590
|
+
if c.respond_to?(:to_phlex_attribute_value)
|
|
591
|
+
c.to_phlex_attribute_value
|
|
592
|
+
elsif c.respond_to?(:to_str)
|
|
593
|
+
c.to_str
|
|
594
|
+
else
|
|
595
|
+
c.to_s
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
end
|
|
481
599
|
|
|
482
|
-
|
|
483
|
-
|
|
600
|
+
# @api private
|
|
601
|
+
def __styles__(s)
|
|
602
|
+
style = case s
|
|
603
|
+
when String
|
|
604
|
+
s
|
|
605
|
+
when Symbol
|
|
606
|
+
s.name.tr("_", "-")
|
|
607
|
+
when Integer, Float
|
|
608
|
+
s.to_s
|
|
609
|
+
when Array, Set
|
|
610
|
+
s.filter_map { |s| __styles__(s) }.join
|
|
611
|
+
when Hash
|
|
612
|
+
buffer = +""
|
|
613
|
+
s.each do |k, v|
|
|
614
|
+
prop = case k
|
|
615
|
+
when String then k
|
|
616
|
+
when Symbol then k.name.tr("_", "-")
|
|
617
|
+
else raise Phlex::ArgumentError.new("Style keys should be Strings or Symbols.")
|
|
484
618
|
end
|
|
485
619
|
|
|
486
|
-
|
|
487
|
-
when true
|
|
488
|
-
buffer << " " << name
|
|
489
|
-
when String
|
|
490
|
-
buffer << " " << name << '="' << Phlex::Escape.html_escape(v) << '"'
|
|
491
|
-
when Symbol
|
|
492
|
-
buffer << " " << name << '="' << Phlex::Escape.html_escape(v.name) << '"'
|
|
493
|
-
when Integer, Float
|
|
494
|
-
buffer << " " << name << '="' << v.to_s << '"'
|
|
495
|
-
when Hash
|
|
496
|
-
__build_attributes__(
|
|
497
|
-
v.transform_keys { |subkey|
|
|
498
|
-
case subkey
|
|
499
|
-
when Symbol then"#{name}-#{subkey.name.tr('_', '-')}"
|
|
500
|
-
else "#{name}-#{subkey}"
|
|
501
|
-
end
|
|
502
|
-
}, buffer: buffer
|
|
503
|
-
)
|
|
504
|
-
when Array
|
|
505
|
-
buffer << " " << name << '="' << Phlex::Escape.html_escape(v.compact.join(" ")) << '"'
|
|
506
|
-
when Set
|
|
507
|
-
buffer << " " << name << '="' << Phlex::Escape.html_escape(v.to_a.compact.join(" ")) << '"'
|
|
508
|
-
else
|
|
509
|
-
value = if v.respond_to?(:to_phlex_attribute_value)
|
|
510
|
-
v.to_phlex_attribute_value
|
|
511
|
-
elsif v.respond_to?(:to_str)
|
|
512
|
-
v.to_str
|
|
513
|
-
else
|
|
514
|
-
v.to_s
|
|
515
|
-
end
|
|
620
|
+
value = __styles__(v)
|
|
516
621
|
|
|
517
|
-
|
|
622
|
+
if value
|
|
623
|
+
buffer << prop << ":" << value
|
|
518
624
|
end
|
|
519
625
|
end
|
|
520
|
-
|
|
521
626
|
buffer
|
|
627
|
+
when nil, false
|
|
628
|
+
return nil
|
|
629
|
+
else
|
|
630
|
+
if s.respond_to?(:to_phlex_attribute_value)
|
|
631
|
+
s.to_phlex_attribute_value
|
|
632
|
+
elsif s.respond_to?(:to_str)
|
|
633
|
+
s.to_str
|
|
634
|
+
else
|
|
635
|
+
s.to_s
|
|
636
|
+
end
|
|
522
637
|
end
|
|
638
|
+
|
|
639
|
+
style.end_with?(";") ? style : "#{style};"
|
|
523
640
|
end
|
|
524
641
|
end
|