pakyow-presenter 0.8.rc4 → 0.8.0

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.
@@ -2,6 +2,9 @@ module Pakyow
2
2
  module Presenter
3
3
  class View
4
4
  include DocHelpers
5
+ include TitleHelpers
6
+
7
+ PARTIAL_REGEX = /<!--\s*@include\s*([a-zA-Z0-9\-_]*)\s*-->/
5
8
 
6
9
  class << self
7
10
  attr_accessor :binders
@@ -19,15 +22,9 @@ module Pakyow
19
22
  end
20
23
  end
21
24
 
22
- attr_accessor :doc, :scoped_as, :scopes, :related_views
25
+ attr_accessor :doc, :scoped_as, :scopes, :related_views, :context, :composer
23
26
  attr_writer :bindings
24
27
 
25
- def dup
26
- view = self.class.from_doc(@doc.dup)
27
- view.scoped_as = scoped_as
28
- return view
29
- end
30
-
31
28
  def initialize(contents = '', format = :html)
32
29
  @related_views = []
33
30
 
@@ -40,6 +37,15 @@ module Pakyow
40
37
  end
41
38
  end
42
39
 
40
+ def initialize_copy(original_view)
41
+ super
42
+
43
+ @doc = original_view.doc.dup
44
+ @scoped_as = original_view.scoped_as
45
+ @context = @context
46
+ @composer = @composer
47
+ end
48
+
43
49
  def self.from_doc(doc)
44
50
  view = self.new
45
51
  view.doc = doc
@@ -47,35 +53,20 @@ module Pakyow
47
53
  end
48
54
 
49
55
  def self.load(path)
50
- format = StringUtils.split_at_last_dot(path)[-1]
56
+ format = Utils::String.split_at_last_dot(path)[-1]
51
57
  contents = File.read(path)
52
58
 
53
59
  return self.new(contents, format)
54
60
  end
55
61
 
56
- def title=(title)
57
- if @doc
58
- if o = @doc.css('title').first
59
- o.inner_html = Nokogiri::HTML::fragment(title)
60
- else
61
- if o = @doc.css('head').first
62
- o.add_child(Nokogiri::HTML::fragment("<title>#{title}</title>"))
63
- end
64
- end
65
- end
66
- end
67
-
68
- def title
69
- o = @doc.css('title').first
70
- o.inner_html if o
71
- end
72
-
73
62
  # Allows multiple attributes to be set at once.
74
63
  # root_view.find(selector).attributes(:class => my_class, :style => my_style)
75
64
  #
76
65
  def attributes(attrs = {})
66
+ #TODO this is not invalidating composer
67
+
77
68
  if attrs.empty?
78
- return Attributes.new(self.doc)
69
+ return Attributes.new(self.doc, @composer)
79
70
  else
80
71
  self.bind_attributes_to_doc(attrs, doc)
81
72
  end
@@ -84,8 +75,14 @@ module Pakyow
84
75
  alias :attrs :attributes
85
76
 
86
77
  def remove
87
- self.doc.remove
88
- self.refind_significant_nodes
78
+ if doc.parent.nil?
79
+ # best we can do is to remove the children
80
+ doc.children.remove
81
+ else
82
+ doc.remove
83
+ end
84
+
85
+ invalidate!
89
86
  end
90
87
 
91
88
  alias :delete :remove
@@ -93,7 +90,7 @@ module Pakyow
93
90
  def clear
94
91
  return if self.doc.blank?
95
92
  self.doc.inner_html = ''
96
- self.refind_significant_nodes
93
+ self.invalidate!
97
94
  end
98
95
 
99
96
  def text
@@ -103,7 +100,7 @@ module Pakyow
103
100
  def text=(text)
104
101
  text = text.call(self.text) if text.is_a?(Proc)
105
102
  self.doc.content = text.to_s
106
- self.refind_significant_nodes
103
+ self.invalidate!
107
104
  end
108
105
 
109
106
  def html
