pakyow-presenter 0.8.0 → 0.9.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.
@@ -0,0 +1,18 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class StringDocRenderer
4
+ def self.render(structure)
5
+ structure.flatten.reject(&:empty?).map { |s|
6
+ s.is_a?(Hash) ? attrify(s) : s
7
+ }.join
8
+ end
9
+
10
+ IGNORED_ATTRS = %i[container partial]
11
+ def self.attrify(attrs)
12
+ attrs.delete_if { |a| a.nil? || IGNORED_ATTRS.include?(a) }.map { |attr|
13
+ attr[0].to_s + '="' + attr[1].to_s + '"'
14
+ }.join(' ')
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,24 +1,21 @@
1
1
  module Pakyow
2
2
  module Presenter
3
3
  class Template < View
4
- include DocHelpers
5
- include TitleHelpers
6
-
7
4
  attr_accessor :name, :doc
8
5
 
9
6
  class << self
10
7
  def load(path)
11
- format = Utils::String.split_at_last_dot(path)[-1]
8
+ format = String.split_at_last_dot(path)[-1]
12
9
  contents = File.read(path)
13
10
  name = File.basename(path, '.*').to_sym
14
11
 
15
- return self.new(name, contents, format)
12
+ self.new(name, contents, format: format)
16
13
  end
17
14
  end
18
15
 
19
- def initialize(name, contents = '', format = :html)
16
+ def initialize(name, contents = '', format: :html)
20
17
  @name = name
21
- super(contents, format)
18
+ super(contents, format: format)
22
19
  end
23
20
 
24
21
  def initialize_copy(original_template)
@@ -26,49 +23,28 @@ module Pakyow
26
23
 
27
24
  # copy doc
28
25
  @doc = original_template.doc.dup
29
- @context = original_template.context
30
- @composer = original_template.composer
31
26
  end
32
27
 
33
28
  def container(name = :default)
34
- container = @containers[name.to_sym]
35
- return view_from_path(container[:path])
36
- end
37
-
38
- def containers(refind = false)
39
- @containers = (!@containers || refind) ? find_containers : @containers
29
+ View.from_doc(@doc.container(name.to_sym))
40
30
  end
41
31
 
42
32
  def build(page)
43
- # add content to each container
44
- containers.each do |container|
33
+ @doc.containers.each do |container|
45
34
  name = container[0]
46
35
 
47
36
  begin
48
- container(name).replace(page.content(name))
37
+ container[1][:doc].replace(page.content(name))
49
38
  rescue MissingContainer
50
- Pakyow.logger.debug "No content for '#{name}' in page '#{page.path}'"
39
+ # This hasn't proven to be useful in dev (or prd for that matter)
40
+ # so decided to remove it. It'll save us from filling console / log
41
+ # with information that will most likely just be ignored.
42
+ #
43
+ # Pakyow.logger.info "No content for '#{name}' in page '#{page.path}'"
51
44
  end
52
45
  end
53
46
 
54
- return View.from_doc(doc)
55
- end
56
-
57
- private
58
-
59
- # returns an array of hashes, each with the container name and doc
60
- def find_containers
61
- containers = {}
62
-
63
- @doc.traverse {|e|
64
- next unless e.is_a?(Nokogiri::XML::Comment)
65
- next unless match = e.text.strip.match(/@container( ([a-zA-Z0-9\-_]*))*/)
66
- name = match[2] || :default
67
-
68
- containers[name.to_sym] = { doc: e, path: path_to(e) }
69
- }
70
-
71
- return containers
47
+ View.from_doc(doc)
72
48
  end
73
49
  end
74
50
  end
@@ -1,40 +1,28 @@
1
+ require 'forwardable'
2
+
1
3
  module Pakyow
2
4
  module Presenter
3
5
  class View
4
- include DocHelpers
5
- include TitleHelpers
6
-
7
- PARTIAL_REGEX = /<!--\s*@include\s*([a-zA-Z0-9\-_]*)\s*-->/
8
-
9
- class << self
10
- attr_accessor :binders
6
+ extend Forwardable
11
7
 
12
- def self_closing_tag?(tag)
13
- %w[area base basefont br hr input img link meta].include? tag
14
- end
15
-
16
- def form_field?(tag)
17
- %w[input select textarea button].include? tag
18
- end
8
+ def_delegators :@doc, :title=, :title, :remove, :clear, :text, :html
19
9
 
