goat 0.3.45 → 0.3.46

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.
@@ -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