├── live-demo.html ├── .gitignore ├── bower.json ├── package.json ├── .jshintrc ├── remove_spaces.py ├── test ├── unit │ ├── string-comparator.spec.js │ ├── dom-node-comparator.spec.js │ ├── dom-node.spec.js │ ├── dom-node-string-pointer.spec.js │ └── dom-match-finder.spec.js └── test-index.html ├── Gruntfile.js ├── README.md ├── src ├── utils.js ├── string-comparator.js ├── dom-node-string-pointer.js ├── dom-node-comparator.js ├── dom-node.js ├── dom-match-finder.js └── dom-comparator.js └── dist └── dom-comparator.min.js /live-demo.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .sass-cache 3 | .sass-cache/* 4 | .bundle 5 | public/ 6 | public/* 7 | bin/* 8 | tmp/ 9 | jsdoc 10 | jsdoc/* 11 | docs 12 | docs/* 13 | doc.sh 14 | vwoapp/* 15 | .idea/* 16 | docs/* 17 | test/unit-tests.js 18 | testem.json 19 | node_modules 20 | bower_components 21 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom-comparator", 3 | "version": "0.0.1", 4 | "author": "Wingify", 5 | "ignore": [ 6 | "**/.*", 7 | "test" 8 | ], 9 | "dependencies": { 10 | "jasmine": "~1.3.1", 11 | "jquery": "~2.1.1", 12 | "underscore": "~1.6.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom-comparator", 3 | "version": "0.1.0", 4 | "author": "Wingify", 5 | "private": true, 6 | "devDependencies": { 7 | "grunt": "~0.4.1", 8 | "grunt-contrib-jshint": "~0.7.1", 9 | "grunt-contrib-concat": "~0.3.0", 10 | "grunt-contrib-copy": "~0.4.1", 11 | "grunt-contrib-watch": "~0.5.3", 12 | "testem": "~0.5.9", 13 | "glob": "~3.2.8", 14 | "bower": "~1.3.1", 15 | "exec-sync": "~0.1.6" 16 | }, 17 | "dependencies": { 18 | "grunt-contrib-uglify": "^0.5.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "options": { 3 | "curly": true, 4 | "eqeqeq": true, 5 | "immed": true, 6 | "latedef": true, 7 | "newcap": true, 8 | "noarg": true, 9 | "sub": true, 10 | "undef": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "es5": false, 14 | "strict": true, 15 | "multistr": true, 16 | "globals": { 17 | "define": true, 18 | "describe": true, 19 | "it": true, 20 | "beforeEach": true, 21 | "angular": true, 22 | "alert": true, 23 | "__dirname": true, 24 | "require": true 25 | } 26 | }, 27 | "backend": { 28 | "files": { 29 | "src": "<%= lint.backend %>" 30 | }, 31 | "options": { 32 | "strict": false, 33 | "node": true 34 | } 35 | }, 36 | "frontend": { 37 | "files": { 38 | "src": "<%= lint.frontend %>" 39 | }, 40 | "options": { 41 | "browser": true 42 | } 43 | }, 44 | "globals": { 45 | "require": false, 46 | "define": false, 47 | "expect": false, 48 | "jasmine": false, 49 | "it": false, 50 | "xit": false, 51 | "describe": false, 52 | "xdescribe": false, 53 | "protractor": false, 54 | "by": false, 55 | "browser": false, 56 | "element": false, 57 | "beforeEach": false, 58 | "afterEach": false, 59 | "inject": false, 60 | "module": false 61 | } 62 | } -------------------------------------------------------------------------------- /remove_spaces.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | a = """ 5 | 6 | 7 | 8 | 9 | 10 |
11 | 17 |
CHANGED TEXT
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | """ 27 | 28 | 29 | 30 | 31 | p = a.splitlines() 32 | ans = '' 33 | for i in range(len(p)): 34 | if len(p[i]) == 0: 35 | continue 36 | x = p[i].strip() 37 | ans += x 38 | 39 | # to remove the spaces between any " > < " ..... since spliting happens arount extra spaces .... 40 | #ans = re.sub(r'\s+([<"])', r'\1', ans) 41 | ans = re.sub(r'\s+([<"])', r'\1', ans) 42 | #print ans 43 | 44 | #print '\n\nremoving the spaces ....... ++++++++ inserting \ for escape characters \n' 45 | 46 | 47 | # for removing the spaces between any 'name ="....." name .... Since the spiliting happens around spaces/tabs also ... 48 | final = '' 49 | l = len(ans) 50 | i = 0 51 | while(i < l): 52 | if ans[i] == '"' : 53 | final += ans[i] 54 | while(1): 55 | i = i + 1 56 | if ans[i] == '"' : 57 | final += ans[i] 58 | break 59 | 60 | if i+1 < l and ans[i+1] == "'": 61 | final += ans[i] 62 | final += '\\' 63 | continue 64 | 65 | if ans[i] == ' ' : 66 | if ans[i+1].isalpha() : 67 | final += ans[i] 68 | continue 69 | else : 70 | continue 71 | 72 | final += ans[i] 73 | else : 74 | # for making apostrophe as an escape character e.g making ' it's ---> it\'s ' 75 | if i+1 < l and ans[i+1] == "'": 76 | final += ans[i] 77 | final += '\\' 78 | else : 79 | final += ans[i] 80 | i = i + 1 81 | 82 | print final 83 | -------------------------------------------------------------------------------- /test/unit/string-comparator.spec.js: -------------------------------------------------------------------------------- 1 | describe('module: StringComparator', function () { 2 | describe('method: compare', function () { 3 | it('compares two strings and gives back strings added, removed and changed', function () { 4 | var str1 = 'line 1\n' + 5 | 'line 2\n' + 6 | 'line 3\n' + 7 | 'line 4\n' + 8 | 'line 5'; 9 | var str2 = 'line 1\n' + 10 | 'line 2\n' + 11 | 'line 30\n' + 12 | 'line 4\n' + 13 | 'line 5'; 14 | 15 | var comparator = VWO.StringComparator.create({ 16 | stringA: str1, 17 | stringB: str2, 18 | matchA: {}, 19 | matchB: {}, 20 | couA: 6, // Added as hard code .... generally it gets calculated as number of strings after spliting 21 | couB: 6, 22 | ignoreA: [], 23 | ignoreB: [], 24 | splitOn: '\n' 25 | }); 26 | 27 | comparator.compare(); 28 | expect(comparator.stringsInA).toEqual(['line 1', 'line 2', 'line 3', 'line 4', 'line 5']); 29 | expect(comparator.stringsUnchanged).toEqual([ 30 | new VWO.StringComparisonResult('line 1', 0, 0), 31 | new VWO.StringComparisonResult('line 2', 1, 1), 32 | new VWO.StringComparisonResult('line 4', 3, 3), 33 | new VWO.StringComparisonResult('line 5', 4, 4) 34 | ]); 35 | 36 | expect(comparator.stringsDeletedFromA).toEqual([new VWO.StringComparisonResult('line 3', 2, -1)]); 37 | 38 | expect(comparator.stringsAddedInB).toEqual([ 39 | new VWO.StringComparisonResult('line 30', -1, 2) 40 | ]); 41 | 42 | 43 | expect(comparator.diffUnion).toEqual([ 44 | new VWO.StringComparisonResult('line 1', 0, 0), 45 | new VWO.StringComparisonResult('line 2', 1, 1), 46 | new VWO.StringComparisonResult('line 30', -1, 2), 47 | new VWO.StringComparisonResult('line 3', 2, -1), 48 | new VWO.StringComparisonResult('line 4', 3, 3), 49 | new VWO.StringComparisonResult('line 5', 4, 4) 50 | ]); 51 | 52 | }); 53 | }); 54 | 55 | 56 | }); -------------------------------------------------------------------------------- /test/unit/dom-node-comparator.spec.js: -------------------------------------------------------------------------------- 1 | describe('module: DOMNode-Comparator', function () { 2 | describe('method: nodeName', function () { 3 | it('compares how closely doms are related', function () { 4 | var domNode = VWO.DOMNodeComparator.create({ 5 | nodeA: VWO.DOMNode.create({ 6 | el: $('