20
- def tag_without_value?(tag)
21
- %w[select].include? tag
22
- end
23
- end
24
-
25
- attr_accessor :doc, :scoped_as, :scopes, :related_views, :context, :composer
26
- attr_writer :bindings
27
-
28
- def initialize(contents = '', format = :html)
29
- @related_views = []
10
+ # The object responsible for parsing, manipulating, and rendering
11
+ # the underlying HTML document for the view.
12
+ #
13
+ attr_reader :doc
30
14
 
31
- processed = Presenter.process(contents, format)
15
+ # The scope, if any, that the view belongs to.
16
+ #
17
+ attr_accessor :scoped_as
32
18
 
33
- if processed.match(/<html.*>/)
34
- @doc = Nokogiri::HTML::Document.parse(processed)
35
- else
36
- @doc = Nokogiri::HTML.fragment(processed)
37
- end
19
+ # Creates a view, running `contents` through any registered view processors for `format`.
20
+ #
21
+ # @param contents [String] the contents of the view
22
+ # @param format [Symbol] the format of contents
23
+ #
24
+ def initialize(contents = '', format: :html)
25
+ @doc = Config.presenter.view_doc_class.new(Presenter.process(contents, format))
38
26
  end
39
27
 
40
28
  def initialize_copy(original_view)
@@ -42,179 +30,94 @@ module Pakyow
42
30
 
43
31
  @doc = original_view.doc.dup
44
32
  @scoped_as = original_view.scoped_as
45
- @context = @context
46
- @composer = @composer
47
33
  end
48
34
 
49
- def self.from_doc(doc)
50
- view = self.new
51
- view.doc = doc
52
- return view
53
- end
54
-
55
- def self.load(path)
56
- format = Utils::String.split_at_last_dot(path)[-1]
57
- contents = File.read(path)
58
-
59
- return self.new(contents, format)
35
+ # Creates a new view with a soft copy of doc.
36
+ #
37
+ def soft_copy
38
+ copy = View.from_doc(@doc.soft_copy)
39
+ copy.scoped_as = scoped_as
40
+ copy
60
41
  end
61
42
 
62
- # Allows multiple attributes to be set at once.
63
- # root_view.find(selector).attributes(:class => my_class, :style => my_style)
43
+ # Creates a view from a doc.
64
44
  #
65
- def attributes(attrs = {})
66
- #TODO this is not invalidating composer
67
-
68
- if attrs.empty?
69
- return Attributes.new(self.doc, @composer)
70
- else
71
- self.bind_attributes_to_doc(attrs, doc)
72
- end
45
+ # @see StringDoc
46
+ # @see NokogiriDoc
47
+ #
48
+ def self.from_doc(doc)
49
+ view = new
50
+ view.instance_variable_set(:@doc, doc)
51
+ view
73
52
  end
74
53
 
75
- alias :attrs :attributes
76
-
77
- def remove
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!
54
+ # Creates a view from a file.
55
+ #
56
+ def self.load(path)
57
+ new(File.read(path), format: File.format(path))
86
58
  end
87
59
 
88
- alias :delete :remove
89
-
90
- def clear
91
- return if self.doc.blank?
92
- self.doc.inner_html = ''
93
- self.invalidate!
60
+ def ==(other)
61
+ self.class == other.class && @doc == other.doc
94
62
  end
95
63
 
96
- def text
97
- self.doc.inner_text
64
+ # Allows multiple attributes to be set at once.
65
+ #
66
+ # view.attrs(class: '...', style: '...')
67
+ #
68
+ def attrs(attrs = {})
69
+ return Attributes.new(@doc) if attrs.empty?
70
+ bind_attributes_to_doc(attrs, @doc)
98
71
  end
99
72
 
100
73
  def text=(text)
101
74
  text = text.call(self.text) if text.is_a?(Proc)
102
- self.doc.content = text.to_s
103
- self.invalidate!
104
- end
105
-
106
- def html
107
- self.doc.inner_html
75
+ @doc.text = text
108
76
  end
109
77
 
110
78
  def html=(html)
111
79
  html = html.call(self.html) if html.is_a?(Proc)
112
- self.doc.inner_html = Nokogiri::HTML.fragment(html.to_s)
113
- self.invalidate!
80
+ @doc.html = html
114
81
  end
