goat 0.3.45 → 0.3.46

Sign up to get free protection for your applications and to get access to all the features.
@@ -54,23 +54,32 @@ module Goat
54
54
  end
55
55
 
56
56
  class ComponentUpdate
57
- attr_reader :skel, :mutations
57
+ attr_reader :skel, :mutations, :added, :removed
58
58
  attr_accessor :version
59
59
 
60
- def initialize(skel, mutations)
60
+ def initialize(skel, mutations, added, removed)
61
61
  @skel = skel
62
62
  @mutations = mutations
63
+ @added = added
64
+ @removed = removed
63
65
  end
64
66
 
65
67
  def to_hash
66
68
  {
67
69
  'skel' => @skel.to_hash,
68
- 'mutations' => @mutations
70
+ 'mutations' => @mutations,
71
+ 'added' => @added.map(&:to_hash),
72
+ 'removed' => @removed
69
73
  }
70
74
  end
71
75
 
72
76
  def self.from_hash(h)
73
- self.new(ComponentSkeleton.from_hash(h['skel']), h['mutations'])
77
+ self.new(
78
+ ComponentSkeleton.from_hash(h['skel']),
79
+ h['mutations'],
80
+ h['added'].map{|a| ComponentSkeleton.from_hash(a)},
81
+ h['removed']
82
+ )
74
83
  end
75
84
  end
76
85
  end
