phlex 2.0.0.rc1 → 2.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/phlex/sgml.rb CHANGED
@@ -8,6 +8,7 @@ class Phlex::SGML
8
8
  autoload :Elements, "phlex/sgml/elements"
9
9
  autoload :SafeObject, "phlex/sgml/safe_object"
10
10
  autoload :SafeValue, "phlex/sgml/safe_value"
11
+ autoload :State, "phlex/sgml/state"
11
12
 
12
13
  include Phlex::Helpers
13
14
 
@@ -28,25 +29,13 @@ class Phlex::SGML
28
29
  super
29
30
  end
30
31
  end
31
-
32
- def __element_method__?(method_name)
33
- if instance_methods.include?(method_name)
34
- owner = instance_method(method_name).owner
35
-
36
- if Phlex::SGML::Elements === owner && owner.__registered_elements__[method_name]
37
- true
38
- else
39
- false
40
- end
41
- else
42
- false
43
- end
44
- end
45
32
  end
46
33
 
47
34
  def view_template
48
35
  if block_given?
49
36
  yield
37
+ else
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"
50
39
  end
51
40
  end
52
41
 
@@ -54,25 +43,35 @@ class Phlex::SGML
54
43
  proc { |c| c.render(self) }
55
44
  end
56
45
 
57
- def call(buffer = +"", context: {}, view_context: nil, parent: nil, fragments: nil, &block)
58
- @_buffer = buffer
59
- @_context = phlex_context = parent&.__context__ || Phlex::Context.new(user_context: context, view_context:)
60
- @_parent = parent
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
+ )
53
+
54
+ internal_call(parent: nil, state:, &)
55
+
56
+ state.output_buffer << state.buffer
57
+ end
61
58
 
62
- raise Phlex::DoubleRenderError.new("You can't render a #{self.class.name} more than once.") if @_rendered
63
- @_rendered = true
59
+ def internal_call(parent: nil, state: nil, &block)
60
+ return "" unless render?
64
61
 
65
- if fragments
66
- phlex_context.target_fragments(fragments)
62
+ if @_state
63
+ raise Phlex::DoubleRenderError.new(
64
+ "You can't render a #{self.class.name} more than once."
65
+ )
67
66
  end
68
67
 
69
- block ||= @_content_block
68
+ @_state = state
70
69
 
71
- return "" unless render?
70
+ block ||= @_content_block
72
71
 
73
- Thread.current[:__phlex_component__] = [self, Fiber.current.object_id]
72
+ Thread.current[:__phlex_component__] = [self, Fiber.current.object_id].freeze
74
73
 
75
- phlex_context.around_render do
74
+ state.around_render(self) do
76
75
  before_template(&block)
77
76
 
78
77
  around_template do
@@ -91,18 +90,12 @@ class Phlex::SGML
91
90
 
92
91
  after_template(&block)
93
92
  end
94
-
95
- unless parent
96
- buffer << phlex_context.buffer
97
- end
98
93
  ensure
99
- Thread.current[:__phlex_component__] = [parent, Fiber.current.object_id]
94
+ Thread.current[:__phlex_component__] = [parent, Fiber.current.object_id].freeze
100
95
  end
101
96
 
102
- protected def __context__ = @_context
103
-
104
97
  def context
105
- @_context.user_context
98
+ @_state.user_context
106
99
  end
107
100
 
108
101
  # Output plain text.
@@ -116,10 +109,10 @@ class Phlex::SGML
116
109
 
117
110
  # Output a single space character. If a block is given, a space will be output before and after the block.
118
111
  def whitespace(&)
119
- context = @_context
120
- return if context.fragments && !context.in_target_fragment
112
+ state = @_state
113
+ return unless state.should_render?
121
114
 
122
- buffer = context.buffer
115
+ buffer = state.buffer
123
116
 
124
117
  buffer << " "
125
118
 
@@ -135,10 +128,10 @@ class Phlex::SGML
135
128
  #
136
129
  # [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Comments)
137
130
  def comment(&)
138
- context = @_context
139
- return if context.fragments && !context.in_target_fragment
131
+ state = @_state
132
+ return unless state.should_render?
140
133
 
141
- buffer = context.buffer
134
+ buffer = state.buffer
142
135
 
143
136
  buffer << "<!-- "
144
137
  __yield_content__(&)
@@ -151,10 +144,10 @@ class Phlex::SGML
151
144
  def raw(content)
152
145
  case content
153
146
  when Phlex::SGML::SafeObject
154
- context = @_context
155
- return if context.fragments && !context.in_target_fragment
147
+ state = @_state
148
+ return unless state.should_render?
156
149
 
157
- context.buffer << content.to_s
150
+ state.buffer << content.to_s
158
151
  when nil, "" # do nothing
159
152
  else
160
153
  raise Phlex::ArgumentError.new("You passed an unsafe object to `raw`.")
@@ -168,12 +161,21 @@ class Phlex::SGML
168
161
  return "" unless block
169
162
 
170
163
  if args.length > 0