@@ -113,7 +110,7 @@ module Pakyow
113
110
  def html=(html)
114
111
  html = html.call(self.html) if html.is_a?(Proc)
115
112
  self.doc.inner_html = Nokogiri::HTML.fragment(html.to_s)
116
- self.refind_significant_nodes
113
+ self.invalidate!
117
114
  end
118
115
 
119
116
  def append(view)
@@ -124,7 +121,7 @@ module Pakyow
124
121
  self.doc.add_child(view.doc)
125
122
 
126
123
  self.update_binding_offset_at_path(num, path)
127
- self.refind_significant_nodes
124
+ self.invalidate!
128
125
  end
129
126
 
130
127
  def prepend(view)
@@ -139,7 +136,7 @@ module Pakyow
139
136
  end
140
137
 
141
138
  self.update_binding_offset_at_path(num, path)
142
- self.refind_significant_nodes
139
+ self.invalidate!
143
140
  end
144
141
 
145
142
  def after(view)
@@ -150,7 +147,7 @@ module Pakyow
150
147
  self.doc.after(view.doc)
151
148
 
152
149
  self.update_binding_offset_at_path(num, path)
153
- self.refind_significant_nodes
150
+ self.invalidate!
154
151
  end
155
152
 
156
153
  def before(view)
@@ -161,22 +158,35 @@ module Pakyow
161
158
  self.doc.before(view.doc)
162
159
 
163
160
  self.update_binding_offset_at_path(num, path)
164
- self.refind_significant_nodes
161
+ self.invalidate!
165
162
  end
166
163
 
167
164
  def replace(view)
168
- doc.replace(view)
165
+ view = view.doc if view.is_a?(View)
166
+
167
+ if doc.parent.nil?
168
+ doc.children.remove
169
+ doc.inner_html = view
170
+ else
171
+ doc.replace(view)
172
+ end
173
+
174
+ invalidate!
169
175
  end
170
176
 
171
177
  def scope(name)
172
178
  name = name.to_sym
173
179
 
174
180
  views = ViewCollection.new
181
+ views.context = @context
182
+ views.composer = @composer
175
183
  self.bindings.select{|b| b[:scope] == name}.each{|s|
176
184
  v = self.view_from_path(s[:path])
177
185
 
178
186
  v.bindings = self.update_binding_paths_from_path([s].concat(s[:nested_bindings]), s[:path])
179
187
  v.scoped_as = s[:scope]
188
+ v.context = @context
189
+ v.composer = @composer
180
190
 
181
191
  views << v
182
192
  }
@@ -188,6 +198,8 @@ module Pakyow
188
198
  name = name.to_sym
189
199
 
190
200
  views = ViewCollection.new
201
+ views.context = @context
202
+ views.composer = @composer
191
203
 
192
204
  if binding = self.bindings.select{|binding| binding[:scope] == self.scoped_as}[0]
193
205
  binding[:props].each {|prop|
@@ -195,6 +207,8 @@ module Pakyow
195
207
  v = self.view_from_path(prop[:path])
196
208
 
197
209
  v.scoped_as = self.scoped_as
210
+ v.context = @context
211
+ v.composer = @composer
198
212
  views << v
199
213
  end
200
214
  }
@@ -208,11 +222,13 @@ module Pakyow
208
222
  #
209
223
  # Creates a context in which view manipulations can be performed.
210
224
  #
211
- # Unlike previous versions, the context can only be referenced by the
212
- # block argument. No `context` method will be available.s
213
- #
214
- def with
215
- yield(self)
225
+ def with(&block)
226
+ if block.arity == 0
227
+ self.instance_exec(&block)
228
+ else
229
+ yield(self)
230
+ end
231
+
216
232
  self
217
233
  end
218
234
 
@@ -228,7 +244,27 @@ module Pakyow
228
244
  def for(data, &block)
229
245
  data = data.to_a if data.is_a?(Enumerator)
230
246
  data = [data] if (!data.is_a?(Enumerable) || data.is_a?(Hash))
