mirror of
https://github.com/flynx/diff.js.git
synced 2025-10-29 11:00:12 +00:00
rewritten the handler code -- now simpler yet more flexible...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
parent
91bc0ce704
commit
5e32c539ba
371
diff2.js
371
diff2.js
@ -34,6 +34,11 @@ var types = require('ig-types')
|
|||||||
//
|
//
|
||||||
/*********************************************************************/
|
/*********************************************************************/
|
||||||
|
|
||||||
|
var STOP =
|
||||||
|
module.STOP =
|
||||||
|
types.STOP
|
||||||
|
|
||||||
|
|
||||||
var EMPTY =
|
var EMPTY =
|
||||||
module.EMPTY =
|
module.EMPTY =
|
||||||
//Symbol.EMPTY =
|
//Symbol.EMPTY =
|
||||||
@ -48,133 +53,148 @@ module.CONTENT =
|
|||||||
Symbol('CONTENT')
|
Symbol('CONTENT')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
|
||||||
//
|
//
|
||||||
// Format:
|
// Format:
|
||||||
// {
|
// {
|
||||||
// <name>: {
|
// <name>: {
|
||||||
// // optional
|
|
||||||
// final: <bool>,
|
|
||||||
//
|
|
||||||
// match: <name> | <func>,
|
|
||||||
//
|
|
||||||
// handle: <name> | <func>,
|
// handle: <name> | <func>,
|
||||||
|
//
|
||||||
|
// ...
|
||||||
// },
|
// },
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
|
// .handle(obj, res, next, stop)
|
||||||
|
// -> [path, res]
|
||||||
|
// -> undefined
|
||||||
|
//
|
||||||
|
// res ::= [path] | [path, value]
|
||||||
|
//
|
||||||
|
// next([path, value], ..)
|
||||||
|
// -> true
|
||||||
|
//
|
||||||
|
// stop(value)
|
||||||
|
// stop(path, value)
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
// NOTE: this is more of a grammar than a set of object handlers, nother
|
// NOTE: this is more of a grammar than a set of object handlers, nother
|
||||||
// way to think of this is as a set of handlrs of aspects of objects
|
// way to think of this is as a set of handlrs of aspects of objects
|
||||||
// and not full objects...
|
// and not full objects...
|
||||||
// XXX not sure if this is how this is going to continue though as
|
// XXX not sure if this is how this is going to continue though as
|
||||||
// we'll need to organize constructors preferably within this
|
// we'll need to organize constructors preferably within this
|
||||||
// structure and keep it extensible...
|
// structure and keep it extensible...
|
||||||
//
|
//
|
||||||
// XXX might be nice to have conditional stopping...
|
|
||||||
// a-la event.preventDefault()
|
|
||||||
// XXX need option threading...
|
// XXX need option threading...
|
||||||
// XXX need to deal with functions...
|
// XXX need to deal with functions...
|
||||||
|
// XXX add a tree mode -- containers as levels...
|
||||||
|
// XXX need a way to index the path...
|
||||||
|
// ...and to filter paths by pattern...
|
||||||
|
// XXX might be a good idea to generate "structural hashes" for objects...
|
||||||
var HANDLERS =
|
var HANDLERS =
|
||||||
module.HANDLERS = {
|
module.HANDLERS = {
|
||||||
|
|
||||||
// null...
|
// null...
|
||||||
//
|
//
|
||||||
null: {
|
null: {
|
||||||
final: true,
|
handle: function(obj, res, next, stop){
|
||||||
match: function(obj){
|
obj === null
|
||||||
return obj === null },
|
&& stop(obj) }, },
|
||||||
handle: 'value', },
|
|
||||||
|
|
||||||
// Functions...
|
// Functions...
|
||||||
//
|
//
|
||||||
// XXX EXPERIMENTAL...
|
// XXX EXPERIMENTAL...
|
||||||
// XXX STUB...
|
// XXX STUB...
|
||||||
|
// XXX can we reuse object's .handle(..) here???
|
||||||
func: {
|
func: {
|
||||||
match: function(obj){
|
handle: function(obj, res, next, stop){
|
||||||
return typeof(obj) == 'function' },
|
return typeof(obj) == 'function' ?
|
||||||
handle: 'object', },
|
{
|
||||||
|
// XXX need to check if a constructor is built-in...
|
||||||
|
type: obj.constructor.name,
|
||||||
|
// Object generations:
|
||||||
|
// 1 - directly constructed objects
|
||||||
|
// 2 - objects at least one level deeper than gen 1
|
||||||
|
gen: obj.constructor.prototype === obj.__proto__ ? 1 : 2,
|
||||||
|
// XXX
|
||||||
|
source: obj,
|
||||||
|
}
|
||||||
|
: undefined }, },
|
||||||
|
|
||||||
// Text...
|
// Text...
|
||||||
//
|
//
|
||||||
// XXX EXPERIMENTAL...
|
// XXX EXPERIMENTAL...
|
||||||
text: {
|
text: {
|
||||||
final: true,
|
handle: function(obj, res, next, stop){
|
||||||
match: function(obj){
|
typeof(obj) == 'string'
|
||||||
return typeof(obj) == 'string'
|
|
||||||
// XXX make this more optimal...
|
// XXX make this more optimal...
|
||||||
&& obj.includes('\n') },
|
&& obj.includes('\n')
|
||||||
handle: function(obj){
|
&& next(
|
||||||
return [[],
|
...obj.split(/\n/g)
|
||||||
{
|
.map(function(line, i){
|
||||||
type: 'Text',
|
return [[module.CONTENT, i], line] }))
|
||||||
source: obj,
|
&& stop({
|
||||||
},
|
type: 'Text',
|
||||||
obj.split(/\n/g)
|
source: obj,
|
||||||
.map(function(line, i){
|
}) }, },
|
||||||
return [[module.CONTENT, i], line] }) ] }, },
|
|
||||||
|
|
||||||
// Non-Objects...
|
// Non-Objects...
|
||||||
//
|
//
|
||||||
// NOTE: this will include undefined and NaN...
|
// NOTE: this will include undefined and NaN...
|
||||||
value: {
|
value: {
|
||||||
final: true,
|
handle: function(obj, res, next, stop){
|
||||||
match: function(obj){
|
typeof(obj) != 'object'
|
||||||
return typeof(obj) != 'object'
|
&& typeof(obj) != 'function'
|
||||||
&& typeof(obj) != 'function' },
|
&& stop(obj) }, },
|
||||||
handle: function(obj){
|
|
||||||
return [[], obj] }, },
|
|
||||||
|
|
||||||
// Base objects...
|
// Base objects...
|
||||||
//
|
//
|
||||||
object: {
|
object: {
|
||||||
match: function(obj){
|
handle: function(obj, res, next, stop){
|
||||||
return typeof(obj) == 'object' },
|
return typeof(obj) == 'object' ?
|
||||||
handle: function(obj){
|
{
|
||||||
return [[], {
|
// XXX need to check if a constructor is built-in...
|
||||||
// XXX need to check if a constructor is built-in...
|
type: obj.constructor.name,
|
||||||
type: obj.constructor.name,
|
// Object generations:
|
||||||
|
// 1 - directly constructed objects
|
||||||
// Object generations:
|
// 2 - objects at least one level deeper than gen 1
|
||||||
// 1 - directly constructed objects
|
gen: obj.constructor.prototype === obj.__proto__ ? 1 : 2,
|
||||||
// 2 - objects at least one level deeper than gen 1
|
// XXX
|
||||||
gen: obj.constructor.prototype === obj.__proto__ ? 1 : 2,
|
source: obj,
|
||||||
|
}
|
||||||
// XXX
|
: undefined }, },
|
||||||
source: obj,
|
|
||||||
}] }, },
|
|
||||||
// special keys...
|
// special keys...
|
||||||
proto: {
|
proto: {
|
||||||
match: function(obj){
|
handle: function(obj, res, next, stop){
|
||||||
return typeof(obj) == 'object'
|
typeof(obj) == 'object'
|
||||||
&& obj.constructor.prototype !== obj.__proto__ },
|
&& obj.constructor.prototype !== obj.__proto__
|
||||||
handle: function(obj){
|
&& next([['__proto__'], obj.__proto__]) }, },
|
||||||
return [[ [['__proto__'], obj.__proto__], ]] }, },
|
|
||||||
// XXX any other special keys???
|
// XXX any other special keys???
|
||||||
// - non-iterable?
|
// - non-iterable?
|
||||||
|
|
||||||
|
|
||||||
// Entries / Non-attribute (encapsulated) content...
|
// Entries / Non-attribute (encapsulated) content...
|
||||||
//
|
//
|
||||||
setEntries: {
|
setEntries: {
|
||||||
match: function(obj){
|
handle: function(obj, res, next, stop){
|
||||||
return obj instanceof Set },
|
obj instanceof Set
|
||||||
// NOTE: we are indexing sets...
|
&& next(
|
||||||
handle: function(obj){
|
...obj.values()
|
||||||
return [ obj.values()
|
.map(function(v, i){
|
||||||
.map(function(v, i){
|
return [[module.CONTENT, i], v] })) }, },
|
||||||
return [[module.CONTENT, i], v] })
|
|
||||||
.toArray() ] }, },
|
|
||||||
mapEntries: {
|
mapEntries: {
|
||||||
match: function(obj){
|
handle: function(obj, res, next, stop){
|
||||||
return obj instanceof Map },
|
obj instanceof Map
|
||||||
handle: function(obj, path, options){
|
&& next(
|
||||||
return [ obj.entries()
|
...obj.entries()
|
||||||
.map(function([k, v], i){
|
.map(function([k, v], i){
|
||||||
return [
|
return [
|
||||||
[[module.CONTENT, i +'@key'], k],
|
[[module.CONTENT, i +'@key'], k],
|
||||||
[[module.CONTENT, i], v],
|
[[module.CONTENT, i], v],
|
||||||
] })
|
] })
|
||||||
.flat()
|
.flat()) }, },
|
||||||
.toArray() ] }, },
|
|
||||||
|
|
||||||
// Keys / Attributes...
|
// Keys / Attributes...
|
||||||
//
|
//
|
||||||
@ -186,15 +206,15 @@ module.HANDLERS = {
|
|||||||
// - skip repeating keys
|
// - skip repeating keys
|
||||||
// ...this could be done on the root handler level...
|
// ...this could be done on the root handler level...
|
||||||
keys: {
|
keys: {
|
||||||
//match: 'object',
|
handle: function(obj, res, next, stop){
|
||||||
//match: ['object', 'func'],
|
;(typeof(obj) == 'object'
|
||||||
match: function(obj, handlers){
|
|| typeof(obj) == 'function')
|
||||||
return handlers.object.match(obj)
|
&& next(
|
||||||
|| handlers.func.match(obj) },
|
...Object.entries(obj)
|
||||||
handle: function(obj){
|
.map(function([k, v]){
|
||||||
return [ Object.entries(obj)
|
return [[k], v] })) }, },
|
||||||
.map(function([k, v]){
|
|
||||||
return [[k], v] }), ] }, },
|
|
||||||
/* XXX
|
/* XXX
|
||||||
props: {
|
props: {
|
||||||
//match: 'object',
|
//match: 'object',
|
||||||
@ -227,157 +247,74 @@ module.HANDLERS = {
|
|||||||
//false: { match: false },
|
//false: { match: false },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
// XXX use STOP...
|
|
||||||
// XXX might be good to cache output via some citeria (type?)...
|
|
||||||
// ...this criteria needs to be consistent with how .match(..) works...
|
|
||||||
// XXX does .match(..) need options???
|
|
||||||
// XXX do we warn of orphans???
|
|
||||||
var getHandlers =
|
|
||||||
module.getHandlers =
|
|
||||||
function(obj, handlers=module.HANDLERS){
|
|
||||||
return [...Object.entries(handlers)
|
|
||||||
.iter()
|
|
||||||
.filter(function([k, v]){
|
|
||||||
var stop = !!v.final
|
|
||||||
// expand aliases...
|
|
||||||
var seen = new Set()
|
|
||||||
while(v && typeof(v.match) == 'string'){
|
|
||||||
var n = v.match
|
|
||||||
if(seen.has(n)){
|
|
||||||
throw new Error('.match(..): alias loop detected:\n\t'
|
|
||||||
+ [...seen, n].join('\n \t -> ')) }
|
|
||||||
seen.add(n)
|
|
||||||
v = handlers[n] }
|
|
||||||
// orphan or falsy .match...
|
|
||||||
if(!v){
|
|
||||||
return false }
|
|
||||||
// handle .final/final...
|
|
||||||
if(stop
|
|
||||||
&& v.match
|
|
||||||
&& v.match(obj, handlers)){
|
|
||||||
throw types.STOP(true) }
|
|
||||||
// normal match...
|
|
||||||
return v.match
|
|
||||||
&& v.match(obj, handlers) })
|
|
||||||
.map(function([k, v]){
|
|
||||||
return v })] }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Format:
|
// Format:
|
||||||
// [
|
// [
|
||||||
// [<path>, {type: <name>}],
|
// // primitive value...
|
||||||
//
|
|
||||||
// [<path>, ['LINK', <path>]],
|
|
||||||
//
|
|
||||||
// [<path>, <value>],
|
// [<path>, <value>],
|
||||||
|
//
|
||||||
|
// // constructed object...
|
||||||
|
// [<path>, {
|
||||||
|
// type: <name>,
|
||||||
|
// gen: <generation>,
|
||||||
|
// source: <obj>,
|
||||||
|
// }],
|
||||||
|
//
|
||||||
|
// // link...
|
||||||
|
// [<path>, 'LINK', <path>],
|
||||||
|
//
|
||||||
// ]
|
// ]
|
||||||
//
|
//
|
||||||
// XXX add a tree mode -- containers as levels...
|
var handle =
|
||||||
// XXX need a way to index the path...
|
module.handle =
|
||||||
// ...and to filter paths by pattern...
|
|
||||||
// XXX might be a good idea to generate "structural hashes" for objects...
|
|
||||||
// XXX can we combine .handle(..) and .match(..) ???
|
|
||||||
// ...for example via .handle(obj, '?') protocol...
|
|
||||||
// .....or even simpler, just thread the object through all the
|
|
||||||
// handlers in one go -- unless there is a fast way to test and
|
|
||||||
// classify object predictably there is no point in a test stage...
|
|
||||||
// .....would also be nice to support a STOP(res) instead of .final
|
|
||||||
var handle =
|
|
||||||
module.handle =
|
|
||||||
function*(obj, path=[], options={}){
|
function*(obj, path=[], options={}){
|
||||||
// handle recursive structures...
|
// handle object loops...
|
||||||
var seen = options.seen =
|
var seen = options.seen =
|
||||||
options.seen || new Map()
|
options.seen || new Map()
|
||||||
if(seen.has(obj)){
|
if(seen.has(obj)){
|
||||||
yield [path, ['LINK', seen.get(obj)]]
|
yield [path, 'LINK', seen.get(obj)]
|
||||||
return }
|
return }
|
||||||
typeof(obj) == 'object'
|
typeof(obj) == 'object'
|
||||||
&& seen.set(obj, path)
|
&& seen.set(obj, path)
|
||||||
|
|
||||||
// get compatible handler list...
|
var __next = []
|
||||||
var cache = options.cache =
|
var next = function(...values){
|
||||||
options.cache || new Map()
|
__next.splice(__next.length, 0, ...values)
|
||||||
var HANDLERS = options.handlers || module.HANDLERS
|
return true }
|
||||||
var handlers = module.getHandlers(obj, HANDLERS)
|
var stop = function(p, v){
|
||||||
|
throw module.STOP(arguments.length == 1 ?
|
||||||
|
[path, arguments[0]]
|
||||||
|
: [p, v]) }
|
||||||
|
|
||||||
// XXX might be a good idea to move this up (or into options) so as
|
// handle the object...
|
||||||
// not to define this on each call...
|
var handlers = options.handlers || module.HANDLERS
|
||||||
// ...we'll need to somehow curry in the path which is now passed
|
var res = [path]
|
||||||
// via a closure...
|
yield* Object.values(handlers)
|
||||||
var subtree = function*(data){
|
|
||||||
// a handler just returned a list of next objects to handle...
|
|
||||||
if(data.length == 1){
|
|
||||||
var next = data.pop()
|
|
||||||
var p = path
|
|
||||||
// a normal handler...
|
|
||||||
} else {
|
|
||||||
var [k, v, next] = data
|
|
||||||
var p = path.concat(k)
|
|
||||||
yield [p, v] }
|
|
||||||
// process queued/next objects...
|
|
||||||
yield* (next || [])
|
|
||||||
.iter()
|
|
||||||
.map(function*([k, v]){
|
|
||||||
yield* handle(v, p.concat(k), options) }) }
|
|
||||||
|
|
||||||
// apply the handlers...
|
|
||||||
yield* handlers
|
|
||||||
.iter()
|
.iter()
|
||||||
.filter(function(handler){
|
.filter(function(handler){
|
||||||
return !!handler.handle })
|
return !!handler.handle })
|
||||||
.map(function*(handler){
|
.map(function*(handler){
|
||||||
var h = handler
|
|
||||||
// expand aliases...
|
// expand aliases...
|
||||||
|
var h = handler
|
||||||
while(h && typeof(h.handle) == 'string'){
|
while(h && typeof(h.handle) == 'string'){
|
||||||
h = HANDLERS[h.handle] }
|
h = handlers[h.handle] }
|
||||||
yield* h.handle instanceof types.Generator ?
|
// XXX should .handle(..) be called in the context of h or handler???
|
||||||
// XXX should .handle(..) be called in the context of h or handler???
|
res = h.handle.call(handler, obj, res, next, stop, options)
|
||||||
h.handle.call(handler, obj, path, options)
|
yield res
|
||||||
.map(subtree)
|
&& [path, res] })
|
||||||
: subtree(h.handle.call(handler, obj, path, options)) }) }
|
.filter(function(e){
|
||||||
|
return !!e })
|
||||||
var HANDLERS2 =
|
// handle the next stuff...
|
||||||
module.HANDLERS2 = {
|
yield* __next.splice(0, __next.length)
|
||||||
null: {
|
.iter()
|
||||||
handle: function(obj){
|
.map(function*([k, v]){
|
||||||
if(obj === null){
|
yield* handle(v, path.concat(k), options) }) }
|
||||||
throw module.STOP(obj) } }, },
|
|
||||||
value: {
|
|
||||||
handle: function(obj){
|
|
||||||
if(typeof(obj) != 'object'
|
|
||||||
&& typeof(obj) != 'function'){
|
|
||||||
throw module.STOP(obj) } },
|
|
||||||
|
|
||||||
object: {
|
|
||||||
handle: function(obj){
|
|
||||||
return typeof(obj) == 'object' ?
|
|
||||||
[[], {
|
|
||||||
// XXX need to check if a constructor is built-in...
|
|
||||||
type: obj.constructor.name,
|
|
||||||
|
|
||||||
// Object generations:
|
|
||||||
// 1 - directly constructed objects
|
|
||||||
// 2 - objects at least one level deeper than gen 1
|
|
||||||
gen: obj.constructor.prototype === obj.__proto__ ? 1 : 2,
|
|
||||||
|
|
||||||
// XXX
|
|
||||||
source: obj,
|
|
||||||
}]
|
|
||||||
: undefined }, },
|
|
||||||
}
|
|
||||||
|
|
||||||
var handle2 =
|
|
||||||
module.handle2 =
|
|
||||||
function(){
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
|
||||||
// path2str(..)
|
// path2str(..)
|
||||||
//
|
//
|
||||||
// XXX need to figure out a way to avoid clashes with module.CONTENT in
|
// XXX need to figure out a way to avoid clashes with module.CONTENT in
|
||||||
@ -454,11 +391,10 @@ function(str){
|
|||||||
var serializePaths =
|
var serializePaths =
|
||||||
module.serializePaths =
|
module.serializePaths =
|
||||||
types.generator.iter
|
types.generator.iter
|
||||||
.map(function([p, v]){
|
.map(function([p, v, ...rest]){
|
||||||
return v instanceof Array && v[0] == 'LINK' ?
|
return rest.length > 0 && v == 'LINK' ?
|
||||||
// link...
|
// link...
|
||||||
[path2str(p),
|
[path2str(p), v, path2str(rest[0])]
|
||||||
'LINK', path2str(v[1])]
|
|
||||||
: [path2str(p), v] })
|
: [path2str(p), v] })
|
||||||
|
|
||||||
|
|
||||||
@ -711,6 +647,7 @@ var o = module.o = {
|
|||||||
o.object.y = o.object
|
o.object.y = o.object
|
||||||
|
|
||||||
|
|
||||||
|
// generate spec...
|
||||||
console.log([
|
console.log([
|
||||||
...handle(o)
|
...handle(o)
|
||||||
.chain(
|
.chain(
|
||||||
@ -720,6 +657,7 @@ console.log([
|
|||||||
)])
|
)])
|
||||||
|
|
||||||
|
|
||||||
|
// use spec to create a new object...
|
||||||
console.log('\n\n---\n',
|
console.log('\n\n---\n',
|
||||||
[...handle(write(null, handle(o)))
|
[...handle(write(null, handle(o)))
|
||||||
.chain(
|
.chain(
|
||||||
@ -730,6 +668,5 @@ console.log('\n\n---\n',
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**********************************************************************
|
/**********************************************************************
|
||||||
* vim:set ts=4 sw=4 : */ return module })
|
* vim:set ts=4 sw=4 : */ return module })
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user