├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── cjs
├── index.js
└── package.json
├── esm
└── index.js
├── index.js
├── min.js
├── new.js
├── package.json
├── rollup
├── babel.config.js
└── new.config.js
├── test
├── dommy.js
├── index.html
├── index.js
├── js-diff-benchmark.js
├── select.html
├── single.js
├── test.js
├── utils.js
└── verify.js
└── udomdiff-head.jpg
/.gitignore:
--------------------------------------------------------------------------------
1 | .nyc_output/
2 | node_modules/
3 | package-lock.json
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .nyc_output/
2 | node_modules/
3 | rollup/
4 | test/
5 | package-lock.json
6 | udomdiff-head.jpg
7 | .travis.yml
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - stable
4 | git:
5 | depth: 1
6 | branches:
7 | only:
8 | - master
9 | after_success:
10 | - "npm run coveralls"
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ISC License
2 |
3 | Copyright (c) 2020, Andrea Giammarchi, @WebReflection
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15 | PERFORMANCE OF THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # µdomdiff
2 |
3 | [](https://travis-ci.com/WebReflection/udomdiff) [](https://coveralls.io/github/WebReflection/udomdiff?branch=master)
4 |
5 | 
6 |
7 | **Social Media Photo by [Christopher Rusev](https://unsplash.com/@ralics) on [Unsplash](https://unsplash.com/)**
8 |
9 | An essential diffing algorithm for [µhtml](https://github.com/WebReflection/uhtml#readme).
10 |
11 |
12 | ### Signature
13 |
14 | ```js
15 | futureNodes = udomdiff(
16 | parentNode, // where changes happen
17 | [...currentNodes], // Array of current items/nodes
18 | [...futureNodes], // Array of future items/nodes (returned)
19 | get(node, toDoWhat), // a callback to retrieve the node
20 | before // the anchored node to insertBefore
21 | );
22 | ```
23 |
24 | ### What is `get` and how does it work?
25 |
26 | You can find all info from [domdiff](https://github.com/WebReflection/domdiff#a-node-generic-info--node-callback-for-complex-data), as it's exactly the same concept:
27 |
28 | * `get(node, 1)` to retrieve the node that's being appended
29 | * `get(node, 0)` to get the node to use for an `insertBefore` operation
30 | * `get(node, -0)` to get the node to use for an `insertAfter` operation
31 | * `get(node, -1)` to retrieve the node that's being removed
32 |
33 | If you don't care about any of those second arguments values, `const get = o => o;` is a valid get too.
34 |
35 |
36 | ### How to import it:
37 |
38 | * via **CDN**, as global variable: `https://unpkg.com/udomdiff`
39 | * via **ESM**, as external module: `import udomdiff from 'https://unpkg.com/udomdiff/esm/index.js'`
40 | * via **CJS**: `const udomdiff = require('udomdiff');` ( or `require('udomdiff/cjs')` )
41 | * via bundlers/transpilers: `import udomdiff from 'udomdiff';` ( or `from 'udomdiff/esm'` )
42 |
--------------------------------------------------------------------------------
/cjs/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /**
3 | * ISC License
4 | *
5 | * Copyright (c) 2020, Andrea Giammarchi, @WebReflection
6 | *
7 | * Permission to use, copy, modify, and/or distribute this software for any
8 | * purpose with or without fee is hereby granted, provided that the above
9 | * copyright notice and this permission notice appear in all copies.
10 | *
11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
12 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
13 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
14 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
15 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
16 | * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17 | * PERFORMANCE OF THIS SOFTWARE.
18 | */
19 |
20 | /**
21 | * @param {Node} parentNode The container where children live
22 | * @param {Node[]} a The list of current/live children
23 | * @param {Node[]} b The list of future children
24 | * @param {(entry: Node, action: number) => Node} get
25 | * The callback invoked per each entry related DOM operation.
26 | * @param {Node} [before] The optional node used as anchor to insert before.
27 | * @returns {Node[]} The same list of future children.
28 | */
29 | module.exports = (parentNode, a, b, get, before) => {
30 | const bLength = b.length;
31 | let aEnd = a.length;
32 | let bEnd = bLength;
33 | let aStart = 0;
34 | let bStart = 0;
35 | let map = null;
36 | while (aStart < aEnd || bStart < bEnd) {
37 | // append head, tail, or nodes in between: fast path
38 | if (aEnd === aStart) {
39 | // we could be in a situation where the rest of nodes that
40 | // need to be added are not at the end, and in such case
41 | // the node to `insertBefore`, if the index is more than 0
42 | // must be retrieved, otherwise it's gonna be the first item.
43 | const node = bEnd < bLength ?
44 | (bStart ?
45 | (get(b[bStart - 1], -0).nextSibling) :
46 | get(b[bEnd], 0)) :
47 | before;
48 | while (bStart < bEnd)
49 | parentNode.insertBefore(get(b[bStart++], 1), node);
50 | }
51 | // remove head or tail: fast path
52 | else if (bEnd === bStart) {
53 | while (aStart < aEnd) {
54 | // remove the node only if it's unknown or not live
55 | if (!map || !map.has(a[aStart]))
56 | parentNode.removeChild(get(a[aStart], -1));
57 | aStart++;
58 | }
59 | }
60 | // same node: fast path
61 | else if (a[aStart] === b[bStart]) {
62 | aStart++;
63 | bStart++;
64 | }
65 | // same tail: fast path
66 | else if (a[aEnd - 1] === b[bEnd - 1]) {
67 | aEnd--;
68 | bEnd--;
69 | }
70 | // The once here single last swap "fast path" has been removed in v1.1.0
71 | // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
72 | // reverse swap: also fast path
73 | else if (
74 | a[aStart] === b[bEnd - 1] &&
75 | b[bStart] === a[aEnd - 1]
76 | ) {
77 | // this is a "shrink" operation that could happen in these cases:
78 | // [1, 2, 3, 4, 5]
79 | // [1, 4, 3, 2, 5]
80 | // or asymmetric too
81 | // [1, 2, 3, 4, 5]
82 | // [1, 2, 3, 5, 6, 4]
83 | const node = get(a[--aEnd], -0).nextSibling;
84 | parentNode.insertBefore(
85 | get(b[bStart++], 1),
86 | get(a[aStart++], -0).nextSibling
87 | );
88 | parentNode.insertBefore(get(b[--bEnd], 1), node);
89 | // mark the future index as identical (yeah, it's dirty, but cheap 👍)
90 | // The main reason to do this, is that when a[aEnd] will be reached,
91 | // the loop will likely be on the fast path, as identical to b[bEnd].
92 | // In the best case scenario, the next loop will skip the tail,
93 | // but in the worst one, this node will be considered as already
94 | // processed, bailing out pretty quickly from the map index check
95 | a[aEnd] = b[bEnd];
96 | }
97 | // map based fallback, "slow" path
98 | else {
99 | // the map requires an O(bEnd - bStart) operation once
100 | // to store all future nodes indexes for later purposes.
101 | // In the worst case scenario, this is a full O(N) cost,
102 | // and such scenario happens at least when all nodes are different,
103 | // but also if both first and last items of the lists are different
104 | if (!map) {
105 | map = new Map;
106 | let i = bStart;
107 | while (i < bEnd)
108 | map.set(b[i], i++);
109 | }
110 | // if it's a future node, hence it needs some handling
111 | if (map.has(a[aStart])) {
112 | // grab the index of such node, 'cause it might have been processed
113 | const index = map.get(a[aStart]);
114 | // if it's not already processed, look on demand for the next LCS
115 | if (bStart < index && index < bEnd) {
116 | let i = aStart;
117 | // counts the amount of nodes that are the same in the future
118 | let sequence = 1;
119 | while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
120 | sequence++;
121 | // effort decision here: if the sequence is longer than replaces
122 | // needed to reach such sequence, which would brings again this loop
123 | // to the fast path, prepend the difference before a sequence,
124 | // and move only the future list index forward, so that aStart
125 | // and bStart will be aligned again, hence on the fast path.
126 | // An example considering aStart and bStart are both 0:
127 | // a: [1, 2, 3, 4]
128 | // b: [7, 1, 2, 3, 6]
129 | // this would place 7 before 1 and, from that time on, 1, 2, and 3
130 | // will be processed at zero cost
131 | if (sequence > (index - bStart)) {
132 | const node = get(a[aStart], 0);
133 | while (bStart < index)
134 | parentNode.insertBefore(get(b[bStart++], 1), node);
135 | }
136 | // if the effort wasn't good enough, fallback to a replace,
137 | // moving both source and target indexes forward, hoping that some
138 | // similar node will be found later on, to go back to the fast path
139 | else {
140 | parentNode.replaceChild(
141 | get(b[bStart++], 1),
142 | get(a[aStart++], -1)
143 | );
144 | }
145 | }
146 | // otherwise move the source forward, 'cause there's nothing to do
147 | else
148 | aStart++;
149 | }
150 | // this node has no meaning in the future list, so it's more than safe
151 | // to remove it, and check the next live node out instead, meaning
152 | // that only the live list index should be forwarded
153 | else
154 | parentNode.removeChild(get(a[aStart++], -1));
155 | }
156 | }
157 | return b;
158 | };
159 |
--------------------------------------------------------------------------------
/cjs/package.json:
--------------------------------------------------------------------------------
1 | {"type":"commonjs"}
--------------------------------------------------------------------------------
/esm/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ISC License
3 | *
4 | * Copyright (c) 2020, Andrea Giammarchi, @WebReflection
5 | *
6 | * Permission to use, copy, modify, and/or distribute this software for any
7 | * purpose with or without fee is hereby granted, provided that the above
8 | * copyright notice and this permission notice appear in all copies.
9 | *
10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
11 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
12 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
13 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
14 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
15 | * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
16 | * PERFORMANCE OF THIS SOFTWARE.
17 | */
18 |
19 | /**
20 | * @param {Node} parentNode The container where children live
21 | * @param {Node[]} a The list of current/live children
22 | * @param {Node[]} b The list of future children
23 | * @param {(entry: Node, action: number) => Node} get
24 | * The callback invoked per each entry related DOM operation.
25 | * @param {Node} [before] The optional node used as anchor to insert before.
26 | * @returns {Node[]} The same list of future children.
27 | */
28 | export default (parentNode, a, b, get, before) => {
29 | const bLength = b.length;
30 | let aEnd = a.length;
31 | let bEnd = bLength;
32 | let aStart = 0;
33 | let bStart = 0;
34 | let map = null;
35 | while (aStart < aEnd || bStart < bEnd) {
36 | // append head, tail, or nodes in between: fast path
37 | if (aEnd === aStart) {
38 | // we could be in a situation where the rest of nodes that
39 | // need to be added are not at the end, and in such case
40 | // the node to `insertBefore`, if the index is more than 0
41 | // must be retrieved, otherwise it's gonna be the first item.
42 | const node = bEnd < bLength ?
43 | (bStart ?
44 | (get(b[bStart - 1], -0).nextSibling) :
45 | get(b[bEnd], 0)) :
46 | before;
47 | while (bStart < bEnd)
48 | parentNode.insertBefore(get(b[bStart++], 1), node);
49 | }
50 | // remove head or tail: fast path
51 | else if (bEnd === bStart) {
52 | while (aStart < aEnd) {
53 | // remove the node only if it's unknown or not live
54 | if (!map || !map.has(a[aStart]))
55 | parentNode.removeChild(get(a[aStart], -1));
56 | aStart++;
57 | }
58 | }
59 | // same node: fast path
60 | else if (a[aStart] === b[bStart]) {
61 | aStart++;
62 | bStart++;
63 | }
64 | // same tail: fast path
65 | else if (a[aEnd - 1] === b[bEnd - 1]) {
66 | aEnd--;
67 | bEnd--;
68 | }
69 | // The once here single last swap "fast path" has been removed in v1.1.0
70 | // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
71 | // reverse swap: also fast path
72 | else if (
73 | a[aStart] === b[bEnd - 1] &&
74 | b[bStart] === a[aEnd - 1]
75 | ) {
76 | // this is a "shrink" operation that could happen in these cases:
77 | // [1, 2, 3, 4, 5]
78 | // [1, 4, 3, 2, 5]
79 | // or asymmetric too
80 | // [1, 2, 3, 4, 5]
81 | // [1, 2, 3, 5, 6, 4]
82 | const node = get(a[--aEnd], -0).nextSibling;
83 | parentNode.insertBefore(
84 | get(b[bStart++], 1),
85 | get(a[aStart++], -0).nextSibling
86 | );
87 | parentNode.insertBefore(get(b[--bEnd], 1), node);
88 | // mark the future index as identical (yeah, it's dirty, but cheap 👍)
89 | // The main reason to do this, is that when a[aEnd] will be reached,
90 | // the loop will likely be on the fast path, as identical to b[bEnd].
91 | // In the best case scenario, the next loop will skip the tail,
92 | // but in the worst one, this node will be considered as already
93 | // processed, bailing out pretty quickly from the map index check
94 | a[aEnd] = b[bEnd];
95 | }
96 | // map based fallback, "slow" path
97 | else {
98 | // the map requires an O(bEnd - bStart) operation once
99 | // to store all future nodes indexes for later purposes.
100 | // In the worst case scenario, this is a full O(N) cost,
101 | // and such scenario happens at least when all nodes are different,
102 | // but also if both first and last items of the lists are different
103 | if (!map) {
104 | map = new Map;
105 | let i = bStart;
106 | while (i < bEnd)
107 | map.set(b[i], i++);
108 | }
109 | // if it's a future node, hence it needs some handling
110 | if (map.has(a[aStart])) {
111 | // grab the index of such node, 'cause it might have been processed
112 | const index = map.get(a[aStart]);
113 | // if it's not already processed, look on demand for the next LCS
114 | if (bStart < index && index < bEnd) {
115 | let i = aStart;
116 | // counts the amount of nodes that are the same in the future
117 | let sequence = 1;
118 | while (++i < aEnd && i < bEnd && map.get(a[i]) === (index + sequence))
119 | sequence++;
120 | // effort decision here: if the sequence is longer than replaces
121 | // needed to reach such sequence, which would brings again this loop
122 | // to the fast path, prepend the difference before a sequence,
123 | // and move only the future list index forward, so that aStart
124 | // and bStart will be aligned again, hence on the fast path.
125 | // An example considering aStart and bStart are both 0:
126 | // a: [1, 2, 3, 4]
127 | // b: [7, 1, 2, 3, 6]
128 | // this would place 7 before 1 and, from that time on, 1, 2, and 3
129 | // will be processed at zero cost
130 | if (sequence > (index - bStart)) {
131 | const node = get(a[aStart], 0);
132 | while (bStart < index)
133 | parentNode.insertBefore(get(b[bStart++], 1), node);
134 | }
135 | // if the effort wasn't good enough, fallback to a replace,
136 | // moving both source and target indexes forward, hoping that some
137 | // similar node will be found later on, to go back to the fast path
138 | else {
139 | parentNode.replaceChild(
140 | get(b[bStart++], 1),
141 | get(a[aStart++], -1)
142 | );
143 | }
144 | }
145 | // otherwise move the source forward, 'cause there's nothing to do
146 | else
147 | aStart++;
148 | }
149 | // this node has no meaning in the future list, so it's more than safe
150 | // to remove it, and check the next live node out instead, meaning
151 | // that only the live list index should be forwarded
152 | else
153 | parentNode.removeChild(get(a[aStart++], -1));
154 | }
155 | }
156 | return b;
157 | };
158 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var udomdiff = (function (exports) {
2 | 'use strict';
3 |
4 | /**
5 | * ISC License
6 | *
7 | * Copyright (c) 2020, Andrea Giammarchi, @WebReflection
8 | *
9 | * Permission to use, copy, modify, and/or distribute this software for any
10 | * purpose with or without fee is hereby granted, provided that the above
11 | * copyright notice and this permission notice appear in all copies.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
14 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
16 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
17 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
18 | * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
19 | * PERFORMANCE OF THIS SOFTWARE.
20 | */
21 |
22 | /**
23 | * @param {Node} parentNode The container where children live
24 | * @param {Node[]} a The list of current/live children
25 | * @param {Node[]} b The list of future children
26 | * @param {(entry: Node, action: number) => Node} get
27 | * The callback invoked per each entry related DOM operation.
28 | * @param {Node} [before] The optional node used as anchor to insert before.
29 | * @returns {Node[]} The same list of future children.
30 | */
31 | var index = (function (parentNode, a, b, get, before) {
32 | var bLength = b.length;
33 | var aEnd = a.length;
34 | var bEnd = bLength;
35 | var aStart = 0;
36 | var bStart = 0;
37 | var map = null;
38 | while (aStart < aEnd || bStart < bEnd) {
39 | // append head, tail, or nodes in between: fast path
40 | if (aEnd === aStart) {
41 | // we could be in a situation where the rest of nodes that
42 | // need to be added are not at the end, and in such case
43 | // the node to `insertBefore`, if the index is more than 0
44 | // must be retrieved, otherwise it's gonna be the first item.
45 | var node = bEnd < bLength ? bStart ? get(b[bStart - 1], -0).nextSibling : get(b[bEnd], 0) : before;
46 | while (bStart < bEnd) parentNode.insertBefore(get(b[bStart++], 1), node);
47 | }
48 | // remove head or tail: fast path
49 | else if (bEnd === bStart) {
50 | while (aStart < aEnd) {
51 | // remove the node only if it's unknown or not live
52 | if (!map || !map.has(a[aStart])) parentNode.removeChild(get(a[aStart], -1));
53 | aStart++;
54 | }
55 | }
56 | // same node: fast path
57 | else if (a[aStart] === b[bStart]) {
58 | aStart++;
59 | bStart++;
60 | }
61 | // same tail: fast path
62 | else if (a[aEnd - 1] === b[bEnd - 1]) {
63 | aEnd--;
64 | bEnd--;
65 | }
66 | // The once here single last swap "fast path" has been removed in v1.1.0
67 | // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
68 | // reverse swap: also fast path
69 | else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) {
70 | // this is a "shrink" operation that could happen in these cases:
71 | // [1, 2, 3, 4, 5]
72 | // [1, 4, 3, 2, 5]
73 | // or asymmetric too
74 | // [1, 2, 3, 4, 5]
75 | // [1, 2, 3, 5, 6, 4]
76 | var _node = get(a[--aEnd], -0).nextSibling;
77 | parentNode.insertBefore(get(b[bStart++], 1), get(a[aStart++], -0).nextSibling);
78 | parentNode.insertBefore(get(b[--bEnd], 1), _node);
79 | // mark the future index as identical (yeah, it's dirty, but cheap 👍)
80 | // The main reason to do this, is that when a[aEnd] will be reached,
81 | // the loop will likely be on the fast path, as identical to b[bEnd].
82 | // In the best case scenario, the next loop will skip the tail,
83 | // but in the worst one, this node will be considered as already
84 | // processed, bailing out pretty quickly from the map index check
85 | a[aEnd] = b[bEnd];
86 | }
87 | // map based fallback, "slow" path
88 | else {
89 | // the map requires an O(bEnd - bStart) operation once
90 | // to store all future nodes indexes for later purposes.
91 | // In the worst case scenario, this is a full O(N) cost,
92 | // and such scenario happens at least when all nodes are different,
93 | // but also if both first and last items of the lists are different
94 | if (!map) {
95 | map = new Map();
96 | var i = bStart;
97 | while (i < bEnd) map.set(b[i], i++);
98 | }
99 | // if it's a future node, hence it needs some handling
100 | if (map.has(a[aStart])) {
101 | // grab the index of such node, 'cause it might have been processed
102 | var index = map.get(a[aStart]);
103 | // if it's not already processed, look on demand for the next LCS
104 | if (bStart < index && index < bEnd) {
105 | var _i = aStart;
106 | // counts the amount of nodes that are the same in the future
107 | var sequence = 1;
108 | while (++_i < aEnd && _i < bEnd && map.get(a[_i]) === index + sequence) sequence++;
109 | // effort decision here: if the sequence is longer than replaces
110 | // needed to reach such sequence, which would brings again this loop
111 | // to the fast path, prepend the difference before a sequence,
112 | // and move only the future list index forward, so that aStart
113 | // and bStart will be aligned again, hence on the fast path.
114 | // An example considering aStart and bStart are both 0:
115 | // a: [1, 2, 3, 4]
116 | // b: [7, 1, 2, 3, 6]
117 | // this would place 7 before 1 and, from that time on, 1, 2, and 3
118 | // will be processed at zero cost
119 | if (sequence > index - bStart) {
120 | var _node2 = get(a[aStart], 0);
121 | while (bStart < index) parentNode.insertBefore(get(b[bStart++], 1), _node2);
122 | }
123 | // if the effort wasn't good enough, fallback to a replace,
124 | // moving both source and target indexes forward, hoping that some
125 | // similar node will be found later on, to go back to the fast path
126 | else {
127 | parentNode.replaceChild(get(b[bStart++], 1), get(a[aStart++], -1));
128 | }
129 | }
130 | // otherwise move the source forward, 'cause there's nothing to do
131 | else aStart++;
132 | }
133 | // this node has no meaning in the future list, so it's more than safe
134 | // to remove it, and check the next live node out instead, meaning
135 | // that only the live list index should be forwarded
136 | else parentNode.removeChild(get(a[aStart++], -1));
137 | }
138 | }
139 | return b;
140 | });
141 |
142 | exports["default"] = index;
143 |
144 | Object.defineProperty(exports, '__esModule', { value: true });
145 |
146 | return exports;
147 |
148 | })({}).default;
149 |
--------------------------------------------------------------------------------
/min.js:
--------------------------------------------------------------------------------
1 | var udomdiff=(e=>(e.default=function(e,r,i,f,l){for(var n=i.length,t=r.length,o=n,s=0,a=0,v=null;s{const f=i.length;let n=t.length,s=f,o=0,c=0,u=null;for(;or-c){const f=l(t[o],0);for(;c {
20 | const {parentNode} = node;
21 | node.parentNode = null;
22 | if (parentNode) {
23 | const {childNodes} = parentNode;
24 | const i = childNodes.indexOf(node);
25 | if (-1 < i)
26 | childNodes.splice(i, 1);
27 | }
28 | };
29 |
30 | class Siblings {
31 | get nextSibling() {
32 | const {parentNode} = this;
33 | if (parentNode) {
34 | const {childNodes} = parentNode;
35 | const i = childNodes.indexOf(this) + 1;
36 | if (0 < i && i < childNodes.length)
37 | return childNodes[i];
38 | }
39 | return null;
40 | }
41 | get previousSibling() {
42 | const {parentNode} = this;
43 | if (parentNode) {
44 | const {childNodes} = parentNode;
45 | const i = childNodes.indexOf(this) - 1;
46 | if (-1 < i)
47 | return childNodes[i];
48 | }
49 | return null;
50 | }
51 | }
52 |
53 | class Nody extends Siblings {
54 | constructor(textContent) {
55 | super();
56 | this.parentNode = null;
57 | this.textContent = textContent;
58 | }
59 | }
60 |
61 | class Dommy extends Siblings {
62 | constructor(tagName) {
63 | super();
64 | this.parentNode = null;
65 | this.childNodes = [];
66 | this.tagName = tagName;
67 | }
68 | get firstChild() {
69 | return this.childNodes[0];
70 | }
71 | get lastChild() {
72 | return this.childNodes[this.childNodes.length - 1];
73 | }
74 | get textContent() {
75 | return this.childNodes.map(node => node.textContent).join('');
76 | }
77 | set textContent(value) {
78 | this.childNodes.splice(0).forEach(remove);
79 | if (value)
80 | this.appendChild(document.createTextNode(value));
81 | }
82 | appendChild(newNode) {
83 | if (!newNode)
84 | throw new Error('invalid appendChild');
85 | remove(newNode);
86 | this.childNodes.push(newNode);
87 | newNode.parentNode = this;
88 | return newNode;
89 | }
90 | insertBefore(newNode, oldNode) {
91 | if (newNode !== oldNode) {
92 | remove(newNode);
93 | const {childNodes} = this;
94 | if (oldNode) {
95 | const i = childNodes.indexOf(oldNode);
96 | if (i < 0)
97 | throw new Error('invalid insertBefore');
98 | childNodes.splice(i, 0, newNode);
99 | }
100 | else
101 | childNodes.push(newNode);
102 | newNode.parentNode = this;
103 | }
104 | return newNode;
105 | }
106 | removeChild(oldNode) {
107 | const {childNodes} = this;
108 | const i = childNodes.indexOf(oldNode);
109 | if (i < 0)
110 | throw new Error('invalid removeChild');
111 | childNodes.splice(i, 1);
112 | oldNode.parentNode = null;
113 | return oldNode;
114 | }
115 | replaceChild(newNode, oldNode) {
116 | remove(newNode);
117 | const {childNodes} = this;
118 | const i = childNodes.indexOf(oldNode);
119 | if (i < 0)
120 | throw new Error('invalid replaceChild');
121 | childNodes[i] = newNode;
122 | oldNode.parentNode = null;
123 | newNode.parentNode = this;
124 | return newNode;
125 | }
126 | }
127 |
128 | module.exports = {
129 | createElement: tagName => new Dommy(tagName),
130 | createTextNode: textContent => new Nody(textContent)
131 | };
132 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | global.document = {
2 | createElement: function (tagName) {
3 | return {tagName: tagName, value: '\n'};
4 | },
5 | createTextNode: function (value) {
6 | return Object.defineProperty(
7 | {value: value},
8 | 'nextSibling',
9 | {get: function () {
10 | var cn = document.body.childNodes;
11 | return cn[cn.indexOf(this) + 1];
12 | }}
13 | );
14 | },
15 | importNode: function () {},
16 | body: {
17 | get lastElementChild() {
18 | return this.childNodes[this.childNodes.length - 1];
19 | },
20 | get textContent() {
21 | return this.childNodes.map(node => node.value).join('');
22 | },
23 | appendChild: function (node) {
24 | this.removeChild(node);
25 | this.childNodes.push(node);
26 | node.parentNode = this;
27 | },
28 | childNodes: [],
29 | insertBefore: function (before, after) {
30 | if (before !== after) {
31 | this.removeChild(before);
32 | var cn = this.childNodes;
33 | cn.splice(cn.indexOf(after), 0, before);
34 | before.parentNode = this;
35 | }
36 | },
37 | removeChild: function (child) {
38 | delete child.parentNode;
39 | var cn = this.childNodes;
40 | var i = cn.indexOf(child);
41 | if (-1 < i)
42 | cn.splice(i, 1);
43 | },
44 | replaceChild: function (newChild, oldChild) {
45 | this.removeChild(newChild);
46 | var cn = this.childNodes;
47 | cn.splice(cn.indexOf(oldChild), 1, newChild);
48 | newChild.parentNode = this;
49 | delete oldChild.parentNode;
50 | }
51 | }
52 | };
53 |
54 | global.udomdiff = require('../cjs');
55 | require('./test.js');
56 | global.test();
57 |
--------------------------------------------------------------------------------
/test/js-diff-benchmark.js:
--------------------------------------------------------------------------------
1 | // source: https://github.com/luwes/js-diff-benchmark
2 | const fs = require('fs');
3 | const c = require('ansi-colors');
4 | var Terser = require('terser');
5 | const gzipSize = require('gzip-size');
6 | const Table = require('cli-table');
7 | const microtime = require('microtime');
8 | const document = require('./dommy.js');
9 | const get = o => o;
10 |
11 | const libs = [
12 | 'udomdiff w/out before',
13 | 'udomdiff with before'
14 | ];
15 |
16 | const cols = [
17 | '',
18 | '1k',
19 | 'Repl',
20 | 'Shufle',
21 | 'Invers',
22 | 'Clear',
23 | 'Append',
24 | 'Prepend',
25 | 'Swap2',
26 | 'Up10th',
27 | '10k',
28 | 'Swap2',
29 | 'Total',
30 | 'Size',
31 | ];
32 |
33 | const table = new Table({
34 | head: cols,
35 | colAligns: cols.map(() => 'middle'),
36 | style: {
37 | head: ['green'],
38 | },
39 | });
40 |
41 | let shuffleSeed;
42 |
43 | // in case we'd like to test "pinnability" of the differ
44 | let before;// = document.createTextNode('');
45 |
46 | const parent = document.createElement('div');
47 |
48 | const {
49 | clear, reset, verifyNodes,
50 | random, reverse,
51 | create1000, create10000,
52 | append1000, prepend1000,
53 | swapRows, updateEach10thRow
54 | } = require('./utils.js')(document, parent, () => before);
55 |
56 | libs.forEach((lib, i) => {
57 | if (i)
58 | before = document.createTextNode('');
59 |
60 | const libResults = [];
61 | table.push({ [lib]: libResults });
62 |
63 | const file = `../cjs/index.js`;
64 | const diff = require(file);
65 |
66 | var code = fs.readFileSync(require.resolve(file), 'utf8');
67 | var gzip = gzipSize.sync(Terser.minify(code).code);
68 |
69 | // clean up the parent
70 | // clean up the parent
71 | parent.textContent = '';
72 | if (before)
73 | parent.appendChild(before);
74 |
75 | //* warm up + checking everything works upfront
76 | let childNodes = create1000(diff, []);
77 | console.assert(
78 | verifyNodes(childNodes, 1000),
79 | '%s warmup create',
80 | lib
81 | );
82 |
83 | childNodes = create1000(diff, childNodes);
84 | console.assert(
85 | verifyNodes(childNodes, 1000),
86 | '%s warmup replace',
87 | lib
88 | );
89 |
90 | if (!shuffleSeed) {
91 | // create a fixed shuffled seed so each library does the same.
92 | const shuffle = childNodes.slice().sort(
93 | () => Math.random() - Math.random()
94 | );
95 | shuffleSeed = shuffle.map((node) => childNodes.indexOf(node));
96 | }
97 |
98 | childNodes = append1000(diff, childNodes);
99 | console.assert(
100 | verifyNodes(childNodes, 2000),
101 | '%s warmup append',
102 | lib
103 | );
104 | childNodes = prepend1000(diff, childNodes);
105 | console.assert(
106 | verifyNodes(childNodes, 3000),
107 | '%s warmup prepend',
108 | lib
109 | );
110 | childNodes = clear(diff, childNodes);
111 | console.assert(
112 | verifyNodes(childNodes, 0),
113 | '%s warmup clear',
114 | lib
115 | );
116 | childNodes = create10000(diff, childNodes);
117 | console.assert(
118 | verifyNodes(childNodes, 10000),
119 | '%s warmup 10k',
120 | lib
121 | );
122 | childNodes = clear(diff, childNodes);
123 | console.assert(
124 | verifyNodes(childNodes, 0),
125 | '%s warmup clear 10k',
126 | lib
127 | );
128 | childNodes = create1000(diff, childNodes);
129 | childNodes = swapRows(diff, childNodes);
130 | console.assert(childNodes[1].textContent == 998, '%s warmup swap', lib);
131 | console.assert(childNodes[998].textContent == 1, '%s warmup swap', lib);
132 | childNodes = clear(diff, childNodes);
133 | childNodes = create1000(diff, childNodes);
134 | childNodes = updateEach10thRow(diff, childNodes);
135 | console.assert(
136 | /!$/.test(childNodes[0].textContent),
137 | '%s warmup update',
138 | lib
139 | );
140 | console.assert(
141 | !/!$/.test(childNodes[1].textContent),
142 | '%s warmup update',
143 | lib
144 | );
145 | console.assert(
146 | /!$/.test(childNodes[10].textContent),
147 | '%s warmup update',
148 | lib
149 | );
150 | childNodes = clear(diff, childNodes);
151 | console.assert(
152 | verifyNodes(childNodes, 0),
153 | '%s warmup clear',
154 | lib
155 | );
156 | //*/
157 |
158 | // console.time(lib.toUpperCase());
159 |
160 | const totalStart = microtime.now();
161 |
162 | let begin;
163 | const start = () => {
164 | reset();
165 | begin = microtime.now();
166 | };
167 | const stop = (count, operationMax) => {
168 | const end = microtime.now() - begin;
169 | const delta = count - operationMax;
170 | libResults.push(`${(end / 1000).toPrecision(2)}ms
171 | ${c.gray(count)}${
172 | count > operationMax
173 | ? (delta > 99 ? '\n' : ' ') + c.bgRed.black(`+${delta}`)
174 | : ''
175 | }`.replace(/^\s+/m, ''));
176 | };
177 |
178 | // actual benchmark
179 |
180 | start();
181 | childNodes = create1000(diff, childNodes);
182 | stop(parent.count(), 1000);
183 | console.assert(
184 | verifyNodes(childNodes, 1000),
185 | '%s 1k',
186 | lib
187 | );
188 |
189 | start();
190 | childNodes = create1000(diff, childNodes);
191 | stop(parent.count(), 2000);
192 | console.assert(
193 | verifyNodes(childNodes, 1000),
194 | '%s replace',
195 | lib
196 | );
197 |
198 | start();
199 | childNodes = random(shuffleSeed, diff, childNodes);
200 | stop(parent.count(), 2000);
201 | console.assert(
202 | verifyNodes(childNodes, 1000),
203 | '%s random',
204 | lib
205 | );
206 |
207 | start();
208 | childNodes = reverse(diff, childNodes);
209 | stop(parent.count(), 2000);
210 | console.assert(
211 | verifyNodes(childNodes, 1000),
212 | '%s reverse',
213 | lib
214 | );
215 |
216 | start();
217 | childNodes = clear(diff, childNodes);
218 | stop(parent.count(), 1000);
219 | console.assert(
220 | verifyNodes(childNodes, 0),
221 | '%s clear',
222 | lib
223 | );
224 |
225 | childNodes = create1000(diff, childNodes);
226 |
227 | start();
228 | childNodes = append1000(diff, childNodes);
229 | stop(parent.count(), 2000);
230 | console.assert(
231 | verifyNodes(childNodes, 2000),
232 | '%s append 1k',
233 | lib
234 | );
235 |
236 | start();
237 | childNodes = prepend1000(diff, childNodes);
238 | stop(parent.count(), 1000);
239 | console.assert(
240 | verifyNodes(childNodes, 3000),
241 | '%s prepend 1k',
242 | lib
243 | );
244 |
245 | childNodes = clear(diff, childNodes);
246 | childNodes = create1000(diff, childNodes);
247 |
248 | start();
249 | childNodes = swapRows(diff, childNodes);
250 | stop(parent.count(), 4);
251 | console.assert(
252 | parent.childNodes[1].textContent == 998 &&
253 | parent.childNodes[998].textContent == 1 &&
254 | verifyNodes(childNodes, 1000),
255 | '%s swap2 1k',
256 | lib
257 | );
258 |
259 | start();
260 | childNodes = updateEach10thRow(diff, childNodes);
261 | stop(parent.count(), 200);
262 | console.assert(
263 | verifyNodes(childNodes, 1000),
264 | '%s update 10th',
265 | lib
266 | );
267 |
268 | childNodes = clear(diff, childNodes);
269 |
270 | start();
271 | childNodes = create10000(diff, childNodes);
272 | stop(parent.count(), 10000);
273 | console.assert(
274 | verifyNodes(childNodes, 10000),
275 | '%s 10k',
276 | lib
277 | );
278 |
279 | start();
280 | childNodes = swapRows(diff, childNodes);
281 | stop(parent.count(), 4);
282 | console.assert(
283 | parent.childNodes[1].textContent == 9998 &&
284 | parent.childNodes[9998].textContent == 1 &&
285 | verifyNodes(childNodes, 10000),
286 | '%s swap2 10k',
287 | lib
288 | );
289 |
290 | childNodes = clear(diff, childNodes);
291 | reset();
292 |
293 | //*/
294 |
295 | libResults.push(`${((microtime.now() - totalStart) / 1000).toPrecision(3)}ms`);
296 | libResults.push(`${gzip}B`);
297 |
298 | // const used = process.memoryUsage().heapUsed / 1024 / 1024;
299 | // console.log(`The script uses approximately ${Math.round(used * 100) / 100} MB`);
300 |
301 | try {
302 | if (global.gc) {
303 | global.gc();
304 | }
305 | } catch (e) {
306 | process.exit();
307 | }
308 | });
309 |
310 | table.sort((a, b) => {
311 | a = Object.values(a)[0];
312 | b = Object.values(b)[0];
313 | return parseInt(a[a.length - 2]) - parseInt(b[b.length - 2]);
314 | });
315 |
316 | console.log(table.toString());
317 |
--------------------------------------------------------------------------------
/test/select.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
44 |
45 |
46 |
47 |
48 |