@@ -0,0 +1,627 @@
1
+ require 'rack/utils'
2
+
3
+ module Goat
4
+ module DOMTools
5
+ def tag(node); node[0]; end
6
+ def is_attrs?(x); x.kind_of?(Hash) || x.kind_of?(DOMAttrs); end
7
+ def attrs(node); node[1] if is_attrs?(node[1]); end
8
+ def body(node); is_attrs?(node[1]) ? node[2..-1] : node[1..-1]; end
9
+ def domid(node); attrs(node) ? attrs(node)[:id] : nil; end
10
+ def dom_node?(node); node.is_a?(Array) && node.first.is_a?(Symbol); end
11
+ def nested_body?(node); node.is_a?(Array) && !node.first.is_a?(Symbol); end
12
+ def normalized_tags(body); nested_body?(body) ? body.map{|b| normalized_tags(b)}.flatten(1) : [body]; end
13
+ def car_tag(tree)
14
+ if (b = tree).is_a?(Array)
15
+ b = b.first while b.is_a?(Array)
16
+ b
17
+ end
18
+ end
19
+
20
+ module_function :tag, :attrs, :body, :domid, :dom_node?, :is_attrs?, :nested_body?,
21
+ :normalized_tags, :car_tag
22
+
23
+ class Traverser
24
+ include DOMTools
25
+
26
+ def initialize(tree, dlg, transpose)
27
+ @tree = tree
28
+ @dlg = dlg
29
+ @transpose = transpose
30
+ end
31
+
32
+ def to_node(tag, attrs, body)
33
+ tag = [tag]
34
+ tag << attrs if attrs
35
+ tag += normalized_tags(body)
36
+ end
37
+
38
+ def replacement_block
39
+ @rep = nil
40
+ @replacement_block ||= lambda {|new| @rep = new}
41
+ end
42
+
43
+ def traverse(node)
44
+ if node.is_a?(String)
45
+ @dlg.string(node, &replacement_block)
46
+ @rep || node
47
+ elsif node == nil
48
+ @dlg.string('', &replacement_block)
49
+ @rep || nil
50
+ elsif dom_node?(node)
51
+ stay_traversing = @dlg.node(node, &replacement_block)
52
+ rep = @rep || node
53
+
54
+ if @transpose
55
+ if !rep.equal?(node)
56
+ if stay_traversing
57
+ to_node(tag(rep), attrs(rep), traverse(body(rep)))
58
+ else
59
+ rep
60
+ end
61
+ else
62
+ to_node(tag(node), attrs(node), traverse(body(node)))
63
+ end
64
+ else
65
+ traverse(body(node))
66
+ end
67
+ elsif node.is_a?(Array)
68
+ if node.size == 1
69
+ traverse(node.first)
70
+ else
71
+ if @transpose
72
+ node.map{|x| traverse(x)}
73
+ else
74
+ node.each{|x| traverse(x)}
75
+ end
76
+ end
77
+ elsif node.kind_of?(Component)
78
+ @dlg.component(node, &replacement_block)
79
+ @rep || node
80
+ else
81
+ raise "Unknown object in the dom: #{node.inspect}"
82
+ end
83
+ end
84
+
85
+ def traverse!
86
+ traverse(@tree)
87
+ end
88
+
89
+ class BlockTraverser
90
+ # would be infinitely nicer if you could just yield from a block
91
+ def initialize(blk, transpose)
92
+ @blk = blk
93
+ @transpose = transpose
94
+ end
95
+
96
+ def node(node, &blk)
97
+ if @transpose
98
+ @blk.call(node, blk)
99
+ else
100
+ @blk.call(node)
101
+ end
102
+ end
103
+
104
+ def string(str, &blk)
105
+ if @transpose
106
+ @blk.call(str, blk)
107
+ else
108
+ @blk.call(str)
109
+ end
110
+ end
111
+
112
+ def component(c, &blk)
113
+ if @transpose
114
+ @blk.call(c, blk)
115
+ else
116
+ @blk.call(c)
117
+ end
118
+ end
119
+ end
120
+
121
+ def self.traverse(tree, dlg=nil, transpose=false, &blk)
122
+ d = nil
123
+
124
+ if dlg
125
+ d = dlg
126
+ elsif blk
127
+ d = BlockTraverser.new(blk, transpose)
128
+ else
129
+ raise "Need a delegate"
130
+ end
131
+
132
+ self.new(tree, d, transpose).traverse!
133
+ end
134
+
135
+ def self.transpose(tree, dlg=nil, &blk)
136
+ traverse(tree, dlg, true, &blk)
137
+ end
138
+ end
139
+
140
+ def self.traverse(tree, dlg=nil, &blk); Traverser.traverse(tree, dlg, &blk); end
141
+ def self.transpose(tree, dlg=nil, &blk); Traverser.transpose(tree, dlg, &blk); end
142
+
143
+ def self.expanded_dom(dom)
144
+ DOMTools.transpose(dom) do |elt, update|
145
+ if elt.kind_of?(Component)
146
+ raise "Component #{elt} has no ID: was super's initialize called?" unless elt.id
147
+ Dynamic[:expander].component_used(elt)
148
+ update.call(elt.component(elt.expanded_dom))
149
+ elsif dom_node?(elt)
150
+ tag = tag(elt)
151
+
152
+ if tag == :tbody
153
+ b = body(elt)
154
+ b = b.first while b.is_a?(Array)
155
+ if b == :tbody
156
+ update.call(expanded_dom(body(elt)))
157
+ end
158
+ elsif tag == :table
159
+ a = attrs(elt)
160
+ tbody_id = (a && a[:id]) ? "#{a[:id]}_tbody" : 'tbody_' + String.random
161
+ update.call([:table, a ? a : {}, [:tbody, {:id => tbody_id}, expanded_dom(body(elt))]])
162
+ end
163
+ end
164
+ true
165
+ end
166
+ end
167
+
168
+ def self.inject_prefixes(id, dom)
169
+ DOMTools.traverse(dom) do |elt|
170
+ if elt.kind_of?(Array) && elt.first.is_a?(Symbol) && elt[1].kind_of?(Hash)
171
+ attrs = elt[1]
172
+ elt[1] = attrs.map_to_hash do |k, v|
173
+ if v.kind_of?(String)
174
+ [k, v.prefix_ns(id)]
175
+ elsif v.kind_of?(Array) && HTMLBuilder::ARRAY_ATTRS.include?(k)
176
+ [k, v]
177
+ elsif v.kind_of?(Integer) && HTMLBuilder::INTEGER_ATTRS.include?(k)
178
+ [k, v]
179
+ else
180
+ raise "Invalid object #{v.inspect} to get a prefix in dom:\n#{dom.inspect}"
181
+ end
182
+ end
183
+ end
184
+ end
185
+ dom
186
+ end
187
+
188
+ def self.dom_components(tree)
189
+ c = []
190
+ DOMTools.traverse(tree) do |elt|
191
+ # dom_\w{10} is the form of a component ID; anything more liberal might catch things like
192
+ # dom_xxxxxxxxxx_table_border
193
+ if dom_node?(elt) && (attrs = attrs(elt)) && attrs.include?(:id) && attrs[:id] =~ /^dom_\w{10}$/
194
+ c << attrs[:id]
195
+ end
196
+ end
197
+ c
198
+ end
199
+
200
+ def self.find_component(tree, id)
201
+ catch(:find_component) do
202
+ DOMTools.traverse(tree) do |elt|
203
+ if dom_node?(elt) & (attrs = attrs(elt)) && attrs[:id] == id
204
+ throw :find_component, elt
205
+ end
206
+ nil
207
+ end
208
+ end
209
+ end
210
+
211
+ class ::String
212
+ def to_html(builder)
213
+ Rack::Utils.escape_html(self)
214
+ end
215
+ end
216
+
217
+ class ::NilClass
218
+ def to_html(builder)
219
+ ''
220
+ end
221
+ end
222
+
223
+ class ::Goat::HTMLString < String
224
+ def to_html(builder)
225
+ self
226
+ end
227
+ end
228
+
229
+ class ::Array
230
+ def to_html(builder)
231
+ builder.array_to_html(self)
232
+ end
233
+ end
234
+
235
+ class InvalidBodyError < RuntimeError
236
+ attr_reader :body
237
+
238
+ def initialize(body)
239
+ super("Invalid body: #{body.inspect}")
240
+ @body = body
241
+ end
242
+ end
243
+
244
+ class HTMLBuilder
245
+ ARRAY_ATTRS = [:class]
246
+ INTEGER_ATTRS = [:colspan, :rowspan]
247
+
248
+ class TagBuilder
249
+ # TODO: gmail trick of only a single onclick() handler
250
+
251
+ def self.build(tag, attrs, body)
252
+ self.new(tag, attrs, body).dispatch
253
+ end
254
+
255
+ def initialize(tag, attrs, body)
256
+ @tag = tag
257
+ @attrs = attrs
258
+ @body = body
259
+
260
+ rewrite_attrs
261
+ end
262
+
263
+ def rewrite_attrs
264
+ new = {}
265
+
266
+ @attrs.map_to_hash do |k, v|
267
+ if k == :class && v.kind_of?(Array)
268
+ new[:class] = @attrs[:class].join(' ')
269
+ else
270
+ new[k] = v
271
+ end
272
+ end
273
+
274
+ @attrs = new
275
+ end
276
+
277
+ def build_node
278
+ [@tag, @attrs, @body]
279
+ end
280
+
281
+ def a_tag
282
+ unless @attrs.include?(:href)
283
+ @attrs[:href] = 'javascript:void(0)'
284
+ end
285
+
286
+ build_node
287
+ end
288
+
289
+ def input_tag
290
+ unless @attrs.include?(:name)
291
+ $stderr.puts "Warning: no name for <#{@tag} #{@attrs.inspect}>#{@body.inspect}</#{@tag}>"
292
+
293
+ # this is somewhat ungainly: we generate a name automatically by hashing the values of the
294
+ # other attrs. this may conflict - ideally would track per-page state for these.
295
+ # purpose is to have name-less inputs preserve their values when user goes back to page.
296
+ # webkit in safari 5 gets confused when inputs are nameless.
297
+ unless (vals = @attrs.values{|k, v| v.kind_of?(String)}).empty?
298
+ $stderr.puts "Generating a name automatically..."
299
+ @attrs[:name] = vals.join.md5[0..10]
300
+ end
301
+ end
302
+
303
+ build_node
304
+ end
305
+
306
+ def dispatch
307
+ meth = "#{@tag}_tag".to_sym
308
+
309
+ if self.respond_to?(meth)
310
+ self.send(meth)
311
+ else
312
+ build_node
313
+ end
314
+ end
315
+ end
316
+
317
+ def standalone_tags
318
+ %w{br img input}
319
+ end
320
+
321
+ def attrs_to_html(attrs)
322
+ attrs.map {|k, v| "#{k}=\"#{v}\""}.join(' ')
323
+ end
324
+
325
+ def array_to_html(ar)
326
+ if ar.first.kind_of?(Symbol)
327
+ tag = ar[0]
328
+ have_attrs = ar[1].kind_of?(Hash)
329
+ attrs = have_attrs ? ar[1] : {}
330
+ body = ar[(have_attrs ? 2 : 1)..-1]
331
+
332
+ tag, attrs, body = TagBuilder.build(tag, attrs, body)
333
+ lonely = standalone_tags.include?(tag.to_s)
334
+
335
+ open_tag = "<#{tag}#{attrs.empty? ? '' : (' ' + attrs_to_html(attrs))}>"
336
+ body_html = body.empty? ? '' : body.map{|x| x.to_html(self)}.join
337
+ close_tag = (lonely ? '' : "</#{tag}>")
338
+
339
+ "#{open_tag}#{body_html}#{close_tag}"
340
+ else
341
+ ar.map{|x| x.to_html(self)}.join
342
+ end
343
+ end
344
+
345
+ def initialize(input)
346
+ @input = input
347
+ end
348
+
349
+ def html
350
+ @input.to_html(self)
351
+ end
352
+ end
353
+
354
+ class DOMAttrs < ::Hash
355
+ def self.from_hash(hash, id=nil)
356
+ d = self[hash]
357
+ d.id = id
358
+ d.clean
359
+ d
360
+ end
361
+
362
+ def deleted
363
+ @deleted ||= {}
364
+ end
365
+
366
+ def id=(id); @id = id; end
367
+
368
+ def clean
369
+ self.each do |k, v|
370
+ if v.kind_of?(String) && v.start_with?('dom_') && (!@id || v !~ /#{@id}/)
371
+ deleted[k] = v
372
+ self.delete(k)
373
+ end
374
+ end
375
+ end
376
+
377
+ def reconstitute
378
+ (deleted.keys - self.keys).each{|k| self[k] = deleted[k]}
379
+ self
380
+ end
381
+
382
+ def inspect
383
+ start = super[0..-2]
384
+ o = []
385
+ deleted.each do |k, v|
386
+ o << "#{k.inspect}=>**#{v.inspect}**"
387
+ end
388
+
389
+ start + (o.empty? ? '' : ', ' + o.join(', ')) + '}'
390
+ end
391
+ end
392
+
393
+ class PartialUpdateFailed < StandardError; end
394
+
395
+ module DiffTools
396
+ class NoParentDOMID < PartialUpdateFailed; end
397
+ class TextualTranspose < PartialUpdateFailed; end
398
+
399
+ def type_of(x)
400
+ if x.is_a?(String)
401
+ :string
402
+ elsif x.is_a?(Array)
403
+ dom_node?(x) ? :node : :array
404
+ else
405
+ ('cls_' + x.class.name.downcase).to_sym
406
+ end
407
+ end
408
+
409
+ def dom_node?(node)
410
+ # TODO take from dom.rb
411
+ node.is_a?(Array) && node.first.is_a?(Symbol)
412
+ end
413
+
414
+ def lcs_is_replacement?(d1, d2)
415
+ d1 && d2 && d1.position == d2.position && (
416
+ (d1.action == '+' && d2.action == '-') ||
417
+ (d1.action == '-' && d2.action == '+'))
418
+ end
419
+
420
+ def lcs_old_and_new(d1, d2)
421
+ d1.action == '+' ? [d2, d1] : [d1, d2]
422
+ end
423
+
424
+ def added(obj, par, pos)
425
+ raise NoParentDOMID if par.nil?
426
+ raise TextualTranspose if obj.kind_of?(String)
427
+ [:add, par, pos, *normalized_tags(obj)]
428
+ end
429
+
430
+ def removed(obj, par, pos)
431
+ raise NoParentDOMID if par.nil?
432
+ raise TextualTranspose if obj.kind_of?(String)
433
+ [:rem, par, pos, *normalized_tags(obj)]
434
+ end
435
+
436
+ def lcs_is_addition?(diff); diff.action == '+'; end
437
+ def lcs_is_removal?(diff); diff.action == '-'; end
438
+ end
439
+
440
+
441
+ class DOMDiff
442
+ include DiffTools
443
+ include DOMTools
444
+ class << self; include DOMTools; end
445
+
446
+ # the constraints:
447
+ # - arrays might be tags or they might be arrays
448
+ # - we can't directly address textual entities; they must be addressed by their enclosing tag
449
+ # (i.e. <li>foo</li> -> <li>bar</li> is a change to an li tag, not a change to the *contents*
450
+ # of an li tag. Why, you ask? Here:
451
+ # > $("#foo").innerHTML
452
+ # "<span>blub</span>foo<span>qux</span>bar"
453
+ # > $("#foo").children
454
+ # [<span>blub</span>, <span>qux</span>]
455
+ # Text isn't a thing in the DOM tree. So, in order to traverse, we have to restrict ourselves
456
+ # to DOM nodes, i.e. tags, not raw textual elements in the tree. I don't know why the DOM
457
+ # works this way.
458
+ # - we only try to deal with changes where the parent element of the changed element has a
459
+ # [unique] ID. We could address by route through the tree, but we don't attempt to store
460
+ # this.
461
+ # - we can't always tell in advance where we should make a change. For example,
462
+ # [:ul, [:li, [:a, {:id => 'hello'}, [:b, 'test']]]] changing to:
463
+ # [:ul, [:li, [:a, {:id => 'hello'}, [:b, 'blub']]]]
464
+ # When we're looking at the ul, and before we know what exactly is different below us,
465
+ # it *may* be the case that the change can be encapsulated more neatly somewhere
466
+ # in the subtree, but it may not -- and even if the change is nice and isolated further
467
+ # down, there might not be a parent ID for us to hook off. This is what NoParentDOMID
468
+ # exceptions are used for.
469
+ # - we want to ignore artefacts from randomization of IDs. (i.e. cases where nothing has
470
+ # changed except the dom_XXXXXXXX ID of some element. This is pretty easy intuitively, but a
471
+ # bit more subtle in practice. The obvious thing to do is to use an alternate comparison
472
+ # function, instead of ==, but this may be slow, and would (worse) need a lot of care to
473
+ # make sure that your code (and outside libraries, such as Diff::LCS, which we use) never
474
+ # call it. Next obvious thing is to create a subclass of Hash, make all hash attributes
475
+ # ({:id => 'dom_XXXXXXX', :class => '...'}) instances of it, and then redefine == on the
476
+ # subclass. This doesn't work, though, because ruby's crappy C implementation won't
477
+ # dispatch to the subclass's == -- for hashes, arrays, etc., the comparison code is
478
+ # wired in and can't be overridden. So instead, we have our subclass actually remove
479
+ # the unwanted elements, and store them in a separate @deleted ivar. The crappy part of
480
+ # this is that you need to manually go through the dom and stick them all back in the
481
+ # "actual" hash once you've finished diffing.
482
+ # - we *don't* want to remove IDs that don't change. For example, during a rerender, we
483
+ # preserve the ID of the rerendered-component (just not the IDs of any child components).
484
+ # If we deleted the IDs of these, constraint #3 would mean we'd miss a lot of potential
485
+ # diffs, since we'd think there were changes we couldn't make. (I.e. think a child's
486
+ # parent doesn't have an ID when in fact it does.) One todo here is to make the differ
487
+ # check the @deleted ivar of a DOMAttrs instance for IDs, since they're perfectly
488
+ # valid hooks to use when removing or adding components. If you do this, conflicts become
489
+ # much more likely though -- say you have multiple changes to apply; a node whose ID
490
+ # you depend on may be removed.
491
+
492
+ def self.diff(old, new)
493
+ d = self.new(old, new).diff
494
+ end
495
+
496
+ def self.preproc(tree, id)
497
+ DOMTools.transpose(tree) do |elt, update|
498
+ if dom_node?(elt) && attrs(elt)
499
+ update.call([tag(elt), DOMAttrs.from_hash(attrs(elt), id), *normalized_tags(body(elt))])
500
+ end
501
+ true
502
+ end
503
+ end
504
+
505
+ def self.unproc(tree)
506
+ tree.flatten.each{|elt| elt.reconstitute if elt.is_a?(DOMAttrs)}
507
+ tree
508
+ end
509
+
510
+ def self.dom_diff(old, new, id)
511
+ trees = []
512
+ [old, new].each {|tree| trees << preproc(tree, id)}
513
+ diff(*trees).map do |diff|
514
+ if diff.first == :add
515
+ diff[0..-2] + [unproc(diff[3])]
516
+ else
517
+ diff
518
+ end
519
+ end
520
+ end
521
+
522
+ def initialize(old, new)
523
+ @old = old
524
+ @new = new
525
+ end
526
+
527
+ def diff
528
+ cmp(@old, @new, nil)
529
+ end
530
+
531
+ def transpose(old, new, par)
532
+ [removed(old, par, 0), added(new, par, 0)]
533
+ end
534
+
535
+ def cmp_node(old, new, par)
536
+ oattrs = attrs(old)
537
+ if tag(old) == tag(new) && \
538
+ oattrs == attrs(new)
539
+ begin
540
+ cmp(body(old), body(new), oattrs ? domid(old) : nil)
541
+ rescue NoParentDOMID => e
542
+ transpose(old, new, par)
543
+ rescue TextualTranspose => e
544
+ transpose(old, new, par)
545
+ end
546
+ else
547
+ transpose(old, new, par)
548
+ end
549
+ end
550
+
551
+ def cmp_array(old, new, par)
552
+ if old.size == 1 && new.size == 1
553
+ return cmp(old.first, new.first, par)
554
+ end
555
+
556
+ patch = Diff::LCS.diff(old, new).flatten(1)
557
+
558
+ left, right = patch
559
+ if patch.size == 2 && lcs_is_replacement?(left, right)
560
+ # if we're just replacing an element, maybe we can do it more elegantly. e.g.,
561
+ # [:p, [:li, 'foo'], [:li, 'bar']] -> [:p, [:li, 'foo'], [:li, 'bar'], [:li, 'baz']]
562
+ # can be represented as a replacement of the p tag itself, or the insertion of
563
+ # [:li, 'baz']. If the patch looks like a simple replacement, we delve deeper.
564
+ dold, dnew = lcs_old_and_new(left, right)
565
+ old, new = dold.element, dnew.element
566
+ begin
567
+ return cmp(old, new, domid(old))
568
+ rescue NoParentDOMID, TextualTranspose
569
+ # fine fine if you hate fun we can just continue on our merry way below
570
+ end
571
+ end
572
+
573
+ a, b, off = 0, 0, 0
574
+ chgs = patch.map do |ch|
575
+ if lcs_is_addition?(ch)
576
+ while b < ch.position
577
+ a += 1; b += 1; off += 1
578
+ end
579
+ b += 1
580
+
581
+ mut = [added(ch.element, par, off)]
582
+ off += 1
583
+ mut
584
+ elsif lcs_is_removal?(ch)
585
+ while a < ch.position
586
+ a += 1; b += 1; off += 1
587
+ end
588
+ a += 1
589
+
590
+ [removed(ch.element, par, off)]
591
+ else
592
+ raise "Bad diff: #{ch.inspect}"
593
+ end
594
+ end
595
+
596
+ chgs.flatten(1)
597
+ end
598
+
599
+ def cmp(old, new, par)
600
+ return [] if old == new
601
+
602
+ told, tnew = type_of(old), type_of(new)
603
+ if told != tnew
604
+ if told == :string || tnew == :string
605
+ raise TextualTranspose
606
+ else
607
+ return transpose(old, new, par)
608
+ end
609
+ end
610
+
611
+ if told == :string || tnew == :string && old != new
612
+ raise TextualTranspose
613
+ end
614
+
615
+ type = told
616
+
617
+ if type == :node
618
+ cmp_node(old, new, par)
619
+ elsif type == :array
620
+ cmp_array(old, new, par)
621
+ else
622
+ raise "Unknown thing: #{old.inspect}"
623
+ end
624
+ end
625
+ end
626
+ end
627
+ end