171
- @_context.capturing_into(+"") { __yield_content_with_args__(*args, &block) }
164
+ @_state.capturing_into(+"") { __yield_content_with_args__(*args, &block) }
172
165
  else
173
- @_context.capturing_into(+"") { __yield_content__(&block) }
166
+ @_state.capturing_into(+"") { __yield_content__(&block) }
174
167
  end
175
168
  end
176
169
 
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
177
+ end
178
+
177
179
  # Mark the given string as safe for HTML output.
178
180
  def safe(value)
179
181
  case value
@@ -187,20 +189,18 @@ class Phlex::SGML
187
189
  alias_method :🦺, :safe
188
190
 
189
191
  def flush
190
- return if @_context.capturing
191
-
192
- buffer = @_context.buffer
193
- @_buffer << buffer.dup
194
- buffer.clear
192
+ @_state.flush
195
193
  end
196
194
 
197
195
  def render(renderable = nil, &)
198
196
  case renderable
199
197
  when Phlex::SGML
200
- renderable.call(@_buffer, parent: self, &)
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
201
201
  when Class
202
202
  if renderable < Phlex::SGML
203
- renderable.new.call(@_buffer, parent: self, &)
203
+ render(renderable.new, &)
204
204
  end
205
205
  when Enumerable
206
206
  renderable.each { |r| render(r, &) }
@@ -221,15 +221,78 @@ class Phlex::SGML
221
221
  nil
222
222
  end
223
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
+
224
287
  private
225
288
 
226
289
  def vanish(*args)
227
290
  return unless block_given?
228
291
 
229
292
  if args.length > 0
230
- @_context.capturing_into(Phlex::Vanish) { yield(*args) }
293
+ @_state.capturing_into(Phlex::Vanish) { yield(*args) }
231
294
  else
232
- @_context.capturing_into(Phlex::Vanish) { yield(self) }
295
+ @_state.capturing_into(Phlex::Vanish) { yield(self) }
233
296
  end
234
297
 
235
298
  nil
@@ -262,7 +325,7 @@ class Phlex::SGML
262
325
  def __yield_content__
263
326
  return unless block_given?
264
327
 
265
- buffer = @_context.buffer
328
+ buffer = @_state.buffer
266
329
 
267
330
  original_length = buffer.bytesize
268
331
  content = yield(self)
@@ -274,7 +337,7 @@ class Phlex::SGML
274
337
  def __yield_content_with_no_args__
275
338
  return unless block_given?
276
339
 
277
- buffer = @_context.buffer
340
+ buffer = @_state.buffer
278
341
 
279
342
  original_length = buffer.bytesize
280
343
  content = yield
@@ -286,7 +349,7 @@ class Phlex::SGML
286
349
  def __yield_content_with_args__(*a)
287
350
  return unless block_given?
288
351
 
289
- buffer = @_context.buffer
352
+ buffer = @_state.buffer
290
353
 
291
354
  original_length = buffer.bytesize
292
355
  content = yield(*a)
@@ -296,21 +359,21 @@ class Phlex::SGML
296
359
  end
297
360
 
298
361
  def __implicit_output__(content)
299
- context = @_context
300
- return true if context.fragments && !context.in_target_fragment
362
+ state = @_state
363
+ return true unless state.should_render?
301
364
 
302
365
  case content
303
366
  when Phlex::SGML::SafeObject
304
- context.buffer << content.to_s
367
+ state.buffer << content.to_s
305
368
  when String
306
- context.buffer << Phlex::Escape.html_escape(content)
369
+ state.buffer << Phlex::Escape.html_escape(content)
307
370
  when Symbol
308
- context.buffer << Phlex::Escape.html_escape(content.name)
371
+ state.buffer << Phlex::Escape.html_escape(content.name)
309
372
  when nil
310
373
  nil
311
374
  else
312
375
  if (formatted_object = format_object(content))
313
- context.buffer << Phlex::Escape.html_escape(formatted_object)
376
+ state.buffer << Phlex::Escape.html_escape(formatted_object)
314
377
  else
315
378
  return false
316
379
  end
@@ -321,19 +384,19 @@ class Phlex::SGML
321
384
 
322
385
  # same as __implicit_output__ but escapes even `safe` objects
323
386
  def __text__(content)
324
- context = @_context
325
- return true if context.fragments && !context.in_target_fragment
387
+ state = @_state
388
+ return true unless state.should_render?
326
389
 
327
390
  case content
328
391
  when String
329
- context.buffer << Phlex::Escape.html_escape(content)
392
+ state.buffer << Phlex::Escape.html_escape(content)
330
393
  when Symbol
331
- context.buffer << Phlex::Escape.html_escape(content.name)
394
+ state.buffer << Phlex::Escape.html_escape(content.name)
332
395
  when nil
333
396
  nil
334
397
  else
335
398
  if (formatted_object = format_object(content))
336
- context.buffer << Phlex::Escape.html_escape(formatted_object)
399
+ state.buffer << Phlex::Escape.html_escape(formatted_object)
337
400
  else
338
401
  return false
339
402
  end