diff.js/diff.js
Alex A. Naanou 906efabc69 some minor refactoring...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2018-07-24 15:31:16 +03:00

1285 lines
31 KiB
JavaScript

/**********************************************************************
*
*
*
**********************************************************************/
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
/*********************************************************************/
var ANY = {type: 'ANY_PLACEHOLDER'}
var NONE = {type: 'NONE_PLACEHOLDER'}
var EMPTY = {type: 'EMPTY_PLACEHOLDER'}
//---------------------------------------------------------------------
// Helpers...
// zip(array, array, ...)
// -> [[item, item, ...], ...]
//
// zip(func, array, array, ...)
// -> [func(i, [item, item, ...]), ...]
//
// XXX revise -- is this too complicated...
var zip = function(func, ...arrays){
var i = arrays[0] instanceof Array ? 0 : arrays.shift()
if(func instanceof Array){
arrays.splice(0, 0, func)
func = null
}
// build the zip item...
// NOTE: this is done this way to preserve array sparseness...
var s = arrays
.reduce(function(res, a, j){
//a.length > i
i in a
&& (res[j] = a[i])
return res
}, new Array(arrays.length))
return arrays
// check that at least one array is longer than i...
.reduce(function(res, a){
return Math.max(res, i, a.length) }, 0) > i ?
// collect zip item...
[func ? func(i, s) : s]
// get next...
.concat(zip(func, i+1, ...arrays))
// done...
: [] }
// Get common chunks (LCS)...
//
// Format:
// [
// <total-intersection-length>,
//
// {
// A: <offset-A>,
// B: <offset-B>,
// length: <section-length>,
// },
// ...
// ]
//
var getCommonSections = function(A, B, cmp, min_chunk){
cmp = cmp || function(a, b){
return a === b || a == b }
// XXX do we actually need this???
min_chunk = min_chunk || 1
var cache = cache || []
var _getCommonSections = function(a, b){
// cache...
var res = (cache[a] || [])[b]
if(res != null){
return res
}
// collect common chunk...
var chunk = {
A: a,
B: b,
length: 0,
}
var l = chunk.length
while((a+l < A.length && b+l < B.length)
// cmp non-empty slots only...
&& ((a+l in A && b+l in B) ?
cmp(A[a+l], B[b+l])
: (!(a+l in A) && !(b+l in B)))){
l = chunk.length += 1
}
// ignore small chunks...
l = chunk.length >= min_chunk ?
chunk.length
: 0
// get next chunks...
var L = A.length > a+l + min_chunk ?
_getCommonSections(l+a+1, l+b)
: [0]
var R = B.length > b+l + min_chunk ?
_getCommonSections(l+a, l+b+1)
: [0]
// select the best chunk-set...
// NOTE: we maximize the number of elements in a chunk set then
// minimize the number of chunks per set...
var next = L[0] == R[0] ?
(L.length < R.length ? L : R)
: L[0] > R[0] ?
L
: R
var res =
// non-empty chunk and next...
next[0] > 0 && l > 0 ?
[l + next[0], chunk].concat(next.slice(1))
// non-empty chunk and empty next...
: l > 0 ?
[l, chunk]
// empty chunk...
: next
// cache...
cache[a] = cache[a] || []
cache[a][b] = res
return res
}
return _getCommonSections(0, 0)
}
// Get diff sections...
//
// This is the reverse of getCommonSections(..)
//
// Format:
// [
// [
// [<offset-A>,
// [ <item>, ... ]],
// [<offset-B>,
// [ <item>, ... ]],
// ],
// ...
// ]
//
var getDiffSections = function(A, B, cmp, min_chunk){
// find the common sections...
var common_sections = getCommonSections(A, B, cmp, min_chunk)
common_sections.shift()
// collect gaps between common sections...
var a = 0
var b = 0
var gaps = []
common_sections
// make this consider the tail gap...
.concat({
A: A.length,
B: B.length,
length: 0,
})
.forEach(function(e){
// store the gap...
;(a != e.A || b != e.B)
&& gaps.push([
[a, A.slice(a, e.A)],
[b, B.slice(b, e.B)],
])
// go to next gap...
a = e.A + e.length
b = e.B + e.length
})
return gaps
}
// Make a proxy method...
//
// proxy('path.to.attr')
// -> method
//
// proxy('path.to.attr', function)
// -> method
//
var proxy = function(path, func){
path = path instanceof Array ?
path.slice()
: path.split(/\./)
var method = path.pop()
return function(...args){
var res = path.reduce(function(res, e){ return res[e] }, this)[method](...args)
return func ?
func.call(this, res, ...args)
: res
}
}
//---------------------------------------------------------------------
// Diff framework...
//
//
// Format (tree):
// <diff> ::=
// // no difference...
// null
//
// // A and/or B is a basic value...
// | {
// type: 'Basic',
//
// A: <value>,
// B: <value>,
//
// // optional payload data...
// ...
// }
//
// // A and B are arrays...
// | {
// type: 'Array',
//
// // NOTE: this is present only if A and B lengths are different...
// length: [<A-length>, <B-length>],
//
// // holds both index and attribute keys (mode-dependant)...
// items: [
// // NOTE: if an item does not exist in either A or B its
// // key will be null...
// [<key-a>, <key-b>, <diff>],
//
// // Slice change, the <diff> is treated as two arrays that
// // must be sliced in/out of the targets...
// // XXX not implemented -- need to think about this...
// [[<key-a>], [<key-b>], <diff>],
//
// ...
// ],
// // only for non-index keys...
// // XXX not implemented...
// item_order: <array-diff>,
// }
//
// // A and B are objects...
// | {
// type: 'Object',
//
// items: [
// [<key>, <diff>],
//
// // XXX not implemented....
// [<key-a>, <key-b>, <diff>],
// ...
// ],
// // XXX not implemented...
// item_order: <array-diff>,
// }
//
// // A and B are long strings...
// | {
// type: 'Text',
//
// // same structure as for 'Array'...
// ...
// }
//
//
// Format (flat):
// [
// // change...
// {
// // Change type (optional)...
// //
// // If not present then the change is simple item insertion
// // or splicing...
// //
// // NOTE: insertion vs. splicing depends on the values of .A,
// // .B and/or .path, see docs for those...
// type: <change-type>,
//
// // The path to the item in the object tree...
// //
// // Keys can be:
// // string - normal object key
// // number - array key
// // NOTE: this is actually not different
// // from a string...
// // array/2 - a set of 2 keys for A and B respectively
// // NOTE: if one of the array items in undefined
// // or null then it means that the item
// // does not exist in the corresponding
// // array...
// // NOTE: if both of the array items are arrays
// // it means that we are splicing array
// // sections instead of array elements...
// path: [<key>, ...],
//
// // values in A and B...
// //
// // Special values:
// // NONE - the slot does not exist (splice)
// // NOTE: unless options.keep_none is true,
// // NONE elements are not included in the
// // change...
// // EMPTY - the slot exists but it is empty (set/delete)
// A: <value> | EMPTY | NONE,
// B: <value> | EMPTY | NONE,
//
// // used if we are splicing array sections to indicate section
// // lengths, useful when splicing sparse sections...
// length: [a, b],
// },
// ...
// ]
//
// NOTE: all indexes (for arrays) are given relative to the actual input
// objects respectively as they were given. This does not account for
// the patch process.
// NOTE: this will lose some meta-information the diff format contains
// like the type information which is not needed for patching but
// may be useful for a more thorough compatibility check.
//
// XXX should we change this to a different API?
// ...the only "bad" thing I can think of is the dependency on object.js
// Example:
// var d = new Diff(A, B)
// d.patch(X)
// d.undo(X) // same as: d.reverse().patch(X)
// d.json()
// ...
//
// API (class):
// .cmp(A, B)
// .diff(A, B)
// ...
// API (instance):
// .patch(X)
// .check(X)
// .json()
// .load(json) // same as: new Diff(json)
// ...
var Types = {
__cache: null,
// Object-level utilities...
clone: function(){
var res = Object.create(this)
res.__cache = null
res.handlers = new Map(this.handlers.entries())
return res
},
clear: function(){
// XXX should we instead this.handlers.clear() ???
this.handlers = new Map()
return this
},
// Placeholder objects...
//
// Inseted when an item exists on one side and does not on the other.
//
// NOTE: for Array items this does not shift positions of other item
// positions nor does it affect the the array lengths.
// NOTE: these are compared by identity while diffing but are compared
// by value when patching...
NONE: NONE,
EMPTY: EMPTY,
get DIFF_TYPES(){
return new Set([
this.NONE,
this.EMPTY,
]) },
// Type handlers...
handlers: new Map(),
has: proxy('handlers.has'),
// Get handler...
//
// .get(object)
// .get(handler-type)
// .get(handler-type-name)
// -> handler | null
//
get: function(o){
var h = this.handlers
// get the type if there is no direct match...
o = !h.has(o) ? this.detect(o) : o
// resolve aliases...
do {
o = h.get(o)
} while(o != null && h.has(o))
return o
},
set: proxy('handlers.set',
function(res, key, handler){
// auto-alias...
key.name
&& this.set(key.name, key)
return res
}),
delete: proxy('handlers.delete'),
// sorted list of types...
// XXX do we need to cache this???
get types(){
var h = this.handlers
var order = new Map()
var i = 0
return [...h.keys()]
.filter(function(k){
k = h.get(k)
return k != null
&& !h.has(k)
&& order.set(k, i++)
})
.sort(function(a, b){
a = h.get(a)
b = h.get(b)
return a.priority && b.priority ?
(b.priority - a.priority
|| order.get(a) - order.get(b))
: a.priority ?
a.priority > 0 ? -1 : 1
: b.priority ?
b.priority > 0 ? 1 : -1
: order.get(a) - order.get(b)
})
},
get typeNames(){
return this.types.map(function(e){ return e.name || e }) },
// Detect handler type...
//
// Detect handler type for A...
// .detect(A)
// -> handler-type
//
// Detect common handler type for A and B...
// .detect(A, B)
// -> handler-type
//
//
// NOTE: if A and B types mismatch we treat them as Object...
detect: function(A, B, options){
var type
var types = this.types
// explicit checkers have priority over instance tests...
for(var t of types){
var h = this.get(t)
if(h.check
&& h.check(A, options)){
type = t
break
}
}
// search instances...
if(!type){
type = Object
for(var t of types){
// leave pure objects for last...
if(t === Object
// skip non-conctructor stuff...
|| !(t instanceof Function)){
continue
}
// full hit -- type match...
if(A instanceof t){
type = t
break
}
}
}
// combinational types...
if(B !== undefined){
var typeB = this.detect(B, undefined, options)
// type match...
if(type === typeB){
return type
// partial hit -- type mismatch...
} else {
return 'Basic'
}
}
return type
},
// Handle the difference between A and B...
//
handle: function(type, obj, diff, A, B, options){
// set .type
type = type == null ? this.detect(A, B, options) : type
obj.type = obj.type || (type.name ? type.name : type)
// get the handler + resolve aliases...
var handler = this.get(type)
// unhandled type...
if(handler == null
|| !(handler instanceof Function
|| handler.handle)){
throw new TypeError('Diff: can\'t handle: ' + type)
}
// call the handler...
handler.handle ?
handler.handle.call(this, obj, diff, A, B, options)
: handler.call(this, obj, diff, A, B, options)
return obj
},
// Diff format walker...
//
// XXX need to sort out return values...
walk: function(diff, func, path){
// no changes...
if(diff == null){
return null
}
// flat diff...
if(diff instanceof Array){
return diff.map(func)
// tree diff...
} else {
var handler = this.get(diff.type)
if(handler == null || !handler.walk){
throw new TypeError('Can\'t walk type: '+ diff.type)
}
return handler.walk.call(this, diff, func, path || [])
}
},
// Flatten the tree diff format...
//
// XXX might be good to include some type info so as to enable patching
// custom stuff like Text...
// XXX does change order matter here???
// ...some changes can affect changes after them (like splicing
// with arrays), this ultimately affects how patching is done...
// ...or is this a question of how we treat indexes and the patching
// algorithm???
// XXX we should be able to provide "fuzz" (context, horizontal) to
// the changes in ordered containers...
// ...it might also be possible to provide vertical/topological
// "fuzz", need to think about this...
// XXX TEST: the format should survive JSON.parse(JSON.stringify(..))...
flatten: function(diff, options){
options = options || {}
var res = []
this.walk(diff, function(change){ res.push(change) })
return res
},
// User API...
// Build a diff between A and B...
//
// NOTE: this will include direct links to items.
// NOTE: for format info see doc for Types...
//
// XXX might be a good idea to make a .walk(..) version of this...
// ...i.e. pass a function a nd call it with each change...
// XXX special case: empty sections do not need to be inserted...
// ...splice in a sparse array and store an Array diff with only
// length changed...
// XXX do we need to differentiate things like: new Number(123) vs. 123???
// XXX might be a god idea to mix in default options (different
// defaults per mode)...
// XXX TEST: the format should survive JSON.parse(JSON.stringify(..))...
diff: function(A, B, options, cache){
var that = this
options = options ? Object.create(options) : {}
options.cmp = options.cmp || function(a, b){
return a === b
|| a == b
// NOTE: diff(..) is in closure, see cache setup below...
|| (diff(a, b) == null) }
options.as_object = options.as_object || []
// same object...
// XXX do we need to differentiate things like: new Number(123) vs. 123???
if(A === B || A == B){
return null
}
// builtin types...
if(this.DIFF_TYPES.has(A) || this.DIFF_TYPES.has(B)){
return this.handle('Basic', {}, diff, A, B, options)
}
// cache...
cache = this.__cache = cache || this.__cache || new Map()
var diff = cache.diff = cache.diff || function(a, b){
var l2 = cache.get(a) || new Map()
var d = l2.get(b) || that.diff(a, b, options, cache)
cache.set(a, l2.set(b, d))
return d
}
// find the matching type...
var type = this.detect(A, B, options)
// handle type...
var res = this.handle(type, {}, diff, A, B, options)
// handle things we treat as objects (skipping object itself)...
if(type !== Object && type != 'Basic'
&& (options.as_object == 'all'
|| options.as_object.indexOf(type) >= 0
|| (type.name && options.as_object.indexOf(type.name) >= 0))){
this.handle(Object, res, diff, A, B, options)
}
// cleanup -- remove items containing empty arrays...
Object.keys(res)
.filter(function(k){
return res[k] instanceof Array && res[k].length == 0 })
.map(function(k){
delete res[k] })
// return only non-empty diff states...
return Object.keys(res).length == 1 ?
null
: res
},
// Deep-compare A and B...
//
cmp: function(A, B, options){
return this.diff(A, B, options) == null },
// Patch (update) obj via diff...
//
// XXX this needs to be able to replace obj or parts of it...
// XXX this does not work for:
// patch(diff(4,5), 4)
patch: function(diff, obj, options){
var that = this
var NONE = diff.placeholders.NONE
var EMPTY = diff.placeholders.EMPTY
var options = diff.options
this.walk(diff.diff, function(change){
// replace the object itself...
if(change.path.length == 0){
return change.B
}
var type = change.type || Object
var target = change.path
.slice(0, -1)
.reduce(function(res, e){
return res[e]}, obj)
var key = change.path[change.path.length-1]
// XXX this needs to be able to replace the target...
that.get(type).patch.call(that, target, key, change, options)
})
return obj
},
// Reverse diff...
//
// XXX should we do this or reverse patch / undo-patch???
reverse: function(diff){
// XXX
},
// Check if diff is applicable to obj...
//
check: function(diff, obj, options){
// XXX
},
}
//---------------------------------------------------------------------
// Specific type setup...
//
// Handler format:
// {
// // Type check priority (optional)...
// //
// // Types are checked in order of occurrence in .handlers unless
// // type .priority is set to a non 0 value.
// //
// // Default priorities:
// // Text: 100
// // Needs to run checks before 'Basic' as its targets are
// // long strings that 'Basic' also catches.
// // Basic: 50
// // Needs to be run before we do other object checks.
// // Object: -100
// // Needs to run after everything else as it will match any
// // set of objects.
// //
// // General guide:
// // >50 - to be checked before 'Basic'
// // <50 and >0 - after Basic but before unprioritized types
// // <-50 and <0 - after unprioritized types but before Object
// // <-100 - to be checked after Object -- this is a bit
// // pointless in JavaScript.
// //
// // NOTE: when this is set to 0, then type will be checked in
// // order of occurrence...
// priority: number | null,
//
// // Check if obj is compatible (optional)...
// //
// // .check(obj[, options])
// // -> bool
// //
// check: function(obj, options){
// ..
// },
//
// // Handle/populate the diff of A and B...
// //
// // Input diff format:
// // {
// // type: <type-name>,
// // }
// //
// handle: function(obj, diff, A, B, options){
// ..
// },
//
// // Walk the diff...
// //
// // This will pass each change to func(..) and return its result...
// //
// // .walk(diff, func, path)
// // -> res
// //
// walk: function(diff, func, path){
// ..
// },
// }
//
//
// NOTE: to add attribute checking to a specific type add it to
// options.as_object...
// XXX need a more flexible way to configure this, preferably from
// within the handler...
// XXX would also need to enable attribute filtering, at least for
// arrays as this will also check all the number keys...
//
// XXX do not like that we need to explicitly pass context to helper
// methods...
//
//
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Basic type / type-set...
//
// This is used to store two objects of either basic JavaScript
// types (string, number, bool, ...) or store two objects of
// mismatching types...
//
// NOTE: a basic type is one that returns a specific non-'object'
// typeof...
// i.e. when typeof(x) != 'object'
// NOTE: this does not need a .patch(..) method because it is not a
// container...
Types.set('Basic', {
priority: 50,
check: function(obj, options){
return typeof(obj) != 'object' },
handle: function(obj, diff, A, B, options){
;(!options.keep_none && A === NONE)
|| (obj.A = A)
;(!options.keep_none && B === NONE)
|| (obj.B = B)
},
walk: function(diff, func, path){
var change = Object.assign({
path: path,
}, diff)
delete change.type
return func(change)
},
})
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Object...
// XXX add attr order support...
Types.set(Object, {
priority: -100,
handle: function(obj, diff, A, B, options){
// attrebutes/items...
obj.items = (obj.items || [])
.concat(this.get(Object).attributes.call(this, diff, A, B, options))
// XXX optional stuff:
// - attr ordering...
// - prototypes
},
walk: function(diff, func, path){
var that = this
return (diff.items || [])
.map(function(e){
var i = e[0]
var p = path.concat([i])
var v = e[1]
return that.walk(v, func, p)
})
},
// XXX add object compatibility checks...
patch: function(obj, key, change){
// object attr...
if(typeof(key) == typeof('str')){
if(this.cmp(change.B, EMPTY)){
delete obj[key]
} else {
obj[key] = change.B
}
// array item...
// XXX should this make this decision???
} else {
return this.get(Array).patch.call(this, obj, key, change)
}
return obj
},
// part handlers...
attributes: function(diff, A, B, options, filter){
// JSON mode -> ignore attr order...
var kA = Object.keys(A)
var kB = Object.keys(B)
if(filter){
kA = filter instanceof Array ?
filter.slice()
: kA.filter(filter)
kB = filter instanceof Array ?
filter.slice()
: kB.filter(filter)
}
var B_index = kB.reduce(function(res, k){
res[k] = null
return res
}, {})
// items...
// XXX use zip(..)...
var items = kA
// A keys...
.map(function(ka){
var res = [ka,
diff(
A[ka],
ka in B_index ? B[ka] : EMPTY,
options)]
// remove seen keys...
delete B_index[ka]
return res
})
// keys present only in B...
.concat(Object.keys(B_index)
.map(function(kb){
return [kb,
diff(
EMPTY,
B[kb],
options)]}))
// cleanup...
.filter(function(e){
return e[1] !== null })
return items
},
})
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Array...
// XXX might be a good idea to add sub-section splicing, i.e. sub-arrays
// and not just rely on item-level...
// XXX add item order support...
Types.set(Array, {
handle: function(obj, diff, A, B, options){
obj.length = A.length != B.length ? [A.length, B.length] : []
obj.items = this.get(Array).items.call(this, diff, A, B, options)
},
walk: function(diff, func, path){
var that = this
var NONE = this.NONE
var res = []
//*/
// items...
return res.concat((diff.items || [])
.map(function(e){
var v = e[2]
// index...
var i = e[0] == e[1] ?
e[0]
: [e[0], e[1]]
var p = path.concat([i])
return that.walk(v, func, p)
}))
// length...
// NOTE: we keep this last as the length should be the last
// thing to get patched...
.concat(diff.length != null ?
func({
path: path.concat('length'),
A: diff.length[0],
B: diff.length[1],
})
: [])
},
// XXX add object compatibility checks...
patch: function(obj, key, change){
var i = key instanceof Array ? key[0] : key
var j = key instanceof Array ? key[1] : key
// sub-array manipulation...
if(i instanceof Array){
i = i[0]
j = j[0]
// XXX check compatibility...
obj.splice(j,
'A' in change ?
change.A.length
: change.length[0],
...('B' in change ?
change.B
: new Array(change.length[1])))
// item manipulation...
} else {
if(i == null){
// XXX this will mess up the indexing for the rest of
// item removals...
obj.splice(j, 0, change.B)
} else if(j == null){
// obj explicitly empty...
if('B' in change && this.cmp(change.B, EMPTY)){
delete obj[i]
// splice out obj...
} else if(!('B' in change) || this.cmp(change.B, NONE)){
// NOTE: this does not affect the later elements
// indexing as it essentially shifts the
// indexes to their obj state for next
// changes...
obj.splice(i, 1)
// XXX
} else {
// XXX
console.log('!!!!!!!!!!')
}
} else if(i == j){
obj[j] = change.B
} else {
obj[j] = change.B
}
}
return obj
},
// part handlers...
items: function(diff, A, B, options){
var NONE = this.NONE
var EMPTY = this.EMPTY
var sections = getDiffSections(A, B, options.cmp)
// special case: last section set consists of sparse/empty arrays...
var last = sections[sections.length-1]
last
&& last[0][1]
.concat(last[1][1])
.filter(function(e){ return e }).length == 0
&& sections.pop()
return sections
.map(function(gap){
var i = gap[0][0]
var j = gap[1][0]
var a = gap[0][1]
var b = gap[1][1]
// split into two: a common-length section and tails of
// 0 and l lengths...
var l = Math.min(a.length, b.length)
var ta = a.slice(l)
var tb = b.slice(l)
// tail sections...
// XXX hack???
// XXX should we use a different type/sub-type???
var tail = { type: 'Basic', }
ta.filter(() => true).length > 0
&& (tail.A = ta)
tb.filter(() => true).length > 0
&& (tail.B = tb)
tail.length = [ta.length, tb.length]
a = a.slice(0, l)
b = b.slice(0, l)
return zip(
function(n, elems){
return [
// if a slot exists it gets an index,
// otherwise null...
(0 in elems || n < a.length) ?
i+n
: null,
(1 in elems || n < b.length) ?
j+n
: null,
diff(
// use value, EMPTY or NONE...
0 in elems ?
elems[0]
: n < a.length ?
EMPTY
: NONE,
1 in elems ?
elems[1]
: n < b.length ?
EMPTY
: NONE,
options),
] },
a, b)
// clear matching stuff...
.filter(function(e){
return e[2] != null})
// splice array sub-sections...
.concat(ta.length + tb.length > 0 ?
[[
[i+l],
[j+l],
tail,
]]
: [])
})
.reduce(function(res, e){
return res.concat(e) }, [])
},
// XXX
order: function(diff, A, B, options){
// XXX
},
})
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX add JS types like Map, Set, ...
// XXX
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Text...
// XXX add .patch(..)
Types.set('Text', {
// this must be checked before the 'Base'...
priority: 100,
check: function(obj, options){
options = options || {}
min = options.min_text_length || 1000
return typeof(obj) == 'string'
&& min > 0
&& (obj.length > min
&& /\n/.test(obj))
},
handle: function(obj, diff, A, B, options){
options = Object.create(options || {})
// do not treat substrings as text...
options.min_text_length = -1
return this.handle(Array, obj, diff, A.split(/\n/), B.split(/\n/), options)
},
walk: function(diff, func, path){
// use the array walk but add 'Text' type to each change...
// NOTE: we need to abide by the protocol and call Array's
// .flatten(..) the context of the main object...
return this.get(Array).walk.call(this, diff, function(c){
// skip length changes...
if(c.path[c.path.length-1] == 'length'){
return
}
c.type = 'Text'
return func(c)
}, path)
},
// XXX
// XXX add object compatibility checks...
patch: function(change, obj){
// XXX
return obj
},
})
//---------------------------------------------------------------------
// Deep-compare objects...
//
// XXX would be nice to do a fast fail version of this, i.e. fail on
// first mismatch and do not waste time compiling a full diff we
// are going to throw away anyway...
// ...this would be possible with a live .walk(..) that would
// report changes as it finds them...
var cmp =
module.cmp =
function(A, B){
return Types.clone().cmp(A, B) }
// Diff interface function...
//
// This is a front-end to Types.diff(..), adding a metadata wrapper to
// the format, and optionally handling the topology of the output...
//
//
// Options format:
// {
// // if true return a tree diff format...
// tree_diff: false | true,
//
// // if true, NONE change items will not be removed from the diff...
// keep_none: false | true,
//
// // Minimum length of a string for it to be treated as Text...
// //
// // If this is set to a negative number Text diffing is disabled.
// //
// // NOTE: a string must also contain at least one \n to be text
// // diffed...
// min_text_length: 1000 | -1,
//
// // list of types we treat as objects, i.e. check attributes...
// as_object: [ .. ] | Set([ .. ]),
//
// // Plaeholders to be used in the diff..
// //
// // Set these if the default values conflict with your data...
// //
// // XXX remove these from options in favor of auto conflict
// // detection and hashing...
// NONE: null | { .. },
// EMPTY: null | { .. },
//
//
// // Internal options...
//
// // do not include length changes in flattened array diffs...
// // NOTE: if this is not set by user then this is set by Text's
// // .flatten(..) to exclude the .length changes form the
// // text diff.
// no_length: false | true,
//
// // element compare function...
// cmp: function(a, b){ .. },
// }
//
//
// Output format:
// {
// format: 'object-diff',
// version: '0.0.0',
// structure: 'flat' | 'tree',
// // NOTE: these are stored in the diff to make the diff independent
// // of future changes to the values of the placeholder, both
// // in spec and as means to avoid data collisions...
// // NOTE: these are compared by identity while diffing but are
// // compared by value when patching...
// placeholders: {
// ...
// },
//
// options: <user-options>,
//
// diff: <diff>,
// }
//
//
// NOTE: the format itself is JSON compatible but the data in the changes
// may not be, so if JSON compatibility is desired, the inputs or
// at least the differences between them must be JSON compatible.
// NOTE: recursive inputs will result in recursive diff objects.
//
// XXX should we instantiate Types here so as to make all the caching
// call-specific???
// XXX revise how the types can be passed in...
var diff =
module.diff =
function(A, B, options, types){
options = options || {}
types = types || Types.clone()
return {
// system meta information...
format: 'object-diff',
varsion: '0.0.0',
structure: options.tree_diff ? 'tree' : 'flat',
placeholders: {
NONE: options.NONE || Types.NONE,
EMPTY: options.NONE || Types.EMPTY,
},
// user data...
options: Object.assign({}, options),
diff: options.tree_diff ?
types.diff(A, B, options)
//: types.flatten(Types.diff(A, B, options), null, null, options)
: types.flatten(Types.diff(A, B, options), options)
}}
// Apply diff (patch) to obj...
//
// This is a front-end to Types.patch(..), handling loading the options
// from the diff...
//
var patch =
module.patch =
function(diff, obj, options, types){
var t = Object.create(types || Types)
diff.placeholders
&& Object.assign(t, diff.placeholders)
return t.patch(diff, obj, options)
}
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })