diff --git a/README.md b/README.md
index ef4d0ca..c31ffd4 100644
--- a/README.md
+++ b/README.md
@@ -93,7 +93,8 @@ Mergely will emit an `updated` event when the editor is first initialized, and e
|ignorews|boolean|`false`|Ignores white-space.|
|ignorecase|boolean|`false`|Ignores case.|
|ignoreaccents|boolean|`false`|Ignores accented characters.|
-|lcs|boolean|`true`|Enables/disables LCS computation for paragraphs (char-by-char changes). Disabling can give a performance gain for large documents.|
+|inline|string|`chars`|The line-by-line (inline) type of diff. Valid values are: `none`, `chars`, `words`. When `none`, inline diff is disabled. When `chars` differientiation is done on a character-by-character basis. When `words` differentiation is done on a whitespace basis.|
+|lcs|boolean|`true`|:warning: **Deprecated**, use [`inline`](#inline). Enables/disables LCS computation for paragraphs (char-by-char changes). Disabling can give a performance gain for large documents.|
|lhs|boolean,`function handler(setValue)`|`null`|Sets the value of the editor on the left-hand side.|
|license|string|`lgpl`|The choice of license to use with Mergely. Valid values are: `lgpl`, `gpl`, `mpl` or `lgpl-separate-notice`, `gpl-separate-notice`, `mpl-separate-notice` (the license requirements are met in a separate notice file).|
|line_numbers|boolean|`true`|Enables/disables line numbers. Enabling line numbers will toggle the visibility of the line number margins.|
diff --git a/examples/app.js b/examples/app.js
index f686f3a..9dd7bf7 100644
--- a/examples/app.js
+++ b/examples/app.js
@@ -4,15 +4,9 @@ require('codemirror/addon/selection/mark-selection.js');
require('codemirror/lib/codemirror.css');
require('../src/mergely.css');
-const lhs = `\
-the quick red fox
-jumped over the hairy dog
-`;
+const lhs = `hello`;
-const rhs = `\
-the quick brown fox
-jumped over the lazy dog
-`;
+const rhs = `hello\ngoodbye`;
document.onreadystatechange = function () {
@@ -22,6 +16,7 @@ document.onreadystatechange = function () {
const mergely = new Mergely('#compare', {
license: 'lgpl',
+ inline: 'words',
lhs,
rhs
});
diff --git a/examples/change-styles.html b/examples/change-styles.html
index d0b7a2d..8daf1c1 100755
--- a/examples/change-styles.html
+++ b/examples/change-styles.html
@@ -304,7 +304,6 @@ dog
and the postman
`
}];
- console.log(data.length);
for (let i = 0; i < data.length; ++i) {
const { lhs, rhs } = data[i];
const darkModeOptions = i === 11 ? {
diff --git a/examples/editor.html b/examples/editor.html
index 1ce2cfe..b64971b 100644
--- a/examples/editor.html
+++ b/examples/editor.html
@@ -298,7 +298,6 @@
rhs.style = 'color:initial';
});
jQuery('#search-text').on('keypress', (ev) => {
- console.log(ev.which)
if (event.which === 13) {
ev.preventDefault();
jQuery('#search').click();
diff --git a/src/diff-view.js b/src/diff-view.js
index 5619585..f601816 100644
--- a/src/diff-view.js
+++ b/src/diff-view.js
@@ -941,7 +941,6 @@ CodeMirrorDiffView.prototype._markupLineChanges = function (changes) {
for (let i = 0; i < changes.length; ++i) {
const change = changes[i];
const isCurrent = current_diff === i;
- const lineDiff = this.settings.lcs !== false;
const lhsInView = this._isChangeInView('lhs', lhsvp, change);
const rhsInView = this._isChangeInView('rhs', rhsvp, change);
@@ -952,7 +951,7 @@ CodeMirrorDiffView.prototype._markupLineChanges = function (changes) {
vdoc.addRender('lhs', change, i, {
isCurrent,
- lineDiff,
+ lineDiff: this.settings.inline !== 'none',
// TODO: move out of loop
getMergeHandler: (change, side, oside) => {
return () => this._merge_change(change, side, oside);
@@ -969,7 +968,7 @@ CodeMirrorDiffView.prototype._markupLineChanges = function (changes) {
vdoc.addRender('rhs', change, i, {
isCurrent,
- lineDiff,
+ lineDiff: this.settings.inline !== 'none',
// TODO: move out of loop
getMergeHandler: (change, side, oside) => {
return () => this._merge_change(change, side, oside);
@@ -979,13 +978,14 @@ CodeMirrorDiffView.prototype._markupLineChanges = function (changes) {
});
}
- if (lineDiff
+ if (this.settings.inline !== 'none'
&& (lhsInView || rhsInView)
&& change.op === 'c') {
vdoc.addInlineDiff(change, i, {
ignoreaccents: this.settings.ignoreaccents,
ignorews: this.settings.ignorews,
ignorecase: this.settings.ignorecase,
+ split: this.settings.inline,
getText: (side, lineNum) => {
if (side === 'lhs') {
const text = led.getLine(lineNum);
diff --git a/src/diff.js b/src/diff.js
index da859a3..e6ba3ef 100644
--- a/src/diff.js
+++ b/src/diff.js
@@ -1,43 +1,52 @@
+const Encoder = require('./encoder.js');
+
const SMS_TIMEOUT_SECONDS = 1.0;
-function diff(lhs, rhs, options = {}) {
+function diff(lhs, rhs, opts) {
const {
ignorews = false,
ignoreaccents = false,
ignorecase = false,
split = 'lines'
- } = options;
-
- this.codeify = new CodeifyText(lhs, rhs, {
+ } = opts || {};
+ const options = {
ignorews,
ignoreaccents,
ignorecase,
split
- });
- const lhs_ctx = {
- codes: this.codeify.getCodes('lhs'),
+ };
+
+ const encoder = new Encoder();
+ const lhsCodes = encoder.encode(lhs, options);
+ const rhsCodes = encoder.encode(rhs, options);
+ const lhsCtx = {
+ codes: lhsCodes.codes,
+ length: lhsCodes.length,
+ parts: lhsCodes.parts,
modified: {}
};
- const rhs_ctx = {
- codes: this.codeify.getCodes('rhs'),
+ const rhsCtx = {
+ codes: rhsCodes.codes,
+ length: rhsCodes.length,
+ parts: rhsCodes.parts,
modified: {}
};
const vector_d = [];
const vector_u = [];
- this._lcs(lhs_ctx, 0, lhs_ctx.codes.length, rhs_ctx, 0, rhs_ctx.codes.length, vector_u, vector_d);
- this._optimize(lhs_ctx);
- this._optimize(rhs_ctx);
- this.items = this._create_diffs(lhs_ctx, rhs_ctx);
+ this._lcs(lhsCtx, 0, lhsCodes.length, rhsCtx, 0, rhsCodes.length, vector_u, vector_d);
+ this._optimize(lhsCtx);
+ this._optimize(rhsCtx);
+ this.items = this._create_diffs(lhsCtx, rhsCtx, options);
+ this.sides = {
+ lhs: lhsCtx,
+ rhs: rhsCtx
+ };
};
diff.prototype.changes = function() {
return this.items;
};
-diff.prototype.getLines = function(side) {
- return this.codeify.getLines(side);
-};
-
diff.prototype.normal_form = function() {
let nf = '';
for (let index = 0; index < this.items.length; ++index) {
@@ -57,8 +66,8 @@ diff.prototype.normal_form = function() {
else rhs_str = (item.rhs_start + 1) + ',' + (item.rhs_start + item.rhs_inserted_count);
nf += lhs_str + change + rhs_str + '\n';
- const lhs_lines = this.getLines('lhs');
- const rhs_lines = this.getLines('rhs');
+ const lhs_lines = this.sides.lhs.parts;
+ const rhs_lines = this.sides.rhs.parts;
if (rhs_lines && lhs_lines) {
let i;
// if rhs/lhs lines have been retained, output contextual diff
@@ -102,7 +111,7 @@ diff.prototype._lcs = function(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower
diff.prototype._sms = function(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower, rhs_upper, vector_u, vector_d) {
const timeout = Date.now() + SMS_TIMEOUT_SECONDS * 1000;
- const max = lhs_ctx.codes.length + rhs_ctx.codes.length + 1;
+ const max = lhs_ctx.length + rhs_ctx.length + 1;
const kdown = lhs_lower - rhs_lower;
const kup = lhs_upper - rhs_upper;
const delta = (lhs_upper - lhs_lower) - (rhs_upper - rhs_lower);
@@ -115,12 +124,13 @@ diff.prototype._sms = function(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower
const ret = { x:0, y:0 }
let x;
let y;
+ let k;
for (let d = 0; d <= maxd; ++d) {
if (SMS_TIMEOUT_SECONDS && Date.now() > timeout) {
// bail if taking too long
return { x: lhs_lower, y: rhs_upper };
}
- for (let k = kdown - d; k <= kdown + d; k += 2) {
+ for (k = kdown - d; k <= kdown + d; k += 2) {
if (k === kdown - d) {
x = vector_d[ offset_down + k + 1 ];//down
}
@@ -178,15 +188,15 @@ diff.prototype._sms = function(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower
diff.prototype._optimize = function(ctx) {
let start = 0;
let end = 0;
- while (start < ctx.codes.length) {
- while ((start < ctx.codes.length) && (ctx.modified[start] === undefined || ctx.modified[start] === false)) {
+ while (start < ctx.length) {
+ while ((start < ctx.length) && (ctx.modified[start] === undefined || ctx.modified[start] === false)) {
start++;
}
end = start;
- while ((end < ctx.codes.length) && (ctx.modified[end] === true)) {
+ while ((end < ctx.length) && (ctx.modified[end] === true)) {
end++;
}
- if ((end < ctx.codes.length) && (ctx.codes[start] === ctx.codes[end])) {
+ if ((end < ctx.length) && (ctx.codes[start] === ctx.codes[end])) {
ctx.modified[start] = false;
ctx.modified[end] = true;
}
@@ -196,16 +206,16 @@ diff.prototype._optimize = function(ctx) {
}
};
-diff.prototype._create_diffs = function(lhs_ctx, rhs_ctx) {
+diff.prototype._create_diffs = function(lhs_ctx, rhs_ctx, options) {
const items = [];
let lhs_start = 0;
let rhs_start = 0;
let lhs_line = 0;
let rhs_line = 0;
- while (lhs_line < lhs_ctx.codes.length || rhs_line < rhs_ctx.codes.length) {
- if ((lhs_line < lhs_ctx.codes.length) && (!lhs_ctx.modified[lhs_line])
- && (rhs_line < rhs_ctx.codes.length) && (!rhs_ctx.modified[rhs_line])) {
+ while (lhs_line < lhs_ctx.length || rhs_line < rhs_ctx.length) {
+ if ((lhs_line < lhs_ctx.length) && (!lhs_ctx.modified[lhs_line])
+ && (rhs_line < rhs_ctx.length) && (!rhs_ctx.modified[rhs_line])) {
// equal lines
lhs_line++;
rhs_line++;
@@ -215,19 +225,50 @@ diff.prototype._create_diffs = function(lhs_ctx, rhs_ctx) {
lhs_start = lhs_line;
rhs_start = rhs_line;
- while (lhs_line < lhs_ctx.codes.length && (rhs_line >= rhs_ctx.codes.length || lhs_ctx.modified[lhs_line]))
+ while (lhs_line < lhs_ctx.length && (rhs_line >= rhs_ctx.length || lhs_ctx.modified[lhs_line]))
lhs_line++;
- while (rhs_line < rhs_ctx.codes.length && (lhs_line >= lhs_ctx.codes.length || rhs_ctx.modified[rhs_line]))
+ while (rhs_line < rhs_ctx.length && (lhs_line >= lhs_ctx.length || rhs_ctx.modified[rhs_line]))
rhs_line++;
if ((lhs_start < lhs_line) || (rhs_start < rhs_line)) {
// store a new difference-item
+ let deleted_count;
+ let inserted_count;
+ let lhs_start = lhs_line;
+ let rhs_start = rhs_line;
+ if (options.split === 'lines') {
+ lhs_start = lhs_line;
+ rhs_start = rhs_line;
+ deleted_count = lhs_line - lhs_start;
+ inserted_count = rhs_line - rhs_start;
+ } else {
+ const ditem_lhs_start = (lhs_start >= lhs_ctx.length)
+ ? lhs_ctx.length
+ : lhs_ctx.parts[lhs_start].from;
+ const ditem_rhs_start = (rhs_start >= rhs_ctx.length)
+ ? rhs_ctx.length
+ : rhs_ctx.parts[rhs_start].from;
+
+ const ditem_lhs_end = (lhs_line >= lhs_ctx.length)
+ ? lhs_ctx.length
+ : lhs_ctx.parts[lhs_line].from;
+ const ditem_rhs_end = (rhs_line >= rhs_ctx.length)
+ ? rhs_ctx.length
+ : rhs_ctx.parts[rhs_line];
+ // const delim_len = (options.split === 'words') ? 1 : 0;
+ // const ditemLhs
+
+ deleted_count = ditem_lhs_end - ditem_lhs_start;
+ inserted_count = ditem_rhs_end - ditem_rhs_start;
+ lhs_start = ditem_lhs_start;
+ rhs_start = ditem_rhs_start;
+ }
items.push({
lhs_start: lhs_start,
rhs_start: rhs_start,
- lhs_deleted_count: lhs_line - lhs_start,
- rhs_inserted_count: rhs_line - rhs_start
+ lhs_deleted_count: deleted_count,
+ rhs_inserted_count: inserted_count
});
}
}
@@ -235,77 +276,4 @@ diff.prototype._create_diffs = function(lhs_ctx, rhs_ctx) {
return items;
};
-function CodeifyText(lhs, rhs, options) {
- this._max_code = 0;
- this._diff_codes = {};
- this.ctxs = {};
- this.options = options;
- this.options.split = this.options.split || 'lines';
-
- if (typeof lhs === 'string') {
- if (this.options.split === 'chars') {
- this.lhs = lhs.split('');
- } else if (this.options.split === 'words') {
- this.lhs = lhs.split(/\s/);
- } else if (this.options.split === 'lines') {
- this.lhs = lhs.split('\n');
- }
- } else {
- this.lhs = lhs;
- }
- if (typeof rhs === 'string') {
- if (this.options.split === 'chars') {
- this.rhs = rhs.split('');
- } else if (this.options.split === 'words') {
- this.rhs = rhs.split(/\s/);
- } else if (this.options.split === 'lines') {
- this.rhs = rhs.split('\n');
- }
- } else {
- this.rhs = rhs;
- }
-};
-
-CodeifyText.prototype.getCodes = function(side) {
- if (!this.ctxs.hasOwnProperty(side)) {
- var ctx = this._diff_ctx(this[side]);
- this.ctxs[side] = ctx;
- ctx.codes.length = Object.keys(ctx.codes).length;
- }
- return this.ctxs[side].codes;
-}
-
-CodeifyText.prototype.getLines = function(side) {
- return this.ctxs[side].lines;
-}
-
-CodeifyText.prototype._diff_ctx = function(lines) {
- var ctx = {i: 0, codes: {}, lines: lines};
- this._codeify(lines, ctx);
- return ctx;
-}
-
-CodeifyText.prototype._codeify = function(lines, ctx) {
- for (let i = 0; i < lines.length; ++i) {
- let line = lines[i];
- if (this.options.ignorews) {
- line = line.replace(/\s+/g, '');
- }
- if (this.options.ignorecase) {
- line = line.toLowerCase();
- }
- if (this.options.ignoreaccents) {
- line = line.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
- }
- const aCode = this._diff_codes[line];
- if (aCode !== undefined) {
- ctx.codes[i] = aCode;
- } else {
- ++this._max_code;
- this._diff_codes[line] = this._max_code;
- ctx.codes[i] = this._max_code;
- }
- }
-}
-
module.exports = diff;
diff --git a/src/encoder.js b/src/encoder.js
new file mode 100644
index 0000000..a2f7373
--- /dev/null
+++ b/src/encoder.js
@@ -0,0 +1,79 @@
+class Encoder {
+ constructor() {
+ this._maxCode = 0;
+ this._codes = {};
+ }
+
+ encode(text, options) {
+ let exp;
+ let fudge = 0;
+ if (options.split === 'chars') {
+ exp = /./g;
+ fudge = 1;
+ } else if (options.split === 'words') {
+ exp = /\s+/g;
+ } else {
+ exp = /\n/g;
+ }
+ let match;
+ let p0 = -1;
+ const parts = [];
+ while ((match = exp.exec(text)) !== null) {
+ const from = (options.split === 'lines') ? parts.length : p0 + 1;
+ const to = (options.split === 'lines') ? parts.length + 1 : match.index + fudge;
+ const item = {
+ from,
+ to,
+ text: text.substr(p0 + 1, match.index - p0 - 1 + fudge)
+ };
+ parts.push(item);
+ p0 = match.index;
+ }
+ const from = (options.split === 'lines') ? parts.length : p0 + 1;
+ const to = (options.split === 'lines') ? parts.length + 1 : text.length;
+ parts.push({
+ from,
+ to,
+ text: text.substr(p0 + 1)
+ });
+ const hash = this._hash(parts, options)
+ return {
+ codes: hash.codes,
+ parts: hash.parts,
+ length: Object.keys(hash.codes).length
+ };
+ }
+
+ _hash(parts, options) {
+ const codes = {};
+ let i = 0;
+ for (const part of parts) {
+ let text = part.text;
+
+ if (options.ignorews) {
+ text = text.replace(/\s+/g, '');
+ }
+ if (options.ignorecase) {
+ text = text.toLowerCase();
+ }
+ if (options.ignoreaccents) {
+ text = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
+ }
+ const code = this._codes[text];
+ if (code !== undefined) {
+ codes[i] = code;
+ } else {
+ ++this._maxCode;
+ this._codes[text] = this._maxCode;
+ codes[i] = this._maxCode;
+ }
+ i += 1;
+ }
+ return {
+ codes,
+ parts
+ };
+ }
+}
+
+module.exports = Encoder;
diff --git a/src/mergely.js b/src/mergely.js
index 5ef8b6e..9735944 100644
--- a/src/mergely.js
+++ b/src/mergely.js
@@ -12,6 +12,7 @@ const defaultOptions = {
wrap_lines: false,
line_numbers: true,
lcs: true,
+ inline: 'chars',
sidebar: true,
viewport: false,
ignorews: false,
@@ -119,13 +120,15 @@ class Mergely {
const colors = dom.getColors(this.el);
this._options = {
...defaultOptions,//lgpl
+ ...{
+ // default inline based off `lcs`
+ inline: options && options.lcs === false ? 'none' : 'chars'
+ },
...this._initOptions,
- ...options//lgpl-separate-notice
+ ...options,//lgpl-separate-notice
};
this._viewOptions = {
- ...defaultOptions,
- ...this._initOptions,
- ...options,
+ ...this._options,
_colors: colors
};
}
diff --git a/src/vdoc.js b/src/vdoc.js
index 190927c..eb4db60 100644
--- a/src/vdoc.js
+++ b/src/vdoc.js
@@ -124,7 +124,7 @@ class VDoc {
this._setRenderedChange(side, changeId);
}
- addInlineDiff(change, changeId, { getText, ignorews, ignoreaccents, ignorecase }) {
+ addInlineDiff(change, changeId, { getText, ignorews, ignoreaccents, ignorecase, split = 'chars' }) {
if (this.options._debug) {
trace('vdoc#addInlineDiff', changeId, change);
}
@@ -152,7 +152,7 @@ class VDoc {
ignoreaccents,
ignorews,
ignorecase,
- split: 'chars'
+ split
});
for (const change of results.changes()) {
const {
diff --git a/test/diff.spec.js b/test/diff.spec.js
new file mode 100644
index 0000000..41b3a1d
--- /dev/null
+++ b/test/diff.spec.js
@@ -0,0 +1,16 @@
+const diff = require('../src/diff');
+
+describe('diff', () => {
+ it('should insert one line when lhs is empty and rhs has no line ending', () => {
+ const _diff = new diff('', 'hello', { split: 'lines' });
+ const changes = _diff.changes();
+ console.log(changes);
+ // with lhs_start at 1, the insert is at the end
+ expect(changes).to.deep.equal([{
+ lhs_start: 1,
+ rhs_start: 1,
+ lhs_deleted_count: 0,
+ rhs_inserted_count: 0
+ }]);
+ });
+});
diff --git a/test/markup.spec.js b/test/markup.spec.js
index 6e2be09..57fc6ed 100644
--- a/test/markup.spec.js
+++ b/test/markup.spec.js
@@ -189,7 +189,6 @@ describe('markup', () => {
expect(editor.querySelectorAll('.mergely.rhs')).to.have.length(0);
}
},
-
{
name: 'Changed lines (lhs)',
lhs: 'the quick red fox\njumped over the hairy dog',
@@ -233,6 +232,46 @@ describe('markup', () => {
expect(rhs_spans[1].innerText).to.equal('h');
expect(rhs_spans[2].innerText).to.equal('ir');
}
+ },
+ {
+ name: 'Changed lines with inline words (lhs)',
+ lhs: 'the quick red fox\njumped over the hairy dog',
+ rhs: 'the quick brown fox\njumped over the lazy dog',
+ options: { inline: 'words' },
+ check: (editor) => {
+ expect(editor.querySelectorAll(LHS_CHANGE_START + '.cid-0')).to.have.length(1);
+ expect(editor.querySelectorAll(LHS_CHANGE_END + '.cid-0')).to.have.length(1);
+ expect(editor.querySelectorAll(RHS_CHANGE_START + '.cid-0')).to.have.length(1);
+ expect(editor.querySelectorAll(RHS_CHANGE_END + '.cid-0')).to.have.length(1);
+ const lhs_spans = editor.querySelectorAll(LHS_INLINE_TEXT + '.cid-0');
+ expect(lhs_spans).to.have.length(2);
+ expect(lhs_spans[0].innerText).to.equal('red');
+ expect(lhs_spans[1].innerText).to.equal('hairy');
+ const rhs_spans = editor.querySelectorAll(RHS_INLINE_TEXT + '.cid-0');
+ expect(rhs_spans).to.have.length(2);
+ expect(rhs_spans[0].innerText).to.equal('brown');
+ expect(rhs_spans[1].innerText).to.equal('lazy');
+ }
+ },
+ {
+ name: 'Changed lines (rhs)',
+ lhs: 'the quick brown fox\njumped over the lazy dog',
+ rhs: 'the quick red fox\njumped over the hairy dog',
+ options: { inline: 'words' },
+ check: (editor) => {
+ expect(editor.querySelectorAll(LHS_CHANGE_START + '.cid-0')).to.have.length(1);
+ expect(editor.querySelectorAll(LHS_CHANGE_END + '.cid-0')).to.have.length(1);
+ expect(editor.querySelectorAll(RHS_CHANGE_START + '.cid-0')).to.have.length(1);
+ expect(editor.querySelectorAll(RHS_CHANGE_END + '.cid-0')).to.have.length(1);
+ const lhs_spans = editor.querySelectorAll(LHS_INLINE_TEXT + '.cid-0');
+ expect(lhs_spans).to.have.length(2);
+ expect(lhs_spans[0].innerText).to.equal('brown');
+ expect(lhs_spans[1].innerText).to.equal('lazy');
+ const rhs_spans = editor.querySelectorAll(RHS_INLINE_TEXT + '.cid-0');
+ expect(rhs_spans).to.have.length(2);
+ expect(rhs_spans[0].innerText).to.equal('red');
+ expect(rhs_spans[1].innerText).to.equal('hairy');
+ }
}
];
@@ -246,8 +285,9 @@ describe('markup', () => {
license: 'lgpl-separate-notice',
change_timeout: 0,
_debug: debug,
- lhs: (setValue) => setValue(opt.lhs),
- rhs: (setValue) => setValue(opt.rhs)
+ lhs: opt.lhs,
+ rhs: opt.rhs,
+ ...opt.options
});
const test = () => {
try {
diff --git a/test/mergely.spec.js b/test/mergely.spec.js
index 47f8675..9e9598e 100644
--- a/test/mergely.spec.js
+++ b/test/mergely.spec.js
@@ -8,6 +8,7 @@ const defaultOptions = {
rhs_margin: 'right',
wrap_lines: false,
line_numbers: true,
+ inline: 'chars',
lcs: true,
sidebar: true,
viewport: false,