pakyow-presenter 0.8.0 → 0.9.0

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