115
82
 
116
83
  def append(view)
117
- doc = view.doc
118
- num = doc.children.count
119
- path = self.path_to(doc)
120
-
121
- self.doc.add_child(view.doc)
122
-
123
- self.update_binding_offset_at_path(num, path)
124
- self.invalidate!
84
+ @doc.append(view.doc)
125
85
  end
126
86
 
127
87
  def prepend(view)
128
- doc = view.doc
129
- num = doc.children.count
130
- path = self.path_to(doc)
131
-
132
- if first_child = self.doc.children.first
133
- first_child.add_previous_sibling(doc)
134
- else
135
- self.doc = doc
136
- end
137
-
138
- self.update_binding_offset_at_path(num, path)
139
- self.invalidate!
88
+ @doc.prepend(view.doc)
140
89
  end
141
90
 
91
+ #TODO allow strings?
142
92
  def after(view)
143
- doc = view.doc
144
- num = doc.children.count
145
- path = self.path_to(doc)
146
-
147
- self.doc.after(view.doc)
148
-
149
- self.update_binding_offset_at_path(num, path)
150
- self.invalidate!
93
+ @doc.after(view.doc)
151
94
  end
152
95
 
153
96
  def before(view)
154
- doc = view.doc
155
- num = doc.children.count
156
- path = self.path_to(doc)
157
-
158
- self.doc.before(view.doc)
159
-
160
- self.update_binding_offset_at_path(num, path)
161
- self.invalidate!
97
+ @doc.before(view.doc)
162
98
  end
163
99
 
164
100
  def 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!
101
+ replacement = view.is_a?(View) ? view.doc : view
102
+ @doc.replace(replacement)
175
103
  end
176
104
 
177
105
  def scope(name)
178
106
  name = name.to_sym
179
-
180
- views = ViewCollection.new
181
- views.context = @context
182
- views.composer = @composer
183
- self.bindings.select{|b| b[:scope] == name}.each{|s|
184
- v = self.view_from_path(s[:path])
185
-
186
- v.bindings = self.update_binding_paths_from_path([s].concat(s[:nested_bindings]), s[:path])
187
- v.scoped_as = s[:scope]
188
- v.context = @context
189
- v.composer = @composer
190
-
191
- views << v
192
- }
193
-
194
- views
107
+ @doc.scope(name).inject(ViewCollection.new) do |coll, scope|
108
+ view = View.from_doc(scope[:doc])
109
+ view.scoped_as = name
110
+ coll << view
111
+ end
195
112
  end
196
113
 
197
114
  def prop(name)
198
115
  name = name.to_sym
199
-
200
- views = ViewCollection.new
201
- views.context = @context
202
- views.composer = @composer
203
-
204
- if binding = self.bindings.select{|binding| binding[:scope] == self.scoped_as}[0]
205
- binding[:props].each {|prop|
206
- if prop[:prop] == name
207
- v = self.view_from_path(prop[:path])
208
-
209
- v.scoped_as = self.scoped_as
210
- v.context = @context
211
- v.composer = @composer
212
- views << v
213
- end
214
- }
116
+ @doc.prop(scoped_as, name).inject(ViewCollection.new) do |coll, prop|
117
+ view = View.from_doc(prop[:doc])
118
+ view.scoped_as = scoped_as
119
+ coll << view
215
120
  end
216
-
217
- views
218
121
  end
219
122
 
220
123
  # call-seq:
@@ -224,7 +127,7 @@ module Pakyow
224
127
  #
225
128
  def with(&block)
226
129
  if block.arity == 0
227
- self.instance_exec(&block)
130
+ instance_exec(&block)
228
131
  else
229
132
  yield(self)
230
133
  end
@@ -242,13 +145,11 @@ module Pakyow
242
145
  # (this is basically Bret's `map` function)
243
146
  #
244
147
  def for(data, &block)
245
- data = data.to_a if data.is_a?(Enumerator)
246
- data = [data] if (!data.is_a?(Enumerable) || data.is_a?(Hash))
247
-
148
+ datum = Array.ensure(data).first
248
149
  if block.arity == 1
249
- self.instance_exec(data[0], &block)
150
+ instance_exec(datum, &block)
250
151
  else
251
- block.call(self, data[0])
152
+ block.call(self, datum)
252
153
  end
253
154
  end
254
155
 
