pakyow-presenter 0.8.rc4 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)