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