phlex 2.0.0.rc1 → 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.
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