@@ -258,7 +159,7 @@ module Pakyow
258
159
  # Yields a view, its matching dataum, and the index. See #for.
259
160
  #
260
161
  def for_with_index(data, &block)
261
- self.for(data) do |ctx, datum|
162
+ self.for(data) do |ctx, datum|
262
163
  if block.arity == 2
263
164
  ctx.instance_exec(datum, 0, &block)
264
165
  else
@@ -275,27 +176,29 @@ module Pakyow
275
176
  # of self, where n = data.length.
276
177
  #
277
178
  def match(data)
278
- data = data.to_a if data.is_a?(Enumerator)
279
- data = [data] if (!data.is_a?(Enumerable) || data.is_a?(Hash))
179
+ data = Array.ensure(data)
180
+ coll = ViewCollection.new
280
181
 
281
- views = ViewCollection.new
282
- views.context = @context
283
- views.composer = @composer
284
- data.each {|datum|
285
- d_v = self.doc.dup
286
- self.doc.before(d_v)
182
+ # an empty set always means an empty view
183
+ if data.empty?
184
+ remove
185
+ else
186
+ # dup for later
187
+ original_view = dup if data.length > 1
287
188
 
288
- v = View.from_doc(d_v)
289
- v.bindings = self.bindings.dup
290
- v.scoped_as = self.scoped_as
291
- v.context = @context
292
- v.composer = @composer
189
+ # the original view match the first datum
190
+ coll << self
293
191
 
294
- views << v
295
- }
192
+ # create views for the other datums
193
+ data[1..-1].inject(coll) { |coll|
194
+ duped_view = original_view.dup
195
+ after(duped_view)
196
+ coll << duped_view
197
+ }
198
+ end
296
199
 
297
- self.remove
298
- views
200
+ # return the new collection
201
+ coll
299
202
  end
300
203
 
301
204
  # call-seq:
@@ -304,7 +207,7 @@ module Pakyow
304
207
  # Matches self with data and yields a view/datum pair.
305
208
  #
306
209
  def repeat(data, &block)
307
- self.match(data).for(data, &block)
210
+ match(data).for(data, &block)
308
211
  end
309
212
 
310
213
  # call-seq:
@@ -313,29 +216,23 @@ module Pakyow
313
216
  # Matches self with data and yields a view/datum pair with index.
314
217
  #
315
218
  def repeat_with_index(data, &block)
316
- self.match(data).for_with_index(data, &block)
219
+ match(data).for_with_index(data, &block)
317
220
  end
318
221
 
319
222
  # call-seq:
320
223
  # bind(data)
321
224
  #
322
- # Binds data across existing scopes.
225
+ # Binds a single datum across existing scopes.
323
226
  #
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
-
328
- scope_info = self.bindings.first
329
-
330
- self.bind_data_to_scope(data[0], scope_info, bindings)
331
- invalidate!(true)
332
-
227
+ def bind(data, bindings: {}, context: nil, &block)
228
+ datum = Array.ensure(data).first
229
+ bind_data_to_scope(datum, doc.scopes.first, bindings, context)
333
230
  return if block.nil?
334
231
 
335
232
  if block.arity == 1
336
- self.instance_exec(data[0], &block)
233
+ instance_exec(datum, &block)
337
234
  else
338
- block.call(self, data[0])
235
+ block.call(self, datum)
339
236
  end
340
237
  end
341
238
 
@@ -344,8 +241,8 @@ module Pakyow
344
241
  #
345
242
  # Binds data across existing scopes, yielding a view/datum pair with index.
346
243
  #
347
- def bind_with_index(data, bindings = {}, &block)
348
- self.bind(data) do |ctx, datum|
244
+ def bind_with_index(*a, **k, &block)
245
+ bind(*a, **k) do |ctx, datum|
349
246
  if block.arity == 2
350
247
  ctx.instance_exec(datum, 0, &block)
351
248
  else
@@ -359,207 +256,75 @@ module Pakyow
359
256
  #
360
257
  # Matches self to data then binds data to the view.
361
258
  #
362
- def apply(data, bindings = {}, &block)
363
- self.match(data).bind(data, bindings, &block)
364
- end
365
-
366
- def bindings(refind = false)
367
- @bindings = (!@bindings || refind) ? self.find_bindings : @bindings
259
+ def apply(data, bindings: {}, context: nil, &block)
260
+ match(data).bind(data, bindings: bindings, context: context, &block)
368
261
  end