231
- block.call(self, data[0], 0) if block_given?
247
+
248
+ if block.arity == 1
249
+ self.instance_exec(data[0], &block)
250
+ else
251
+ block.call(self, data[0])
252
+ end
253
+ end
254
+
255
+ # call-seq:
256
+ # for_with_index {|view, datum, i| block}
257
+ #
258
+ # Yields a view, its matching dataum, and the index. See #for.
259
+ #
260
+ def for_with_index(data, &block)
261
+ self.for(data) do |ctx, datum|
262
+ if block.arity == 2
263
+ ctx.instance_exec(datum, 0, &block)
264
+ else
265
+ block.call(ctx, datum, 0)
266
+ end
267
+ end
232
268
  end
233
269
 
234
270
  # call-seq:
@@ -243,6 +279,8 @@ module Pakyow
243
279
  data = [data] if (!data.is_a?(Enumerable) || data.is_a?(Hash))
244
280
 
245
281
  views = ViewCollection.new
282
+ views.context = @context
283
+ views.composer = @composer
246
284
  data.each {|datum|
247
285
  d_v = self.doc.dup
248
286
  self.doc.before(d_v)
@@ -250,6 +288,8 @@ module Pakyow
250
288
  v = View.from_doc(d_v)
251
289
  v.bindings = self.bindings.dup
252
290
  v.scoped_as = self.scoped_as
291
+ v.context = @context
292
+ v.composer = @composer
253
293
 
254
294
  views << v
255
295
  }
@@ -267,16 +307,51 @@ module Pakyow
267
307
  self.match(data).for(data, &block)
268
308
  end
269
309
 
310
+ # call-seq:
311
+ # repeat_with_index(data) {|view, datum, i| block}
312
+ #
313
+ # Matches self with data and yields a view/datum pair with index.
314
+ #
315
+ def repeat_with_index(data, &block)
316
+ self.match(data).for_with_index(data, &block)
317
+ end
318
+
270
319
  # call-seq:
271
320
  # bind(data)
272
321
  #
273
322
  # Binds data across existing scopes.
274
323
  #
275
324
  def bind(data, bindings = {}, &block)
325
+ data = data.to_a if data.is_a?(Enumerator)
326
+ data = [data] if (!data.is_a?(Enumerable) || data.is_a?(Hash))
327
+
276
328
  scope_info = self.bindings.first
277
329
 
278
- self.bind_data_to_scope(data, scope_info, bindings)
279
- yield(self, data, 0) if block_given?
330
+ self.bind_data_to_scope(data[0], scope_info, bindings)
331
+ invalidate!(true)
332
+
333
+ return if block.nil?
334
+
335
+ if block.arity == 1
336
+ self.instance_exec(data[0], &block)
337
+ else
338
+ block.call(self, data[0])
339
+ end
340
+ end
341
+
342
+ # call-seq:
343
+ # bind_with_index(data)
344
+ #
345
+ # Binds data across existing scopes, yielding a view/datum pair with index.
346
+ #
347
+ def bind_with_index(data, bindings = {}, &block)
348
+ self.bind(data) do |ctx, datum|
349
+ if block.arity == 2
350
+ ctx.instance_exec(datum, 0, &block)
351
+ else
352
+ block.call(ctx, datum, 0)
353
+ end
354
+ end
280
355
  end
281
356
 
282
357
  # call-seq:
@@ -285,15 +360,74 @@ module Pakyow
285
360
  # Matches self to data then binds data to the view.
286
361
  #
287
362
  def apply(data, bindings = {}, &block)
288
- views = self.match(data).bind(data, bindings, &block)
363
+ self.match(data).bind(data, bindings, &block)
289
364
  end
290
365
 
291
366
  def bindings(refind = false)
292
367
  @bindings = (!@bindings || refind) ? self.find_bindings : @bindings
293
368
  end
294
369
 
