pakyow-presenter 0.11.3 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +5 -5
  2. data/{pakyow-presenter/CHANGELOG.md → CHANGELOG.md} +0 -5
  3. data/LICENSE +4 -0
  4. data/{pakyow-presenter/README.md → README.md} +1 -2
  5. data/lib/pakyow/generators/presenter/presenter_generator.rb +28 -0
  6. data/lib/pakyow/generators/presenter/templates/presenter.rb.tt +2 -0
  7. data/lib/pakyow/plugin/helpers/rendering.rb +19 -0
  8. data/lib/pakyow/presenter/actions/auto_render.rb +15 -0
  9. data/lib/pakyow/presenter/attributes/attribute.rb +22 -0
  10. data/lib/pakyow/presenter/attributes/boolean.rb +24 -0
  11. data/lib/pakyow/presenter/attributes/hash.rb +89 -0
  12. data/lib/pakyow/presenter/attributes/set.rb +61 -0
  13. data/lib/pakyow/presenter/attributes/string.rb +38 -0
  14. data/lib/pakyow/presenter/attributes.rb +126 -0
  15. data/lib/pakyow/presenter/behavior/config.rb +29 -0
  16. data/lib/pakyow/presenter/behavior/error_rendering.rb +124 -0
  17. data/lib/pakyow/presenter/behavior/exposures.rb +29 -0
  18. data/lib/pakyow/presenter/behavior/implicit_rendering.rb +21 -0
  19. data/lib/pakyow/presenter/behavior/initializing.rb +37 -0
  20. data/lib/pakyow/presenter/behavior/modes.rb +117 -0
  21. data/lib/pakyow/presenter/behavior/versions.rb +71 -0
  22. data/lib/pakyow/presenter/behavior/watching.rb +21 -0
  23. data/lib/pakyow/presenter/binder.rb +115 -0
  24. data/lib/pakyow/presenter/binding_parts.rb +64 -0
  25. data/lib/pakyow/presenter/component.rb +74 -0
  26. data/lib/pakyow/presenter/composers/component.rb +78 -0
  27. data/lib/pakyow/presenter/composers/view.rb +73 -0
  28. data/lib/pakyow/presenter/errors.rb +63 -0
  29. data/lib/pakyow/presenter/framework.rb +203 -0
  30. data/lib/pakyow/presenter/front_matter_parser.rb +42 -0
  31. data/lib/pakyow/presenter/helpers/rendering.rb +18 -0
  32. data/lib/pakyow/presenter/presentable.rb +28 -0
  33. data/lib/pakyow/presenter/presentable_error.rb +19 -0
  34. data/lib/pakyow/presenter/presenter.rb +766 -0
  35. data/lib/pakyow/presenter/presenters/endpoint.rb +158 -0
  36. data/lib/pakyow/presenter/presenters/form.rb +522 -0
  37. data/lib/pakyow/presenter/processor.rb +61 -0
  38. data/lib/pakyow/presenter/renderable.rb +28 -0
  39. data/lib/pakyow/presenter/renderer.rb +269 -0
  40. data/lib/pakyow/presenter/rendering/actions/cleanup_prototype_nodes.rb +21 -0
  41. data/lib/pakyow/presenter/rendering/actions/cleanup_unbound_bindings.rb +35 -0
  42. data/lib/pakyow/presenter/rendering/actions/create_template_nodes.rb +27 -0
  43. data/lib/pakyow/presenter/rendering/actions/insert_prototype_bar.rb +101 -0
  44. data/lib/pakyow/presenter/rendering/actions/install_authenticity.rb +42 -0
  45. data/lib/pakyow/presenter/rendering/actions/place_in_mode.rb +56 -0
  46. data/lib/pakyow/presenter/rendering/actions/render_components.rb +279 -0
  47. data/lib/pakyow/presenter/rendering/actions/set_page_title.rb +35 -0
  48. data/lib/pakyow/presenter/rendering/actions/setup_endpoints.rb +62 -0
  49. data/lib/pakyow/presenter/rendering/actions/setup_forms.rb +174 -0
  50. data/lib/pakyow/presenter/significant_nodes.rb +309 -0
  51. data/lib/pakyow/presenter/templates.rb +229 -0
  52. data/lib/pakyow/presenter/versioned_view.rb +209 -0
  53. data/lib/pakyow/presenter/view.rb +586 -0
  54. data/lib/pakyow/presenter/views/form.rb +58 -0
  55. data/lib/pakyow/presenter/views/layout.rb +32 -0
  56. data/lib/pakyow/presenter/views/page.rb +72 -0
  57. data/lib/pakyow/presenter/views/partial.rb +28 -0
  58. data/lib/pakyow/presenter.rb +33 -0
  59. data/lib/pakyow/views/errors/layouts/development_error.html +102 -0
  60. data/lib/pakyow/views/errors/layouts/production_error.html +27 -0
  61. data/lib/pakyow/views/errors/pages/404.html +12 -0
  62. data/lib/pakyow/views/errors/pages/500.html +12 -0
  63. data/lib/pakyow/views/errors/pages/development/500.html +39 -0
  64. data/lib/string_doc/attributes.rb +109 -0
  65. data/lib/string_doc/meta_attributes.rb +48 -0
  66. data/lib/string_doc/meta_node.rb +328 -0
  67. data/lib/string_doc/node.rb +377 -0
  68. data/lib/string_doc.rb +642 -0
  69. metadata +95 -75
  70. data/pakyow-presenter/LICENSE +0 -20
  71. data/pakyow-presenter/lib/pakyow/presenter/attributes.rb +0 -228
  72. data/pakyow-presenter/lib/pakyow/presenter/base.rb +0 -38
  73. data/pakyow-presenter/lib/pakyow/presenter/binder.rb +0 -116
  74. data/pakyow-presenter/lib/pakyow/presenter/binder_set.rb +0 -94
  75. data/pakyow-presenter/lib/pakyow/presenter/binding_eval.rb +0 -37
  76. data/pakyow-presenter/lib/pakyow/presenter/config/presenter.rb +0 -42
  77. data/pakyow-presenter/lib/pakyow/presenter/container.rb +0 -6
  78. data/pakyow-presenter/lib/pakyow/presenter/doc_helpers.rb +0 -17
  79. data/pakyow-presenter/lib/pakyow/presenter/exceptions.rb +0 -11
  80. data/pakyow-presenter/lib/pakyow/presenter/ext/app.rb +0 -33
  81. data/pakyow-presenter/lib/pakyow/presenter/ext/call_context.rb +0 -28
  82. data/pakyow-presenter/lib/pakyow/presenter/helpers.rb +0 -46
  83. data/pakyow-presenter/lib/pakyow/presenter/page.rb +0 -110
  84. data/pakyow-presenter/lib/pakyow/presenter/partial.rb +0 -6
  85. data/pakyow-presenter/lib/pakyow/presenter/presenter.rb +0 -232
  86. data/pakyow-presenter/lib/pakyow/presenter/string_doc.rb +0 -380
  87. data/pakyow-presenter/lib/pakyow/presenter/string_doc_parser.rb +0 -144
  88. data/pakyow-presenter/lib/pakyow/presenter/string_doc_renderer.rb +0 -18
  89. data/pakyow-presenter/lib/pakyow/presenter/template.rb +0 -51
  90. data/pakyow-presenter/lib/pakyow/presenter/view.rb +0 -541
  91. data/pakyow-presenter/lib/pakyow/presenter/view_collection.rb +0 -330
  92. data/pakyow-presenter/lib/pakyow/presenter/view_composer.rb +0 -237
  93. data/pakyow-presenter/lib/pakyow/presenter/view_context.rb +0 -111
  94. data/pakyow-presenter/lib/pakyow/presenter/view_store.rb +0 -262
  95. data/pakyow-presenter/lib/pakyow/presenter/view_store_loader.rb +0 -43
  96. data/pakyow-presenter/lib/pakyow/presenter/view_version.rb +0 -113
  97. data/pakyow-presenter/lib/pakyow/presenter.rb +0 -8
  98. data/pakyow-presenter/lib/pakyow/views/errors/404.erb +0 -26
  99. data/pakyow-presenter/lib/pakyow/views/errors/500.erb +0 -23
  100. data/pakyow-presenter/lib/pakyow-presenter.rb +0 -1