369
262
 
370
263
  def includes(partial_map)
264
+ partials = @doc.partials
371
265
  partial_map = partial_map.dup
372
266
 
373
267
  # 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])
268
+ partials.each do |partial_info|
269
+ partial = partial_map[partial_info[0]]
270
+ next if partial.nil?
271
+ partial_info[1].replace(partial.doc.dup)
381
272
  end
382
273
 
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
274
+ # refind the partials
275
+ partials = @doc.partials
391
276
 
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
277
+ # if mixed in partials included partials, we want to run includes again with a new map
278
+ if partials.count > 0 && (partial_map.keys - partials.keys).count < partial_map.keys.count
279
+ includes(partial_map)
280
+ end
400
281
 
401
- protected
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
-
431
- # populates the root_view using view_store data by recursively building
432
- # and substituting in child views named in the structure
433
- def populate_view(root_view, view_store, view_info)
434
- root_view.containers.each {|e|
435
- next unless path = view_info[e[:name]]
436
-
437
- v = self.populate_view(View.new(path, view_store), view_store, view_info)
438
- v.context = @context
439
- v.composer = @composer
440
- self.reset_container(e[:doc])
441
- self.add_content_to_container(v, e[:doc])
442
- }
443
- root_view
444
- end
445
-
446
-
447
- # returns an array of hashes that describe each scope
448
- def find_bindings(doc = @doc, ignore_root = false)
449
- bindings = []
450
- breadth_first(doc) {|o|
451
- next if o == doc && ignore_root
452
- next if !scope = o[Config::Presenter.scope_attribute]
453
-
454
- bindings << {
455
- :scope => scope.to_sym,
456
- :path => path_to(o),
457
- :props => find_props(o)
458
- }
459
-
460
- if o == doc
461
- # this is the root node, which we need as the first hash in the
462
- # list of bindings, but we don't want to nest other scopes inside
463
- # of it in this case
464
- bindings.last[:nested_bindings] = []
465
- else
466
- bindings.last[:nested_bindings] = find_bindings(o, true)
467
- # reject so children aren't traversed
468
- throw :reject
469
- end
470
- }
471
-
472
- # find unscoped props
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
481
-
482
- return bindings
483
- end
484
-
485
- def find_props(o)
486
- props = []
487
- breadth_first(o) {|so|
488
- # don't go into deeper scopes
489
- throw :reject if so != o && so[Config::Presenter.scope_attribute]
490
-
491
- next unless prop = so[Config::Presenter.prop_attribute]
492
- props << {:prop => prop.to_sym, :path => path_to(so)}
493
- }
494
-
495
- return props
496
- end
497
-
498
- # returns a new binding set that takes into account the starting point of `path`
499
- def update_binding_paths_from_path(bindings, path)
500
- return bindings.collect { |binding|
501
- dup_binding = binding.dup
502
- dup_binding[:path] = dup_binding[:path][path.length..-1] || []
503
-
504
- dup_binding[:props] = dup_binding[:props].collect {|prop|
505
- dup_prop = prop.dup
506
- dup_prop[:path] = dup_prop[:path][path.length..-1]
507
- dup_prop
508
- }
509
-
510
- dup_binding[:nested_bindings] = update_binding_paths_from_path(dup_binding[:nested_bindings], path)
511
-
512
- dup_binding
513
- }
282
+ self
514
283
  end
515
284
 
516
- def update_binding_offset_at_path(offset, path)
517
- # update binding paths for bindings we're iterating on
518
- self.bindings.each {|binding|
519
- next unless self.path_within_path?(binding[:path], path)
285
+ def to_html
286
+ @doc.to_html
287
+ end
288
+ alias :to_s :to_html
520
289
 
521
- binding[:path][0] += offset if binding[:path][0]
290
+ private
522
291
 
523
- binding[:props].each { |prop|
524
- prop[:path][0] += offset if prop[:path][0]
525
- }
526
- }
527
- end
528
-
529
- def bind_data_to_scope(data, scope_info, bindings = {})
292
+ def bind_data_to_scope(data, scope_info, bindings, ctx)
530
293
  return unless data
294
+ return unless scope_info
531
295
 
532
296
  scope = scope_info[:scope]
297
+ bind_data_to_root(data, scope, bindings, ctx)
533
298
 