370
+ def includes(partial_map)
371
+ partial_map = partial_map.dup
372
+
373
+ # mixin all the partials
374
+ partials.each do |partial|
375
+ partial[1].replace(partial_map[partial[0]].to_s)
376
+ end
377
+
378
+ # now delete them from the map
379
+ partials.each do |partial|
380
+ partial_map.delete(partial[0])
381
+ end
382
+
383
+ # we have more partials
384
+ if partial_map.count > 0
385
+ # initiate another build if content contains partials
386
+ includes(partial_map) if partials(true).count > 0
387
+ end
388
+
389
+ return self
390
+ end
391
+
392
+ def invalidate!(composer_only = false)
393
+ self.bindings(true) unless composer_only
394
+ @composer.dirty! unless @composer.nil?
395
+
396
+ @related_views.each {|v|
397
+ v.invalidate!(composer_only)
398
+ }
399
+ end
400
+
295
401
  protected
296
402
 
403
+ def partials(refind = false)
404
+ @partials = (!@partials || refind) ? find_partials : @partials
405
+ end
406
+
407
+ def partials_in(content)
408
+ partials = []
409
+
410
+ content.scan(PARTIAL_REGEX) do |m|
411
+ partials << m[0].to_sym
412
+ end
413
+
414
+ return partials
415
+ end
416
+
417
+ def find_partials
418
+ partials = []
419
+
420
+ @doc.traverse { |e|
421
+ next unless e.is_a?(Nokogiri::XML::Comment)
422
+ next unless match = e.to_html.strip.match(PARTIAL_REGEX)
423
+
424
+ name = match[1]
425
+ partials << [name.to_sym, e]
426
+ }
427
+
428
+ return partials
429
+ end
430
+
297
431
  # populates the root_view using view_store data by recursively building
298
432
  # and substituting in child views named in the structure
299
433
  def populate_view(root_view, view_store, view_info)
@@ -301,6 +435,8 @@ module Pakyow
301
435
  next unless path = view_info[e[:name]]
302
436
 
303
437
  v = self.populate_view(View.new(path, view_store), view_store, view_info)
438
+ v.context = @context
439
+ v.composer = @composer
304
440
  self.reset_container(e[:doc])
305
441
  self.add_content_to_container(v, e[:doc])
306
442
  }
@@ -325,7 +461,7 @@ module Pakyow
325
461
  # this is the root node, which we need as the first hash in the
326
462
  # list of bindings, but we don't want to nest other scopes inside
327
463
  # of it in this case
328
- bindings.last[:nested_bindings] = {}
464
+ bindings.last[:nested_bindings] = []
329
465
  else
330
466
  bindings.last[:nested_bindings] = find_bindings(o, true)
331
467
  # reject so children aren't traversed
@@ -334,12 +470,14 @@ module Pakyow
334
470
  }
335
471
 
336
472
  # find unscoped props
337
- bindings.unshift({
338
- :scope => nil,
339
- :path => [],
340
- :props => find_props(doc),
341
- :nested_bindings => {}
342
- })
473
+ unless doc[Config::Presenter.scope_attribute]
474
+ bindings.unshift({
475
+ :scope => nil,
476
+ :path => [],
477
+ :props => find_props(doc),
478
+ :nested_bindings => []
479
+ })
480
+ end
343
481
 
344
482
  return bindings
345
483
  end
@@ -388,42 +526,42 @@ module Pakyow
388
526
  }
389
527
  end
390
528
 
391
- def refind_significant_nodes
392
- self.bindings(true)
393
-
394
- @related_views.each {|v|
395
- v.refind_significant_nodes
396
- }
397
- end
398
-
399
529
  def bind_data_to_scope(data, scope_info, bindings = {})
400
530
  return unless data
401
531
 
402
532
  scope = scope_info[:scope]
403
533
 
404
- # handle root binding
405
- if value = Pakyow.app.presenter.binder.value_for_prop(:_root, scope, data, bindings)
406
- value.is_a?(Hash) ? self.bind_attributes_to_doc(value, self.doc) : self.bind_value_to_doc(value, self.doc)
407
- end
534
+ bind_data_to_root(data, scope, bindings)
408
535
 
