├── history.md
├── index.js
├── .gitignore
├── Makefile
├── profile
└── html-parser.js
├── test
├── index.html
├── memory.html
├── renderer.js
└── serialize-html.js
├── lib
├── render-queue.js
├── keypath.js
├── hashify.js
├── serialize-dom.js
├── elements-pool.js
├── adler32.js
├── renderer.js
├── serialize-html.js
├── modifier.js
└── node.js
├── bower.json
├── component.json
├── bench
└── index.js
├── package.json
├── LICENSE
├── x-package.json5
├── demo
└── playground.html
├── readme.md
└── dist
└── diff-renderer.js
/history.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/renderer')
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | components
3 | .DS_Store
4 | npm-debug.log
5 | tmp
6 | *~
7 | *.swp
8 | *.log
9 | *.pid
10 | *.swo
11 | coverage
12 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | node_modules/browserify/bin/cmd.js -e ./index.js -o dist/diff-renderer.js -s DiffRenderer
3 | xpkg .
4 |
5 | bench:
6 | node bench
7 |
8 | test:
9 | node_modules/.bin/qunit -c DiffRenderer:./index.js -t ./test/serialize-html.js --cov
10 |
11 | .PHONY: build bench test
12 |
--------------------------------------------------------------------------------
/profile/html-parser.js:
--------------------------------------------------------------------------------
1 | var serializeHtml = require('../lib/serialize-html')
2 | var html = require('fs').readFileSync(__dirname + '/../bench/test.html', 'utf8')
3 |
4 | var i = 10000
5 |
6 | var now = Date.now()
7 |
8 | while (i > 0) {
9 | serializeHtml(html)
10 | i--
11 | }
12 |
13 | console.log(Date.now() - now)
14 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DiffRenderer
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/lib/render-queue.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /**
4 | * Any changed nodes land here to get considered for rendering.
5 | *
6 | * @type {Array}
7 | * @api private
8 | */
9 | var queue = module.exports = []
10 |
11 | /**
12 | * Add node to the queue.
13 | *
14 | * @param {Node} node
15 | * @api private
16 | */
17 | queue.enqueue = function(node) {
18 | queue.push(node)
19 | }
20 |
21 | /**
22 | * Empty the queue.
23 | *
24 | * @param {Node} node
25 | * @api private
26 | */
27 | queue.empty = function() {
28 | queue.splice(0)
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "diff-renderer",
3 | "version": "0.1.1",
4 | "description": "Render diff to the dom.",
5 | "keywords": [
6 | "diff",
7 | "renderer",
8 | "dom",
9 | "virtual dom",
10 | "react",
11 | "binding"
12 | ],
13 | "author": {
14 | "name": "Oleg Slobodskoi",
15 | "email": "oleg008@gmail.com"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git@github.com:kof/diff-renderer.git"
20 | },
21 | "licenses": [
22 | {
23 | "type": "MIT",
24 | "url": "http://www.opensource.org/licenses/mit-license.php"
25 | }
26 | ]
27 | }
--------------------------------------------------------------------------------
/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "diff-renderer",
3 | "version": "0.1.1",
4 | "description": "Render diff to the dom.",
5 | "keywords": [
6 | "diff",
7 | "renderer",
8 | "dom",
9 | "virtual dom",
10 | "react",
11 | "binding"
12 | ],
13 | "author": {
14 | "name": "Oleg Slobodskoi",
15 | "email": "oleg008@gmail.com"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git@github.com:kof/diff-renderer.git"
20 | },
21 | "licenses": [
22 | {
23 | "type": "MIT",
24 | "url": "http://www.opensource.org/licenses/mit-license.php"
25 | }
26 | ]
27 | }
--------------------------------------------------------------------------------
/bench/index.js:
--------------------------------------------------------------------------------
1 | var html = require('fs').readFileSync(__dirname + '/test.html', 'utf8'),
2 | serializeHtml = require('../lib/serialize-html'),
3 | htmlParser = require('html-minifier/src/htmlparser'),
4 | htmltree = require('htmltree')
5 |
6 | var noop = function() {}
7 |
8 | exports.compare = {}
9 |
10 | exports.compare.diffRenderer = function() {
11 | serializeHtml(html)
12 | }
13 |
14 | exports.compare.htmlParser = function() {
15 | htmlParser.HTMLParser(html, {})
16 | }
17 |
18 | exports.compare.htmltree = function() {
19 | htmltree(html, noop)
20 | }
21 |
22 | exports.stepsPerLap = 10
23 |
24 | require('bench').runMain()
25 |
--------------------------------------------------------------------------------
/lib/keypath.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /**
4 | * Find value in json obj using array or dot path notation.
5 | *
6 | * http://docs.mongodb.org/manual/core/document/#document-dot-notation
7 | *
8 | * {a: {b: {c: 3}}}
9 | * 'a.b.c' // 3
10 | *
11 | * {a: {b: {c: [1,2,3]}}}
12 | * 'a.b.c.1' // 2
13 | *
14 | * @param {Object|Array} obj
15 | * @param {String|Array} path
16 | * @return {Mixed}
17 | */
18 | module.exports = function(obj, path) {
19 | var parts, i
20 |
21 | if (!obj || !path) return obj
22 |
23 | parts = typeof path == 'string' ? path.split('.') : path
24 |
25 | for (i = 0; i < parts.length; i++) {
26 | obj = obj[parts[i]]
27 | }
28 |
29 | return obj
30 | }
31 |
--------------------------------------------------------------------------------
/test/memory.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Parse with DOMParser 10000
4 | Parse with diff-renderer 10000
5 |
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "diff-renderer",
3 | "version": "0.1.1",
4 | "description": "Render diff to the dom.",
5 | "keywords": [
6 | "diff",
7 | "renderer",
8 | "dom",
9 | "virtual dom",
10 | "react",
11 | "binding"
12 | ],
13 | "author": {
14 | "name": "Oleg Slobodskoi",
15 | "email": "oleg008@gmail.com"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git@github.com:kof/diff-renderer.git"
20 | },
21 | "licenses": [
22 | {
23 | "type": "MIT",
24 | "url": "http://www.opensource.org/licenses/mit-license.php"
25 | }
26 | ],
27 | "devDependencies": {
28 | "bench": "~0.3.5",
29 | "browserify": "~3.24.13",
30 | "html-minifier": "~0.5.5",
31 | "htmltree": "~0.0.4",
32 | "qunitjs": "~1.14.0",
33 | "qunit": "~0.7.2"
34 | },
35 | "dependencies": {
36 | "docdiff": "0.0.1"
37 | }
38 | }
--------------------------------------------------------------------------------
/lib/hashify.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var adler32 = require('./adler32')
4 |
5 | /**
6 | * Add hashes to every node.
7 | * Hash is calculated using node name, text, attributes and child nodes.
8 | *
9 | * @param {Object} node
10 | * @return {String} str which is used to generate a hash
11 | * @api private
12 | */
13 | module.exports = function hashify(node) {
14 | var attr, i
15 | var str = ''
16 | var nodes
17 |
18 | if (!node) return str
19 |
20 | if (node.name) {
21 | str += node.name
22 | if (node.text) str += node.text
23 |
24 | for (attr in node.attributes) {
25 | str += attr + node.attributes[attr]
26 | }
27 |
28 | nodes = node.children
29 | // Its a collection.
30 | } else {
31 | nodes = node
32 | }
33 |
34 | for (i in nodes) {
35 | str += hashify(nodes[i])
36 | }
37 |
38 | node.hash = adler32(str)
39 |
40 | return str
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2008-2014 Oleg Slobodskoi
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 |
--------------------------------------------------------------------------------
/x-package.json5:
--------------------------------------------------------------------------------
1 | {
2 | name: 'diff-renderer',
3 | version: '0.1.1',
4 | description: 'Render diff to the dom.',
5 | keywords: ['diff', 'renderer', 'dom', 'virtual dom', 'react', 'binding'],
6 | author: {
7 | name: 'Oleg Slobodskoi',
8 | email: 'oleg008@gmail.com'
9 | },
10 | repository: {
11 | type: 'git',
12 | url: 'git@github.com:kof/diff-renderer.git'
13 | },
14 | licenses: [
15 | {
16 | type: 'MIT',
17 | url: 'http://www.opensource.org/licenses/mit-license.php'
18 | }
19 | ],
20 | overlay: {
21 | npm: {
22 | devDependencies: {
23 | bench: '~0.3.5',
24 | browserify: '~3.24.13',
25 | 'html-minifier': '~0.5.5',
26 | htmltree: '~0.0.4',
27 | qunitjs: '~1.14.0',
28 | qunit: '~0.7.2'
29 | },
30 | dependencies: {
31 | docdiff: '0.0.1'
32 | }
33 | },
34 | bower: true,
35 | component: true
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/serialize-dom.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /**
4 | * Walk through the dom and create a json snapshot.
5 | *
6 | * @param {Element} element
7 | * @return {Object}
8 | * @api private
9 | */
10 | module.exports = function serialize(element) {
11 | var json = {
12 | name: element.nodeName.toLowerCase(),
13 | element: element
14 | }
15 |
16 | if (json.name == '#text') {
17 | json.text = element.textContent
18 | return json
19 | }
20 |
21 | var attr = element.attributes
22 | if (attr && attr.length) {
23 | json.attributes = {}
24 | var attrLength = attr.length
25 | for (var i = 0; i < attrLength; i++) {
26 | json.attributes[attr[i].name] = attr[i].value
27 | }
28 | }
29 |
30 | var childNodes = element.childNodes
31 | if (childNodes && childNodes.length) {
32 | json.children = {length: childNodes.length}
33 | var childNodesLength = childNodes.length
34 | for (var i = 0; i < childNodesLength; i++) {
35 | json.children[i] = serialize(childNodes[i])
36 | }
37 | }
38 |
39 | return json
40 | }
41 |
--------------------------------------------------------------------------------
/lib/elements-pool.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /**
4 | * Dom nodes pool for dom reuse.
5 | *
6 | * @api private
7 | */
8 | function ElementsPool() {
9 | this.store = {}
10 | }
11 |
12 | module.exports = ElementsPool
13 |
14 | /**
15 | * Get a dom element. Create if needed.
16 | *
17 | * @param {String} name
18 | * @return {Node}
19 | * @api private
20 | */
21 | ElementsPool.prototype.allocate = function(name) {
22 | var nodes = this.store[name]
23 | return nodes && nodes.length ? nodes.shift() : this.createElement(name)
24 | }
25 |
26 | /**
27 | * Release a dom element.
28 | *
29 | * @param {Node} element
30 | * @api private
31 | */
32 | ElementsPool.prototype.deallocate = function(element) {
33 | var name = element.nodeName.toLowerCase()
34 | if (this.store[name]) this.store[name].push(element)
35 | else this.store[name] = [element]
36 | }
37 |
38 | /**
39 | * Create dom element.
40 | *
41 | * @param {String} name - #text, div etc.
42 | * @return {Element}
43 | * @api private
44 | */
45 | ElementsPool.prototype.createElement = function(name) {
46 | return name == '#text' ? document.createTextNode('') : document.createElement(name)
47 | }
48 |
--------------------------------------------------------------------------------
/lib/adler32.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2013-2014 Facebook, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | * @providesModule adler32
17 | */
18 |
19 | /* jslint bitwise:true */
20 |
21 | "use strict";
22 |
23 | var MOD = 65521;
24 |
25 | // This is a clean-room implementation of adler32 designed for detecting
26 | // if markup is not what we expect it to be. It does not need to be
27 | // cryptographically strong, only reasonable good at detecting if markup
28 | // generated on the server is different than that on the client.
29 | function adler32(data) {
30 | var a = 1;
31 | var b = 0;
32 | for (var i = 0; i < data.length; i++) {
33 | a = (a + data.charCodeAt(i)) % MOD;
34 | b = (b + a) % MOD;
35 | }
36 | return a | (b << 16);
37 | }
38 |
39 | module.exports = adler32;
40 |
--------------------------------------------------------------------------------
/demo/playground.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | DiffRenderer playground.
4 |
41 |
42 |
43 |
44 |
45 |
New html
46 | Render diff
47 |
48 |
49 |
50 |
Rendered view
51 |
Render time:
52 |
53 |
54 |
55 |
60 |
61 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/lib/renderer.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var docdiff = require('docdiff')
4 | var keypath = require('./keypath')
5 | var Node = require('./node')
6 | var Modifier = require('./modifier')
7 | var serializeDom = require('./serialize-dom')
8 | var serializeHtml = require('./serialize-html')
9 | var renderQueue = require('./render-queue')
10 | var hashify = require('./hashify')
11 |
12 | /**
13 | * Renderer constructor.
14 | *
15 | * @param {Element} element dom node for serializing and updating.
16 | * @api public
17 | */
18 | function Renderer(element) {
19 | if (!element) throw new TypeError('DOM element required')
20 | if (!(this instanceof Renderer)) return new Renderer(element)
21 | this.node = null
22 | this.modifier = null
23 | this.refresh(element)
24 | }
25 |
26 | module.exports = Renderer
27 |
28 | Renderer.serializeDom = serializeDom
29 | Renderer.serializeHtml = serializeHtml
30 | Renderer.keypath = keypath
31 | Renderer.docdiff = docdiff
32 | Renderer.hashify = hashify
33 |
34 | /**
35 | * Start checking render queue and render.
36 | *
37 | * @api public
38 | */
39 | Renderer.start = function() {
40 | function check() {
41 | if (!Renderer.running) return
42 | Renderer.render()
43 | requestAnimationFrame(check)
44 | }
45 |
46 | Renderer.running = true
47 | requestAnimationFrame(check)
48 | }
49 |
50 | /**
51 | * Stop checking render queue and render.
52 | *
53 | * @api public
54 | */
55 | Renderer.stop = function() {
56 | Renderer.running = false
57 | }
58 |
59 | /**
60 | * Render all queued nodes.
61 | *
62 | * @api public
63 | */
64 | Renderer.render = function() {
65 | if (!renderQueue.length) return
66 | for (var i = 0; i < renderQueue.length; i++) {
67 | renderQueue[i].render()
68 | }
69 | renderQueue.empty()
70 |
71 | return this
72 | }
73 |
74 | /**
75 | * Create a snapshot from the dom.
76 | *
77 | * @param {Element} [element]
78 | * @return {Renderer} this
79 | * @api public
80 | */
81 | Renderer.prototype.refresh = function(element) {
82 | if (!element && this.node) element = this.node.target
83 | if (this.node) this.node.unlink()
84 | var json = serializeDom(element)
85 | this.node = Node.create(json)
86 | this.modifier = new Modifier(this.node)
87 |
88 | return this
89 | }
90 |
91 | /**
92 | * Find changes and apply them to virtual nodes.
93 | *
94 | * @param {String} html
95 | * @return {Renderer} this
96 | * @api public
97 | */
98 | Renderer.prototype.update = function(html) {
99 | var next = serializeHtml(html).children
100 | // Everything has been removed.
101 | if (!next) {
102 | this.node.removeChildren()
103 | return this
104 | }
105 | var current = this.node.toJSON().children || {}
106 | var diff = docdiff(current, next)
107 | this.modifier.apply(diff)
108 |
109 | return this
110 | }
111 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## Smart innerHTML for your views.
2 |
3 | DiffRenderer is a rendering component for your views, think of smart .innerHTML which renders only the changes.
4 |
5 | - high performance rendering
6 | - dramatically simplifies your view logic in non-trivial tasks
7 | - plays nicely with any library and web components
8 |
9 | Feel free to use any other project for events handling, templates, animations etc.
10 |
11 | ```javascript
12 |
13 | var DiffRenderer = require('diff-renderer')
14 |
15 | var el = document.getElementById('my-view')
16 |
17 | // Create renderer instance associated with some parent DOM element.
18 | var renderer = new DiffRenderer(el)
19 |
20 | // Put html you want to render.
21 | renderer.update('My content
')
22 |
23 | // Start rendering loop once.
24 | DiffRenderer.start()
25 | ```
26 |
27 | ## DOM is slow.
28 |
29 | If your view state has changed, you would normally manually go through changes and apply them to the dom by manipulating dom nodes or overwriting the view completely with .innerHTML.
30 |
31 | 1. Manipulating dom nodes in non trivial applications makes you view logic complicated.
32 | 2. Using .innerHTML causes performance and memory issues.
33 |
34 | ## Playground
35 |
36 | Visit [playground](//kof.github.com/diff-renderer/demo/playground.html) to see it in action.
37 |
38 | ## Use cases
39 |
40 | 1. Replacement for jQuery's dom manipulation methods and any direct dom manipulation.
41 | 1. Easy RESTful http API implementation:
42 | Client sends an object to the server, server validates it, client gets cleaned object back. With DiffRenderer we can apply the new one to the DOM without any checks.
43 | 1. Full bidirectional binding. For this you need to add the part for handling events and changing the state/data objects manually.
44 | 1. Real time data manipulation / rendering.
45 |
46 | ## How
47 |
48 | 1. It accepts a snapshot of your state in html or json format. You can use any template engine or none.
49 | 1. It calculates the difference to the cached state of the dom.
50 | 1. Intelligently renders the difference by only modifying/adding/removing nodes it has to.
51 | 1. It always reuses DOM elements.
52 |
53 | ## Gotchas
54 |
55 | 1. Don't attach listeners to the elements within renderer container. Do event delegation.
56 | 1. Don't change elements directly, use DiffRenderer. If you (or some lib) changed an element directly - refresh DiffRenderer.
57 |
58 | ## Bench
59 |
60 | - html parser - 200kb of html parsed in 15ms to json on my mb air.
61 | ```
62 | npm i
63 | make bench
64 | ```
65 | - jsperf of html parser vs. dom parsers http://jsperf.com/domparser-vs-jsparser
66 | - manual html parser memory test: open ./test/memory.html, observe your engines memory, click some times on buttons and see what happens
67 |
68 | ## Api
69 |
70 | - Use [requestAnimationFrame shim](https://github.com/kof/animation-frame) for older browsers.
71 |
72 | ### Get the api
73 |
74 | 1. Commonjs `var DiffRenderer = require('diff-renderer')`
75 | 2. From global
76 | - add script with browserified version from ./dist/diff-renderer.js
77 | - var DiffRenderer = window.DiffRenderer
78 |
79 | ### DiffRenderer(element)
80 |
81 | Create a renderer instance. Pass DOM element which you don't want to modify by DiffRenderer. Think of main view element f.e. like Backbone.View.prototype.el.
82 |
83 | ```javascript
84 | var el = document.getElementById('my-view')
85 | var renderer = new DiffRenderer(el)
86 | ```
87 |
88 | ### DiffRenderer#update(html)
89 |
90 | Update renderer state with the new html.
91 |
92 | ```javascript
93 | renderer.update('My new html
')
94 | ```
95 |
96 | ### DiffRenderer#refresh()
97 |
98 | Serialize dom elements within renderer main element. You might need this if you modified the dom directly.
99 |
100 | ```javascript
101 | var el = document.getElementById('my-view')
102 | var renderer = new DiffRenderer(el)
103 | renderer.update('My new html
')
104 |
105 | // Now me or some other library modifies the content (NOT RECOMMENDED)
106 | el.innerHTML = 'Test '
107 |
108 | // Now you want renderer let know that content has changed.
109 | renderer.refresh()
110 | ```
111 |
112 | ### DiffRenderer.start()
113 |
114 | Start the renderer loop. Now on each animation frame renderer will render all queued changes.
115 |
116 | ```javascript
117 | DiffRenderer.start()
118 |
119 | var el = document.getElementById('my-view')
120 | var renderer = new Renderer(el)
121 |
122 | renderer.update('My fresh content will be rendered in the next animation frame.')
123 | ```
124 |
125 | ### DiffRenderer.stop()
126 |
127 | Stop render loop.
128 |
129 | ### DiffRenderer.render()
130 |
131 | Render all queued changes from all renderer instances to the DOM. In the most cases you want to use `Renderer.start` instead.
132 |
133 | ```javascript
134 | var el1 = document.getElementById('my-view-1')
135 | var renderer1 = new Renderer(el1)
136 | var el2 = document.getElementById('my-view-2')
137 | var renderer2 = new Renderer(el2)
138 |
139 | // Now all virtual changes will be applied to the DOM.
140 | DiffRenderer.render()
141 | ```
142 |
143 | ## Test
144 | - `make build`
145 | - Open the test suite ./test/index.html
146 |
--------------------------------------------------------------------------------
/lib/serialize-html.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /**
4 | * Simplified html parser. The fastest one written in javascript.
5 | * It is naive and requires valid html.
6 | * You might want to validate your html before to pass it here.
7 | *
8 | * @param {String} html
9 | * @param {Object} [parent]
10 | * @return {Object}
11 | * @api private
12 | */
13 | module.exports = function serialize(str, parent) {
14 | if (!parent) parent = {name: 'root'}
15 | if (!str) return parent
16 |
17 | var i = 0
18 | var end = false
19 | var added = false
20 | var current
21 | var isWhite, isSlash, isOpen, isClose
22 | var inTag = false
23 | var inTagName = false
24 | var inAttrName = false
25 | var inAttrValue = false
26 | var inCloser = false
27 | var inClosing = false
28 | var isQuote, openQuote
29 | var attrName, attrValue
30 | var inText = false
31 |
32 | var json = {
33 | parent: parent,
34 | name: ''
35 | }
36 |
37 | while (!end) {
38 | current = str[i]
39 | isWhite = current == ' ' || current == '\t' || current == '\r' || current == '\n'
40 | isSlash = current == '/'
41 | isOpen = current == '<'
42 | isClose = current == '>'
43 | isQuote = current == "'" || current == '"'
44 | if (isSlash) inClosing = true
45 | if (isClose) inCloser = false
46 |
47 | if (current == null) {
48 | end = true
49 | } else {
50 | if (inTag) {
51 | if (inCloser) {
52 | delete json.name
53 | // Tag name
54 | } else if (inTagName || !json.name) {
55 | inTagName = true
56 | if ((json.name && isWhite) || isSlash) {
57 | inTagName = false
58 | if (!json.name) {
59 | inCloser = true
60 | if (parent.parent) parent = parent.parent
61 | }
62 | } else if (isClose) {
63 | serialize(str.substr(i + 1), inClosing || inCloser ? parent : json)
64 | return parent
65 | } else if (!isWhite) {
66 | json.name += current
67 | }
68 | // Attribute name
69 | } else if (inAttrName || !attrName) {
70 | inAttrName = true
71 | if (attrName == null) attrName = ''
72 | if (isSlash ||
73 | (attrName && isWhite) ||
74 | (attrName && current == '=')) {
75 |
76 | inAttrName = false
77 | if (attrName) {
78 | if (!json.attributes) json.attributes = {}
79 | json.attributes[attrName] = ''
80 | }
81 | } else if (isClose) {
82 | serialize(str.substr(i + 1), inClosing || inCloser ? parent : json)
83 | return parent
84 | } else if (!isWhite) {
85 | attrName += current
86 | }
87 | // Attribute value
88 | } else if (inAttrValue || attrName) {
89 | if (attrValue == null) attrValue = ''
90 |
91 | if (isQuote) {
92 | if (inAttrValue) {
93 | if (current == openQuote) {
94 | if (attrValue) json.attributes[attrName] = attrValue
95 | inAttrValue = false
96 | attrName = attrValue = null
97 | } else {
98 | attrValue += current
99 | }
100 | } else {
101 | inAttrValue = true
102 | openQuote = current
103 | }
104 | } else if (inAttrValue) {
105 | attrValue += current
106 | }
107 | }
108 | } else if (isOpen) {
109 | if (inText) {
110 | serialize(str.substr(i), parent)
111 | return parent
112 | }
113 | inTag = true
114 | } else if (isSlash && !inAttrValue) {
115 | end = true
116 | } else {
117 | inText = true
118 | inTag = false
119 | if (!json.name) json.name = '#text'
120 | if (json.text == null) json.text = ''
121 | json.text += current
122 | }
123 |
124 | if (json.name && !added) {
125 | if (!parent.children) parent.children = {length: 0}
126 | parent.children[parent.children.length] = json
127 | parent.children.length++
128 | added = true
129 | }
130 | }
131 |
132 | if (isClose) inClosing = false
133 |
134 | ++i
135 | }
136 |
137 | return parent
138 | }
139 |
--------------------------------------------------------------------------------
/lib/modifier.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var keypath = require('./keypath')
4 | var Node = require('./node')
5 |
6 | /**
7 | * Modifier applies changes to the node.
8 | *
9 | * @param {Node} node
10 | * @api private
11 | */
12 | function Modifier(node) {
13 | this.node = node
14 | }
15 |
16 | module.exports = Modifier
17 |
18 | /**
19 | * Exclude properties from applying the diff.
20 | *
21 | * @type {Object}
22 | * @api private
23 | */
24 | Modifier.EXCLUDE = {
25 | length: true,
26 | parent: true
27 | }
28 |
29 | /**
30 | * Apply the changes to the node.
31 | *
32 | * @param {Array} changes
33 | * @api private
34 | */
35 | Modifier.prototype.apply = function(changes) {
36 | for (var i = 0; i < changes.length; i++) {
37 | var change = changes[i]
38 | var prop = change.path[change.path.length - 1]
39 |
40 | if (Modifier.EXCLUDE[prop]) continue
41 |
42 | var propIsNum = false
43 | if (!isNaN(prop)) {
44 | propIsNum = true
45 | prop = Number(prop)
46 | }
47 |
48 | var method = this[prop]
49 | if (!method) {
50 | if (propIsNum) method = this['children']
51 | else method = this['attributes']
52 | }
53 | method.call(this, change, prop)
54 | }
55 | }
56 |
57 | /**
58 | * Modify a text node.
59 | *
60 | * @param {Change} change
61 | * @param {String} prop
62 | * @api private
63 | */
64 | Modifier.prototype.text = function(change, prop) {
65 | var path = change.path.slice(0, change.path.length - 1)
66 | var now = change.values.now
67 | var node = keypath(this.node.children, path)
68 | node.setText(now)
69 | }
70 |
71 | /**
72 | * Insert/remove child nodes.
73 | *
74 | * @param {Change} change
75 | * @param {String|Number} prop
76 | * @api private
77 | */
78 | Modifier.prototype.children = function(change, prop) {
79 | var now = change.values.now
80 | var node
81 | var path
82 |
83 | if (change.change == 'add') {
84 | // Insert node at specific position.
85 | if (typeof prop == 'number') {
86 | // Find a path to the parent node.
87 | if (change.path.length > 1) {
88 | path = change.path.slice(0, change.path.length - 1)
89 | path.push(prop - 1)
90 | node = keypath(this.node.children, path)
91 | } else {
92 | node = this.node
93 | }
94 | node.insertAt(prop, Node.create(now, node))
95 | // Append children.
96 | } else {
97 | path = change.path.slice(0, change.path.length - 1)
98 | node = keypath(this.node.children, path)
99 | for (var key in now) {
100 | if (!Modifier.EXCLUDE[key]) node.append(Node.create(now[key], node))
101 | }
102 | }
103 | } else if (change.change == 'remove') {
104 | // Remove all children.
105 | if (prop == 'children') {
106 | path = change.path.slice(0, change.path.length - 1)
107 | node = keypath(this.node.children, path)
108 | node.removeChildren()
109 | } else {
110 | path = change.path
111 | node = keypath(this.node.children, path)
112 | if (node) node.parent.removeChild(node)
113 | }
114 | }
115 | }
116 |
117 | /**
118 | * Modify attributes.
119 | *
120 | * @param {Change} change
121 | * @param {String} prop
122 | * @api private
123 | */
124 | Modifier.prototype.attributes = function(change, prop) {
125 | var now = change.values.now
126 | var path
127 | var node
128 |
129 | if (change.change == 'add') {
130 | if (prop == 'attributes') {
131 | path = change.path.slice(0, change.path.length - 1)
132 | node = keypath(this.node.children, path)
133 | node.setAttributes(now)
134 | } else {
135 | path = change.path.slice(0, change.path.length - 2)
136 | node = keypath(this.node.children, path)
137 | node.setAttribute(prop, now)
138 | }
139 | } else if (change.change == 'update') {
140 | path = change.path.slice(0, change.path.length - 2)
141 | node = keypath(this.node.children, path)
142 | node.setAttribute(prop, now)
143 | } else if (change.change == 'remove') {
144 | if (prop == 'attributes') {
145 | path = change.path.slice(0, change.path.length - 1)
146 | node = keypath(this.node.children, path)
147 | for (prop in change.values.original) {
148 | node.removeAttribute(prop)
149 | }
150 | } else {
151 | path = change.path.slice(0, change.path.length - 2)
152 | node = keypath(this.node.children, path)
153 | node.removeAttribute(prop)
154 | }
155 | }
156 | }
157 |
158 | /**
159 | * Change tag name.
160 | *
161 | * @param {Change} change
162 | * @param {String} prop
163 | * @api private
164 | */
165 | Modifier.prototype.name = function(change, prop) {
166 | var path = change.path.slice(0, change.path.length - 1)
167 | var node = keypath(this.node.children, path)
168 | var now = change.values.now
169 | node.setName(now)
170 | }
171 |
--------------------------------------------------------------------------------
/test/renderer.js:
--------------------------------------------------------------------------------
1 | function getView() {
2 | var element = document.createElement('div')
3 | var renderer = new DiffRenderer(element)
4 | return {
5 | render: function(html) {
6 | renderer.update(html)
7 | DiffRenderer.render()
8 | },
9 | element: element
10 | }
11 | }
12 |
13 | module('DiffRenderer')
14 |
15 | test('render a tag', function() {
16 | var view = getView()
17 | view.render(' ')
18 | equal(view.element.innerHTML, ' ', 'self closing')
19 | view.render(' ')
20 | equal(view.element.innerHTML, ' ', 'not self closing')
21 | })
22 |
23 | test('render a text node', function() {
24 | var view = getView()
25 | view.render('abc')
26 | equal(view.element.innerHTML, 'abc', 'without spaces')
27 | view.render(' abc ')
28 | equal(view.element.innerHTML, ' abc ', 'with spaces')
29 | })
30 |
31 | test('add an attribute', function() {
32 | var view = getView()
33 | view.render(' ')
34 | view.render(' ')
35 | equal(view.element.innerHTML, ' ', 'add class')
36 | view.render(' ')
37 | equal(view.element.innerHTML, ' ', 'add id')
38 | view.render(' ')
39 | equal(view.element.innerHTML, ' ', 'add empty href')
40 | view.render(' ')
41 | equal(view.element.innerHTML, ' ', 'add disabled')
42 | })
43 |
44 | test('add an attribute', function() {
45 | var view = getView()
46 | view.render(' ')
47 | view.render(' ')
48 | equal(view.element.innerHTML, ' ', 'add class')
49 | view.render(' ')
50 | equal(view.element.innerHTML, ' ', 'add id')
51 | view.render(' ')
52 | equal(view.element.innerHTML, ' ', 'add empty href')
53 | view.render(' ')
54 | equal(view.element.innerHTML, ' ', 'add disabled')
55 | })
56 |
57 | test('change an attribute', function() {
58 | var view = getView()
59 | view.render(' ')
60 | view.render(' ')
61 | equal(view.element.innerHTML, ' ')
62 | })
63 |
64 | test('change an attributes', function() {
65 | var view = getView()
66 | view.render(' ')
67 | view.render(' ')
68 | equal(view.element.innerHTML, ' ')
69 | })
70 |
71 | test('remove an attribute', function() {
72 | var view = getView()
73 | view.render(' ')
74 | view.render(' ')
75 | equal(view.element.innerHTML, ' ')
76 | })
77 |
78 | test('remove all attributes', function() {
79 | var view = getView()
80 | view.render(' ')
81 | view.render(' ')
82 | equal(view.element.innerHTML, ' ')
83 | })
84 |
85 | test('remove one of attributes', function() {
86 | var view = getView()
87 | view.render(' ')
88 | view.render(' ')
89 | equal(view.element.innerHTML, ' ')
90 | })
91 |
92 | test('change text node text', function() {
93 | var view = getView()
94 | view.render('abc')
95 | view.render('a')
96 | equal(view.element.innerHTML, 'a')
97 | })
98 |
99 | test('change text node text within a tag', function() {
100 | var view = getView()
101 | view.render('aaa ')
102 | view.render('a ')
103 | equal(view.element.innerHTML, 'a ')
104 | })
105 |
106 | test('replace tag by another tag', function() {
107 | var view = getView()
108 | view.render(' ')
109 | view.render(' ')
110 | equal(view.element.innerHTML, ' ')
111 | })
112 |
113 | test('replace multiple tags by 1 other tag', function() {
114 | var view = getView()
115 | view.render(' ')
116 | view.render(' ')
117 | equal(view.element.innerHTML, ' ')
118 | })
119 |
120 | test('replace multiple tags by 1 text node', function() {
121 | var view = getView()
122 | view.render(' ')
123 | view.render('aaa')
124 | equal(view.element.innerHTML, 'aaa')
125 | })
126 |
127 | test('replace multiple tags by multiple tags', function() {
128 | var view = getView()
129 | view.render(' ')
130 | view.render(' ')
131 | equal(view.element.innerHTML, ' ')
132 | })
133 |
134 | test('replace first tag by another one', function() {
135 | var view = getView()
136 | view.render(' ')
137 | view.render(' ')
138 | equal(view.element.innerHTML, ' ')
139 | })
140 |
141 | test('replace first tag by text node', function() {
142 | var view = getView()
143 | view.render(' ')
144 | view.render('a ')
145 | equal(view.element.innerHTML, 'a ')
146 | })
147 |
148 | test('replace last tag by another one', function() {
149 | var view = getView()
150 | view.render(' ')
151 | view.render(' ')
152 | equal(view.element.innerHTML, ' ')
153 | })
154 |
155 | test('replace middle tag by another one', function() {
156 | var view = getView()
157 | view.render(' ')
158 | view.render(' ')
159 | equal(view.element.innerHTML, ' ')
160 | })
161 |
162 | test('append a tag', function() {
163 | var view = getView()
164 | view.render(' ')
165 | view.render(' ')
166 | equal(view.element.innerHTML, ' ')
167 | })
168 |
169 | test('append a text node', function() {
170 | var view = getView()
171 | view.render(' ')
172 | view.render('b ')
173 | equal(view.element.innerHTML, 'b ')
174 | })
175 |
176 | test('prepend a tag', function() {
177 | var view = getView()
178 | view.render(' ')
179 | view.render(' ')
180 | equal(view.element.innerHTML, ' ')
181 | })
182 |
183 | test('prepend multiple tags', function() {
184 | var view = getView()
185 | view.render(' ')
186 | view.render(' ')
187 | equal(view.element.innerHTML, ' ')
188 | })
189 |
190 | test('prepend a text node', function() {
191 | var view = getView()
192 | view.render(' ')
193 | view.render('b ')
194 | equal(view.element.innerHTML, 'b ')
195 | })
196 |
197 | test('insert a tag after', function() {
198 | var view = getView()
199 | view.render(' ')
200 | view.render(' ')
201 | equal(view.element.innerHTML, ' ')
202 | })
203 |
204 | test('insert multiple tags after', function() {
205 | var view = getView()
206 | view.render(' ')
207 | view.render(' ')
208 | equal(view.element.innerHTML, ' ')
209 | })
210 |
211 | test('insert multiple tags in the middle', function() {
212 | var view = getView()
213 | view.render(' ')
214 | view.render(' ')
215 | equal(view.element.innerHTML, ' ')
216 | })
217 |
218 | test('migrate children', function() {
219 | var view = getView()
220 | view.render('a ')
221 | view.render('a ')
222 | equal(view.element.innerHTML, 'a ')
223 | })
224 |
225 | test('remove text node within a tag', function() {
226 | var view = getView()
227 | view.render('abc ')
228 | view.render(' ')
229 | equal(view.element.innerHTML, ' ')
230 | })
231 |
232 | test('remove text node', function() {
233 | var view = getView()
234 | view.render('abc')
235 | view.render('')
236 | equal(view.element.innerHTML, '')
237 | })
238 |
239 | test('remove first tag', function() {
240 | var view = getView()
241 | view.render(' ')
242 | view.render(' ')
243 | equal(view.element.innerHTML, ' ')
244 | })
245 |
246 | test('remove middle tag', function() {
247 | var view = getView()
248 | view.render(' ')
249 | view.render(' ')
250 | equal(view.element.innerHTML, ' ')
251 | })
252 |
253 | test('remove last tag', function() {
254 | var view = getView()
255 | view.render(' ')
256 | view.render(' ')
257 | equal(view.element.innerHTML, ' ')
258 | })
259 |
260 | test('remove last tag', function() {
261 | var view = getView()
262 | view.render(' ')
263 | view.render(' ')
264 | equal(view.element.innerHTML, ' ')
265 | })
266 |
267 | test('allow nesting of renderers', function() {
268 | var element1 = document.createElement('div')
269 | element1.className = '1'
270 | var element2 = document.createElement('div')
271 | element2.className = '2'
272 | var element3 = document.createElement('div')
273 | element3.className = '3'
274 | element1.appendChild(element2)
275 | element2.appendChild(element3)
276 |
277 | var renderer1 = new DiffRenderer(element1)
278 | var renderer2 = new DiffRenderer(element2)
279 |
280 | ok(renderer1.node.children[0] === renderer2.node)
281 | ok(renderer1.node.children[0].children[0] === renderer2.node.children[0])
282 | })
283 |
--------------------------------------------------------------------------------
/test/serialize-html.js:
--------------------------------------------------------------------------------
1 | var serializeHtml = DiffRenderer.serializeHtml
2 | var hashify = DiffRenderer.hashify
3 |
4 | QUnit.module('serialize-html')
5 |
6 | test('text node 0', function() {
7 | var doc = serializeHtml('a')
8 | var node = doc.children[0]
9 | hashify(node)
10 | equal(doc.name, 'root', 'root name')
11 | equal(node.name, '#text', 'node name')
12 | equal(node.text, 'a', 'node text')
13 | equal(node.hash, 123798090, 'node hash')
14 | })
15 |
16 | test('text node 1', function() {
17 | var node = serializeHtml(' abc ').children[0]
18 | hashify(node)
19 | equal(node.name, '#text', 'node name')
20 | equal(node.text, ' abc ', 'node text')
21 | equal(node.hash, 315884367, 'node hash')
22 | })
23 |
24 | test('empty node 0', function() {
25 | var node = serializeHtml(' ').children[0]
26 | hashify(node)
27 | equal(node.name, 'a', 'node name')
28 | equal(node.hash, 6422626, 'node hash')
29 | })
30 |
31 | test('empty node 1', function() {
32 | var node = serializeHtml('< a/>').children[0]
33 | hashify(node)
34 | equal(node.name, 'a', 'node name')
35 | equal(node.hash, 6422626, 'node hash')
36 | })
37 |
38 | test('empty node 2', function() {
39 | var node = serializeHtml('< a / >').children[0]
40 | hashify(node)
41 | equal(node.name, 'a', 'node name')
42 | equal(node.hash, 6422626, 'node hash')
43 | })
44 |
45 | test('empty node 3', function() {
46 | var node = serializeHtml('<\na\n/\n>').children[0]
47 | hashify(node)
48 | equal(node.name, 'a', 'node name')
49 | equal(node.hash, 6422626, 'node hash')
50 | })
51 |
52 | test('closing tag', function() {
53 | deepEqual(serializeHtml(''), {name: 'root'})
54 | deepEqual(serializeHtml('< / a\n >'), {name: 'root'})
55 | deepEqual(serializeHtml('<\n/\n a\n >'), {name: 'root'})
56 | })
57 |
58 | test('attributes 0', function() {
59 | var node = serializeHtml(' ').children[0]
60 | hashify(node)
61 | equal(node.name, 'a', 'node name')
62 | deepEqual(node.attributes, {id: ''}, 'attributes')
63 | equal(node.hash, 39584047, 'node hash')
64 | })
65 |
66 | test('attributes 1', function() {
67 | var node = serializeHtml(' ').children[0]
68 | hashify(node)
69 | equal(node.name, 'a', 'node name')
70 | deepEqual(node.attributes, {id: ''}, 'attributes')
71 | equal(node.hash, 39584047, 'node hash')
72 | })
73 |
74 | test('attributes 2', function() {
75 | var node = serializeHtml('').children[0]
76 | hashify(node)
77 | equal(node.name, 'a', 'node name')
78 | deepEqual(node.attributes, {id: ''}, 'attribtues')
79 | equal(node.hash, 39584047, 'node hash')
80 | })
81 |
82 | test('attributes 3', function() {
83 | var node = serializeHtml(' ').children[0]
84 | hashify(node)
85 | equal(node.name, 'a', 'node name')
86 | deepEqual(node.attributes, {id: 'a'}, 'attributes')
87 | equal(node.hash, 65798544, 'node hash')
88 | })
89 |
90 | test('attributes 4', function() {
91 | var node = serializeHtml('').children[0]
92 | hashify(node)
93 | equal(node.name, 'a', 'node name')
94 | deepEqual(node.attributes, {id: 'a'}, 'attributes')
95 | equal(node.hash, 65798544, 'node hash')
96 | })
97 |
98 | test('attributes 5', function() {
99 | var node = serializeHtml(' ').children[0]
100 | hashify(node)
101 | equal(node.name, 'a', 'node name')
102 | deepEqual(node.attributes, {id: "a'"}, 'attributes')
103 | equal(node.hash, 94568887, 'node hash')
104 | })
105 |
106 | test('attributes 6', function() {
107 | var node = serializeHtml(' ').children[0]
108 | hashify(node)
109 | equal(node.name, 'a', 'node name')
110 | deepEqual(node.attributes, {id: "a='b'"}, 'attributes')
111 | equal(node.hash, 209715837, 'node hash')
112 | })
113 |
114 | test('attributes 7', function() {
115 | var node = serializeHtml(' ').children[0]
116 | hashify(node)
117 | equal(node.name, 'a', 'node name')
118 | deepEqual(node.attributes, {id: 'a', class: '\nb '}, 'attributes')
119 | equal(node.hash, 499844146, 'node hash')
120 | })
121 |
122 | test('attributes 8', function() {
123 | var node = serializeHtml(' ').children[0]
124 | hashify(node)
125 | equal(node.name, 'a', 'node name')
126 | deepEqual(node.attributes, {attr1: 'first', attr2: 'second'}, 'attributes')
127 | equal(node.hash, 1708460255, 'node hash')
128 | })
129 |
130 | test('attributes 9', function() {
131 | var node = serializeHtml(' ').children[0]
132 | hashify(node)
133 | equal(node.name, 'a', 'node name')
134 | deepEqual(node.attributes, {attr: ''}, 'attributes')
135 | equal(node.hash, 239928071, 'node hash')
136 | })
137 |
138 | test('children 0', function() {
139 | var node = serializeHtml('a ').children[0]
140 | hashify(node)
141 | equal(node.name, 'a', 'node name')
142 | equal(node.hash, 168362667, 'node hash')
143 | equal(node.children[0].name, '#text', 'child name')
144 | equal(node.children[0].text, 'a', 'child text')
145 | equal(node.children.length, 1, 'children length')
146 | })
147 |
148 | test('children 1', function() {
149 | var node = serializeHtml('\n ').children[0]
150 | hashify(node)
151 | equal(node.name, 'a', 'node name')
152 | equal(node.hash, 162660948, 'node hash')
153 | equal(node.children[0].name, '#text', 'child 0 name')
154 | equal(node.children[0].text, '\n', 'child 0 text')
155 | equal(node.children[0].hash, 118096371, 'child 0 hash')
156 | equal(node.children.length, 1, 'children length')
157 | })
158 |
159 | test('children 2', function() {
160 | var node = serializeHtml(' a \n b ').children[0]
161 | hashify(node)
162 | equal(node.name, 'a', 'node name')
163 | equal(node.hash, 479593367, 'node hash')
164 | equal(node.children[0].name, '#text', 'child 0 name')
165 | equal(node.children[0].text, ' a \n b ', 'child 0 text')
166 | equal(node.children[0].hash, 396886838, 'child 0 hash')
167 | equal(node.children.length, 1, 'children length')
168 | })
169 |
170 | test('children 3', function() {
171 | var node = serializeHtml('\n ').children[0]
172 | hashify(node)
173 | equal(node.name, 'a', 'node name')
174 | equal(node.hash, 208143030, 'node hash')
175 | equal(node.children[0].name, '#text', 'child 0 name')
176 | equal(node.children[0].text, '\n', 'child 0 text')
177 | equal(node.children[0].hash, 118096371, 'child 0 hash')
178 | equal(node.children[1].name, 'b', 'child 1 name')
179 | equal(node.children[1].hash, 6488163, 'child 1 hash')
180 | equal(node.children.length, 2, 'children length')
181 | })
182 |
183 | test('children 4', function() {
184 | var node = serializeHtml('\n \n\n ').children[0]
185 | hashify(node)
186 | equal(node.name, 'a', 'node name')
187 | equal(node.hash, 762774805, 'node hash')
188 | equal(node.children[0].name, '#text', 'child 0 name')
189 | equal(node.children[0].text, '\n', 'child 0 text')
190 | equal(node.children[0].hash, 118096371, 'child 0 hash')
191 | equal(node.children[1].name, 'b', 'child 1 name')
192 | equal(node.children[1].hash, 6488163, 'child 1 hash')
193 | equal(node.children[2].name, '#text', 'child 2 name')
194 | equal(node.children[2].text, '\n\n', 'child 2 text')
195 | equal(node.children[2].hash, 151454205, 'child 2 hash')
196 | equal(node.children[3].name, 'c', 'child 3 name')
197 | equal(node.children[3].hash, 6553700, 'child 3 hash')
198 | equal(node.children.length, 4, 'children length')
199 | })
200 |
201 | test('children 5', function() {
202 | var node = serializeHtml(' ').children[0]
203 | hashify(node)
204 | equal(node.name, 'a', 'node name')
205 | equal(node.hash, 19202243, 'node hash')
206 | equal(node.children[0].name, 'a', 'child 0 name')
207 | equal(node.children[0].hash, 6422626, 'child 0 hash')
208 | equal(node.children.length, 1, 'children length')
209 | })
210 |
211 | test('children 6', function() {
212 | var node = serializeHtml(' ').children[0]
213 | hashify(node)
214 | equal(node.name, 'a', 'node name')
215 | equal(node.hash, 19267780, 'node hash')
216 | equal(node.children[0].name, 'b', 'child 0 name')
217 | equal(node.children[0].hash, 6488163, 'child 0 hash')
218 | equal(node.children.length, 1, 'children length')
219 | })
220 |
221 | test('children 7', function() {
222 | var node = serializeHtml(' ').children[0]
223 | hashify(node)
224 | equal(node.name, 'a', 'node name')
225 | equal(node.hash, 240255805, 'node hash')
226 | equal(node.children[0].name, 'b', 'child 0 name')
227 | equal(node.children[0].hash, 189334236, 'child 0 hash')
228 | deepEqual(node.children[0].attributes, {class: 'c'}, 'child 0 attributes')
229 | equal(node.children.length, 1, 'children length')
230 | })
231 |
232 | test('collection 0', function() {
233 | var nodes = serializeHtml(' ').children
234 | hashify(nodes)
235 | equal(nodes[0].name, 'a', 'node 0 name')
236 | equal(nodes[0].hash, 6422626, 'node 0 hash')
237 | equal(nodes[0].parent.name, 'root' , 'node 0 parent')
238 | equal(nodes[1].name, 'b', 'node 1 name')
239 | equal(nodes[1].hash, 6488163, 'node 1 hash')
240 | equal(nodes[1].parent.name, 'root' , 'node 1 parent')
241 | equal(nodes[2].name, 'c', 'node 2 name')
242 | equal(nodes[2].hash, 6553700, 'node 2 hash')
243 | equal(nodes[2].parent.name, 'root' , 'node 2 parent')
244 | equal(nodes.length, 3, 'nodes length')
245 | })
246 |
247 | test('collection 1', function() {
248 | var nodes = serializeHtml(' ').children
249 | hashify(nodes)
250 | equal(nodes[0].name, 'a', 'node 0 name')
251 | equal(nodes[0].hash, 6422626, 'node 0 hash')
252 | equal(nodes[0].parent.name, 'root' , 'node 0 parent')
253 | equal(nodes[1].name, 'b', 'node 1 name')
254 | equal(nodes[1].hash, 6488163, 'node 1 hash')
255 | equal(nodes[1].parent.name, 'root' , 'node 1 parent')
256 | equal(nodes[2].name, 'c', 'node 2 name')
257 | equal(nodes[2].hash, 6553700, 'node 2 hash')
258 | equal(nodes[2].parent.name, 'root' , 'node 2 parent')
259 | equal(nodes.length, 3, 'nodes length')
260 | })
261 |
262 | test('collection 2', function() {
263 | var node = serializeHtml(' ').children[0]
264 | hashify(node)
265 | equal(node.name, 'a', 'node name')
266 | equal(node.hash, 38600999, 'node hash')
267 | equal(node.children[0].name, 'b', 'child 0 name')
268 | equal(node.children[0].hash, 6488163, 'child 0 hash')
269 | equal(node.children[1].name, 'c', 'child 1 name')
270 | equal(node.children[1].hash, 6553700, 'child 1 hash')
271 | equal(node.children.length, 2, 'children length')
272 | })
273 |
274 | test('collection 3', function() {
275 | var node = serializeHtml(' ').children[0]
276 | hashify(node)
277 | equal(node.name, 'a', 'node name')
278 | equal(node.hash, 38600999, 'node hash')
279 | equal(node.children[0].name, 'b', 'child 0 name')
280 | equal(node.children[0].hash, 6488163, 'child 0 hash')
281 | equal(node.children[1].name, 'c', 'child 1 name')
282 | equal(node.children[1].hash, 6553700, 'child 1 hash')
283 | equal(node.children.length, 2, 'children length')
284 | })
285 |
286 |
287 |
--------------------------------------------------------------------------------
/lib/node.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var ElementsPool = require('./elements-pool')
4 | var queue = require('./render-queue')
5 |
6 | // Global elements pool for all nodes and all renderer instances.
7 | var pool = new ElementsPool()
8 |
9 | var counter = 0
10 |
11 | var nodesMap = {}
12 |
13 | var ID_NAMESPACE = '__diffRendererId__'
14 |
15 | /**
16 | * Abstract node which can be rendered to a dom node.
17 | *
18 | * @param {Object} options
19 | * @param {String} options.name tag name
20 | * @param {String} [options.text] text for the text node
21 | * @param {Object} [options.attributes] key value hash of name/values
22 | * @param {Element} [options.element] if element is passed, node is already rendered
23 | * @param {Object} [options.children] NodeList like collection
24 | * @param {Node} [parent]
25 | * @api private
26 | */
27 | function Node(options, parent) {
28 | this.id = counter++
29 | this.name = options.name
30 | this.parent = parent
31 | if (options.text) this.text = options.text
32 | if (options.attributes) this.attributes = options.attributes
33 | if (options.element) {
34 | this.setTarget(options.element)
35 | // Not dirty if element passed.
36 | } else {
37 | this.dirty('name', true)
38 | if (this.text) this.dirty('text', true)
39 | if (this.attributes) this.dirty('attributes', this.attributes)
40 | }
41 |
42 | if (options.children) {
43 | this.children = []
44 | for (var i in options.children) {
45 | if (i != 'length') {
46 | this.children[i] = Node.create(options.children[i], this)
47 | if (this.children[i].dirty()) this.dirty('children', true)
48 | }
49 | }
50 | }
51 |
52 | nodesMap[this.id] = this
53 | }
54 |
55 | module.exports = Node
56 |
57 | /**
58 | * Create Node instance, check if passed element has already a Node.
59 | *
60 | * @see {Node}
61 | * @api private
62 | * @return {Node}
63 | */
64 | Node.create = function(options, parent) {
65 | if (options.element && options.element[ID_NAMESPACE]) {
66 | return nodesMap[options.element[ID_NAMESPACE]]
67 | }
68 | return new Node(options, parent)
69 | }
70 |
71 | /**
72 | * Serialize instance to json data.
73 | *
74 | * @return {Object}
75 | * @api private
76 | */
77 | Node.prototype.toJSON = function() {
78 | var json = {name: this.name}
79 | if (this.text) json.text = this.text
80 | if (this.attributes) json.attributes = this.attributes
81 | if (this.children) {
82 | json.children = {length: this.children.length}
83 | for (var i = 0; i < this.children.length; i++) {
84 | json.children[i] = this.children[i].toJSON()
85 | }
86 | }
87 |
88 | return json
89 | }
90 |
91 | /**
92 | * Allocate, setup current target, insert children to the dom.
93 | *
94 | * @api private
95 | */
96 | Node.prototype.render = function() {
97 | if (!this._dirty) return
98 | if (this.dirty('name')) {
99 | if (this.target) this.migrate()
100 | else this.setTarget(pool.allocate(this.name))
101 | this.dirty('name', null)
102 | }
103 |
104 | // Handle children
105 | if (this.dirty('children') && this.children) {
106 | var newChildren = []
107 | for (var i = 0; i < this.children.length; i++) {
108 | var child = this.children[i]
109 | // Children can be dirty for removal or for insertions only.
110 | // All other changes are handled by the child.render.
111 | if (child.dirty()) {
112 | if (child.dirty('remove')) {
113 | this.removeChildAt(i)
114 | child.dirty('remove', null)
115 | delete nodesMap[child.id]
116 | // Handle insert.
117 | } else {
118 | var next = this.children[i + 1]
119 | child.render()
120 | this.target.insertBefore(child.target, next && next.target)
121 | newChildren.push(child)
122 | child.dirty('insert', null)
123 | child.dirty('name', null)
124 | }
125 | } else {
126 | newChildren.push(child)
127 | }
128 | }
129 | // We migrate children to the new array because some of them might be removed
130 | // and if we splice them directly, we will remove wrong elements.
131 | if (newChildren) this.children = newChildren
132 | this.dirty('children', null)
133 | }
134 |
135 | // Handle textContent.
136 | if (this.dirty('text') && this.text) {
137 | this.target.textContent = this.text
138 | this.dirty('text', null)
139 | }
140 |
141 | // Handle attribtues.
142 | if (this.dirty('attributes')) {
143 | var attributes = this.dirty('attributes')
144 | for (var attrName in attributes) {
145 | var value = attributes[attrName]
146 | if (value == null) {
147 | delete this.attributes[attrName]
148 | if (this.name != '#text') this.target.removeAttribute(attrName)
149 | } else {
150 | if (!this.attributes) this.attributes = {}
151 | this.attributes[attrName] = value
152 | this.target.setAttribute(attrName, value)
153 | }
154 | }
155 | this.dirty('attributes', null)
156 | }
157 | }
158 |
159 | /**
160 | * Remove child DOM element at passed position without removing from children array.
161 | *
162 | * @param {Number} position
163 | * @api private
164 | */
165 | Node.prototype.removeChildAt = function(position) {
166 | var child = this.children[position]
167 | child.detach()
168 | child.cleanup()
169 | if (child.children) {
170 | for (var i = 0; i < child.children.length; i++) {
171 | child.removeChildAt(i)
172 | }
173 | }
174 | pool.deallocate(child.target)
175 | child.unlink()
176 | }
177 |
178 | /**
179 | * Migrate target DOM element and its children to a new DOM element.
180 | * F.e. because tagName changed.
181 | *
182 | * @api private
183 | */
184 | Node.prototype.migrate = function() {
185 | this.detach()
186 | this.cleanup()
187 | var oldTarget = this.target
188 | this.setTarget(pool.allocate(this.name))
189 |
190 | // Migrate children.
191 | if (this.name == '#text') {
192 | this.children = null
193 | } else {
194 | this.text = null
195 | while (oldTarget.hasChildNodes()) {
196 | this.target.appendChild(oldTarget.removeChild(oldTarget.firstChild))
197 | }
198 | }
199 | pool.deallocate(oldTarget)
200 | this.dirty('insert', true)
201 | }
202 |
203 | /**
204 | * Remove target DOM element from the render tree.
205 | *
206 | * @api private
207 | */
208 | Node.prototype.detach = function() {
209 | var parentNode = this.target.parentNode
210 | if (parentNode) parentNode.removeChild(this.target)
211 | }
212 |
213 | /**
214 | * Clean up everything changed on the target DOM element.
215 | *
216 | * @api private
217 | */
218 | Node.prototype.cleanup = function() {
219 | if (this.attributes) {
220 | for (var attrName in this.attributes) this.target.removeAttribute(attrName)
221 | }
222 | if (this.text) this.target.textContent = ''
223 | delete this.target[ID_NAMESPACE]
224 | }
225 |
226 | /**
227 | * Set all child nodes set dirty for removal.
228 | *
229 | * @api private
230 | */
231 | Node.prototype.removeChildren = function() {
232 | if (!this.children) return
233 |
234 | for (var i = 0; i < this.children.length; i++) {
235 | this.children[i].dirty('remove', true)
236 | }
237 |
238 | this.dirty('children', true)
239 | }
240 |
241 | /**
242 | * Set child element as dirty for removal.
243 | *
244 | * @param {Node} node
245 | * @api private
246 | */
247 | Node.prototype.removeChild = function(child) {
248 | child.dirty('remove', true)
249 | this.dirty('children', true)
250 | }
251 |
252 | /**
253 | * Clean up all references for current and child nodes.
254 | *
255 | * @api private
256 | */
257 | Node.prototype.unlink = function() {
258 | if (this.children) {
259 | for (var i = 0; i < this.children.length; i++) {
260 | this.children[i].unlink()
261 | }
262 | }
263 | delete this.id
264 | delete this.name
265 | delete this.text
266 | delete this.attributes
267 | delete this.parent
268 | delete this.children
269 | delete this.target
270 | delete this._dirty
271 | }
272 |
273 | /**
274 | * Insert a node at specified position.
275 | *
276 | * @param {Number} position
277 | * @param {Node} node
278 | * @api private
279 | */
280 | Node.prototype.insertAt = function(position, node) {
281 | this.dirty('children', true)
282 | node.dirty('insert', true)
283 | if (!this.children) this.children = []
284 | this.children.splice(position, 0, node)
285 | }
286 |
287 | /**
288 | * Insert a node at the end.
289 | *
290 | * @param {Node} node
291 | * @api private
292 | */
293 | Node.prototype.append = function(node) {
294 | var position = this.children ? this.children.length : 0
295 | this.insertAt(position, node)
296 | }
297 |
298 | /**
299 | * Set nodes attributes.
300 | *
301 | * @param {Object} attribtues
302 | * @api private
303 | */
304 | Node.prototype.setAttributes = function(attributes) {
305 | for (var name in attributes) {
306 | this.setAttribute(name, attributes[name])
307 | }
308 | }
309 |
310 | /**
311 | * Set nodes attribute.
312 | *
313 | * @param {String} name
314 | * @param {String|Boolean|Null} value, use null to remove
315 | * @api private
316 | */
317 | Node.prototype.setAttribute = function(name, value) {
318 | if (this.attributes && this.attributes[name] == value) return
319 | var attributes = this.dirty('attributes') || {}
320 | attributes[name] = value
321 | this.dirty('attributes', attributes)
322 | }
323 |
324 | /**
325 | * Remove nodes attribute.
326 | * @param {String} name
327 | * @api private
328 | */
329 | Node.prototype.removeAttribute = function(name) {
330 | if (this.attributes && this.attributes[name] != null) this.setAttribute(name, null)
331 | }
332 |
333 | /**
334 | * Set text content.
335 | *
336 | * @param {String} content
337 | * @api private
338 | */
339 | Node.prototype.setText = function(text) {
340 | if (this.name != '#text' || text == this.text) return
341 | this.dirty('text', true)
342 | this.text = text
343 | }
344 |
345 | /**
346 | * Element name can't be set, we need to swap out the element.
347 | *
348 | * @param {String} name
349 | * @api private
350 | */
351 | Node.prototype.setName = function(name) {
352 | if (name == this.name) return
353 | this.dirty('name', true)
354 | this.parent.dirty('children', true)
355 | this.name = name
356 | }
357 |
358 | /**
359 | * Set target element.
360 | *
361 | * @param {Element} element
362 | * @api private
363 | */
364 | Node.prototype.setTarget = function(element) {
365 | element[ID_NAMESPACE] = this.id
366 | this.target = element
367 | }
368 |
369 | /**
370 | * Get/set/unset a dirty flag, add to render queue.
371 | *
372 | * @param {String} name
373 | * @param {Mixed} [value]
374 | * @api private
375 | */
376 | Node.prototype.dirty = function(name, value) {
377 | // Get flag.
378 | if (value === undefined) {
379 | return this._dirty && name ? this._dirty[name] : this._dirty
380 | }
381 |
382 | // Unset dirty flag.
383 | if (value === null) {
384 | if (this._dirty) {
385 | delete this._dirty[name]
386 | // If if its not empty object - exist.
387 | for (name in this._dirty) return
388 | // For quick check.
389 | delete this._dirty
390 | }
391 | // Set dirty flag.
392 | } else {
393 | if (!this._dirty) {
394 | this._dirty = {}
395 | // Only enqueue if its the first flag.
396 | queue.enqueue(this)
397 | }
398 | this._dirty[name] = value
399 | }
400 | }
401 |
--------------------------------------------------------------------------------
/dist/diff-renderer.js:
--------------------------------------------------------------------------------
1 | !function(e){if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.DiffRenderer=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 1) {
257 | path = change.path.slice(0, change.path.length - 1)
258 | path.push(prop - 1)
259 | node = keypath(this.node.children, path)
260 | } else {
261 | node = this.node
262 | }
263 | node.insertAt(prop, Node.create(now, node))
264 | // Append children.
265 | } else {
266 | path = change.path.slice(0, change.path.length - 1)
267 | node = keypath(this.node.children, path)
268 | for (var key in now) {
269 | if (!Modifier.EXCLUDE[key]) node.append(Node.create(now[key], node))
270 | }
271 | }
272 | } else if (change.change == 'remove') {
273 | // Remove all children.
274 | if (prop == 'children') {
275 | path = change.path.slice(0, change.path.length - 1)
276 | node = keypath(this.node.children, path)
277 | node.removeChildren()
278 | } else {
279 | path = change.path
280 | node = keypath(this.node.children, path)
281 | if (node) node.parent.removeChild(node)
282 | }
283 | }
284 | }
285 |
286 | /**
287 | * Modify attributes.
288 | *
289 | * @param {Change} change
290 | * @param {String} prop
291 | * @api private
292 | */
293 | Modifier.prototype.attributes = function(change, prop) {
294 | var now = change.values.now
295 | var path
296 | var node
297 |
298 | if (change.change == 'add') {
299 | if (prop == 'attributes') {
300 | path = change.path.slice(0, change.path.length - 1)
301 | node = keypath(this.node.children, path)
302 | node.setAttributes(now)
303 | } else {
304 | path = change.path.slice(0, change.path.length - 2)
305 | node = keypath(this.node.children, path)
306 | node.setAttribute(prop, now)
307 | }
308 | } else if (change.change == 'update') {
309 | path = change.path.slice(0, change.path.length - 2)
310 | node = keypath(this.node.children, path)
311 | node.setAttribute(prop, now)
312 | } else if (change.change == 'remove') {
313 | if (prop == 'attributes') {
314 | path = change.path.slice(0, change.path.length - 1)
315 | node = keypath(this.node.children, path)
316 | for (prop in change.values.original) {
317 | node.removeAttribute(prop)
318 | }
319 | } else {
320 | path = change.path.slice(0, change.path.length - 2)
321 | node = keypath(this.node.children, path)
322 | node.removeAttribute(prop)
323 | }
324 | }
325 | }
326 |
327 | /**
328 | * Change tag name.
329 | *
330 | * @param {Change} change
331 | * @param {String} prop
332 | * @api private
333 | */
334 | Modifier.prototype.name = function(change, prop) {
335 | var path = change.path.slice(0, change.path.length - 1)
336 | var node = keypath(this.node.children, path)
337 | var now = change.values.now
338 | node.setName(now)
339 | }
340 |
341 | },{"./keypath":5,"./node":7}],7:[function(_dereq_,module,exports){
342 | 'use strict'
343 |
344 | var ElementsPool = _dereq_('./elements-pool')
345 | var queue = _dereq_('./render-queue')
346 |
347 | // Global elements pool for all nodes and all renderer instances.
348 | var pool = new ElementsPool()
349 |
350 | var counter = 0
351 |
352 | var nodesMap = {}
353 |
354 | var ID_NAMESPACE = '__diffRendererId__'
355 |
356 | /**
357 | * Abstract node which can be rendered to a dom node.
358 | *
359 | * @param {Object} options
360 | * @param {String} options.name tag name
361 | * @param {String} [options.text] text for the text node
362 | * @param {Object} [options.attributes] key value hash of name/values
363 | * @param {Element} [options.element] if element is passed, node is already rendered
364 | * @param {Object} [options.children] NodeList like collection
365 | * @param {Node} [parent]
366 | * @api private
367 | */
368 | function Node(options, parent) {
369 | this.id = counter++
370 | this.name = options.name
371 | this.parent = parent
372 | if (options.text) this.text = options.text
373 | if (options.attributes) this.attributes = options.attributes
374 | if (options.element) {
375 | this.setTarget(options.element)
376 | // Not dirty if element passed.
377 | } else {
378 | this.dirty('name', true)
379 | if (this.text) this.dirty('text', true)
380 | if (this.attributes) this.dirty('attributes', this.attributes)
381 | }
382 |
383 | if (options.children) {
384 | this.children = []
385 | for (var i in options.children) {
386 | if (i != 'length') {
387 | this.children[i] = Node.create(options.children[i], this)
388 | if (this.children[i].dirty()) this.dirty('children', true)
389 | }
390 | }
391 | }
392 |
393 | nodesMap[this.id] = this
394 | }
395 |
396 | module.exports = Node
397 |
398 | /**
399 | * Create Node instance, check if passed element has already a Node.
400 | *
401 | * @see {Node}
402 | * @api private
403 | * @return {Node}
404 | */
405 | Node.create = function(options, parent) {
406 | if (options.element && options.element[ID_NAMESPACE]) {
407 | return nodesMap[options.element[ID_NAMESPACE]]
408 | }
409 | return new Node(options, parent)
410 | }
411 |
412 | /**
413 | * Serialize instance to json data.
414 | *
415 | * @return {Object}
416 | * @api private
417 | */
418 | Node.prototype.toJSON = function() {
419 | var json = {name: this.name}
420 | if (this.text) json.text = this.text
421 | if (this.attributes) json.attributes = this.attributes
422 | if (this.children) {
423 | json.children = {length: this.children.length}
424 | for (var i = 0; i < this.children.length; i++) {
425 | json.children[i] = this.children[i].toJSON()
426 | }
427 | }
428 |
429 | return json
430 | }
431 |
432 | /**
433 | * Allocate, setup current target, insert children to the dom.
434 | *
435 | * @api private
436 | */
437 | Node.prototype.render = function() {
438 | if (!this._dirty) return
439 | if (this.dirty('name')) {
440 | if (this.target) this.migrate()
441 | else this.setTarget(pool.allocate(this.name))
442 | this.dirty('name', null)
443 | }
444 |
445 | // Handle children
446 | if (this.dirty('children') && this.children) {
447 | var newChildren = []
448 | for (var i = 0; i < this.children.length; i++) {
449 | var child = this.children[i]
450 | // Children can be dirty for removal or for insertions only.
451 | // All other changes are handled by the child.render.
452 | if (child.dirty()) {
453 | if (child.dirty('remove')) {
454 | this.removeChildAt(i)
455 | child.dirty('remove', null)
456 | delete nodesMap[child.id]
457 | // Handle insert.
458 | } else {
459 | var next = this.children[i + 1]
460 | child.render()
461 | this.target.insertBefore(child.target, next && next.target)
462 | newChildren.push(child)
463 | child.dirty('insert', null)
464 | child.dirty('name', null)
465 | }
466 | } else {
467 | newChildren.push(child)
468 | }
469 | }
470 | // We migrate children to the new array because some of them might be removed
471 | // and if we splice them directly, we will remove wrong elements.
472 | if (newChildren) this.children = newChildren
473 | this.dirty('children', null)
474 | }
475 |
476 | // Handle textContent.
477 | if (this.dirty('text') && this.text) {
478 | this.target.textContent = this.text
479 | this.dirty('text', null)
480 | }
481 |
482 | // Handle attribtues.
483 | if (this.dirty('attributes')) {
484 | var attributes = this.dirty('attributes')
485 | for (var attrName in attributes) {
486 | var value = attributes[attrName]
487 | if (value == null) {
488 | delete this.attributes[attrName]
489 | if (this.name != '#text') this.target.removeAttribute(attrName)
490 | } else {
491 | if (!this.attributes) this.attributes = {}
492 | this.attributes[attrName] = value
493 | this.target.setAttribute(attrName, value)
494 | }
495 | }
496 | this.dirty('attributes', null)
497 | }
498 | }
499 |
500 | /**
501 | * Remove child DOM element at passed position without removing from children array.
502 | *
503 | * @param {Number} position
504 | * @api private
505 | */
506 | Node.prototype.removeChildAt = function(position) {
507 | var child = this.children[position]
508 | child.detach()
509 | child.cleanup()
510 | if (child.children) {
511 | for (var i = 0; i < child.children.length; i++) {
512 | child.removeChildAt(i)
513 | }
514 | }
515 | pool.deallocate(child.target)
516 | child.unlink()
517 | }
518 |
519 | /**
520 | * Migrate target DOM element and its children to a new DOM element.
521 | * F.e. because tagName changed.
522 | *
523 | * @api private
524 | */
525 | Node.prototype.migrate = function() {
526 | this.detach()
527 | this.cleanup()
528 | var oldTarget = this.target
529 | this.setTarget(pool.allocate(this.name))
530 |
531 | // Migrate children.
532 | if (this.name == '#text') {
533 | this.children = null
534 | } else {
535 | this.text = null
536 | while (oldTarget.hasChildNodes()) {
537 | this.target.appendChild(oldTarget.removeChild(oldTarget.firstChild))
538 | }
539 | }
540 | pool.deallocate(oldTarget)
541 | this.dirty('insert', true)
542 | }
543 |
544 | /**
545 | * Remove target DOM element from the render tree.
546 | *
547 | * @api private
548 | */
549 | Node.prototype.detach = function() {
550 | var parentNode = this.target.parentNode
551 | if (parentNode) parentNode.removeChild(this.target)
552 | }
553 |
554 | /**
555 | * Clean up everything changed on the target DOM element.
556 | *
557 | * @api private
558 | */
559 | Node.prototype.cleanup = function() {
560 | if (this.attributes) {
561 | for (var attrName in this.attributes) this.target.removeAttribute(attrName)
562 | }
563 | if (this.text) this.target.textContent = ''
564 | delete this.target[ID_NAMESPACE]
565 | }
566 |
567 | /**
568 | * Set all child nodes set dirty for removal.
569 | *
570 | * @api private
571 | */
572 | Node.prototype.removeChildren = function() {
573 | if (!this.children) return
574 |
575 | for (var i = 0; i < this.children.length; i++) {
576 | this.children[i].dirty('remove', true)
577 | }
578 |
579 | this.dirty('children', true)
580 | }
581 |
582 | /**
583 | * Set child element as dirty for removal.
584 | *
585 | * @param {Node} node
586 | * @api private
587 | */
588 | Node.prototype.removeChild = function(child) {
589 | child.dirty('remove', true)
590 | this.dirty('children', true)
591 | }
592 |
593 | /**
594 | * Clean up all references for current and child nodes.
595 | *
596 | * @api private
597 | */
598 | Node.prototype.unlink = function() {
599 | if (this.children) {
600 | for (var i = 0; i < this.children.length; i++) {
601 | this.children[i].unlink()
602 | }
603 | }
604 | delete this.id
605 | delete this.name
606 | delete this.text
607 | delete this.attributes
608 | delete this.parent
609 | delete this.children
610 | delete this.target
611 | delete this._dirty
612 | }
613 |
614 | /**
615 | * Insert a node at specified position.
616 | *
617 | * @param {Number} position
618 | * @param {Node} node
619 | * @api private
620 | */
621 | Node.prototype.insertAt = function(position, node) {
622 | this.dirty('children', true)
623 | node.dirty('insert', true)
624 | if (!this.children) this.children = []
625 | this.children.splice(position, 0, node)
626 | }
627 |
628 | /**
629 | * Insert a node at the end.
630 | *
631 | * @param {Node} node
632 | * @api private
633 | */
634 | Node.prototype.append = function(node) {
635 | var position = this.children ? this.children.length : 0
636 | this.insertAt(position, node)
637 | }
638 |
639 | /**
640 | * Set nodes attributes.
641 | *
642 | * @param {Object} attribtues
643 | * @api private
644 | */
645 | Node.prototype.setAttributes = function(attributes) {
646 | for (var name in attributes) {
647 | this.setAttribute(name, attributes[name])
648 | }
649 | }
650 |
651 | /**
652 | * Set nodes attribute.
653 | *
654 | * @param {String} name
655 | * @param {String|Boolean|Null} value, use null to remove
656 | * @api private
657 | */
658 | Node.prototype.setAttribute = function(name, value) {
659 | if (this.attributes && this.attributes[name] == value) return
660 | var attributes = this.dirty('attributes') || {}
661 | attributes[name] = value
662 | this.dirty('attributes', attributes)
663 | }
664 |
665 | /**
666 | * Remove nodes attribute.
667 | * @param {String} name
668 | * @api private
669 | */
670 | Node.prototype.removeAttribute = function(name) {
671 | if (this.attributes && this.attributes[name] != null) this.setAttribute(name, null)
672 | }
673 |
674 | /**
675 | * Set text content.
676 | *
677 | * @param {String} content
678 | * @api private
679 | */
680 | Node.prototype.setText = function(text) {
681 | if (this.name != '#text' || text == this.text) return
682 | this.dirty('text', true)
683 | this.text = text
684 | }
685 |
686 | /**
687 | * Element name can't be set, we need to swap out the element.
688 | *
689 | * @param {String} name
690 | * @api private
691 | */
692 | Node.prototype.setName = function(name) {
693 | if (name == this.name) return
694 | this.dirty('name', true)
695 | this.parent.dirty('children', true)
696 | this.name = name
697 | }
698 |
699 | /**
700 | * Set target element.
701 | *
702 | * @param {Element} element
703 | * @api private
704 | */
705 | Node.prototype.setTarget = function(element) {
706 | element[ID_NAMESPACE] = this.id
707 | this.target = element
708 | }
709 |
710 | /**
711 | * Get/set/unset a dirty flag, add to render queue.
712 | *
713 | * @param {String} name
714 | * @param {Mixed} [value]
715 | * @api private
716 | */
717 | Node.prototype.dirty = function(name, value) {
718 | // Get flag.
719 | if (value === undefined) {
720 | return this._dirty && name ? this._dirty[name] : this._dirty
721 | }
722 |
723 | // Unset dirty flag.
724 | if (value === null) {
725 | if (this._dirty) {
726 | delete this._dirty[name]
727 | // If if its not empty object - exist.
728 | for (name in this._dirty) return
729 | // For quick check.
730 | delete this._dirty
731 | }
732 | // Set dirty flag.
733 | } else {
734 | if (!this._dirty) {
735 | this._dirty = {}
736 | // Only enqueue if its the first flag.
737 | queue.enqueue(this)
738 | }
739 | this._dirty[name] = value
740 | }
741 | }
742 |
743 | },{"./elements-pool":3,"./render-queue":8}],8:[function(_dereq_,module,exports){
744 | 'use strict'
745 |
746 | /**
747 | * Any changed nodes land here to get considered for rendering.
748 | *
749 | * @type {Array}
750 | * @api private
751 | */
752 | var queue = module.exports = []
753 |
754 | /**
755 | * Add node to the queue.
756 | *
757 | * @param {Node} node
758 | * @api private
759 | */
760 | queue.enqueue = function(node) {
761 | queue.push(node)
762 | }
763 |
764 | /**
765 | * Empty the queue.
766 | *
767 | * @param {Node} node
768 | * @api private
769 | */
770 | queue.empty = function() {
771 | queue.splice(0)
772 | }
773 |
774 |
775 | },{}],9:[function(_dereq_,module,exports){
776 | 'use strict'
777 |
778 | var docdiff = _dereq_('docdiff')
779 | var keypath = _dereq_('./keypath')
780 | var Node = _dereq_('./node')
781 | var Modifier = _dereq_('./modifier')
782 | var serializeDom = _dereq_('./serialize-dom')
783 | var serializeHtml = _dereq_('./serialize-html')
784 | var renderQueue = _dereq_('./render-queue')
785 | var hashify = _dereq_('./hashify')
786 |
787 | /**
788 | * Renderer constructor.
789 | *
790 | * @param {Element} element dom node for serializing and updating.
791 | * @api public
792 | */
793 | function Renderer(element) {
794 | if (!element) throw new TypeError('DOM element required')
795 | if (!(this instanceof Renderer)) return new Renderer(element)
796 | this.node = null
797 | this.modifier = null
798 | this.refresh(element)
799 | }
800 |
801 | module.exports = Renderer
802 |
803 | Renderer.serializeDom = serializeDom
804 | Renderer.serializeHtml = serializeHtml
805 | Renderer.keypath = keypath
806 | Renderer.docdiff = docdiff
807 | Renderer.hashify = hashify
808 |
809 | /**
810 | * Start checking render queue and render.
811 | *
812 | * @api public
813 | */
814 | Renderer.start = function() {
815 | function check() {
816 | if (!Renderer.running) return
817 | Renderer.render()
818 | requestAnimationFrame(check)
819 | }
820 |
821 | Renderer.running = true
822 | requestAnimationFrame(check)
823 | }
824 |
825 | /**
826 | * Stop checking render queue and render.
827 | *
828 | * @api public
829 | */
830 | Renderer.stop = function() {
831 | Renderer.running = false
832 | }
833 |
834 | /**
835 | * Render all queued nodes.
836 | *
837 | * @api public
838 | */
839 | Renderer.render = function() {
840 | if (!renderQueue.length) return
841 | for (var i = 0; i < renderQueue.length; i++) {
842 | renderQueue[i].render()
843 | }
844 | renderQueue.empty()
845 |
846 | return this
847 | }
848 |
849 | /**
850 | * Create a snapshot from the dom.
851 | *
852 | * @param {Element} [element]
853 | * @return {Renderer} this
854 | * @api public
855 | */
856 | Renderer.prototype.refresh = function(element) {
857 | if (!element && this.node) element = this.node.target
858 | if (this.node) this.node.unlink()
859 | var json = serializeDom(element)
860 | this.node = Node.create(json)
861 | this.modifier = new Modifier(this.node)
862 |
863 | return this
864 | }
865 |
866 | /**
867 | * Find changes and apply them to virtual nodes.
868 | *
869 | * @param {String} html
870 | * @return {Renderer} this
871 | * @api public
872 | */
873 | Renderer.prototype.update = function(html) {
874 | var next = serializeHtml(html).children
875 | // Everything has been removed.
876 | if (!next) {
877 | this.node.removeChildren()
878 | return this
879 | }
880 | var current = this.node.toJSON().children || {}
881 | var diff = docdiff(current, next)
882 | this.modifier.apply(diff)
883 |
884 | return this
885 | }
886 |
887 | },{"./hashify":4,"./keypath":5,"./modifier":6,"./node":7,"./render-queue":8,"./serialize-dom":10,"./serialize-html":11,"docdiff":13}],10:[function(_dereq_,module,exports){
888 | 'use strict'
889 |
890 | /**
891 | * Walk through the dom and create a json snapshot.
892 | *
893 | * @param {Element} element
894 | * @return {Object}
895 | * @api private
896 | */
897 | module.exports = function serialize(element) {
898 | var json = {
899 | name: element.nodeName.toLowerCase(),
900 | element: element
901 | }
902 |
903 | if (json.name == '#text') {
904 | json.text = element.textContent
905 | return json
906 | }
907 |
908 | var attr = element.attributes
909 | if (attr && attr.length) {
910 | json.attributes = {}
911 | var attrLength = attr.length
912 | for (var i = 0; i < attrLength; i++) {
913 | json.attributes[attr[i].name] = attr[i].value
914 | }
915 | }
916 |
917 | var childNodes = element.childNodes
918 | if (childNodes && childNodes.length) {
919 | json.children = {length: childNodes.length}
920 | var childNodesLength = childNodes.length
921 | for (var i = 0; i < childNodesLength; i++) {
922 | json.children[i] = serialize(childNodes[i])
923 | }
924 | }
925 |
926 | return json
927 | }
928 |
929 | },{}],11:[function(_dereq_,module,exports){
930 | 'use strict'
931 |
932 | /**
933 | * Simplified html parser. The fastest one written in javascript.
934 | * It is naive and requires valid html.
935 | * You might want to validate your html before to pass it here.
936 | *
937 | * @param {String} html
938 | * @param {Object} [parent]
939 | * @return {Object}
940 | * @api private
941 | */
942 | module.exports = function serialize(str, parent) {
943 | if (!parent) parent = {name: 'root'}
944 | if (!str) return parent
945 |
946 | var i = 0
947 | var end = false
948 | var added = false
949 | var current
950 | var isWhite, isSlash, isOpen, isClose
951 | var inTag = false
952 | var inTagName = false
953 | var inAttrName = false
954 | var inAttrValue = false
955 | var inCloser = false
956 | var inClosing = false
957 | var isQuote, openQuote
958 | var attrName, attrValue
959 | var inText = false
960 |
961 | var json = {
962 | parent: parent,
963 | name: ''
964 | }
965 |
966 | while (!end) {
967 | current = str[i]
968 | isWhite = current == ' ' || current == '\t' || current == '\r' || current == '\n'
969 | isSlash = current == '/'
970 | isOpen = current == '<'
971 | isClose = current == '>'
972 | isQuote = current == "'" || current == '"'
973 | if (isSlash) inClosing = true
974 | if (isClose) inCloser = false
975 |
976 | if (current == null) {
977 | end = true
978 | } else {
979 | if (inTag) {
980 | if (inCloser) {
981 | delete json.name
982 | // Tag name
983 | } else if (inTagName || !json.name) {
984 | inTagName = true
985 | if ((json.name && isWhite) || isSlash) {
986 | inTagName = false
987 | if (!json.name) {
988 | inCloser = true
989 | if (parent.parent) parent = parent.parent
990 | }
991 | } else if (isClose) {
992 | serialize(str.substr(i + 1), inClosing || inCloser ? parent : json)
993 | return parent
994 | } else if (!isWhite) {
995 | json.name += current
996 | }
997 | // Attribute name
998 | } else if (inAttrName || !attrName) {
999 | inAttrName = true
1000 | if (attrName == null) attrName = ''
1001 | if (isSlash ||
1002 | (attrName && isWhite) ||
1003 | (attrName && current == '=')) {
1004 |
1005 | inAttrName = false
1006 | if (attrName) {
1007 | if (!json.attributes) json.attributes = {}
1008 | json.attributes[attrName] = ''
1009 | }
1010 | } else if (isClose) {
1011 | serialize(str.substr(i + 1), inClosing || inCloser ? parent : json)
1012 | return parent
1013 | } else if (!isWhite) {
1014 | attrName += current
1015 | }
1016 | // Attribute value
1017 | } else if (inAttrValue || attrName) {
1018 | if (attrValue == null) attrValue = ''
1019 |
1020 | if (isQuote) {
1021 | if (inAttrValue) {
1022 | if (current == openQuote) {
1023 | if (attrValue) json.attributes[attrName] = attrValue
1024 | inAttrValue = false
1025 | attrName = attrValue = null
1026 | } else {
1027 | attrValue += current
1028 | }
1029 | } else {
1030 | inAttrValue = true
1031 | openQuote = current
1032 | }
1033 | } else if (inAttrValue) {
1034 | attrValue += current
1035 | }
1036 | }
1037 | } else if (isOpen) {
1038 | if (inText) {
1039 | serialize(str.substr(i), parent)
1040 | return parent
1041 | }
1042 | inTag = true
1043 | } else if (isSlash && !inAttrValue) {
1044 | end = true
1045 | } else {
1046 | inText = true
1047 | inTag = false
1048 | if (!json.name) json.name = '#text'
1049 | if (json.text == null) json.text = ''
1050 | json.text += current
1051 | }
1052 |
1053 | if (json.name && !added) {
1054 | if (!parent.children) parent.children = {length: 0}
1055 | parent.children[parent.children.length] = json
1056 | parent.children.length++
1057 | added = true
1058 | }
1059 | }
1060 |
1061 | if (isClose) inClosing = false
1062 |
1063 | ++i
1064 | }
1065 |
1066 | return parent
1067 | }
1068 |
1069 | },{}],12:[function(_dereq_,module,exports){
1070 |
1071 | var utils = _dereq_('./utils');
1072 |
1073 | /**
1074 | * Diff Arrays
1075 | *
1076 | * @param {Array} one
1077 | * @param {Array} two
1078 | * @return {Array} Array with values in one but not in two
1079 | */
1080 | var diffArrays = function (one, two) {
1081 | return one.filter(function (val) {
1082 | return two.indexOf(val) === -1;
1083 | });
1084 | };
1085 |
1086 | /**
1087 | * Extract Type
1088 | *
1089 | * Returns a function that can be passed to an iterator (forEach) that will
1090 | * correctly update all.primitives and all.documents based on the values it
1091 | * iteraties over
1092 | *
1093 | * @param {Object} all Object on which primitives/documents will be set
1094 | * @return {Object} The all object, updated based on the looped values
1095 | */
1096 | var extractType = function (all) {
1097 | return function (val) {
1098 | if (utils.isObject(val)) {
1099 | all.primitives = false;
1100 | } else {
1101 | all.documents = false;
1102 | }
1103 |
1104 | if (Array.isArray(val))
1105 | all.primitives = false;
1106 | }
1107 | };
1108 |
1109 | /**
1110 | * ArrayDiff
1111 | *
1112 | * @param {Array} original
1113 | * @param {Array} now
1114 | * @return {Object}
1115 | */
1116 | module.exports = function (original, now) {
1117 |
1118 | var all = { primitives: true, documents: true };
1119 |
1120 | original.forEach(extractType(all));
1121 | now.forEach(extractType(all));
1122 |
1123 | var diff = {
1124 | change: null,
1125 | now: now,
1126 | original: original
1127 | };
1128 |
1129 | if (all.primitives) {
1130 | diff.change = 'primitiveArray';
1131 | diff.added = diffArrays(now, original);
1132 | diff.removed = diffArrays(original, now);
1133 | } else {
1134 | diff.change = all.documents ? 'documentArray' : 'mixedArray';
1135 | }
1136 |
1137 | return diff;
1138 | };
1139 | },{"./utils":14}],13:[function(_dereq_,module,exports){
1140 |
1141 | var arraydiff = _dereq_('./arraydiff');
1142 | var utils = _dereq_('./utils');
1143 |
1144 | /**
1145 | * DocDiff
1146 | *
1147 | * @param {Object} original
1148 | * @param {Object} now
1149 | * @param {Array} path
1150 | * @param {Array} changes
1151 | * @return {Array} Array of changes
1152 | */
1153 | module.exports = function docdiff (original, now, path, changes) {
1154 | if (!original || !now)
1155 | return false;
1156 |
1157 | if (!path)
1158 | path = [];
1159 |
1160 | if (!changes)
1161 | changes = [];
1162 |
1163 | var keys = Object.keys(now);
1164 | keys.forEach(function (key) {
1165 | var newVal = now[key];
1166 | var origVal = original[key];
1167 |
1168 | // Recurse
1169 | if (utils.isObject(newVal) && utils.isObject(origVal)) {
1170 | return docdiff(origVal, newVal, path.concat(key), changes);
1171 | }
1172 |
1173 | // Array diff
1174 | if (Array.isArray(newVal) && Array.isArray(origVal)) {
1175 | var diff = arraydiff(origVal, newVal);
1176 | return changes.push(new Change(path, key, 'update', diff.change, diff.now,
1177 | diff.original, diff.added, diff.removed));
1178 | }
1179 |
1180 | // Primitive updates and additions
1181 | if (origVal !== newVal) {
1182 | var type = origVal === undefined ? 'add' : 'update';
1183 | changes.push(new Change(path, key, type, 'primitive', newVal, origVal));
1184 | }
1185 | });
1186 |
1187 | // Primitve removals
1188 | Object.keys(original).forEach(function (key) {
1189 | if (keys.indexOf(key) === -1)
1190 | changes.push(new Change(path, key, 'remove', 'primitive', null,
1191 | original[key]));
1192 | });
1193 |
1194 | return changes;
1195 | }
1196 |
1197 | /**
1198 | * Change
1199 | *
1200 | * @param {Array} path
1201 | * @param {String} key
1202 | * @param {String} change
1203 | * @param {String} type
1204 | * @param {Mixed} now
1205 | * @param {Mixed} original
1206 | * @param {Array} added
1207 | * @param {Array} removed
1208 | */
1209 | function Change (path, key, change, type, now, original, added, removed) {
1210 | this.path = path.concat(key);
1211 | this.change = change;
1212 | this.type = type;
1213 |
1214 | this.values = {};
1215 |
1216 | if (change !== 'remove')
1217 | this.values.now = now;
1218 |
1219 | if (change !== 'add')
1220 | this.values.original = original;
1221 |
1222 | if (type === 'primitiveArray') {
1223 | this.values.added = added;
1224 | this.values.removed = removed;
1225 | }
1226 | }
1227 |
1228 | },{"./arraydiff":12,"./utils":14}],14:[function(_dereq_,module,exports){
1229 |
1230 | /**
1231 | * isObject
1232 | *
1233 | * @param {Mixed} arg
1234 | * @return {Boolean} If arg is an object
1235 | */
1236 | exports.isObject = function (arg) {
1237 | return typeof arg === 'object' && arg !== null && !Array.isArray(arg);
1238 | };
1239 |
1240 | },{}]},{},[1])
1241 | (1)
1242 | });
--------------------------------------------------------------------------------