├── .gitignore ├── .npmignore ├── perf.js ├── .travis.yml ├── package.json ├── README.md ├── test.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .nyc_output/ 3 | coverage/ 4 | .idea/ 5 | npm-debug.* 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .nyc_output/ 3 | coverage/ 4 | perf.js 5 | test.js 6 | .gitignore 7 | .travis.yml 8 | -------------------------------------------------------------------------------- /perf.js: -------------------------------------------------------------------------------- 1 | var Benchmark = require('benchmark'); 2 | var suite = new Benchmark.Suite(); 3 | 4 | var DiffText = require('./'); 5 | 6 | suite 7 | .add('diff-text', function () { 8 | DiffText('0123456789', '0123abc789'); 9 | }) 10 | .on('cycle', function (e) { 11 | console.log('' + e.target); 12 | }) 13 | .run(); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.12' 5 | - '1' 6 | - '2' 7 | - '3' 8 | - '4' 9 | - '4' 10 | - '5' 11 | - '6' 12 | - '7' 13 | - '8' 14 | script: "npm run test-travis" 15 | # Send coverage data to Coveralls 16 | after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diff-text", 3 | "version": "0.0.2", 4 | "description": "Just get the diff of a simple inline text.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test-travis": "nyc npm test && nyc report --reporter=lcov", 8 | "test": "mocha test.js", 9 | "perf": "node perf.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/hustcc/line-text-diff.git" 14 | }, 15 | "keywords": [ 16 | "text-diff", 17 | "diff", 18 | "input-diff", 19 | "line-text-diff" 20 | ], 21 | "author": "ProtoTeam", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/hustcc/line-text-diff/issues" 25 | }, 26 | "homepage": "https://github.com/hustcc/line-text-diff#readme", 27 | "devDependencies": { 28 | "benchmark": "^2.1.4", 29 | "coveralls": "^2.11.6", 30 | "expect": "^1.14.0", 31 | "mocha": "^2.4.5", 32 | "nyc": "^5.6.0", 33 | "word-table": "^1.0.3", 34 | "string-splice": "*", 35 | "splice-string": "*" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diff-text 2 | 3 | > Inline text diff algorithm. Simplified from [https://neil.fraser.name/writing/diff/](https://neil.fraser.name/writing/diff/). For input text diff in HTML with Chinese input software. 4 | 5 | [![Build Status](https://travis-ci.org/ProtoTeam/diff-text.svg?branch=master)](https://travis-ci.org/ProtoTeam/diff-text) [![Coverage Status](https://coveralls.io/repos/github/ProtoTeam/diff-text/badge.svg)](https://coveralls.io/github/ProtoTeam/diff-text) 6 | 7 | 8 | ## Usage 9 | 10 | > **npm i --save diff-text** 11 | 12 | 13 | ```js 14 | var diffText = require('diff-text'); 15 | 16 | diffText('diff---text', 'diff+++text'); 17 | 18 | // will get array like below: 19 | [ 20 | [0, 'diff'], // equal 21 | [-1, '---'], // delete 22 | [1, '+++'], // add 23 | [0, 'text'] // equal 24 | ] 25 | ``` 26 | 27 | 28 | ## Test & Perf 29 | 30 | ``` 31 | > npm run test 32 | 33 | > npm run pref 34 | 35 | diff-text x 1,238,388 ops/sec ±1.22% (88 runs sampled) 36 | ``` 37 | 38 | 39 | ## License 40 | 41 | ISC@[ProtoTeam](https://github.com/ProtoTeam). 42 | 43 | 44 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var DiffText = require('./'); 2 | var expect = require('expect'); 3 | 4 | describe('diff-text', function() { 5 | it('1. input null', function() { 6 | expect(DiffText.bind(null, null)).toThrow('Text should not be null.'); 7 | expect(DiffText.bind(undefined, undefined)).toThrow('Text should not be null.'); 8 | expect(DiffText.bind(null, undefined)).toThrow('Text should not be null.'); 9 | expect(DiffText.bind(undefined, null)).toThrow('Text should not be null.'); 10 | expect(DiffText.bind(null, '12')).toThrow('Text should not be null.'); 11 | expect(DiffText.bind('12', undefined)).toThrow('Text should not be null.'); 12 | }); 13 | 14 | it('2. input empty', function() { 15 | expect(DiffText('', '')).toEqual([]); 16 | }); 17 | 18 | it('3. input same', function() { 19 | expect(DiffText('difftext', 'difftext')).toEqual([ 20 | [0, 'difftext'] 21 | ]); 22 | expect(DiffText('123', '123')).toEqual([ 23 | [0, '123'] 24 | ]); 25 | }); 26 | 27 | it('4. input just one edit.', function() { 28 | expect(DiffText('', 'diff-text')).toEqual([ 29 | [1, 'diff-text'] 30 | ]); 31 | 32 | expect(DiffText('difftext', 'diff-text')).toEqual([ 33 | [0, 'diff'], 34 | [1, '-'], 35 | [0, 'text'] 36 | ]); 37 | 38 | expect(DiffText('diff-text', 'difftext')).toEqual([ 39 | [0, 'diff'], 40 | [-1, '-'], 41 | [0, 'text'] 42 | ]); 43 | }); 44 | 45 | it('5. input just two edit.', function() { 46 | expect(DiffText('diff12345text', 'diff234text')).toEqual([ 47 | [0, 'diff'], 48 | [-1, '1'], 49 | [0, '234'], 50 | [-1, '5'], 51 | [0, 'text'] 52 | ]); 53 | 54 | expect(DiffText('diff234text', 'diff12345text')).toEqual([ 55 | [0, 'diff'], 56 | [1, '1'], 57 | [0, '234'], 58 | [1, '5'], 59 | [0, 'text'] 60 | ]); 61 | 62 | expect(DiffText('diff+text', 'diff-text')).toEqual([ 63 | [0, 'diff'], 64 | [-1, '+'], 65 | [1, '-'], 66 | [0, 'text'] 67 | ]); 68 | 69 | expect(DiffText('diff---text', 'diff+++text')).toEqual([ 70 | [0, 'diff'], 71 | [-1, '---'], 72 | [1, '+++'], 73 | [0, 'text'] 74 | ]); 75 | }); 76 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by xiaowei.wzw on 17/6/14. 3 | */ 4 | 5 | var DIFF_INSERT = 1; 6 | var DIFF_DELETE = -1; 7 | var DIFF_EQUAL = 0; 8 | 9 | /** 10 | * Get the same substring at the start of strings. 11 | * @param originText 12 | * @param newText 13 | */ 14 | function prefixLength(originText, newText) { 15 | // Quick check for common null cases. 16 | if (!originText || !newText || originText.charCodeAt(0) !== newText.charCodeAt(0)) { 17 | return 0; 18 | } 19 | // Binary search. 20 | var pointerMin = 0; 21 | var pointerMax = Math.min(originText.length, newText.length); 22 | var pointerMid = pointerMax; 23 | var pointerStart = 0; 24 | while (pointerMin < pointerMid) { 25 | if (originText.substring(pointerStart, pointerMid) === newText.substring(pointerStart, pointerMid)) { 26 | pointerMin = pointerMid; 27 | pointerStart = pointerMin; 28 | } else { 29 | pointerMax = pointerMid; 30 | } 31 | pointerMid = Math.floor((pointerMax - pointerMin) / 2 + pointerMin); 32 | } 33 | return pointerMid; 34 | } 35 | 36 | /** 37 | * Get the same substring at the end of strings. 38 | * @param originText 39 | * @param newText 40 | * @returns {number} 41 | */ 42 | function suffixLength(originText, newText) { 43 | // Quick check for common null cases. 44 | if (!originText || !newText || originText.charCodeAt(originText.length - 1) !== 45 | newText.charCodeAt(newText.length - 1)) { 46 | return 0; 47 | } 48 | // Binary search. 49 | var pointerMin = 0; 50 | var pointerMax = Math.min(originText.length, newText.length); 51 | var pointerMid = pointerMax; 52 | var pointerEnd = 0; 53 | while (pointerMin < pointerMid) { 54 | if (originText.substring(originText.length - pointerMid, originText.length - pointerEnd) === 55 | newText.substring(newText.length - pointerMid, newText.length - pointerEnd)) { 56 | pointerMin = pointerMid; 57 | pointerEnd = pointerMin; 58 | } else { 59 | pointerMax = pointerMid; 60 | } 61 | pointerMid = Math.floor((pointerMax - pointerMin) / 2 + pointerMin); 62 | } 63 | return pointerMid; 64 | } 65 | 66 | /** 67 | * add prefix, suffix diff information into diff. 68 | * @param diff 69 | * @param prefix 70 | * @param suffix 71 | * @returns {*} 72 | */ 73 | function addPrefixSuffix(diff, prefix, suffix) { 74 | // at last. then add the common prefix / suffix at the start / end. 75 | if (prefix) { 76 | diff.unshift([DIFF_EQUAL, prefix]); 77 | } 78 | if (suffix) { 79 | diff.push([DIFF_EQUAL, suffix]); 80 | } 81 | return diff; 82 | } 83 | 84 | /** 85 | * 处理 diff 86 | * @param originText 87 | * @param newText 88 | * @returns {*} 89 | */ 90 | function processDiffText(originText, newText) { 91 | // 1. when Singular Insertion/Deletion 92 | if (!originText && !newText) { 93 | // when Equality 94 | return []; 95 | } else { 96 | if (!originText) { 97 | // Just add some text. 98 | return [[DIFF_INSERT, newText]] 99 | } 100 | if (!newText) { 101 | // Just delete some text. 102 | return [[DIFF_DELETE, originText]]; 103 | } 104 | } 105 | 106 | // 2. when Two Edits: two simple insertions or two simple deletions 107 | var longText = originText.length >= newText.length ? originText : newText; 108 | var shortText = originText.length >= newText.length ? newText : originText; 109 | 110 | var i = longText.indexOf(shortText); 111 | 112 | if (i !== -1) { 113 | var what = originText.length > newText.length ? DIFF_DELETE : DIFF_INSERT; 114 | // if Shorter text is inside the longer text. 115 | return [ 116 | [what, longText.substring(0, i)], 117 | [DIFF_EQUAL, shortText], 118 | [what, longText.substring(i + shortText.length)] 119 | ]; 120 | } 121 | // not inside 122 | return [ 123 | [DIFF_DELETE, originText], 124 | [DIFF_INSERT, newText] 125 | ]; 126 | } 127 | 128 | 129 | /** 130 | * Main entry. Get the diff information. 131 | * @param originText 132 | * @param newText 133 | * @returns {Array} 134 | */ 135 | function diffText(originText, newText) { 136 | if (originText == null || newText == null) { 137 | throw new Error('Text should not be null.') 138 | } 139 | 140 | // 1 when Equality 141 | if (originText === '' && newText === '') { 142 | return []; 143 | } 144 | 145 | // 2 remove Common Prefix/Suffix 146 | var prefixLen = prefixLength(originText, newText); 147 | var prefix = originText.substring(0, prefixLen); 148 | originText = originText.substring(prefixLen); 149 | newText = newText.substring(prefixLen); 150 | 151 | var suffixLen = suffixLength(originText, newText); 152 | var suffix = originText.substring(originText.length - suffixLen); 153 | originText = originText.substring(0, originText.length - suffixLen); 154 | newText = newText.substring(0, newText.length - suffixLen); 155 | 156 | // process diff text 157 | var diff = processDiffText(originText, newText); 158 | 159 | // add prefix and suffix. 160 | return addPrefixSuffix(diff, prefix, suffix); 161 | } 162 | 163 | module.exports = diffText; 164 | --------------------------------------------------------------------------------