@@ -0,0 +1,766 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ require "string_doc/meta_node"
6
+
7
+ require "pakyow/support/core_refinements/array/ensurable"
8
+ require "pakyow/support/core_refinements/string/normalization"
9
+
10
+ require "pakyow/support/class_state"
11
+ require "pakyow/support/safe_string"
12
+ require "pakyow/support/string_builder"
13
+
14
+ require "pakyow/presenter/presentable"
15
+
16
+ require "pakyow/presenter/presenters/endpoint"
17
+
18
+ module Pakyow
19
+ module Presenter
20
+ # Presents a view object with dynamic state in context of an app instance. In normal usage you
21
+ # will be interacting with presenters rather than the {View} directly.
22
+ #
23
+ class Presenter
24
+ extend Support::Makeable
25
+ extend Support::ClassState
26
+ class_state :__attached_renders, default: [], inheritable: true
27
+ class_state :__global_options, default: {}, inheritable: true
28
+ class_state :__presentation_logic, default: {}, inheritable: true
29
+ class_state :__versioning_logic, default: {}, inheritable: true
30
+
31
+ using Support::Refinements::Array::Ensurable
32
+
33
+ include Support::SafeStringHelpers
34
+
35
+ include Presentable
36
+
37
+ # The view object being presented.
38
+ #
39
+ attr_reader :view
40
+
41
+ # The logger object.
42
+ #
43
+ attr_reader :logger
44
+
45
+ # Values to be presented.
46
+ #
47
+ attr_reader :presentables
48
+
49
+ # The app object.
50
+ #
51
+ attr_reader :app
52
+
53
+ def initialize(view, app:, presentables: {})
54
+ @app, @view, @presentables = app, view, presentables
55
+ @logger = Pakyow.logger
56
+ @called = false
57
+ end
58
+
59
+ # Returns a presenter for a view binding.
60
+ #
61
+ # @see View#find
62
+ def find(*names)
63
+ result = if found_view = @view.find(*names)
64
+ presenter_for(found_view)
65
+ else
66
+ nil
67
+ end
68
+
69
+ if result && block_given?
70
+ yield result
71
+ end
72
+
73
+ result
74
+ end
75
+
76
+ # Returns an array of presenters, one for each view binding.
77
+ #
78
+ # @see View#find_all
79
+ def find_all(*names)
80
+ @view.find_all(*names).map { |view|
81
+ presenter_for(view)
82
+ }
83
+ end
84
+
85
+ # Returns the named form from the view being presented.
86
+ #
87
+ def form(name)
88
+ if found_form = @view.form(name)
89
+ presenter_for(found_form)
90
+ else
91
+ nil
92
+ end
93
+ end
94
+
95
+ # Returns all forms.
96
+ #
97
+ def forms
98
+ @view.forms.map { |form|
99
+ presenter_for(form)
100
+ }
101
+ end
102
+
103
+ # Returns the component matching +name+.
104
+ #
105
+ def component(name)
106
+ if found_component = @view.component(name)
107
+ presenter_for(found_component)
108
+ else
109
+ nil
110
+ end
111
+ end
112
+
113
+ # Returns all components.
114
+ #
115
+ def components(renderable: false)
116
+ @view.components(renderable: renderable).map { |component|
117
+ presenter_for(component)
118
+ }
119
+ end
120
+
121
+ # Returns the title value from the view being presented.
122
+ #
123
+ def title
124
+ @view.title&.text
125
+ end
126
+
127
+ # Sets the title value on the view.
128
+ #
129
+ def title=(value)
130
+ unless @view.title
131
+ if head_view = @view.head
132
+ title_view = View.new("<title></title>")
133
+ head_view.append(title_view)
134
+ end
135
+ end
136
+
137
+ @view.title&.html = strip_tags(value)
138
+ end
139
+
140
+ # Uses the view matching +version+, removing all other versions.
141
+ #
142
+ def use(version)
143
+ @view.use(version)
144
+ self
145
+ end
146
+
147
+ # Returns a presenter for the view matching +version+.
148
+ #
149
+ def versioned(version)
150
+ presenter_for(@view.versioned(version))
151
+ end
152
+
153
+ # Yields +self+.
154
+ #
155
+ def with
156
+ tap do
157
+ yield self
158
+ end
159
+ end
160
+
161
+ # Transforms the view to match +data+.
162
+ #
163
+ # @see View#transform
164
+ #
165
+ def transform(data, yield_binder = false)
166
+ tap do
167
+ data = Array.ensure(data).reject(&:nil?)
168
+
169
+ if data.respond_to?(:empty?) && data.empty?
170
+ if @view.is_a?(VersionedView) && @view.version?(:empty)
171
+ @view.use(:empty); @view.object.set_label(:bound, true)
172
+ else
173
+ remove
174
+ end
175
+ else
176
+ template = @view.soft_copy
177
+ insertable = @view
178
+ current = @view
179
+
180
+ data.each do |object|
181
+ binder = binder_or_data(object)
182
+
183
+ current.transform(binder)
184
+
185
+ if block_given?
186
+ yield presenter_for(current), yield_binder ? binder : object
187
+ end
188
+
189
+ unless current.equal?(@view)
190
+ insertable.after(current)
191
+ insertable = current
192
+ end
193
+
194
+ current = template.soft_copy
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ # Binds +data+ to the view, using the appropriate binder if available.
201
+ #
202
+ def bind(data)
203
+ tap do
204
+ data = binder_or_data(data)
205
+
206
+ if data.is_a?(Binder)
207
+ bind_binder_to_view(data, @view)
208
+ else
209
+ @view.bind(data)
210
+ end
211
+
212
+ set_binding_info(data)
213
+ set_endpoint_params(data)
214
+ end
215
+ end
216
+
217
+ # Transforms the view to match +data+, then binds, using the appropriate binder if available.
218
+ #
219
+ # @see View#present
220
+ #
221
+ def present(data)
222
+ tap do
223
+ transform(data, true) do |presenter, binder|
224
+ if block_given?
225
+ yield presenter, binder.object
226
+ end
227
+
228
+ unless presenter.view.object.labeled?(:bound) || self.class.__presentation_logic.empty?
229
+ self.class.__presentation_logic[presenter.view.channeled_binding_name].to_a.each do |presentation_logic|
230
+ presenter.instance_exec(binder.object, &presentation_logic[:block])
231
+ end
232
+ end
233
+
234
+ if presenter.view.is_a?(VersionedView)
235
+ unless presenter.view.used? || self.class.__versioning_logic.empty?
236
+ # Use global versions.
237
+ #
238
+ presenter.view.names.each do |version|
239
+ self.class.__versioning_logic[version]&.each do |logic|
240
+ if presenter.instance_exec(binder.object, &logic[:block])
241
+ presenter.use(version); break
242
+ end
243
+ end
244
+ end
245
+ end
246
+
247
+ # If we still haven't used a version, use one implicitly.
248
+ #
249
+ unless presenter.view.used?
250
+ presenter.use_implicit_version
251
+ end
252
+
253
+ used_view = case presenter.view.object
254
+ when StringDoc::MetaNode
255
+ View.from_object(
256
+ presenter.view.object.nodes.find { |node|
257
+ node.labeled?(:versioned)
258
+ }
259
+ )
260
+ else
261
+ presenter.view.versions.find { |version|
262
+ version.object.labeled?(:versioned)
263
+ }
264
+ end
265
+
266
+ used_view.binding_props.map { |binding_prop|
267
+ binding_prop.label(:binding)
268
+ }.uniq.each do |binding_prop_name|
269
+ if found = used_view.find(binding_prop_name)
270
+ presenter_for(found).use_implicit_version unless found.used?
271
+ end
272
+ end
273
+ end
274
+
275
+ presenter.bind(binder)
276
+
277
+ presenter.view.binding_scopes.uniq { |binding_scope|
278
+ binding_scope.label(:binding)
279
+ }.each do |binding_node|
280
+ plural_binding_node_name = Support.inflector.pluralize(binding_node.label(:binding)).to_sym
281
+
282
+ nested_view = presenter.find(binding_node.label(:binding))
283
+
284
+ if binder.object.include?(binding_node.label(:binding))
285
+ nested_view.present(binder.object[binding_node.label(:binding)])
286
+ elsif binder.object.include?(plural_binding_node_name)
287
+ nested_view.present(binder.object[plural_binding_node_name])
288
+ else
289
+ nested_view.remove
290
+ end
291
+ end
292
+ end
293
+ end
294
+ end
295
+
296
+ # @see View#append
297
+ #
298
+ def append(view)
299
+ tap do
300
+ @view.append(view)
301
+ end
302
+ end
303
+
304
+ # @see View#prepend
305
+ #
306
+ def prepend(view)
307
+ tap do
308
+ @view.prepend(view)
309
+ end
310
+ end
311
+
312
+ # @see View#after
313
+ #
314
+ def after(view)
315
+ tap do
316
+ @view.after(view)
317
+ end
318
+ end
319
+
320
+ # @see View#before
321
+ #
322
+ def before(view)
323
+ tap do
324
+ @view.before(view)
325
+ end
326
+ end
327
+
328
+ # @see View#replace
329
+ #
330
+ def replace(view)
331
+ tap do
332
+ @view.replace(view)
333
+ end
334
+ end
335
+
336
+ # @see View#remove
337
+ #
338
+ def remove
339
+ tap do
340
+ @view.remove
341
+ end
342
+ end
343
+
344
+ # @see View#clear
345
+ #
346
+ def clear
347
+ tap do
348
+ @view.clear
349
+ end
350
+ end
351
+
352
+ # Returns true if +self+ equals +other+.
353
+ #
354
+ def ==(other)
355
+ other.is_a?(self.class) && @view == other.view
356
+ end
357
+
358
+ def method_missing(name, *args, &block)
359
+ if @view.respond_to?(name)
360
+ value = @view.public_send(name, *args, &block)
361
+
362
+ if value.equal?(@view)
363
+ self
364
+ else
365
+ value
366
+ end
367
+ else
368
+ super
369
+ end
370
+ end
371
+
372
+ def respond_to_missing?(name, include_private = false)
373
+ @view.respond_to?(name, include_private) || super
374
+ end
375
+
376
+ # @api private
377
+ def wrap_data_in_binder(data)
378
+ if data.is_a?(Binder)
379
+ data
380
+ else
381
+ binder_for_current_scope(data)
382
+ end
383
+ end
384
+
385
+ def to_html(output = String.new)
386
+ @view.object.to_html(output, context: self)
387
+ end
388
+ alias to_s to_html
389
+
390
+ def presenter_for(view, type: nil)
391
+ if view.nil?
392
+ nil
393
+ else
394
+ instance = self.class.new(
395
+ view,
396
+ app: @app,
397
+ presentables: @presentables
398
+ )
399
+
400
+ type ||= view.object.label(:presenter_type)
401
+ type ? type.new(instance) : instance
402
+ end
403
+ end
404
+
405
+ # @api private
406
+ def endpoint(name)
407
+ object.each_significant_node(:endpoint) do |endpoint_node|
408
+ if endpoint_node.label(:endpoint) == name.to_sym
409
+ return presenter_for(View.from_object(endpoint_node))
410
+ end
411
+ end
412
+
413
+ nil
414
+ end
415
+
416
+ # @api private
417
+ def endpoint_action
418
+ endpoint_action_node = object.find_first_significant_node(
419
+ :endpoint_action
420
+ ) || object
421
+
422
+ presenter_for(View.from_object(endpoint_action_node))
423
+ end
424
+
425
+ # @api private
426
+ def use_implicit_version
427
+ case object
428
+ when StringDoc::MetaNode
429
+ if object.internal_nodes.all? { |node| node.labeled?(:version) && node.label(:version) != VersionedView::DEFAULT_VERSION }
430
+ use(object.internal_nodes.first.label(:version))
431
+ else
432
+ use(:default)
433
+ end
434
+ else
435
+ if versions.all? { |view| view.object.labeled?(:version) && view.object.label(:version) != VersionedView::DEFAULT_VERSION }
436
+ use(versions.first.object.label(:version))
437
+ else
438
+ use(:default)
439
+ end
440
+ end
441
+ end
442
+
443
+ private
444
+
445
+ def binder_for_current_scope(data)
446
+ context = if plug = @view.label(:plug)
447
+ @app.plug(plug[:name], plug[:instance])
448
+ else
449
+ @app
450
+ end
451
+
452
+ binder = context.state(:binder).find { |possible_binder|
453
+ possible_binder.__object_name.name == @view.label(:binding)
454
+ }
455
+
456
+ unless binder
457
+ binder = @app.isolated(:Binder)
458
+ context = @app
459
+ end
460
+
461
+ binder.new(data, app: context)
462
+ end
463
+
464
+ def bind_binder_to_view(binder, view)
465
+ view.each_binding_prop do |binding|
466
+ value = binder.__value(binding.label(:binding))
467
+ if value.is_a?(BindingParts) && binding_view = view.find(binding.label(:binding))
468
+ value.accept(*binding_view.label(:include).to_s.split(" "))
469
+ value.reject(*binding_view.label(:exclude).to_s.split(" "))
470
+
471
+ value.non_content_values(binding_view).each_pair do |key, value_part|
472
+ binding_view.attrs[key] = value_part
473
+ end
474
+
475
+ binding_view.object.set_label(:bound, true)
476
+ end
477
+ end
478
+
479
+ binder.binding!
480
+ view.bind(binder)
481
+ end
482
+
483
+ def binder_or_data(data)
484
+ if data.nil? || data.is_a?(Array) || data.is_a?(Binder)
485
+ data
486
+ else
487
+ wrap_data_in_binder(data)
488
+ end
489
+ end
490
+
491
+ def set_binding_info(data)
492
+ object = if data.is_a?(Binder)
493
+ data.object
494
+ else
495
+ data
496
+ end
497
+
498
+ if object && @view.object.labeled?(:binding)
499
+ binding_info = {
500
+ @view.object.label(:binding) => object[:id]
501
+ }
502
+
503
+ set_binding_info_for_node(@view.object, binding_info)
504
+
505
+ @view.object.each_significant_node(:binding, descend: true) do |binding_node|
506
+ set_binding_info_for_node(binding_node, binding_info)
507
+ end
508
+
509
+ @view.object.each_significant_node(:form, descend: true) do |form_node|
510
+ set_binding_info_for_node(form_node, binding_info)
511
+ end
512
+ end
513
+ end
514
+
515
+ def set_binding_info_for_node(node, info)
516
+ unless node.labeled?(:binding_info)
517
+ node.set_label(:binding_info, {})
518
+ end
519
+
520
+ node.label(:binding_info).merge!(info)
521
+ end
522
+
523
+ def set_endpoint_params(data)
524
+ object = if data.is_a?(Binder)
525
+ data.object
526
+ else
527
+ data
528
+ end
529
+
530
+ if @view.object.labeled?(:endpoint)
531
+ set_endpoint_params_for_node(@view.object, object)
532
+ end
533
+
534
+ @view.object.each_significant_node(:endpoint, descend: true) do |endpoint_node|
535
+ set_endpoint_params_for_node(endpoint_node, object)
536
+ end
537
+ end
538
+
539
+ def set_endpoint_params_for_node(node, object)
540
+ endpoint_object = node.label(:endpoint_object)
541
+ endpoint_params = node.label(:endpoint_params)
542
+
543
+ if endpoint_object && endpoint_params
544
+ endpoint_object.params.each do |param|
545
+ if param.to_s.start_with?("#{@view.label(:binding)}_")
546
+ key = param.to_s.split("_", 2)[1].to_sym
547
+
548
+ if object.include?(key)
549
+ endpoint_params[param] = object[key]; next
550
+ end
551
+ end
552
+
553
+ if object.include?(param)
554
+ endpoint_params[param] = object[param]
555
+ end
556
+ end
557
+ end
558
+ end
559
+
560
+ def present?(key, object)
561
+ !internal_presentable?(key) && (object_presents?(object, key) || plug_presents?(object, key))
562
+ end
563
+
564
+ def internal_presentable?(key)
565
+ key.to_s.start_with?("__")
566
+ end
567
+
568
+ def object_presents?(object, key)
569
+ key == plural_channeled_binding_name || key == singular_channeled_binding_name
570
+ end
571
+
572
+ def plug_presents?(object, key)
573
+ key = key.to_s
574
+ object.labeled?(:plug) &&
575
+ key.start_with?(object.label(:plug)[:key]) &&
576
+ # FIXME: Find a more performant way to do this
577
+ #
578
+ object_presents?(object, key.split("#{object.label(:plug)[:key]}.", 2)[1].to_sym)
579
+ end
580
+
581
+ class << self
582
+ using Support::Refinements::String::Normalization
583
+
584
+ attr_reader :path
585
+
586
+ def make(path, **kwargs, &block)
587
+ path = String.normalize_path(path)
588
+ super(path, path: path, **kwargs, &block)
589
+ end
590
+
591
+ # Defines a render to attach to a node.
592
+ #
593
+ def render(*binding_path, node: nil, priority: :default, &block)
594
+ if node && !node.is_a?(Proc)
595
+ raise ArgumentError, "Expected `#{node.class}' to be a proc"
596
+ end
597
+
598
+ if binding_path.empty? && node.nil?
599
+ node = -> { self }
600
+ end
601
+
602
+ @__attached_renders << {
603
+ binding_path: binding_path,
604
+ node: node,
605
+ priority: priority,
606
+ block: block
607
+ }
608
+ end
609
+
610
+ # Defines a presentation block called when +binding_name+ is presented. If +channel+ is
611
+ # provided, the block will only be called for that channel.
612
+ #
613
+ def present(binding_name, &block)
614
+ (@__presentation_logic[binding_name.to_sym] ||= []) << {
615
+ block: block
616
+ }
617
+ end
618
+
619
+ # Defines a versioning block called when +version_name+ is presented.
620
+ #
621
+ def version(version_name, &block)
622
+ (@__versioning_logic[version_name] ||= []) << {
623
+ block: block
624
+ }
625
+ end
626
+
627
+ # Attaches renders to a view's doc.
628
+ #
629
+ def attach(view)
630
+ views_with_renders = {}
631
+
632
+ renders = @__attached_renders.dup
633
+
634
+ # Automatically present exposed values for this view. Doing this dynamically lets us
635
+ # optimize. The alternative is to attach a render to the entire view, which is less
636
+ # performant because the entire structure must be duped.
637
+ #
638
+ view.binding_scopes.map { |binding_node|
639
+ {
640
+ binding_path: [
641
+ binding_node.label(:channeled_binding)
642
+ ]
643
+ }
644
+ }.uniq.each do |binding_render|
645
+ renders << {
646
+ binding_path: binding_render[:binding_path],
647
+ priority: :low,
648
+ block: Proc.new {
649
+ if object.labeled?(:binding) && !object.labeled?(:bound)
650
+ presentables.each do |key, value|
651
+ if present?(key, object)
652
+ present(value); break
653
+ end
654
+ end
655
+ end
656
+ }
657
+ }
658
+ end
659
+
660
+ # Setup binding endpoints in a similar way to automatic presentation above.
661
+ #
662
+ Presenters::Endpoint.attach_to_node(view.object, renders)
663
+
664
+ renders.each do |render|
665
+ return_value = if node = render[:node]
666
+ view.instance_exec(&node)
667
+ else
668
+ view.find(*render[:binding_path])
669
+ end
670
+
671
+ case return_value
672
+ when Array
673
+ return_value.each do |each_value|
674
+ relate_value_to_render(each_value, render, views_with_renders)
675
+ end
676
+ when View, VersionedView
677
+ relate_value_to_render(return_value, render, views_with_renders)
678
+ end
679
+ end
680
+
681
+ views_with_renders.values.each do |view_with_renders, renders_for_view|
682
+ attach_to_node = case view_with_renders
683
+ when VersionedView
684
+ StringDoc::MetaNode.new(view_with_renders.versions.map(&:object))
685
+ when View
686
+ view_with_renders.object
687
+ end
688
+
689
+ if attach_to_node.is_a?(StringDoc)
690
+ attach_to_node = attach_to_node.find_first_significant_node(:html)
691
+ end
692
+
693
+ if attach_to_node
694
+ renders_for_view.each do |render|
695
+ attach_to_node.transform priority: render[:priority], &render_proc(view_with_renders, render, &render[:block])
696
+ end
697
+ end
698
+ end
699
+ end
700
+
701
+ # Defines options attached to a form binding.
702
+ #
703
+ def options_for(form_binding, field_binding, options = nil, &block)
704
+ form_binding = form_binding.to_sym
705
+ field_binding = field_binding.to_sym
706
+
707
+ @__global_options[form_binding] ||= {}
708
+ @__global_options[form_binding][field_binding] = {
709
+ options: options,
710
+ block: block
711
+ }
712
+ end
713
+
714
+ private
715
+
716
+ def render_proc(_view, _render = nil, &block)
717
+ Proc.new do |node, context, string|
718
+ case node
719
+ when StringDoc::MetaNode
720
+ if node.nodes.any?
721
+ returning = node
722
+ presenter = context.presenter_for(
723
+ VersionedView.new([View.from_object(node)])
724
+ )
725
+ else
726
+ next node
727
+ end
728
+ when StringDoc::Node
729
+ returning = StringDoc.empty
730
+ returning.append(node)
731
+ presenter = context.presenter_for(
732
+ View.from_object(node)
733
+ )
734
+ end
735
+
736
+ presenter.instance_exec(node, context, string, &block); returning
737
+ rescue => error
738
+ if presenter.app.config.presenter.features.streaming
739
+ Pakyow.logger.houston(error)
740
+
741
+ presenter.clear
742
+ presenter.attributes[:class] << :"render-failed"
743
+ presenter.view.object.set_label(:failed, true)
744
+ presenter.object
745
+ else
746
+ raise error
747
+ end
748
+ end
749
+ end
750
+
751
+ def relate_value_to_render(value, render, state)
752
+ final_value = case value
753
+ when View, VersionedView
754
+ value
755
+ else
756
+ View.new(value.to_s)
757
+ end
758
+
759
+ # Group the renders by node and view type.
760
+ #
761
+ (state["#{final_value.object.object_id}::#{final_value.class}"] ||= [final_value, []])[1] << render
762
+ end
763
+ end
764
+ end
765
+ end
766
+ end