534
- bind_data_to_root(data, scope, bindings)
535
-
536
- scope_info[:props].each { |prop_info|
537
- catch(:unbound) {
299
+ scope_info[:props].each do |prop_info|
300
+ catch(:unbound) do
538
301
  prop = prop_info[:prop]
539
302
 
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])
303
+ if data_has_prop?(data, prop) || Binder.instance.has_scoped_prop?(scope, prop, bindings)
304
+ value = Binder.instance.value_for_scoped_prop(scope, prop, data, bindings, ctx)
305
+ doc = prop_info[:doc]
543
306
 
544
- if View.form_field?(doc.name)
545
- bind_to_form_field(doc, scope, prop, value, data)
307
+ if DocHelpers.form_field?(doc.tagname)
308
+ bind_to_form_field(doc, scope, prop, value, data, ctx)
546
309
  end
547
310
 
548
311
  bind_data_to_doc(doc, value)
549
312
  else
550
313
  handle_unbound_data(scope, prop)
551
314
  end
552
- }
553
- }
315
+ end
316
+ end
554
317
  end
555
318
 
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)
319
+ def bind_data_to_root(data, scope, bindings, ctx)
320
+ value = Binder.instance.value_for_scoped_prop(scope, :_root, data, bindings, ctx)
321
+ return if value.nil?
322
+
323
+ value.is_a?(Hash) ? bind_attributes_to_doc(value, doc) : bind_value_to_doc(value, doc)
559
324
  end
560
325
 
561
326
  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)
327
+ data.is_a?(Hash) ? bind_attributes_to_doc(data, doc) : bind_value_to_doc(data, doc)
563
328
  end
564
329
 
565
330
  def data_has_prop?(data, prop)
@@ -569,80 +334,57 @@ module Pakyow
569
334
  def bind_value_to_doc(value, doc)
570
335
  value = String(value)
571
336
 
572
- tag = doc.name
573
- return if View.tag_without_value?(tag)
337
+ tag = doc.tagname
338
+ return if DocHelpers.tag_without_value?(tag)
574
339
 
575
- if View.self_closing_tag?(tag)
340
+ if DocHelpers.self_closing_tag?(tag)
576
341
  # don't override value if set
577
- if !doc['value'] || doc['value'].empty?
578
- doc['value'] = value
342
+ if !doc.get_attribute(:value) || doc.get_attribute(:value).empty?
343
+ doc.set_attribute(:value, value)
579
344
  end
580
345
  else
581
- doc.inner_html = value
582
- end
583
- end
584
-
585
- def bind_attributes_to_doc(attrs, doc)
586
- attrs.each do |attr, v|
587
- case attr
588
- when :content
589
- v = v.call(doc.inner_html) if v.is_a?(Proc)
590
- bind_value_to_doc(v, doc)
591
- next
592
- when :view
593
- v.call(self)
594
- next
595
- end
596
-
597
- attr = attr.to_s
598
- attrs = Attributes.new(doc)
599
- v = v.call(attrs.send(attr)) if v.is_a?(Proc)
600
-
601
- if v.nil?
602
- doc.remove_attribute(attr)
603
- else
604
- attrs.send(:"#{attr}=", v)
605
- end
346
+ doc.html = value
606
347
  end
607
348
  end
608
349
 
609
- def bind_to_form_field(doc, scope, prop, value, bindable)
350
+ def bind_to_form_field(doc, scope, prop, value, bindable, ctx)
610
351
  set_form_field_name(doc, scope, prop)
611
352
 
612
353
  # special binding for checkboxes and radio buttons
613
- if doc.name == 'input' && (doc[:type] == 'checkbox' || doc[:type] == 'radio')
354
+ if doc.tagname == 'input' && (doc.get_attribute(:type) == 'checkbox' || doc.get_attribute(:type) == 'radio')
614
355
  bind_to_checked_field(doc, value)
615
- # special binding for selects
616
- elsif doc.name == 'select'
617
- bind_to_select_field(doc, scope, prop, value, bindable)
356
+ # special binding for selects
357
+ elsif doc.tagname == 'select'
358
+ bind_to_select_field(doc, scope, prop, value, bindable, ctx)
618
359
  end
619
360
  end
620
361
 
621
362
  def bind_to_checked_field(doc, value)
