phlex 2.0.0.beta2 → 2.0.0.rc2
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 +8 -11
- data/lib/phlex/csv.rb +4 -11
- data/lib/phlex/error.rb +1 -0
- data/lib/phlex/fifo.rb +11 -3
- data/lib/phlex/fifo_cache_store.rb +49 -0
- data/lib/phlex/helpers.rb +2 -2
- data/lib/phlex/html/standard_elements.rb +932 -722
- data/lib/phlex/html/void_elements.rb +93 -66
- data/lib/phlex/html.rb +4 -10
- data/lib/phlex/kit.rb +39 -22
- data/lib/phlex/null_cache_store.rb +9 -0
- data/lib/phlex/sgml/elements.rb +112 -0
- data/lib/phlex/sgml/state.rb +118 -0
- data/lib/phlex/sgml.rb +326 -305
- data/lib/phlex/svg/standard_elements.rb +417 -449
- data/lib/phlex/svg.rb +0 -3
- data/lib/phlex/{black_hole.rb → vanish.rb} +1 -1
- data/lib/phlex/version.rb +1 -1
- data/lib/phlex.rb +17 -9
- metadata +9 -12
- data/lib/phlex/context.rb +0 -59
- data/lib/phlex/deferred_render.rb +0 -29
- data/lib/phlex/element_clobbering_guard.rb +0 -18
- data/lib/phlex/elements.rb +0 -172
data/lib/phlex/sgml.rb
CHANGED
@@ -2,8 +2,13 @@
|
|
2
2
|
|
3
3
|
# **Standard Generalized Markup Language** for behaviour common to {HTML} and {SVG}.
|
4
4
|
class Phlex::SGML
|
5
|
+
UNSAFE_ATTRIBUTES = Set.new(%w[srcdoc sandbox http-equiv]).freeze
|
6
|
+
REF_ATTRIBUTES = Set.new(%w[href src action formaction lowsrc dynsrc background ping]).freeze
|
7
|
+
|
8
|
+
autoload :Elements, "phlex/sgml/elements"
|
5
9
|
autoload :SafeObject, "phlex/sgml/safe_object"
|
6
10
|
autoload :SafeValue, "phlex/sgml/safe_value"
|
11
|
+
autoload :State, "phlex/sgml/state"
|
7
12
|
|
8
13
|
include Phlex::Helpers
|
9
14
|
|
@@ -15,72 +20,22 @@ class Phlex::SGML
|
|
15
20
|
|
16
21
|
# Create a new instance of the component.
|
17
22
|
# @note The block will not be delegated {#initialize}. Instead, it will be sent to {#template} when rendering.
|
18
|
-
def new(
|
23
|
+
def new(*a, **k, &block)
|
19
24
|
if block
|
20
|
-
object = super(
|
25
|
+
object = super(*a, **k, &nil)
|
21
26
|
object.instance_exec { @_content_block = block }
|
22
27
|
object
|
23
28
|
else
|
24
29
|
super
|
25
30
|
end
|
26
31
|
end
|
27
|
-
|
28
|
-
# @api private
|
29
|
-
def __element_method__?(method_name)
|
30
|
-
if instance_methods.include?(method_name)
|
31
|
-
owner = instance_method(method_name).owner
|
32
|
-
|
33
|
-
if Phlex::Elements === owner && owner.registered_elements[method_name]
|
34
|
-
true
|
35
|
-
else
|
36
|
-
false
|
37
|
-
end
|
38
|
-
else
|
39
|
-
false
|
40
|
-
end
|
41
|
-
end
|
42
32
|
end
|
43
33
|
|
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
34
|
def view_template
|
69
35
|
if block_given?
|
70
36
|
yield
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
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
37
|
else
|
83
|
-
|
38
|
+
plain "Phlex Warning: Your `#{self.class.name}` class doesn't define a `view_template` method. If you are upgrading to Phlex 2.x make sure to rename your `template` method to `view_template`. See: https://beta.phlex.fun/guides/v2-upgrade.html"
|
84
39
|
end
|
85
40
|
end
|
86
41
|
|
@@ -88,68 +43,62 @@ class Phlex::SGML
|
|
88
43
|
proc { |c| c.render(self) }
|
89
44
|
end
|
90
45
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
46
|
+
def call(buffer = +"", context: {}, view_context: nil, fragments: nil, &)
|
47
|
+
state = Phlex::SGML::State.new(
|
48
|
+
user_context: context,
|
49
|
+
view_context:,
|
50
|
+
output_buffer: buffer,
|
51
|
+
fragments: fragments&.to_set,
|
52
|
+
)
|
97
53
|
|
98
|
-
|
99
|
-
@_rendered = true
|
54
|
+
internal_call(parent: nil, state:, &)
|
100
55
|
|
101
|
-
|
102
|
-
|
56
|
+
state.output_buffer << state.buffer
|
57
|
+
end
|
58
|
+
|
59
|
+
def internal_call(parent: nil, state: nil, &block)
|
60
|
+
return "" unless render?
|
61
|
+
|
62
|
+
if @_state
|
63
|
+
raise Phlex::DoubleRenderError.new(
|
64
|
+
"You can't render a #{self.class.name} more than once."
|
65
|
+
)
|
103
66
|
end
|
104
67
|
|
68
|
+
@_state = state
|
69
|
+
|
105
70
|
block ||= @_content_block
|
106
71
|
|
107
|
-
|
72
|
+
Thread.current[:__phlex_component__] = [self, Fiber.current.object_id].freeze
|
108
73
|
|
109
|
-
|
110
|
-
|
111
|
-
Fiber[:__phlex_component__] = self
|
112
|
-
end
|
74
|
+
state.around_render(self) do
|
75
|
+
before_template(&block)
|
113
76
|
|
114
|
-
@_context.around_render do
|
115
77
|
around_template do
|
116
78
|
if block
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
if args.length > 0
|
123
|
-
yield_content_with_args(*args, &block)
|
124
|
-
else
|
125
|
-
yield_content(&block)
|
126
|
-
end
|
79
|
+
view_template do |*args|
|
80
|
+
if args.length > 0
|
81
|
+
__yield_content_with_args__(*args, &block)
|
82
|
+
else
|
83
|
+
__yield_content__(&block)
|
127
84
|
end
|
128
85
|
end
|
129
86
|
else
|
130
87
|
view_template
|
131
88
|
end
|
132
89
|
end
|
133
|
-
end
|
134
90
|
|
135
|
-
|
136
|
-
if Phlex::SUPPORTS_FIBER_STORAGE
|
137
|
-
Fiber[:__phlex_component__] = original_fiber_storage
|
138
|
-
end
|
139
|
-
buffer << context.buffer
|
91
|
+
after_template(&block)
|
140
92
|
end
|
93
|
+
ensure
|
94
|
+
Thread.current[:__phlex_component__] = [parent, Fiber.current.object_id].freeze
|
141
95
|
end
|
142
96
|
|
143
|
-
# Access the current render context data
|
144
|
-
# @return the supplied context object, by default a Hash
|
145
97
|
def context
|
146
|
-
@
|
98
|
+
@_state.user_context
|
147
99
|
end
|
148
100
|
|
149
|
-
# Output
|
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
|
101
|
+
# Output plain text.
|
153
102
|
def plain(content)
|
154
103
|
unless __text__(content)
|
155
104
|
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")
|
@@ -158,50 +107,47 @@ class Phlex::SGML
|
|
158
107
|
nil
|
159
108
|
end
|
160
109
|
|
161
|
-
# Output a
|
162
|
-
# @return [nil]
|
163
|
-
# @yield If a block is given, it yields the block with no arguments.
|
110
|
+
# Output a single space character. If a block is given, a space will be output before and after the block.
|
164
111
|
def whitespace(&)
|
165
|
-
|
166
|
-
return
|
112
|
+
state = @_state
|
113
|
+
return unless state.should_render?
|
167
114
|
|
168
|
-
buffer =
|
115
|
+
buffer = state.buffer
|
169
116
|
|
170
117
|
buffer << " "
|
171
118
|
|
172
119
|
if block_given?
|
173
|
-
|
120
|
+
__yield_content__(&)
|
174
121
|
buffer << " "
|
175
122
|
end
|
176
123
|
|
177
124
|
nil
|
178
125
|
end
|
179
126
|
|
180
|
-
#
|
181
|
-
#
|
127
|
+
# Wrap the output in an HTML comment.
|
128
|
+
#
|
129
|
+
# [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Comments)
|
182
130
|
def comment(&)
|
183
|
-
|
184
|
-
return
|
131
|
+
state = @_state
|
132
|
+
return unless state.should_render?
|
185
133
|
|
186
|
-
buffer =
|
134
|
+
buffer = state.buffer
|
187
135
|
|
188
136
|
buffer << "<!-- "
|
189
|
-
|
137
|
+
__yield_content__(&)
|
190
138
|
buffer << " -->"
|
191
139
|
|
192
140
|
nil
|
193
141
|
end
|
194
142
|
|
195
|
-
#
|
196
|
-
# @param content [String|nil]
|
197
|
-
# @return [nil]
|
143
|
+
# Output the given safe object as-is. You may need to use `safe` to mark a string as a safe object.
|
198
144
|
def raw(content)
|
199
145
|
case content
|
200
146
|
when Phlex::SGML::SafeObject
|
201
|
-
|
202
|
-
return
|
147
|
+
state = @_state
|
148
|
+
return unless state.should_render?
|
203
149
|
|
204
|
-
|
150
|
+
state.buffer << content.to_s
|
205
151
|
when nil, "" # do nothing
|
206
152
|
else
|
207
153
|
raise Phlex::ArgumentError.new("You passed an unsafe object to `raw`.")
|
@@ -210,71 +156,64 @@ class Phlex::SGML
|
|
210
156
|
nil
|
211
157
|
end
|
212
158
|
|
213
|
-
# Capture
|
214
|
-
# @note This only works if the block's receiver is the current component or the block returns a String.
|
215
|
-
# @return [String]
|
159
|
+
# Capture the output of the block and returns it as a string.
|
216
160
|
def capture(*args, &block)
|
217
161
|
return "" unless block
|
218
162
|
|
219
163
|
if args.length > 0
|
220
|
-
@
|
164
|
+
@_state.capturing_into(+"") { __yield_content_with_args__(*args, &block) }
|
221
165
|
else
|
222
|
-
@
|
166
|
+
@_state.capturing_into(+"") { __yield_content__(&block) }
|
223
167
|
end
|
224
168
|
end
|
225
169
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
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
|
236
|
-
|
237
|
-
if registered_elements[normalized_name]
|
238
|
-
public_send(normalized_name, ...)
|
239
|
-
else
|
240
|
-
raise Phlex::ArgumentError.new("Unknown tag: #{normalized_name}")
|
241
|
-
end
|
170
|
+
# Define a named fragment that can be selectively rendered.
|
171
|
+
def fragment(name)
|
172
|
+
state = @_state
|
173
|
+
state.begin_fragment(name)
|
174
|
+
yield
|
175
|
+
state.end_fragment(name)
|
176
|
+
nil
|
242
177
|
end
|
243
178
|
|
179
|
+
# Mark the given string as safe for HTML output.
|
244
180
|
def safe(value)
|
245
|
-
|
181
|
+
case value
|
182
|
+
when String
|
183
|
+
Phlex::SGML::SafeValue.new(value)
|
184
|
+
else
|
185
|
+
raise Phlex::ArgumentError.new("Expected a String.")
|
186
|
+
end
|
246
187
|
end
|
247
188
|
|
248
189
|
alias_method :🦺, :safe
|
249
190
|
|
250
191
|
def flush
|
251
|
-
|
252
|
-
|
253
|
-
buffer = @_context.buffer
|
254
|
-
@_buffer << buffer.dup
|
255
|
-
buffer.clear
|
192
|
+
@_state.flush
|
256
193
|
end
|
257
194
|
|
258
195
|
def render(renderable = nil, &)
|
259
196
|
case renderable
|
260
197
|
when Phlex::SGML
|
261
|
-
|
198
|
+
Thread.current[:__phlex_component__] = [renderable, Fiber.current.object_id].freeze
|
199
|
+
renderable.internal_call(state: @_state, parent: self, &)
|
200
|
+
Thread.current[:__phlex_component__] = [self, Fiber.current.object_id].freeze
|
262
201
|
when Class
|
263
202
|
if renderable < Phlex::SGML
|
264
|
-
renderable.new
|
203
|
+
render(renderable.new, &)
|
265
204
|
end
|
266
205
|
when Enumerable
|
267
206
|
renderable.each { |r| render(r, &) }
|
268
207
|
when Proc, Method
|
269
208
|
if renderable.arity == 0
|
270
|
-
|
209
|
+
__yield_content_with_no_args__(&renderable)
|
271
210
|
else
|
272
|
-
|
211
|
+
__yield_content__(&renderable)
|
273
212
|
end
|
274
213
|
when String
|
275
214
|
plain(renderable)
|
276
215
|
when nil
|
277
|
-
|
216
|
+
__yield_content__(&) if block_given?
|
278
217
|
else
|
279
218
|
raise Phlex::ArgumentError.new("You can't render a #{renderable.inspect}.")
|
280
219
|
end
|
@@ -282,29 +221,87 @@ class Phlex::SGML
|
|
282
221
|
nil
|
283
222
|
end
|
284
223
|
|
224
|
+
# Cache a block of content.
|
225
|
+
#
|
226
|
+
# ```ruby
|
227
|
+
# @products.each do |product|
|
228
|
+
# cache product do
|
229
|
+
# h1 { product.name }
|
230
|
+
# end
|
231
|
+
# end
|
232
|
+
# ```
|
233
|
+
def cache(*cache_key, **, &content)
|
234
|
+
location = caller_locations(1, 1)[0]
|
235
|
+
|
236
|
+
full_key = [
|
237
|
+
Phlex::DEPLOY_KEY, # invalidates the key when deploying new code in case of changes
|
238
|
+
self.class.name, # prevents collisions between classes
|
239
|
+
location.base_label, # prevents collisions between different methods
|
240
|
+
location.lineno, # prevents collisions between different lines
|
241
|
+
cache_key, # allows for custom cache keys
|
242
|
+
].freeze
|
243
|
+
|
244
|
+
low_level_cache(full_key, **, &content)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Cache a block of content where you control the entire cache key.
|
248
|
+
# If you really know what you’re doing and want to take full control
|
249
|
+
# and responsibility for the cache key, use this method.
|
250
|
+
#
|
251
|
+
# ```ruby
|
252
|
+
# low_level_cache([Commonmarker::VERSION, Digest::MD5.hexdigest(@content)]) do
|
253
|
+
# markdown(@content)
|
254
|
+
# end
|
255
|
+
# ```
|
256
|
+
#
|
257
|
+
# Note: To allow you more control, this method does not take a splat of cache keys.
|
258
|
+
# If you need to pass multiple cache keys, you should pass an array.
|
259
|
+
def low_level_cache(cache_key, **options, &content)
|
260
|
+
state = @_state
|
261
|
+
|
262
|
+
cached_buffer, fragment_map = cache_store.fetch(cache_key, **options) { state.caching(&content) }
|
263
|
+
|
264
|
+
if state.should_render?
|
265
|
+
fragment_map.each do |fragment_name, (offset, length, nested_fragments)|
|
266
|
+
state.record_fragment(fragment_name, offset, length, nested_fragments)
|
267
|
+
end
|
268
|
+
state.buffer << cached_buffer
|
269
|
+
else
|
270
|
+
fragment_map.each do |fragment_name, (offset, length, nested_fragments)|
|
271
|
+
if state.fragments.include?(fragment_name)
|
272
|
+
state.fragments.delete(fragment_name)
|
273
|
+
state.fragments.subtract(nested_fragments)
|
274
|
+
state.buffer << cached_buffer.byteslice(offset, length)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# Points to the cache store used by this component.
|
281
|
+
# By default, it points to `Phlex::NullCacheStore`, which does no caching.
|
282
|
+
# Override this method to use a different cache store.
|
283
|
+
def cache_store
|
284
|
+
Phlex::NullCacheStore
|
285
|
+
end
|
286
|
+
|
285
287
|
private
|
286
288
|
|
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
289
|
def vanish(*args)
|
291
290
|
return unless block_given?
|
292
291
|
|
293
|
-
|
292
|
+
if args.length > 0
|
293
|
+
@_state.capturing_into(Phlex::Vanish) { yield(*args) }
|
294
|
+
else
|
295
|
+
@_state.capturing_into(Phlex::Vanish) { yield(self) }
|
296
|
+
end
|
294
297
|
|
295
298
|
nil
|
296
299
|
end
|
297
300
|
|
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
301
|
def render?
|
302
302
|
true
|
303
303
|
end
|
304
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
305
|
def format_object(object)
|
309
306
|
case object
|
310
307
|
when Float, Integer
|
@@ -312,88 +309,94 @@ class Phlex::SGML
|
|
312
309
|
end
|
313
310
|
end
|
314
311
|
|
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
312
|
def around_template
|
318
|
-
before_template
|
319
313
|
yield
|
320
|
-
after_template
|
321
|
-
|
322
314
|
nil
|
323
315
|
end
|
324
316
|
|
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
317
|
def before_template
|
328
318
|
nil
|
329
319
|
end
|
330
320
|
|
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
321
|
def after_template
|
334
322
|
nil
|
335
323
|
end
|
336
324
|
|
337
|
-
|
338
|
-
# @yieldparam component [self]
|
339
|
-
# @return [nil]
|
340
|
-
def yield_content
|
325
|
+
def __yield_content__
|
341
326
|
return unless block_given?
|
342
327
|
|
343
|
-
buffer = @
|
328
|
+
buffer = @_state.buffer
|
344
329
|
|
345
330
|
original_length = buffer.bytesize
|
346
331
|
content = yield(self)
|
347
|
-
|
332
|
+
__implicit_output__(content) if original_length == buffer.bytesize
|
348
333
|
|
349
334
|
nil
|
350
335
|
end
|
351
336
|
|
352
|
-
|
353
|
-
# @yield Yields the block with no arguments.
|
354
|
-
def yield_content_with_no_args
|
337
|
+
def __yield_content_with_no_args__
|
355
338
|
return unless block_given?
|
356
339
|
|
357
|
-
buffer = @
|
340
|
+
buffer = @_state.buffer
|
358
341
|
|
359
342
|
original_length = buffer.bytesize
|
360
343
|
content = yield
|
361
|
-
|
344
|
+
__implicit_output__(content) if original_length == buffer.bytesize
|
362
345
|
|
363
346
|
nil
|
364
347
|
end
|
365
348
|
|
366
|
-
|
367
|
-
# @yield [*args] Yields the given arguments.
|
368
|
-
# @return [nil]
|
369
|
-
def yield_content_with_args(*)
|
349
|
+
def __yield_content_with_args__(*a)
|
370
350
|
return unless block_given?
|
371
351
|
|
372
|
-
buffer = @
|
352
|
+
buffer = @_state.buffer
|
373
353
|
|
374
354
|
original_length = buffer.bytesize
|
375
|
-
content = yield(*)
|
376
|
-
|
355
|
+
content = yield(*a)
|
356
|
+
__implicit_output__(content) if original_length == buffer.bytesize
|
377
357
|
|
378
358
|
nil
|
379
359
|
end
|
380
360
|
|
381
|
-
|
382
|
-
|
361
|
+
def __implicit_output__(content)
|
362
|
+
state = @_state
|
363
|
+
return true unless state.should_render?
|
364
|
+
|
365
|
+
case content
|
366
|
+
when Phlex::SGML::SafeObject
|
367
|
+
state.buffer << content.to_s
|
368
|
+
when String
|
369
|
+
state.buffer << Phlex::Escape.html_escape(content)
|
370
|
+
when Symbol
|
371
|
+
state.buffer << Phlex::Escape.html_escape(content.name)
|
372
|
+
when nil
|
373
|
+
nil
|
374
|
+
else
|
375
|
+
if (formatted_object = format_object(content))
|
376
|
+
state.buffer << Phlex::Escape.html_escape(formatted_object)
|
377
|
+
else
|
378
|
+
return false
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
true
|
383
|
+
end
|
384
|
+
|
385
|
+
# same as __implicit_output__ but escapes even `safe` objects
|
383
386
|
def __text__(content)
|
384
|
-
|
385
|
-
return true
|
387
|
+
state = @_state
|
388
|
+
return true unless state.should_render?
|
386
389
|
|
387
390
|
case content
|
388
391
|
when String
|
389
|
-
|
392
|
+
state.buffer << Phlex::Escape.html_escape(content)
|
390
393
|
when Symbol
|
391
|
-
|
394
|
+
state.buffer << Phlex::Escape.html_escape(content.name)
|
392
395
|
when nil
|
393
396
|
nil
|
394
397
|
else
|
395
398
|
if (formatted_object = format_object(content))
|
396
|
-
|
399
|
+
state.buffer << Phlex::Escape.html_escape(formatted_object)
|
397
400
|
else
|
398
401
|
return false
|
399
402
|
end
|
@@ -402,7 +405,6 @@ class Phlex::SGML
|
|
402
405
|
true
|
403
406
|
end
|
404
407
|
|
405
|
-
# @api private
|
406
408
|
def __attributes__(attributes, buffer = +"")
|
407
409
|
attributes.each do |k, v|
|
408
410
|
next unless v
|
@@ -413,78 +415,87 @@ class Phlex::SGML
|
|
413
415
|
else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols.")
|
414
416
|
end
|
415
417
|
|
416
|
-
|
417
|
-
|
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
|
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
|
418
|
+
value = case v
|
438
419
|
when true
|
439
|
-
|
420
|
+
true
|
440
421
|
when String
|
441
|
-
|
422
|
+
v.gsub('"', """)
|
442
423
|
when Symbol
|
443
|
-
|
424
|
+
v.name.tr("_", "-").gsub('"', """)
|
444
425
|
when Integer, Float
|
445
|
-
|
426
|
+
v.to_s
|
446
427
|
when Hash
|
447
428
|
case k
|
448
|
-
when :class
|
449
|
-
buffer << " " << name << '="' << __classes__(v).gsub('"', """) << '"'
|
450
429
|
when :style
|
451
|
-
|
430
|
+
__styles__(v).gsub('"', """)
|
452
431
|
else
|
453
432
|
__nested_attributes__(v, "#{name}-", buffer)
|
454
433
|
end
|
455
434
|
when Array
|
456
|
-
|
457
|
-
when :class
|
458
|
-
__classes__(v)
|
435
|
+
case k
|
459
436
|
when :style
|
460
|
-
__styles__(v)
|
437
|
+
__styles__(v).gsub('"', """)
|
461
438
|
else
|
462
439
|
__nested_tokens__(v)
|
463
440
|
end
|
464
|
-
|
465
|
-
buffer << " " << name << '="' << value.gsub('"', """) << '"'
|
466
441
|
when Set
|
467
|
-
|
442
|
+
case k
|
443
|
+
when :style
|
444
|
+
__styles__(v).gsub('"', """)
|
445
|
+
else
|
446
|
+
__nested_tokens__(v.to_a)
|
447
|
+
end
|
468
448
|
when Phlex::SGML::SafeObject
|
469
|
-
|
449
|
+
v.to_s.gsub('"', """)
|
470
450
|
else
|
471
|
-
value
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
451
|
+
raise Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
|
452
|
+
end
|
453
|
+
|
454
|
+
lower_name = name.downcase
|
455
|
+
|
456
|
+
unless Phlex::SGML::SafeObject === v
|
457
|
+
normalized_name = lower_name.delete("^a-z-")
|
458
|
+
|
459
|
+
if value != true && REF_ATTRIBUTES.include?(normalized_name)
|
460
|
+
case value
|
461
|
+
when String
|
462
|
+
if value.downcase.delete("^a-z:").start_with?("javascript:")
|
463
|
+
# We just ignore these because they were likely not specified by the developer.
|
464
|
+
next
|
465
|
+
end
|
466
|
+
else
|
467
|
+
raise Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
if normalized_name.bytesize > 2 && normalized_name.start_with?("on") && !normalized_name.include?("-")
|
472
|
+
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
473
|
+
end
|
474
|
+
|
475
|
+
if UNSAFE_ATTRIBUTES.include?(normalized_name)
|
476
|
+
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
477
477
|
end
|
478
|
+
end
|
479
|
+
|
480
|
+
if name.match?(/[<>&"']/)
|
481
|
+
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
482
|
+
end
|
483
|
+
|
484
|
+
if lower_name.to_sym == :id && k != :id
|
485
|
+
raise Phlex::ArgumentError.new(":id attribute should only be passed as a lowercase symbol.")
|
486
|
+
end
|
478
487
|
|
479
|
-
|
488
|
+
case value
|
489
|
+
when true
|
490
|
+
buffer << " " << name
|
491
|
+
when String
|
492
|
+
buffer << " " << name << '="' << value << '"'
|
480
493
|
end
|
481
494
|
end
|
482
495
|
|
483
496
|
buffer
|
484
497
|
end
|
485
498
|
|
486
|
-
# @api private
|
487
|
-
#
|
488
499
|
# Provides the nested-attributes case for serializing out attributes.
|
489
500
|
# This allows us to skip many of the checks the `__attributes__` method must perform.
|
490
501
|
def __nested_attributes__(attributes, base_name, buffer = +"")
|
@@ -497,6 +508,10 @@ class Phlex::SGML
|
|
497
508
|
else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols")
|
498
509
|
end
|
499
510
|
|
511
|
+
if name.match?(/[<>&"']/)
|
512
|
+
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
513
|
+
end
|
514
|
+
|
500
515
|
case v
|
501
516
|
when true
|
502
517
|
buffer << " " << base_name << name
|
@@ -512,22 +527,16 @@ class Phlex::SGML
|
|
512
527
|
buffer << " " << base_name << name << '="' << __nested_tokens__(v) << '"'
|
513
528
|
when Set
|
514
529
|
buffer << " " << base_name << name << '="' << __nested_tokens__(v.to_a) << '"'
|
530
|
+
when Phlex::SGML::SafeObject
|
531
|
+
buffer << " " << base_name << name << '="' << v.to_s.gsub('"', """) << '"'
|
515
532
|
else
|
516
|
-
|
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('"', """) << '"'
|
533
|
+
raise Phlex::ArgumentError.new("Invalid attribute value #{v.inspect}.")
|
524
534
|
end
|
525
535
|
|
526
536
|
buffer
|
527
537
|
end
|
528
538
|
end
|
529
539
|
|
530
|
-
# @api private
|
531
540
|
def __nested_tokens__(tokens)
|
532
541
|
buffer = +""
|
533
542
|
|
@@ -549,93 +558,105 @@ class Phlex::SGML
|
|
549
558
|
else
|
550
559
|
buffer << token.name.tr("_", "-")
|
551
560
|
end
|
552
|
-
when
|
553
|
-
# Do nothing
|
554
|
-
else
|
561
|
+
when Integer, Float, Phlex::SGML::SafeObject
|
555
562
|
if i > 0
|
556
563
|
buffer << " " << token.to_s
|
557
564
|
else
|
558
565
|
buffer << token.to_s
|
559
566
|
end
|
567
|
+
when Array
|
568
|
+
if token.length > 0
|
569
|
+
if i > 0
|
570
|
+
buffer << " " << __nested_tokens__(token)
|
571
|
+
else
|
572
|
+
buffer << __nested_tokens__(token)
|
573
|
+
end
|
574
|
+
end
|
575
|
+
when nil
|
576
|
+
# Do nothing
|
577
|
+
else
|
578
|
+
raise Phlex::ArgumentError.new("Invalid token type: #{token.class}.")
|
560
579
|
end
|
561
580
|
|
562
581
|
i += 1
|
563
582
|
end
|
564
583
|
|
565
|
-
buffer.gsub
|
566
|
-
buffer
|
584
|
+
buffer.gsub('"', """)
|
567
585
|
end
|
568
586
|
|
569
|
-
#
|
570
|
-
def
|
571
|
-
case
|
572
|
-
when String
|
573
|
-
c
|
574
|
-
when Symbol
|
575
|
-
c.name.tr("_", "-")
|
587
|
+
# Result is **unsafe**, so it should be escaped!
|
588
|
+
def __styles__(styles)
|
589
|
+
case styles
|
576
590
|
when Array, Set
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
591
|
+
styles.filter_map do |s|
|
592
|
+
case s
|
593
|
+
when String
|
594
|
+
if s == "" || s.end_with?(";")
|
595
|
+
s
|
596
|
+
else
|
597
|
+
"#{s};"
|
598
|
+
end
|
599
|
+
when Phlex::SGML::SafeObject
|
600
|
+
value = s.to_s
|
601
|
+
value.end_with?(";") ? value : "#{value};"
|
602
|
+
when Hash
|
603
|
+
next __styles__(s)
|
604
|
+
when nil
|
605
|
+
next nil
|
606
|
+
else
|
607
|
+
raise Phlex::ArgumentError.new("Invalid style: #{s.inspect}.")
|
585
608
|
end
|
586
|
-
|
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
|
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
|
609
|
+
end.join(" ")
|
611
610
|
when Hash
|
612
611
|
buffer = +""
|
613
|
-
|
612
|
+
i = 0
|
613
|
+
styles.each do |k, v|
|
614
614
|
prop = case k
|
615
|
-
|
616
|
-
|
617
|
-
|
615
|
+
when String
|
616
|
+
k
|
617
|
+
when Symbol
|
618
|
+
k.name.tr("_", "-")
|
619
|
+
else
|
620
|
+
raise Phlex::ArgumentError.new("Style keys should be Strings or Symbols.")
|
618
621
|
end
|
619
622
|
|
620
|
-
value =
|
623
|
+
value = case v
|
624
|
+
when String
|
625
|
+
v
|
626
|
+
when Symbol
|
627
|
+
v.name.tr("_", "-")
|
628
|
+
when Integer, Float, Phlex::SGML::SafeObject
|
629
|
+
v.to_s
|
630
|
+
when nil
|
631
|
+
nil
|
632
|
+
else
|
633
|
+
raise Phlex::ArgumentError.new("Invalid style value: #{v.inspect}")
|
634
|
+
end
|
621
635
|
|
622
636
|
if value
|
623
|
-
|
637
|
+
if i == 0
|
638
|
+
buffer << prop << ": " << value << ";"
|
639
|
+
else
|
640
|
+
buffer << " " << prop << ": " << value << ";"
|
641
|
+
end
|
624
642
|
end
|
643
|
+
|
644
|
+
i += 1
|
625
645
|
end
|
646
|
+
|
626
647
|
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
|
637
648
|
end
|
649
|
+
end
|
650
|
+
|
651
|
+
private_class_method def self.method_added(method_name)
|
652
|
+
if method_name == :view_template
|
653
|
+
location = instance_method(method_name).source_location[0]
|
638
654
|
|
639
|
-
|
655
|
+
if location[0] in "/" | "."
|
656
|
+
Phlex.__expand_attribute_cache__(location)
|
657
|
+
end
|
658
|
+
else
|
659
|
+
super
|
660
|
+
end
|
640
661
|
end
|
641
662
|
end
|