Tutorial

Hello

').get(0) 7 | }), 8 | nodeB: VWO.DOMNode.create({ 9 | el: $('

Tutorial311

Tutorial

').get(0) 10 | }) 11 | }); 12 | 13 | expect(domNode.indexScore()).toBe(1); 14 | expect(domNode.nodeTypeScore()).toBe(1); 15 | expect(domNode.innerTextScore()).toBe(0); // inner text dont match 16 | expect(domNode.innerHTMLScore()).toBe(0); 17 | expect(domNode.nodeNameScore()).toBe(1); 18 | expect(domNode.parentScore()).toBe(0); 19 | expect(domNode.nextSiblingScore()).toBe(1); 20 | expect(domNode.previousSiblingScore()).toBe(1); 21 | 22 | 23 | // For Node A ... Children details 24 | expect(domNode.nodeA.children().length).toBe(3); 25 | expect(domNode.nodeA.children()[2].outerHTML()).toBe('

Hello

'); 26 | 27 | 28 | expect(domNode.childrenScore()).toEqual(0.6666666666666666); 29 | 30 | // Attributes score 31 | expect(domNode.attributeScore()).toEqual(0); // vs 32 | expect(domNode.addedAttributes()).toEqual({}); 33 | expect(domNode.changedAttributes()).toEqual({ 34 | class: 'chapter' 35 | }); 36 | expect(domNode.removedAttributes()).toEqual({}); 37 | 38 | expect(domNode.styleScore()).toEqual(1); 39 | 40 | // Based on criteria 41 | expect(domNode.finalScore()).toEqual(0.13333333333333333); 42 | 43 | 44 | }); 45 | }); 46 | }) -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function (grunt) { 3 | 'use strict'; 4 | 5 | var licenseBanner = '/*!' + '\n' + 6 | 'The MIT License (MIT)' + '\n' + 7 | 'http://opensource.org/licenses/MIT' + '\n\n' + 8 | 'Copyright (c) 2014 Wingify Software Pvt. Ltd.' + '\n' + 9 | 'http://wingify.com' + '\n' + 10 | '*/\n\n' + 11 | 'var VWO = window.VWO || {}; \n'; 12 | 13 | var fs = require('fs'); 14 | var execSync = require('exec-sync'); 15 | 16 | // Project configuration. 17 | grunt.initConfig({ 18 | pkg: grunt.file.readJSON('package.json'), 19 | jshint: { 20 | all: ['Gruntfile.js', 'src/*.js', 'test/*.js'], 21 | options: { 22 | jshintrc: '.jshintrc' 23 | } 24 | }, 25 | concat: { 26 | options: { 27 | separator: '\n', 28 | process: function (src) { 29 | return licenseBanner + '(function(){\n' + 30 | src + '\n})();'; 31 | } 32 | }, 33 | domComparator: { 34 | dest: 'dist/dom-comparator.js', 35 | src: ['src/*.js'] 36 | }, 37 | unit: { 38 | dest: 'test/unit-tests.js', 39 | src: ['test/unit/*.spec.js'] 40 | } 41 | }, 42 | watch: { 43 | scripts: { 44 | files: ['src/*.js'], 45 | tasks: ['concat'] 46 | }, 47 | tests: { 48 | files: ['test/unit/*.spec.js'], 49 | tasks: ['concat', 'testem'] 50 | }, 51 | options: { 52 | spawn: false, // don't spawn another process 53 | livereload: true // runs livereload server on 35729 54 | } 55 | }, 56 | uglify: { 57 | options: { 58 | mangle: false, 59 | wrap: 'closure', 60 | banner: licenseBanner, 61 | sourceMap: true, 62 | sourceMapIncludeSources: true 63 | }, 64 | domComparator: { 65 | files: { 66 | 'dist/dom-comparator.min.js': 'src/*.js' 67 | } 68 | } 69 | }, 70 | }); 71 | 72 | grunt.loadNpmTasks('grunt-contrib-concat'); 73 | grunt.loadNpmTasks('grunt-contrib-jshint'); 74 | grunt.loadNpmTasks('grunt-contrib-copy'); 75 | grunt.loadNpmTasks('grunt-contrib-concat'); 76 | grunt.loadNpmTasks('grunt-contrib-watch'); 77 | grunt.loadNpmTasks('grunt-contrib-uglify'); 78 | 79 | grunt.registerTask('testem', function () { 80 | var testemConfig = { 81 | 'test_page': 'test/test-index.html', 82 | 'launch_in_ci': ['Chrome'] 83 | }; 84 | fs.writeFileSync('testem.json', JSON.stringify(testemConfig), { 85 | encoding: 'utf8' 86 | }); 87 | var output = execSync('./node_modules/testem/testem.js ci'); 88 | grunt.log.writeln(output); 89 | if (output.indexOf('not ok') >= 0) { 90 | grunt.fail.fatal('one or more tests failed'); 91 | } 92 | }); 93 | 94 | grunt.registerTask('default', ['concat', 'uglify']); 95 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOM Comparator 2 | 3 | DOM Comparator is a library that, simply put, compares two strings of DOM nodes (which are called `stringA` and `stringB`), and returns an output containing the minimal number of steps that must be taken (like attribute changes, style changes, text changes and DOM manimpulation) to convert `stringA` into `stringB`. 4 | 5 | The output returned by DOM Comparator is an array of `VWO.Operation` objects, which can also be expressed as jQuery code. Here's a simple example: 6 | 7 | ```js 8 | var stringA = ''; 9 | var stringB = ''; 10 | 11 | // Compare the two strings 12 | var result = VWO.DOMComparator.create({ 13 | stringA: stringA, 14 | stringB: stringB 15 | }); 16 | 17 | // Expect an array of VWO.Operation objects to be returned. 18 | expect(result).toEqual(jasmine.any(Array)); 19 | expect(result[0]).toEqual(jasmine.any(VWO.Operation)); 20 | 21 | // Expect the first operation to be a 'removeAttr' operation. 22 | expect(result[0].name).toEqual('removeAttr'); 23 | 24 | // The operation is on an element identified by the following selector path 25 | expect(result[0].selectorPath).toEqual('UL:first-child > LI:first-child'); 26 | 27 | // With below content 28 | expect(result[0].content).toEqual({class: 'active'}); 29 | ``` 30 | 31 | ## Setting Up 32 | 33 | ### Installation 34 | 35 | * To install all the dependencies run `npm install`. 36 | * Then run `bower install` for `jasmine`, `jquery` and `underscore` library dependencies. 37 | * Install grunt globally, which is a Javascript Task Runner `npm install -g grunt-cli`. 38 | 39 | ### Downloads 40 | 41 | * [Development version](https://github.com/wingify/dom-comparator/blob/master/dist/dom-comparator.js) (unminified with comments) 42 | * [Production version](https://github.com/wingify/dom-comparator/blob/master/dist/dom-comparator.min.js) (minified) 43 | * [Source map](https://github.com/wingify/dom-comparator/blob/master/dist/dom-comparator.min.js.map) 44 | 45 | ### Live Demo 46 | 47 | A live demo can be found here: http://engineering.wingify.com/dom-comparator/live-demo.html 48 | 49 | ### Running Tests 50 | 51 | * For testing, we use Jasmine. 52 | * Tests are written in the `test/unit` folder. Each file in the `src` directory have different test cases files associataed with them in the `test/unit` directory. The majority of the test cases that test the library as a black box are in `dom-comparator.spec.js`. 53 | * To run tests, run `grunt; testem server;` (from the root directory of the repository) 54 | * To see the final outputs open http://localhost:7357/ in the browser, open the JavaScript console and look for the `final_results` array. 55 | 56 | ### Cases which don't work 57 | * If there are multiple occurrences of a node in the DOM. For example: 58 | 59 | > `nodeA`: 60 | ```html 61 |
62 | 64 |
ORIGINAL TEXT
65 |
66 | ``` 67 | 68 | > `nodeB`: 69 | ```html 70 |
ORIGINAL TEXT
71 |
72 | 74 |
ORIGINAL TEXT
75 |
76 | ``` 77 | 78 | > Here, since there are 2 occurrences of `
ORIGINAL TEXT
` in `nodeB`, the exact match of it cannot be found in `nodeA`, due to which the resulted output is not as expected. 79 | 80 | * When the wrapping of the original node is changed. For example: 81 | 82 | > `nodeA`: 83 | ```html 84 |
85 |
ORIGINAL TEXT
86 |
87 | ``` 88 | 89 | > `nodeB`: 90 | ```html 91 |
92 |
93 |
ORIGINAL TEXT
94 |
95 |
96 | ``` 97 | 98 | > Here, since the wrapping of `nodeB` is changed (wrapped by `
...
`), the whole content in `nodeB` would be considered as inserted (because matching heirarchy is top to bottom). 99 | 100 | ## Documentation 101 | 102 | The general usage documentation can be found on http://engineering.wingify.com/dom-comparator/ 103 | 104 | ## Authors 105 | 106 | * Himanshu Kapoor ([@fleon](http://github.com/fleon)) 107 | * Himanshu Kela ([@himanshukela](http://github.com/himanshukela)) 108 | 109 | ## License 110 | 111 | [The MIT License](http://opensource.org/licenses/MIT) 112 | 113 | Copyright (c) 2014-16 Wingify Software Pvt. Ltd. 114 | -------------------------------------------------------------------------------- /test/test-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DOM Comparator Unit Tests 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 26 | 27 | 28 | 40 | 81 | 101 | 116 |
117 | 118 | 119 | 120 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /test/unit/dom-node.spec.js: -------------------------------------------------------------------------------- 1 | describe('module: DOMNode', function () { 2 | describe('method: nodeName', function () { 3 | it('gets the name of the node', function () { 4 | var domNode = VWO.DOMNode.create({ 5 | el: $('
span
  • item 1
  • item 2
Design

').get(0) 6 | }); 7 | 8 | expect(domNode.nodeName()).toBe('DIV'); 9 | expect(domNode.nodeType()).toBe(Node.ELEMENT_NODE); 10 | //expect(domNode.innerText()).toBe('span content item 1item 2'); 11 | //expect(domNode.innerHTML()).toBe('span content '); 12 | //expect(domNode.outerHTML()).toBe('
span content
  • item 1
  • item 2
'); 13 | 14 | expect(domNode.attributes()).toEqual({ 15 | class: 'a' 16 | }); 17 | expect(domNode.styles()).toEqual({ 18 | color: 'blue' 19 | }); 20 | expect(domNode.children().length).toEqual(2); // Gives all the nodes 21 | var domNode1 = domNode.children()[0]; 22 | 23 | expect(domNode1.nextSibling().outerHTML()).toEqual('
'); 24 | expect(domNode1.ancestors().length).toEqual(1); 25 | expect(domNode1.ancestors()[0].outerHTML()).toEqual('
span
  • item 1
  • item 2
Design

'); 26 | 27 | expect(domNode1.nextElementSibling().outerHTML()).toEqual('
'); 28 | expect(domNode1.masterIndex()).toBe('0:0'); 29 | expect(domNode1.selectorPath()).toBe('DIV > DIV:first-child'); 30 | domNode1 = domNode1.children()[0]; 31 | expect(domNode1.selectorPath()).toBe('DIV > DIV:first-child > SPAN:first-child'); 32 | 33 | // Node added 34 | domNode.addChild(VWO.DOMNode.create({ 35 | el: $('
').get(0) 36 | })); 37 | //expect(domNode.outerHTML()).toBe() 38 | domNode.removeChildAt(2); 39 | //expect(domNode.outerHTML()).toBe() 40 | domNode.addChild(VWO.DOMNode.create({ 41 | el: $('
').get(0) 42 | })); 43 | domNode.swapChildrenAt(0, 2); 44 | //expect(domNode.outerHTML()).toBe() 45 | 46 | }); 47 | }); 48 | 49 | describe('method: Master Index', function () { 50 | it('gets the name of the node', function () { 51 | var domNode = VWO.DOMNode.create({ 52 | el: $('

Tutorial

Himanshu
HI
').get(0) 53 | }); 54 | 55 | expect(domNode.children().length).toBe(2); 56 | expect(domNode.children()[0].outerHTML()).toBe('

Tutorial

'); 57 | expect(domNode.nextSibling().outerHTML()).toBe('
HI
'); 58 | // expect(domNode.children()[0].ancestors()[0].outerHTML()).toBe('

Tutorial

'); 59 | expect(domNode.masterIndex()).toBe('0'); 60 | expect(domNode.children()[0].masterIndex()).toBe('0:0'); 61 | expect(domNode.children()[1].masterIndex()).toBe('0:1'); 62 | 63 | }); 64 | }); 65 | 66 | describe('method: Master Index', function () { 67 | it('Master index details', function () { 68 | var domNode = VWO.DOMNode.create({ 69 | el: $('

Tutorial

').get(0) 70 | }); 71 | 72 | expect(domNode.children().length).toBe(2); 73 | expect(domNode.children()[0].masterIndex()).toBe('0:0'); 74 | expect(domNode.children()[1].masterIndex()).toBe('0:1'); 75 | 76 | }); 77 | }); 78 | 79 | describe('method: Master Index', function () { 80 | it('gets the details of the master index', function () { 81 | var domNode = VWO.DOMNode.create({ 82 | el: $('
\n

Tutorial

\n
\n
\n
\n').get(0) 83 | }); 84 | 85 | expect(domNode.children().length).toBe(2); 86 | expect(domNode.children()[0].masterIndex()).toBe('0:0'); 87 | expect(domNode.children()[1].masterIndex()).toBe('0:1'); 88 | 89 | }); 90 | }); 91 | 92 | 93 | describe('method: Children', function () { 94 | it('Children details', function () { 95 | var domNode = VWO.DOMNode.create({ 96 | el: $('').get(0) 97 | }); 98 | 99 | expect(domNode.children().length).toBe(2); 100 | expect(domNode.children()[0].outerHTML()).toBe('

IT1

'); 101 | expect(domNode.children()[1].outerHTML()).toBe('
  • IT12
  • '); 102 | 103 | 104 | }); 105 | }); 106 | 107 | 108 | }) -------------------------------------------------------------------------------- /test/unit/dom-node-string-pointer.spec.js: -------------------------------------------------------------------------------- 1 | describe('module: DomNode-match-finder', function () { 2 | describe('case:1 method: nodeName', function () { 3 | it('gets the name of the node', function () { 4 | 5 | var domNode = VWO.DOMNodeStringPointer.create({ 6 | haystack: '' 7 | }); 8 | 9 | expect(domNode.allNodePointers()[1].index).toBe(21); //