phlex 1.10.3 → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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 +28 -25
- data/lib/phlex/sgml/safe_object.rb +7 -0
- data/lib/phlex/sgml/safe_value.rb +11 -0
- data/lib/phlex/sgml.rb +524 -359
- 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 +16 -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,476 +1,641 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
class << self
|
9
|
-
# Render the view to a String. Arguments are delegated to {.new}.
|
10
|
-
def call(...)
|
11
|
-
new(...).call
|
12
|
-
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"
|
13
7
|
|
14
|
-
|
15
|
-
# @note The block will not be delegated {#initialize}. Instead, it will be sent to {#template} when rendering.
|
16
|
-
def new(*args, **kwargs, &block)
|
17
|
-
if block
|
18
|
-
object = super(*args, **kwargs, &nil)
|
19
|
-
object.instance_variable_set(:@_content_block, block)
|
20
|
-
object
|
21
|
-
else
|
22
|
-
super
|
23
|
-
end
|
24
|
-
end
|
8
|
+
include Phlex::Helpers
|
25
9
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
10
|
+
class << self
|
11
|
+
# Render the view to a String. Arguments are delegated to {.new}.
|
12
|
+
def call(...)
|
13
|
+
new(...).call
|
14
|
+
end
|
31
15
|
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
35
27
|
|
28
|
+
# @api private
|
29
|
+
def __element_method__?(method_name)
|
30
|
+
if instance_methods.include?(method_name)
|
36
31
|
owner = instance_method(method_name).owner
|
37
32
|
|
38
|
-
|
39
|
-
|
33
|
+
if Phlex::Elements === owner && owner.registered_elements[method_name]
|
34
|
+
true
|
35
|
+
else
|
36
|
+
false
|
37
|
+
end
|
38
|
+
else
|
40
39
|
false
|
41
40
|
end
|
42
41
|
end
|
42
|
+
end
|
43
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
|
-
|
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?
|
69
70
|
yield
|
70
71
|
end
|
72
|
+
end
|
71
73
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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.")
|
76
84
|
end
|
85
|
+
end
|
77
86
|
|
78
|
-
|
79
|
-
|
80
|
-
|
87
|
+
def to_proc
|
88
|
+
proc { |c| c.render(self) }
|
89
|
+
end
|
81
90
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
flush if task.running?
|
89
|
-
task.wait
|
90
|
-
else
|
91
|
-
raise ArgumentError, "Expected an asynchronous task / promise."
|
92
|
-
end
|
93
|
-
end
|
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
|
94
97
|
|
95
|
-
|
96
|
-
|
97
|
-
__final_call__(...).tap do
|
98
|
-
self.class.rendered_at_least_once!
|
99
|
-
end
|
100
|
-
end
|
98
|
+
raise Phlex::DoubleRenderError.new("You can't render a #{self.class.name} more than once.") if @_rendered
|
99
|
+
@_rendered = true
|
101
100
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
@_context = context
|
106
|
-
@_view_context = view_context
|
107
|
-
@_parent = parent
|
108
|
-
if fragments
|
109
|
-
warn "⚠️ [WARNING] Selective Rendering is experimental, incomplete, and may change in future versions."
|
110
|
-
@_context.target_fragments(fragments)
|
111
|
-
end
|
101
|
+
if fragments
|
102
|
+
@_context.target_fragments(fragments)
|
103
|
+
end
|
112
104
|
|
113
|
-
|
105
|
+
block ||= @_content_block
|
114
106
|
|
115
|
-
|
107
|
+
return "" unless render?
|
116
108
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
109
|
+
if !parent && Phlex::SUPPORTS_FIBER_STORAGE
|
110
|
+
original_fiber_storage = Fiber[:__phlex_component__]
|
111
|
+
Fiber[:__phlex_component__] = self
|
112
|
+
end
|
121
113
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
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)
|
135
126
|
end
|
136
127
|
end
|
137
|
-
else
|
138
|
-
view_template
|
139
128
|
end
|
129
|
+
else
|
130
|
+
view_template
|
140
131
|
end
|
141
132
|
end
|
133
|
+
end
|
142
134
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
end
|
147
|
-
buffer << context.buffer
|
135
|
+
unless parent
|
136
|
+
if Phlex::SUPPORTS_FIBER_STORAGE
|
137
|
+
Fiber[:__phlex_component__] = original_fiber_storage
|
148
138
|
end
|
139
|
+
buffer << context.buffer
|
149
140
|
end
|
141
|
+
end
|
150
142
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
148
|
+
|
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")
|
155
156
|
end
|
156
157
|
|
157
|
-
|
158
|
-
|
159
|
-
# @return [nil]
|
160
|
-
# @see #format_object
|
161
|
-
def plain(content)
|
162
|
-
unless __text__(content)
|
163
|
-
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"
|
164
|
-
end
|
158
|
+
nil
|
159
|
+
end
|
165
160
|
|
166
|
-
|
167
|
-
|
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
|
168
167
|
|
169
|
-
|
170
|
-
# @return [nil]
|
171
|
-
# @yield If a block is given, it yields the block with no arguments.
|
172
|
-
def whitespace(&block)
|
173
|
-
context = @_context
|
174
|
-
return if context.fragments && !context.in_target_fragment
|
168
|
+
buffer = context.buffer
|
175
169
|
|
176
|
-
|
170
|
+
buffer << " "
|
177
171
|
|
172
|
+
if block_given?
|
173
|
+
yield_content(&)
|
178
174
|
buffer << " "
|
175
|
+
end
|
179
176
|
|
180
|
-
|
181
|
-
|
182
|
-
buffer << " "
|
183
|
-
end
|
177
|
+
nil
|
178
|
+
end
|
184
179
|
|
185
|
-
|
186
|
-
|
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
|
187
194
|
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
191
201
|
context = @_context
|
192
202
|
return if context.fragments && !context.in_target_fragment
|
193
203
|
|
194
|
-
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
|
195
209
|
|
196
|
-
|
197
|
-
|
198
|
-
buffer << " -->"
|
210
|
+
nil
|
211
|
+
end
|
199
212
|
|
200
|
-
|
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
|
218
|
+
|
219
|
+
if args.length > 0
|
220
|
+
@_context.capturing_into(+"") { yield_content_with_args(*args, &block) }
|
221
|
+
else
|
222
|
+
@_context.capturing_into(+"") { yield_content(&block) }
|
201
223
|
end
|
224
|
+
end
|
202
225
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
208
232
|
|
209
|
-
|
210
|
-
|
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
|
211
236
|
|
212
|
-
|
213
|
-
|
237
|
+
if registered_elements[normalized_name]
|
238
|
+
public_send(normalized_name, ...)
|
239
|
+
else
|
240
|
+
raise Phlex::ArgumentError.new("Unknown tag: #{normalized_name}")
|
214
241
|
end
|
242
|
+
end
|
215
243
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
def capture(*args, &block)
|
220
|
-
return "" unless block
|
244
|
+
def safe(value)
|
245
|
+
Phlex::SGML::SafeValue.new(value)
|
246
|
+
end
|
221
247
|
|
222
|
-
|
223
|
-
|
248
|
+
alias_method :🦺, :safe
|
249
|
+
|
250
|
+
def flush
|
251
|
+
return if @_context.capturing
|
252
|
+
|
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)
|
224
271
|
else
|
225
|
-
|
272
|
+
yield_content(&renderable)
|
226
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}.")
|
227
280
|
end
|
228
281
|
|
229
|
-
|
282
|
+
nil
|
283
|
+
end
|
230
284
|
|
231
|
-
|
232
|
-
def flush
|
233
|
-
return if @_context.capturing
|
285
|
+
private
|
234
286
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
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?
|
239
292
|
|
240
|
-
|
241
|
-
# @return [nil]
|
242
|
-
# @overload render(component, &block)
|
243
|
-
# Renders the component.
|
244
|
-
# @param component [Phlex::SGML]
|
245
|
-
# @overload render(component_class, &block)
|
246
|
-
# Renders a new instance of the component class. This is useful for component classes that take no arguments.
|
247
|
-
# @param component_class [Class<Phlex::SGML>]
|
248
|
-
# @overload render(proc)
|
249
|
-
# Renders the proc with {#yield_content}.
|
250
|
-
# @param proc [Proc]
|
251
|
-
# @overload render(enumerable)
|
252
|
-
# Renders each item of the enumerable.
|
253
|
-
# @param enumerable [Enumerable]
|
254
|
-
# @example
|
255
|
-
# render @items
|
256
|
-
def render(renderable, &block)
|
257
|
-
case renderable
|
258
|
-
when Phlex::SGML
|
259
|
-
renderable.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &block)
|
260
|
-
when Class
|
261
|
-
if renderable < Phlex::SGML
|
262
|
-
renderable.new.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &block)
|
263
|
-
end
|
264
|
-
when Enumerable
|
265
|
-
renderable.each { |r| render(r, &block) }
|
266
|
-
when Proc, Method
|
267
|
-
if renderable.arity == 0
|
268
|
-
yield_content_with_no_args(&renderable)
|
269
|
-
else
|
270
|
-
yield_content(&renderable)
|
271
|
-
end
|
272
|
-
when String
|
273
|
-
plain(renderable)
|
274
|
-
else
|
275
|
-
raise ArgumentError, "You can't render a #{renderable.inspect}."
|
276
|
-
end
|
293
|
+
@_context.capturing_into(Phlex::BlackHole) { yield(*args) }
|
277
294
|
|
278
|
-
|
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
|
279
312
|
end
|
313
|
+
end
|
280
314
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
287
321
|
|
288
|
-
|
322
|
+
nil
|
323
|
+
end
|
289
324
|
|
290
|
-
|
291
|
-
|
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
|
292
330
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
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
|
299
336
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
when Float, Integer
|
306
|
-
object.to_s
|
307
|
-
end
|
308
|
-
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?
|
309
342
|
|
310
|
-
|
311
|
-
# @return [nil]
|
312
|
-
def around_template
|
313
|
-
before_template
|
314
|
-
yield
|
315
|
-
after_template
|
343
|
+
buffer = @_context.buffer
|
316
344
|
|
317
|
-
|
318
|
-
|
345
|
+
original_length = buffer.bytesize
|
346
|
+
content = yield(self)
|
347
|
+
__text__(content) if original_length == buffer.bytesize
|
319
348
|
|
320
|
-
|
321
|
-
|
322
|
-
def before_template
|
323
|
-
nil
|
324
|
-
end
|
349
|
+
nil
|
350
|
+
end
|
325
351
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
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?
|
331
356
|
|
332
|
-
|
333
|
-
# @yieldparam component [self]
|
334
|
-
# @return [nil]
|
335
|
-
def yield_content
|
336
|
-
return unless block_given?
|
357
|
+
buffer = @_context.buffer
|
337
358
|
|
338
|
-
|
359
|
+
original_length = buffer.bytesize
|
360
|
+
content = yield
|
361
|
+
__text__(content) if original_length == buffer.bytesize
|
339
362
|
|
340
|
-
|
341
|
-
|
342
|
-
__text__(content) if original_length == buffer.bytesize
|
363
|
+
nil
|
364
|
+
end
|
343
365
|
|
344
|
-
|
345
|
-
|
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?
|
346
371
|
|
347
|
-
|
348
|
-
# @yield Yields the block with no arguments.
|
349
|
-
def yield_content_with_no_args
|
350
|
-
return unless block_given?
|
372
|
+
buffer = @_context.buffer
|
351
373
|
|
352
|
-
|
374
|
+
original_length = buffer.bytesize
|
375
|
+
content = yield(*)
|
376
|
+
__text__(content) if original_length == buffer.bytesize
|
353
377
|
|
354
|
-
|
355
|
-
|
356
|
-
__text__(content) if original_length == buffer.bytesize
|
378
|
+
nil
|
379
|
+
end
|
357
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
|
358
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
|
359
400
|
end
|
360
401
|
|
361
|
-
|
362
|
-
|
363
|
-
# @return [nil]
|
364
|
-
def yield_content_with_args(*args)
|
365
|
-
return unless block_given?
|
402
|
+
true
|
403
|
+
end
|
366
404
|
|
367
|
-
|
405
|
+
# @api private
|
406
|
+
def __attributes__(attributes, buffer = +"")
|
407
|
+
attributes.each do |k, v|
|
408
|
+
next unless v
|
368
409
|
|
369
|
-
|
370
|
-
|
371
|
-
|
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
|
372
415
|
|
373
|
-
|
374
|
-
end
|
416
|
+
lower_name = name.downcase
|
375
417
|
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
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
|
381
432
|
|
382
|
-
|
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
|
383
440
|
when String
|
384
|
-
|
441
|
+
buffer << " " << name << '="' << v.gsub('"', """) << '"'
|
385
442
|
when Symbol
|
386
|
-
|
387
|
-
when
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
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('"', """) << '"'
|
392
452
|
else
|
393
|
-
|
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)
|
394
463
|
end
|
395
|
-
end
|
396
464
|
|
397
|
-
|
398
|
-
|
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
|
399
478
|
|
400
|
-
|
401
|
-
def __attributes__(**attributes)
|
402
|
-
__final_attributes__(**attributes).tap do |buffer|
|
403
|
-
Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] = buffer.freeze
|
479
|
+
buffer << " " << name << '="' << value.gsub('"', """) << '"'
|
404
480
|
end
|
405
481
|
end
|
406
482
|
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
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")
|
411
498
|
end
|
412
499
|
|
413
|
-
|
414
|
-
|
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
|
415
525
|
|
416
526
|
buffer
|
417
527
|
end
|
528
|
+
end
|
418
529
|
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
next unless v
|
530
|
+
# @api private
|
531
|
+
def __nested_tokens__(tokens)
|
532
|
+
buffer = +""
|
423
533
|
|
424
|
-
|
425
|
-
when String then k
|
426
|
-
when Symbol then k.name.tr("_", "-")
|
427
|
-
else raise ArgumentError, "Attribute keys should be Strings or Symbols."
|
428
|
-
end
|
534
|
+
i, length = 0, tokens.length
|
429
535
|
|
430
|
-
|
431
|
-
|
536
|
+
while i < length
|
537
|
+
token = tokens[i]
|
432
538
|
|
433
|
-
|
434
|
-
|
435
|
-
|
539
|
+
case token
|
540
|
+
when String
|
541
|
+
if i > 0
|
542
|
+
buffer << " " << token
|
543
|
+
else
|
544
|
+
buffer << token
|
436
545
|
end
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
buffer << " " << name
|
441
|
-
when String
|
442
|
-
buffer << " " << name << '="' << Phlex::Escape.html_escape(v) << '"'
|
443
|
-
when Symbol
|
444
|
-
buffer << " " << name << '="' << Phlex::Escape.html_escape(v.name) << '"'
|
445
|
-
when Integer, Float
|
446
|
-
buffer << " " << name << '="' << v.to_s << '"'
|
447
|
-
when Hash
|
448
|
-
__build_attributes__(
|
449
|
-
v.transform_keys { |subkey|
|
450
|
-
case subkey
|
451
|
-
when Symbol then"#{name}-#{subkey.name.tr('_', '-')}"
|
452
|
-
else "#{name}-#{subkey}"
|
453
|
-
end
|
454
|
-
}, buffer: buffer
|
455
|
-
)
|
456
|
-
when Array
|
457
|
-
buffer << " " << name << '="' << Phlex::Escape.html_escape(v.compact.join(" ")) << '"'
|
458
|
-
when Set
|
459
|
-
buffer << " " << name << '="' << Phlex::Escape.html_escape(v.to_a.compact.join(" ")) << '"'
|
546
|
+
when Symbol
|
547
|
+
if i > 0
|
548
|
+
buffer << " " << token.name.tr("_", "-")
|
460
549
|
else
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
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
|
561
|
+
|
562
|
+
i += 1
|
563
|
+
end
|
468
564
|
|
469
|
-
|
565
|
+
buffer.gsub!('"', """)
|
566
|
+
buffer
|
567
|
+
end
|
568
|
+
|
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.")
|
470
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
|
471
596
|
end
|
597
|
+
end
|
598
|
+
end
|
472
599
|
|
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.")
|
618
|
+
end
|
619
|
+
|
620
|
+
value = __styles__(v)
|
621
|
+
|
622
|
+
if value
|
623
|
+
buffer << prop << ":" << value
|
624
|
+
end
|
625
|
+
end
|
473
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
|
474
637
|
end
|
638
|
+
|
639
|
+
style.end_with?(";") ? style : "#{style};"
|
475
640
|
end
|
476
641
|
end
|