409
- scope_info[:props].each {|prop_info|
536
+ scope_info[:props].each { |prop_info|
410
537
  catch(:unbound) {
411
538
  prop = prop_info[:prop]
412
539
 
413
- self.handle_unbound_data(scope, prop) unless data_has_prop?(data, prop) || Pakyow.app.presenter.binder.has_prop?(prop, scope, bindings)
414
- value = Pakyow.app.presenter.binder.value_for_prop(prop, scope, data, bindings)
540
+ if data_has_prop?(data, prop) || Pakyow.app.presenter.binder.has_prop?(prop, scope, bindings)
541
+ value = Pakyow.app.presenter.binder.value_for_prop(prop, scope, data, bindings, context)
542
+ doc = doc_from_path(prop_info[:path])
415
543
 
416
- doc = doc_from_path(prop_info[:path])
417
-
418
- # handle form field
419
- self.bind_to_form_field(doc, scope, prop, value, data) if View.form_field?(doc.name)
544
+ if View.form_field?(doc.name)
545
+ bind_to_form_field(doc, scope, prop, value, data)
546
+ end
420
547
 
421
- # bind attributes or value
422
- value.is_a?(Hash) ? self.bind_attributes_to_doc(value, doc) : self.bind_value_to_doc(value, doc)
548
+ bind_data_to_doc(doc, value)
549
+ else
550
+ handle_unbound_data(scope, prop)
551
+ end
423
552
  }
424
553
  }
425
554
  end
426
555
 
556
+ def bind_data_to_root(data, scope, bindings)
557
+ return unless value = Pakyow.app.presenter.binder.value_for_prop(:_root, scope, data, bindings, context)
558
+ value.is_a?(Hash) ? self.bind_attributes_to_doc(value, self.doc) : self.bind_value_to_doc(value, self.doc)
559
+ end
560
+
561
+ def bind_data_to_doc(doc, data)
562
+ data.is_a?(Hash) ? self.bind_attributes_to_doc(data, doc) : self.bind_value_to_doc(data, doc)
563
+ end
564
+
427
565
  def data_has_prop?(data, prop)
428
566
  (data.is_a?(Hash) && (data.key?(prop) || data.key?(prop.to_s))) || (!data.is_a?(Hash) && data.class.method_defined?(prop))
429
567
  end
@@ -433,6 +571,7 @@ module Pakyow
433
571
 
434
572
  tag = doc.name
435
573
  return if View.tag_without_value?(tag)
574
+
436
575
  if View.self_closing_tag?(tag)
437
576
  # don't override value if set
438
577
  if !doc['value'] || doc['value'].empty?
@@ -458,72 +597,89 @@ module Pakyow
458
597
  attr = attr.to_s
459
598
  attrs = Attributes.new(doc)
460
599
  v = v.call(attrs.send(attr)) if v.is_a?(Proc)
461
- v.nil? ? doc.remove_attribute(attr) : attrs.send(:"#{attr}=", v)
600
+
601
+ if v.nil?
602
+ doc.remove_attribute(attr)
603
+ else
604
+ attrs.send(:"#{attr}=", v)
605
+ end
462
606
  end
463
607
  end
464
608
 
465
609
  def bind_to_form_field(doc, scope, prop, value, bindable)
466
-
467
- # don't overwrite the name if already defined
468
- if !doc['name'] || doc['name'].empty?
469
- # set name on form element
470
- doc['name'] = "#{scope}[#{prop}]"
471
- end
610
+ set_form_field_name(doc, scope, prop)
472
611
 
473
612
  # special binding for checkboxes and radio buttons
474
613
  if doc.name == 'input' && (doc[:type] == 'checkbox' || doc[:type] == 'radio')
475
- if value == true || (doc[:value] && doc[:value] == value.to_s)
476
- doc[:checked] = 'checked'
477
- else
478
- doc.delete('checked')
479
- end
480
-
481
- # coerce to string since booleans are often used
482
- # and fail when binding to a view
483
- value = value.to_s
614
+ bind_to_checked_field(doc, value)
484
615
  # special binding for selects
485
616
  elsif doc.name == 'select'
486
- if options = Pakyow.app.presenter.binder.options_for_prop(prop, scope, bindable)
487
- option_nodes = Nokogiri::HTML::DocumentFragment.parse ""
488
- Nokogiri::HTML::Builder.with(option_nodes) do |h|
489
- until options.length == 0
490
- catch :optgroup do
491
- o = options.first
492
-
493
- # an array containing value/content
494
- if o.is_a?(Array)
495
- h.option o[1], :value => o[0]
617
+ bind_to_select_field(doc, scope, prop, value, bindable)
618
+ end
619
+ end
620
+
621
+ def bind_to_checked_field(doc, value)
622
+ if value == true || (doc[:value] && doc[:value] == value.to_s)
623
+ doc[:checked] = 'checked'
624
+ else
625
+ doc.delete('checked')
626
+ end
627
+
628
+ # coerce to string since booleans are often used and fail when binding to a view
629
+ value = value.to_s
630
+ end
631
+
632
+ def bind_to_select_field(doc, scope, prop, value, bindable)
633
+ create_select_options(doc, scope, prop, value, bindable)
634
+ select_option_with_value(doc, value)
635
+ end
636
+
637
+ def set_form_field_name(doc, scope, prop)
638
+ return if doc['name'] && !doc['name'].empty? # don't overwrite the name if already defined
639
+ doc['name'] = "#{scope}[#{prop}]"
640
+ end
641
+
642
+ def create_select_options(doc, scope, prop, value, bindable)
643
+ return unless options = Pakyow.app.presenter.binder.options_for_prop(prop, scope, bindable, context)
644
+
645
+ option_nodes = Nokogiri::HTML::DocumentFragment.parse ""
646
+ Nokogiri::HTML::Builder.with(option_nodes) do |h|
647
+ until options.length == 0
648
+ catch :optgroup do
649
+ o = options.first
650
+
651
+ # an array containing value/content
652
+ if o.is_a?(Array)
653
+ h.option o[1], :value => o[0]
654
+ options.shift
655
+ # likely an object (e.g. string); start a group
656
+ else
657
+ h.optgroup(:label => o) {
658
+ options.shift
659
+
660
+ options[0..-1].each_with_index { |o2,i2|
661
+ # starting a new group
662
+ throw :optgroup if !o2.is_a?(Array)
663
+
664
+ h.option o2[1], :value => o2[0]
496
665
  options.shift
497
- # likely an object (e.g. string); start a group
498
- else
499
- h.optgroup(:label => o) {
500
- options.shift
501
-
502
- options[0..-1].each_with_index { |o2,i2|
503
- # starting a new group
504
- throw :optgroup if !o2.is_a?(Array)
505
-
506
- h.option o2[1], :value => o2[0]
507
- options.shift
508
- }
509
- }
510
- end
511
- end
666
+ }
667
+ }
512
668
  end
513
669
  end
670
+ end
671
+ end
514
672
 
515
- # remove existing options
516
- doc.children.remove
673
+ # remove existing options
674
+ doc.children.remove
517
675
 
518
- # add generated options
519
- doc.add_child(option_nodes)
520
- end
676
+ # add generated options
677
+ doc.add_child(option_nodes)
678
+ end
521
679
 
522
- # select appropriate option
523
- if o = doc.css('option[value="' + value.to_s + '"]').first
524
- o[:selected] = 'selected'
525
- end
526
- end
680
+ def select_option_with_value(doc, value)
681
+ return unless o = doc.css('option[value="' + value.to_s + '"]').first
682
+ o[:selected] = 'selected'
527
683
  end
528
684
 
529
685
  def handle_unbound_data(scope, prop)