622
- if value == true || (doc[:value] && doc[:value] == value.to_s)
623
- doc[:checked] = 'checked'
363
+ if value == true || (doc.get_attribute(:value) && doc.get_attribute(:value) == value.to_s)
364
+ doc.set_attribute(:checked, 'checked')
624
365
  else
625
- doc.delete('checked')
366
+ doc.remove_attribute(:checked)
626
367
  end
627
368
 
628
369
  # coerce to string since booleans are often used and fail when binding to a view
629
- value = value.to_s
370
+ value.to_s
630
371
  end
631
372
 
632
- def bind_to_select_field(doc, scope, prop, value, bindable)
633
- create_select_options(doc, scope, prop, value, bindable)
373
+ def bind_to_select_field(doc, scope, prop, value, bindable, ctx)
374
+ create_select_options(doc, scope, prop, value, bindable, ctx)
634
375
  select_option_with_value(doc, value)
635
376
  end
636
377
 
637
378
  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}]"
379
+ return if doc.get_attribute(:name) && !doc.get_attribute(:name).empty? # don't overwrite the name if already defined
380
+ doc.set_attribute(:name, "#{scope}[#{prop}]")
640
381
  end
641
382
 
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)
383
+ def create_select_options(doc, scope, prop, value, bindable, ctx)
384
+ options = Binder.instance.options_for_scoped_prop(scope, prop, bindable, ctx)
385
+ return if options.nil?
644
386
 
645
- option_nodes = Nokogiri::HTML::DocumentFragment.parse ""
387
+ option_nodes = Nokogiri::HTML::DocumentFragment.parse('')
646
388
  Nokogiri::HTML::Builder.with(option_nodes) do |h|
647
389
  until options.length == 0
648
390
  catch :optgroup do
@@ -650,18 +392,18 @@ module Pakyow
650
392
 
651
393
  # an array containing value/content
652
394
  if o.is_a?(Array)
653
- h.option o[1], :value => o[0]
395
+ h.option o[1], value: o[0]
654
396
  options.shift
655
397
  # likely an object (e.g. string); start a group
656
398
  else
657
- h.optgroup(:label => o) {
399
+ h.optgroup(label: o) {
658
400
  options.shift
659
401
 
660
402
  options[0..-1].each_with_index { |o2,i2|
661
403
  # starting a new group
662
- throw :optgroup if !o2.is_a?(Array)
404
+ throw :optgroup unless o2.is_a?(Array)
663
405
 
664
- h.option o2[1], :value => o2[0]
406
+ h.option o2[1], value: o2[0]
665
407
  options.shift
666
408
  }
667
409
  }
@@ -671,21 +413,47 @@ module Pakyow
671
413
  end
672
414
 
673
415
  # remove existing options
674
- doc.children.remove
416
+ doc.clear
675
417
 
676
418
  # add generated options
677
- doc.add_child(option_nodes)
419
+ doc.append(option_nodes.to_html)
678
420
  end
679
421
 
680
422
  def select_option_with_value(doc, value)
681
- return unless o = doc.css('option[value="' + value.to_s + '"]').first
682
- o[:selected] = 'selected'
423
+ option = doc.option(value: value)
424
+ return if option.nil?
425
+
426
+ option.set_attribute(:selected, 'selected')
683
427
  end
684
428
 
685
429
  def handle_unbound_data(scope, prop)
686
- Pakyow.logger.warn("Unbound data for #{scope}[#{prop}]")
430
+ Pakyow.logger.warn("Unbound data for #{scope}[#{prop}]") if Pakyow.logger
687
431
  throw :unbound
688
432
  end
433
+
434
+ def bind_attributes_to_doc(attrs, doc)
435
+ attrs.each do |attr, v|
436
+ case attr
437
+ when :content
438
+ v = v.call(doc.inner_html) if v.is_a?(Proc)
439
+ bind_value_to_doc(v, doc)
440
+ next
441
+ when :view
442
+ v.call(self)
443
+ next
444
+ else
445
+ attr = attr.to_s
446
+ attrs = Attributes.new(doc)
447
+ v = v.call(attrs.send(attr)) if v.is_a?(Proc)
448
+
449
+ if v.nil?
450
+ doc.remove_attribute(attr)
451
+ else
452
+ attrs.send(:"#{attr}=", v)
453
+ end
454
+ end
455
+ end
456
+ end
689
457
  end
690
458
  end
691
459
  end