diff.js/diff.js
Alex A. Naanou 1edeb867fa .filter(..) almost ready, still needs more testing..
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2018-08-24 14:39:41 +03:00

2257 lines
56 KiB
JavaScript

/**********************************************************************
*
*
*
**********************************************************************/
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
/*********************************************************************/
var FORMAT_NAME = 'object-diff'
var FORMAT_VERSION = '0.0.0'
var MIN_TEXT_LENGTH = 100
/*********************************************************************/
//
// XXX General ToDo:
// - revise architecture...
// - merge Types and Diff
// - cmp(..) / diff(..) / patch(..) to use Diff(..)
// - revise name -- this contains two parts:
// 1. diff / patch and friends
// 2. cmp and patterns
// we need the name to be short and descriptive, possible
// candidates:
// - objdiff / object-diff
// - diffcmp / diff-cmp
// - compare
// - revise docs...
// ...should be simpler to enter, maybe example-oriented intro
//
//
//---------------------------------------------------------------------
// 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
}
}
//---------------------------------------------------------------------
// Placeholders...
var NONE = {type: 'NONE_PLACEHOLDER'}
var EMPTY = {type: 'EMPTY_PLACEHOLDER'}
//---------------------------------------------------------------------
// Logic patterns...
//
// XXX add use-case docs...
// XXX need to avoid recursion...
//
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var LogicTypeClassPrototype = {
}
var LogicTypePrototype = {
__cmp__: function(obj, cmp){
return false
},
// XXX need to track loops...
cmp: function(obj, cmp, cache){
cmp = cmp || function(a, b){
return a === b
|| a == b
|| (a.__cmp__ && a.__cmp__(b, cmp, cache))
|| (b.__cmp__ && b.__cmp__(a, cmp, cache)) }
// cache...
cache = cache || new Map()
var c = cache.get(this) || new Map()
cache.has(c)
|| cache.set(this, c)
if(c.has(obj)){
return c.get(obj)
}
var res = this.__cmp__(obj, cmp, cache)
|| (obj.__cmp__
&& obj.__cmp__(this, cmp, cache))
c.set(obj, res)
return res
},
}
var LogicType =
module.LogicType =
object.makeConstructor('LogicType',
LogicTypeClassPrototype,
LogicTypePrototype)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX rename -- this makes a constructor/instance combination...
var makeCIPattern = function(name, check, init){
var o = Object.assign(Object.create(LogicTypePrototype), {
__cmp__: check,
})
init
&& (o.__init__ = init)
return object.makeConstructor(name, o, o)
}
// Singleton ANY...
//
// ANY
// -> pattern
//
var ANY =
module.ANY =
makeCIPattern('ANY',
function(){ return true })()
// String pattern...
//
// STRING
// STRING(string)
// STRING(regexp)
// STRING(func)
// STRING(pattern)
// -> pattern
//
var STRING =
module.STRING =
makeCIPattern('STRING',
function(obj, cmp){
return obj === STRING
|| (typeof(obj) == typeof('str') && this.value == null)
|| (typeof(obj) == typeof('str')
&& (this.value instanceof RegExp ?
this.value.test(obj)
: typeof(this.value) == typeof('str') ?
this.value == obj
: this.value instanceof Function ?
this.value(obj)
// pattern...
: this.value != null ?
cmp(this.value, obj)
: true )) },
function(value){ this.value = value })
// Number pattern...
//
// NUMBER
// NUMBER(n)
// NUMBER(min, max)
// NUMBER(min, max, step)
// NUMBER(func)
// NUMBER(pattern)
// -> pattern
//
var NUMBER =
module.NUMBER =
makeCIPattern('NUMBER',
function(obj, cmp){
return obj === NUMBER
|| (typeof(obj) == typeof(123) && this.value == null)
|| (typeof(obj) == typeof(123)
&& (this.value.length == 1
&& typeof(this.value[0]) == typeof(123)?
this.value[0] == obj
// min/max...
: this.value.length == 2 ?
this.value[0] <= obj
&& this.value[1] > obj
// min/max/step...
: this.value.length == 3 ?
this.value[0] <= obj
&& this.value[1] > obj
&& (obj + (this.value[0] % this.value[2])) % this.value[2] == 0
: this.value[0] instanceof Function ?
this.value[0](obj)
// pattern...
: this.value[0] != null ?
cmp(this.value[0], obj)
: true )) },
function(...value){ this.value = value })
// Array pattern...
//
// ARRAY
// ARRAY(length)
// ARRAY(func)
// ARRAY(pattern)
// ARRAY(test, ...)
// -> pattern
//
// NOTE: func and pattern if given are applied to each array item and
// the match is made iff for each item the function returns true or
// the pattern matches.
// NOTE: multiple tests (length, func, pattern) can be combined in any
// order, this is a shorthand:
// ARRAY(4, STRING)
// is the same as:
// AND(ARRAY(4), ARRAY(STRING))
// NOTE: order of arguments is not important, but it is possible to add
// a set of conflicting arguments...
var ARRAY =
module.ARRAY =
makeCIPattern('ARRAY',
function(obj, cmp){
return obj === ARRAY
//|| (obj instanceof Array && this.value.length == 0)
|| (obj instanceof Array
// XXX make this fail on first fail -- currently
// this runs every test on every elem...
&& this.value.filter(function(value){
return (typeof(value) == typeof(123) ?
obj.length == value
// function...
: value instanceof Function ?
obj.filter(value).length == obj.length
// pattern...
: obj.filter(function(e){
return cmp(value, e)
}).length == obj.length)
}).length == this.value.length) },
function(...value){ this.value = value })
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Will compare as true to anything but .value...
var NOT =
module.NOT =
object.makeConstructor('NOT', Object.assign(new LogicType(), {
__cmp__: function(obj, cmp){
return !cmp(this.value, obj) },
__init__: function(value){
this.value = value
},
}))
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Will compare as true if one of the .members compares as true...
var OR =
module.OR =
object.makeConstructor('OR', Object.assign(new LogicType(), {
__cmp__: function(obj, cmp){
for(var m of this.members){
if(cmp(m, obj)){
return true
}
}
return false
},
__init__: function(...members){
this.members = members
},
}))
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Will compare as true if all of the .members compare as true...
var AND =
module.AND =
object.makeConstructor('AND', Object.assign(new LogicType(), {
__cmp__: function(obj, cmp){
for(var m of this.members){
if(!cmp(m, obj)){
return false
}
}
return true
},
__init__: function(...members){
this.members = members
},
}))
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Will match a number greater than or equal to min and less than max...
// XXX rename...
var _NUMBER =
module._NUMBER =
object.makeConstructor('NUMBER', Object.assign(new LogicType(), {
__cmp__: function(obj, cmp){
if(typeof(obj) == 'number'
&& obj >= this.min
&& obj <= this.max){
return true
}
return false
},
__init__: function(min, max){
this.min = min
this.max = max
},
}))
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// IN(A) == L iff A in L
//
// NOTE: since this can do a search using cmp(..) thid will be slow on
// large containers...
//
// XXX add support for other containers...
var IN =
module.IN =
object.makeConstructor('IN', Object.assign(new LogicType(), {
__cmp__: function(obj, cmp){
var p = this.value
// XXX add support for other stuff like sets and maps...
// XXX make this a break-on-match and not a go-through-the-whole-thing
return typeof(obj) == typeof({})
&& (p in obj
|| obj.reduce(function(res, e){
return res === false ?
cmp(p, e)
: res }), false) },
__init__: function(value){
this.value = value
},
}))
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX AT(A, K) == L iff A in L and L[K] == A
// XXX .key can't be a pattern at this point...
var AT =
module.AT =
object.makeConstructor('AT', Object.assign(new LogicType(), {
__cmp__: function(obj, cmp){
if(cmp(obj[this.key], this.value)){
return true
}
return false
},
__init__: function(value, key){
this.key = key
this.value = value
},
}))
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX OF(A, N) == L iff L contains N occurrences of A
var OF =
module.OF =
object.makeConstructor('OF', Object.assign(new LogicType(), {
__cmp__: function(obj, cmp){
// XXX
},
__init__: function(value, count){
this.count = count
this.value = value
},
}))
//---------------------------------------------------------------------
// Diff framework...
//
//
// General architecture:
// Types
// Low-level diff routines.
// Diff
// User interface to Types.
// XXX should this encapsulate or inherit (current) or do a mix of two??
// ...a mix of the two seems logical as we might need access to
// the type handlers (proxy) and the rest of the low-level stuff
// can be hidden apart for very specific things (.cmp(..))...
//
//
// 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...
// [[<key-a>, <length>], [<key-b>, <length>], <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...
// // [<key>, <key>]
// // - a set of 2 keys for A and B respectively,
// // <key> can be one of:
// // null - item does not exist.
// // index - item index.
// // [index, length] - item is a sub-array and
// // will replace a section
// // of length length.
// // 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,
// },
// ...
// ]
//
// 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.
var Types =
module.Types = {
// system meta information...
format: FORMAT_NAME,
version: FORMAT_VERSION,
// Object-level utilities...
clone: function(){
var res = Object.create(this)
//res.__cache = null
res.handlers = new Map([...this.handlers
.entries()]
.map(function(e){
return [
e[0],
e[1].handle ?
Object.create(e[1])
: e[1]
] }))
return res
},
clear: function(){
// XXX should we instead this.handlers.clear() ???
//this.handlers = new Map()
this.handlers.clear()
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...
ANY: ANY,
NONE: NONE,
EMPTY: EMPTY,
get DIFF_TYPES(){
return new Set([
this.ANY,
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 typeKeys(){
var that = this
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.typeKeys.map(function(e){ return e.name || e }) },
get types(){
var that = this
return this.typeKeys
.map(function(e){
return that.get(e) })
},
// helper...
typeCall: function(type, func, ...args){
return this.get(type)[func].call(this, ...args) },
// 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
//
//
// Basic type detection rules (single object):
// 1. use type's .check(..) to check object belongs to a type
// 2. use instanceof / .constructor to get object type
//
// NOTE: for single object stage 2 will return the actual object
// type (.constructor)
//
//
// Basic common type detection rules:
// - A and B types mismatch
// -> 'Basic'
// - A and B types match and type handler is in .types
// -> type
// - A and B types match and type handler is NOT in .types
// -> 'Basic'
//
detect: function(A, B, options){
var type
var types = this.typeKeys
// explicit checkers have priority over instance tests...
for(var t of types){
var h = this.get(t)
if(h.compatible
&& h.compatible(A, options)){
type = t
break
}
}
// search instances...
if(!type){
//type = Object
type = A.constructor
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){
if(type === typeB && this.has(type)){
return type
// partial hit -- type mismatch...
} else {
return 'Basic'
}
}
return type
},
// Handle the difference between A and B...
//
// NOTE: this uses .detect(..) for type detection.
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...
//
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...
flatten: function(diff, options){
options = options || {}
var res = []
this.walk(diff, function(change){ res.push(change) })
return res
},
// Reverse diff...
//
reverse: function(diff){
var that = this
return this.walk(diff, function(change){
var c = Object.assign({}, change)
// path...
c.path = c.path.slice().map(function(e){
return e instanceof Array ?
e.slice().reverse()
: e })
that.types.forEach(function(type){
type.reverse
&& (c = type.reverse.call(that, c)) })
return c
})
},
// 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.as_object = options.as_object || []
// basic compare...
// XXX do we need to differentiate things like: new Number(123) vs. 123???
var bcmp = function(a, b, cmp){
return a === b
|| a == b
// basic patters...
|| a === that.ANY
|| b === that.ANY
// logic patterns...
|| (a instanceof LogicType
&& a.cmp(b, cmp, cache))
|| (b instanceof LogicType
&& b.cmp(a, cmp, cache)) }
// deep compare...
var cmp = options.cmp = options.cmp
|| function(a, b){
return bcmp(a, b, cmp)
// diff...
// NOTE: diff(..) is in closure, so we do not need to
// pass options and cache down.
// see cache setup below...
|| (diff(a, b) == null) }
// cache...
//cache = this.__cache = cache || this.__cache || new Map()
cache = 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
}
// check: if same/matching object...
// NOTE: this will essentially do a full diff of the input trees
// skipping only the top level, the actual A and B...
// NOTE: since actual A and B are not diffed here (as we start with
// bcmp(..) and not cmp(..), see above note), it makes no
// sense to do a cache check after this as we will exit this
// check with everything but the root cached/diffed...
// XXX not sure if this is efficient...
if(bcmp(A, B, cmp)){
return null
}
// check: builtin types...
if(this.DIFF_TYPES.has(A) || this.DIFF_TYPES.has(B)){
return this.handle('Basic', {}, diff, A, B, options)
}
// 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(!options.no_attributes
&& !this.get(type).no_attributes){
// XXX need to strip array items from this...
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...
//
// 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...
cmp: function(A, B, options){
return this.diff(A, B, options) == null },
// Patch (update) obj via diff...
//
// XXX should we check for patch integrity???
// bad patches would include:
// - including both a.b and a.b.c is a conflict.
patch: function(diff, obj, options){
var that = this
var NONE = diff.placeholders.NONE
var EMPTY = diff.placeholders.EMPTY
var options = diff.options
// NOTE: in .walk(..) we always return the root object bing
// patched, this way the handlers have control over the
// patching process and it's results on all levels...
// ...and this is why we can just pop the last item and
// return it...
// NOTE: this will do odd things for conflicting patches...
// a conflict can be for example patching both a.b and
// a.b.c etc.
return this.postPatch(this
.walk(diff.diff, function(change){
// replace the object itself...
if(change.path.length == 0){
return change.B
}
var parent
var parent_key
var target = change.path
.slice(0, -1)
.reduce(function(res, e){
parent = res
parent_key = e
return res[e]
}, obj)
var key = change.path[change.path.length-1]
var type = change.type || Object
// call the actual patch...
var res = that.typeCall(type, 'patch', target, key, change, obj, options)
// replace the parent value...
if(parent){
parent[parent_key] = res
} else {
obj = res
}
return obj
})
.pop())
},
// XXX need to support different path element types...
// ...in addition to Object and Array items, support Map, Set, ...
getPath: function(obj, path){
return path
.reduce(function(res, e){
return res[e] }, obj) },
// XXX make this an extensible walker...
// ...ideally passed a func(A, B, obj, ...) where:
// A - change.A
// B - change.B
// obj - object at change.path
// func(..) should be able to:
// - replace obj with B/A (patch/unpatch)
// ...current implementation of .patch(..)
// - check obj against B/A (check)
// - swap A and B (reverse) ???
// - ...
// one way to do this is to pass func(..) a handler that it
// would call to control the outcome...
// ...still needs thought, but this feels right...
_walk: function(diff, obj, func, options){
var that = this
var NONE = diff.placeholders.NONE
var EMPTY = diff.placeholders.EMPTY
var options = diff.options
// NOTE: in .walk(..) we always return the root object bing
// patched, this way the handlers have control over the
// patching process and it's results on all levels...
// ...and this is why we can just pop the last item and
// return it...
// NOTE: this will do odd things for conflicting patches...
// a conflict can be for example patching both a.b and
// a.b.c etc.
return this.postPatch(this
// XXX do we need to merge .walk(..) into this??
.walk(diff.diff, function(change){
// replace the object itself...
if(change.path.length == 0){
return change.B
}
var parent
var parent_key
var target = change.path
.slice(0, -1)
.reduce(function(res, e){
parent = res
parent_key = e
return res[e]
}, obj)
var key = change.path[change.path.length-1]
var type = change.type || Object
// call the actual patch...
// XXX the key can be contextual so we either need to pass
// down the context (change and what side we are
// looking from, A or B) or make the keys context-free
// and handle them here...
var res = that.typeCall(type, 'get', target, key)
// replace the parent value...
if(parent){
parent[parent_key] = res
} else {
obj = res
}
return obj
})
.pop())
},
// Call the post-patch method of the handlers...
//
postPatch: function(res){
var that = this
return [...this.types]
.filter(function(e){
return !!e.postPatch })
.reduce(function(r, e){
return e.postPatch.call(that, r) }, res) },
// Check if diff is applicable to obj...
//
// XXX should this return a diff???
// XXX need a custom check for custom handler...
// ...we also have a name clash whit .check(..) that checks if
// the object type is compatible to handler...
// XXX EXPERIMENTAL...
// ...this seems to be mirroring most of the patch architecture
// need to either merge or generalize...
check: function(diff, obj, options){
var that = this
options = options || {}
var NONE = options.NONE || this.NONE
var EMPTY = options.EMPTY || this.EMPTY
return this.flatten(diff)
.filter(function(change){
var key = change.path[change.path.length-1]
var target = change.path
.slice(0, -1)
.reduce(function(res, k){
return res[k] }, obj)
// check root...
if(key == null){
return !that.cmp(change.A, target)
}
// keep only the mismatching changes...
return change.type && that.get(change.type).check ?
!that.typeCall(change.type, 'check', target, key, change)
: !('A' in change) || change.A === NONE ?
!(key in target)
: change.A === EMPTY ?
!(!(key in target) && target[key] === undefined)
// XXX should this be handled by Array???
: key instanceof Array ? (
key[0] instanceof Array ?
!that.cmp(change.A,
target.slice(key[0][0], key[0][0] + target.length[0]))
: !that.cmp(change.A, target[key[0]]))
: !that.cmp(change.A, target[key])
})
},
}
//---------------------------------------------------------------------
// 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,
//
// // If set to true will disable additional attribute diffing on
// // matching objects...
// no_attributes: false | true,
//
//
// // Check if obj is compatible (optional)...
// //
// // .compatible(obj[, options])
// // -> bool
// //
// compatible: 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
// //
// // NOTE: by default this will not handle attributes (.attrs), so
// // if one needs to handle them Object's .walk(..) should be
// // explicitly called...
// // Example:
// // walk: function(diff, func, path){
// // var res = []
// //
// // // handle specific diff stuff...
// //
// // return res
// // // pass the .items handling to Object
// // .concat(this.typeCall(Object, 'walk', diff, func, path))
// // }
// // XXX can this be automated???
// walk: function(diff, func, path){
// ..
// },
//
// // Patch the object...
// //
// patch: function(target, key, change, root, options){
// ..
// },
//
// // Finalize the patch process (optional)...
// //
// // This is useful to cleanup and do any final modifications.
// //
// // This is expected to return the result.
// //
// // see: 'Text' for an example.
// postPatch: function(result){
// ..
//
// return result
// },
//
// // Reverse the change...
// //
// reverse: function(change){
// ..
// },
// }
//
//
// 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,
no_attributes: true,
compatible: 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)
},
reverse: function(change){
var b = 'B' in change
var a = 'A' in change
var t = change.B
a ?
(change.B = change.A)
: (delete change.B)
b ?
(change.A = t)
: (delete change.A)
return change
},
})
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Object...
// XXX add attr order support...
Types.set(Object, {
priority: -100,
// NOTE: the object will itself handle the attributes, no need for a
// second pass...
no_attributes: true,
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, ...rest){
// 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 {
this.typeCall(Array, 'patch', obj, key, change, ...rest)
}
return obj
},
// XXX EXPERIMENTAL...
get: function(obj, key){
return typeof(key) == typeof('str') ?
obj[key]
: this.typeCall(Array, 'get', obj, key) },
set: function(obj, key, value){
// XXX
},
// part handlers...
//
// NOTE: attr filtering depends on the order that Object.keys(..)
// returns indexed items and attributes it...
attributes: function(diff, A, B, options){
// get the attributes...
// special case: we omit array indexes from the attribute list...
var kA = Object.keys(A)
kA = A instanceof Array ?
kA.slice(A.filter(function(){ return true }).length)
: kA
var kB = Object.keys(B)
kB = B instanceof Array ?
kB.slice(B.filter(function(){ return true }).length)
: kB
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 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.typeCall(Array, 'items', diff, A, B, options)
},
// XXX need to encode length into path/index...
walk: function(diff, func, path){
var that = this
var NONE = this.NONE
var attrs = []
// items...
return (diff.items || [])
.filter(function(e){
return e.length == 2 ?
attrs.push(e) && false
: true
})
.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],
})
: [])
// attributes...
.concat(this.typeCall(Object, 'walk', {items: attrs}, func, path))
},
// XXX add object compatibility checks...
// XXX revise...
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){
// XXX remove .length support...
var li = i[1]
var lj = j[1]
i = i[0]
j = j[0]
// XXX check compatibility...
obj.splice(j,
'A' in change ?
change.A.length
: li,
...('B' in change ?
change.B
// NOTE: this will insert a bunch of undefined's and
// not empty slots, this we will need to cleanup
// after (see below)...
: new Array(lj)))
// cleanup...
// XXX test...
if(!('B' in change)){
for(var n=j; n <= lj + j - li; n++){
delete obj[n]
}
}
// 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('!!!!!!!!!!')
}
// XXX can we have cases where:
// B is not in change
// B is NONE
// ...no because then j would be null and handled above...
} else if(i == j){
if(this.cmp(change.B, EMPTY)){
delete obj[j]
} else {
obj[j] = change.B
}
// XXX this is essentially the same as the above case, do we need both??
} else {
obj[j] = change.B
}
}
return obj
},
reverse: function(change){
if('length' in change){
change.length = change.length.slice().reverse()
}
return change
},
// XXX EXPERIMENTAL...
get: function(obj, key){
return key instanceof Array ?
obj.slice(key[0], key[0] + (key[1] != null ? key[1] : 1))
: obj[key] },
set: function(obj, key, value){
// sub-array...
if(key instanceof Array){
obj.splice(key[0], key[1] || 0, ...value)
// EMPTY...
} else if(value === this.EMPTY){
delete obj[key]
// NONE...
} else if(value === this.NONE){
obj.splice(key, 0)
// item...
} else {
obj[key] = value
}
return this
},
// 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???
// XXX need to encode length into path/index...
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)
// Builds:
// [
// [i, j, diff],
// ...
// [[i], [i], tail],
// ]
// XXX need to encode length into path/index...
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],
[i+l, ta.length],
[j+l, tb.length],
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 Q: can Map/Set be supported???
// - there is not uniform item access
// -> need to type path elements
// - Sets have no keys
// -> no way to access/identify specific items
// - Maps use specific objects as keys
// -> no way to store a diff and then still match an item
// -> two different keys may be represented by identical in
// topology but different in identity objects...
// Ex:
// var m = new Map([
// [ [], 123 ],
// [ [], 321 ],
// ])
// Possible approaches:
// - index items by order instead of key
// - use a best overall match as indication...
// - serialize...
// ...will need a way to sort the items in a stable way...
/*/ XXX for now unsupported types will be treated as object...
Types.set(Map, {
handle: function(obj, diff, A, B, options){
throw new TypeError('Map handling not implemented.')
},
})
//*/
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Text...
Types.set('Text', {
// this must be checked before the 'Base'...
priority: 100,
no_attributes: true,
compatible: function(obj, options){
options = options || {}
min = options.min_text_length || MIN_TEXT_LENGTH
return typeof(obj) == typeof('str')
&& 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.typeCall(Array, 'walk', diff, function(c){
// skip length changes...
if(c.path[c.path.length-1] == 'length'){
return
}
c.type = 'Text'
return func(c)
}, path)
},
// NOTE: we return here arrays, joining is done in .postPatch(..)
// XXX add object compatibility checks...
patch: function(obj, key, change){
var cache = this._text_cache = this._text_cache || {}
var path = JSON.stringify(change.path.slice(0, -1))
var lines = cache[path] = cache[path] || obj.split(/\n/)
var res = cache[path] = this.typeCall(Array, 'patch', lines, key, change)
return res
},
// XXX EXPERIMENTAL...
get: function(obj, key){
},
set: function(obj, key, value){
},
// replace all the cached text items...
postPatch: function(res){
var cache = this._text_cache = this._text_cache || {}
Object.keys(cache)
.forEach(function(path){
var text = cache[path].join('\n')
path = JSON.parse(path)
// root object...
if(path.length == 0){
res = text
} else {
path.slice(0, -1)
.reduce(function(res, k){
return res[k] }, res)[path.pop()] = text
}
})
return res
},
})
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// LogicType...
/*/ XXX show the actual part of the pattern we got a mismatch...
Types.set(LogicType, {
handle: function(obj, diff, A, B, options){
// XXX
}
walk: function(diff, func, path){
// XXX
},
reverse: function(change){
// XXX
},
})
//*/
//---------------------------------------------------------------------
// The diff object...
//
// Create a diff...
// Diff(A, B[, options])
// new Diff(A, B[, options])
// -> diff
//
//
// 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: 100 | -1,
//
// // If true, disable attribute diff for non Object's...
// //
// // XXX should be more granular...
// no_attributes: false | true,
//
// // 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.
//
//
//
// Extending Diff...
//
// // create a new diff constructor...
// var ExtendedDiff = Diff.clone('ExtendedDiff')
//
// // add a new type...
// ExtendedDiff.types.set(SomeType, {
// ...
// })
//
// // add a new synthetic type...
// ExtendedDiff.types.set('SomeOtherType', {
// compatible: function(..){ .. },
// ...
// })
//
// // remove an existing type...
// ExtendedDiff.types.delete('Text')
//
//
var DiffClassPrototype = {
// encapsulate the low-level types...
types: Types,
// create a new diff constructor with a detached handler set...
clone: function(name){
var cls = Object.create(this.__proto__)
cls.types = this.types.clone()
return object.makeConstructor(name || 'EDiff', cls, this())
},
// proxy generic stuff to .types...
cmp: proxy('types.cmp'),
// XXX do format/version conversion...
fromJSON: function(json){
var diff = new this()
if(json.format == diff.format
&& json.version == diff.version){
// XXX do a deep copy...
diff.options = JSON.parse(JSON.stringify(json.options))
diff.placeholders = JSON.parse(JSON.stringify(json.placeholders))
diff.diff = JSON.parse(JSON.stringify(json.diff))
return diff
// XXX do format conversion...
} else {
}
},
}
// XXX need to make the diff object the universal context...
// ...currently the context for most things is .constructor.types
// which is global this anything that any handler does is not local
// to a particular diff instance...
var DiffPrototype = {
// system meta information...
get format(){
return this.constructor.format },
get version(){
return this.constructor.version },
// XXX is this the right thing to do???
// ...the bad thing here is that this can be mutated from the
// instance when returned like this...
//get types(){
// return this.constructor.type },
structure: null,
placeholders: null,
options: null,
diff: null,
__init__: function(A, B, options){
// XXX should we add a default options as prototype???
options = this.options = options || {}
this.structure = options.tree_diff ? 'tree' : 'flat'
this.placeholders = {
NONE: options.NONE
|| this.constructor.types.NONE,
EMPTY: options.NONE
|| this.constructor.types.EMPTY,
}
var diff = this.constructor.types
// XXX should the Types instance be stored/cached here???
this.diff = arguments.length == 0 ?
null
: options.tree_diff ?
diff.diff(A, B, options)
: diff.flatten(diff.diff(A, B, options), options)
},
// XXX should this be a deep copy???
clone: function(){
var res = new this.constructor()
res.structure = this.structure
res.placeholders = Object.assign({}, this.placeholders)
// XXX should this be a deep copy???
res.options = Object.assign({}, this.options)
// XXX should this be a deep copy???
res.diff = this.diff instanceof Array ?
this.diff.slice()
: this.diff ?
Object.assign({}, this.diff)
: null
return res
},
// NOTE: this will not mutate this...
reverse: function(obj){
var res = this.clone()
res.diff = Object.create(this.constructor.types).reverse(this.diff)
return res
},
check: function(obj){
return Object.create(this.constructor.types).check(this.diff, obj) },
patch: function(obj){
return Object.create(this.constructor.types).patch(this, obj) },
unpatch: function(obj){
return this.reverse().patch(obj) },
// XXX add support for '**' path globs...
filter: function(filter){
var res = this.clone()
// string filter...
filter = typeof(filter) == typeof('str') ?
filter.split(/[\\\/]/)
: filter
// path filter (non-function)...
if(!(filter instanceof Function)){
// normalize path...
// format:
// [
// '**' | [ .. ],
// ...
// ]
// XXX when OF(..) is ready, replace '**' with OF(ANY, ANY)...
var path = (filter instanceof Array ? filter : [filter])
// '*' -> ANY
.map(function(e){
return e == '*' ? ANY : e })
// remove consecutive repeating '**'
.filter(function(e, i, lst){
return e == '**' && lst[i-1] != '**' || true })
// split to array sections at '**'...
.reduce(function(res, e){
var n = res.length-1
e == '**' ?
res.push('**')
: (res.length == 0 || res[n] == '**') ?
res.push([e])
: res[n].push(e)
return res
}, [])
// min length...
var min = path
.reduce(function(l, e){
return l + (e instanceof Array ? e.length : 0) }, 0)
// XXX account for pattern/path end...
var test = function(path, pattern){
return (
// end of path/pattern...
path.length == 0 && pattern.length == 0 ?
true
// consumed pattern with path left over -> fail...
: (path.length > 0 && pattern.length == 0)
|| (path.length == 0 && pattern.length > 1)?
false
// '**' -> test, skip elem and repeat...
: pattern[0] == '**' ?
(test(path, pattern.slice(1))
|| test(path.slice(1), pattern))
// compare sections...
: (cmp(
path.slice(0, pattern[0].length),
pattern[0])
// test next section...
&& test(
path.slice(pattern[0].length),
pattern.slice(1)))) }
// XXX Q: should we ignore the last element of the path???
filter = function(change, i, lst){
//return cmp(path, change.path) }
return test(change.path) }
}
// XXX should we add filter to options or at least set a .filtered attr???
// ...or maybe a reference to the original diff...
// ...might event implement a jQuery-like .end()
res.diff = res.diff.filter(filter.bind(this))
return res
},
// XXX need to normalize .diff and .options...
json: function(){
return {
format: this.format,
version: this.version,
structure: this.structure,
placeholders: this.placeholders,
// XXX these need to be prepared for JSON compatibility...
options: this.options,
diff: this.diff,
}
},
}
var Diff =
module.Diff =
object.makeConstructor('Diff',
DiffClassPrototype,
DiffPrototype)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Short hands...
// Deep-compare objects...
//
var cmp =
module.cmp =
function(A, B){
return Diff.cmp(A, B) }
// 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){
return (diff instanceof Diff ?
diff
: Diff.fromJSON(diff))
.patch(obj, options) }
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })