goat 0.3.45 → 0.3.46
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/state-srv +23 -11
- data/goat.gemspec +2 -2
- data/lib/goat.rb +96 -208
- data/lib/goat/common.rb +13 -4
- data/lib/goat/dom.rb +627 -0
- data/lib/goat/extn.rb +1 -1
- data/lib/goat/goat.js +61 -39
- data/lib/goat/state-srv.rb +47 -7
- metadata +5 -5
- data/lib/goat/html.rb +0 -301
data/lib/goat/common.rb
CHANGED
@@ -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(
|
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
|
data/lib/goat/dom.rb
ADDED
@@ -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
|