From a6149be9b4a2f36ad87c0ad110388d0209ab95e7 Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Sun, 26 Aug 2018 01:49:06 +0300 Subject: [PATCH] reworked .filter(..) path syntax + notes + docs... Signed-off-by: Alex A. Naanou --- README.md | 22 ++++++ diff.js | 216 ++++++++++++++++++++++++++++++------------------------ 2 files changed, 141 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 300cdf8..b54c0e5 100644 --- a/README.md +++ b/README.md @@ -262,9 +262,31 @@ Apply "diff* (or *patch*) `X` to `X'` state. `diff.unpatch(X') -> X` Undo *diff" application to `X'` returning it to `X` state. +This is equivalent to: `diff.reverse().patch(X')` + `diff.check(X) -> bool` Check if *diff* is compatible/applicable to `X`. This essentially checks if the *left hand side* of the *diff* matches the corresponding nodes of `X`. +`diff.reverse() -> diff` +Generate a new *child diff* where `A` and `B` are reversed. + +`diff.filter(path | filter) -> diff` +Generate a new *child diff* leaving only changes that match the `path`/`filter` + +The `path` is a `"/"` or `"\"` separated string that supports the following item syntax: +- `"*"` - matches any item (same as: `ANY`). +- `"**"` - matches 0 or more items. +- `"a|b"` - matches either `a` or `b` (same as: `OR('a', 'b')`) +- `"!a"` - matches anything but `a` (smae as: `NOT('a')`) + +*Note that `"**"` is a special case in that it can not be combined with other patterns above (e.g. in `"a|**"` the string `"**"` is treated literally and has not special meaning).* + +`diff.merge(diff) -> diff` +XXX + +`diff.end() -> diff` +Return the *parent diff* that was used to generate the current *child diff* or the current diff if there is not parent. + `diff.json() -> JSON` Serialize the *diff* to JSON. Note that the output may or may not be JSON compatible depending on the inputs. diff --git a/diff.js b/diff.js index 491ee56..1c97092 100644 --- a/diff.js +++ b/diff.js @@ -969,6 +969,122 @@ module.Types = { }) }, + // Filter diff changes and return a new diff... + // + // .filter(path) + // .filter(func) + // -> diff + // + // path can be either a '/' separated string of path elements or + // an array... + // + // path if given as a string supports the following syntax: + // * - matches any single path element (like ANY) + // a|b - matches either a or b + // !a - matches anything but a + // XXX do we need grouping and quoting??? + // + // Special case: + // ** - matches 0 or more path elements + // NOTE: '**' can't be used with other patterns from + // the above. + // + // NOTE: array path also supports patterns... + filter: function(diff, filter){ + // string filter... + filter = typeof(filter) == typeof('str') ? + filter + .trim() + // remove leading and trailing '/' or '\' + .replace(/(^[\\\/]+|[\\\/]+$)/g, '') + .split(/[\\\/]+/) + // 'a|b' -> OR('a', 'b') + // '*' -> ANY + // '!a' -> NOT('a') + // NOTE: '**' is handled differently and later... + .map(function(e){ + e = e + .split(/\|/) + .map(function(e){ + return e == '*' ? ANY + : e[0] == '!' ? NOT(e.slice(1)) + : e }) + return e.length == 1 ? e[0] : OR(...e) + }) + : filter + + // path filter (non-function)... + if(!(filter instanceof Function)){ + // normalize path... + // format: + // [ + // '**' | [ .. ], + // ... + // ] + // XXX when OF(..) is ready, replace '**' with OF(ANY, ANY)... + var pattern = (filter instanceof Array ? filter : [filter]) + // 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 = pattern + .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 test(change.path, pattern) } + } + + return diff.filter(filter.bind(this)) + }, + + // XXX there are two approaches to this: + // 1) naive: simply concatenate all the changes in order... + // 2) filter and merge changes based on path... + // XXX do we need a conflict resolution policy??? + merge: function(diff, other){ + // XXX + return this.flatten(diff).concat(this.flatten(other)) + }, + // User API... @@ -1260,95 +1376,6 @@ module.Types = { : !that.cmp(change.A, target[key]) }) }, - - - // Filter diff changes and return a new diff... - // - // .filter(path) - // .filter(func) - // -> diff - // - // path can be either a '/' separated string of path elements or - // an array... - // - // path supports a limited glob syntax: - // * - matches any single path element (like ANY) - // ** - matches 0 or more path elements - // - // NOTE: array path also supports patterns... - // - filter: function(diff, filter){ - // 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 pattern = (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 = pattern - .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 test(change.path, pattern) } - } - - return diff.filter(filter.bind(this)) - }, - } @@ -2217,17 +2244,12 @@ var DiffPrototype = { res.parent = this return res }, - // XXX + + // XXX should this set .parent ???? merge: function(diff){ var res = this.clone() - - // XXX there are two approaches to this: - // 1) naive: simply concatenate all the changes in order... - // 2) filter and merge changes based on path... - // XXX do we need a conflict resolution policy??? - + res.diff = this.constructor.types.merge.call(this, this.diff, diff.diff) res.parent = this - return res },