forked from lxm_front/Mergely
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3812dd6026 | ||
|
|
ead86867c4 | ||
|
|
557aa16e23 | ||
|
|
2d0d7e9ce7 | ||
|
|
d7a7ddcd55 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mergely",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.4",
|
||||
"description": "A javascript UI for diff/merge",
|
||||
"directories": {
|
||||
"doc": "doc",
|
||||
|
||||
253
src/mergely.js
253
src/mergely.js
@@ -54,8 +54,8 @@ Mgly.sizeOf = function(obj) {
|
||||
};
|
||||
|
||||
Mgly.LCS = function(x, y) {
|
||||
this.x = x.replace(/[ ]{1}/g, '\n');
|
||||
this.y = y.replace(/[ ]{1}/g, '\n');
|
||||
this.x = (x && x.replace(/[ ]{1}/g, '\n')) || '';
|
||||
this.y = (y && y.replace(/[ ]{1}/g, '\n')) || '';
|
||||
};
|
||||
|
||||
jQuery.extend(Mgly.LCS.prototype, {
|
||||
@@ -686,21 +686,36 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
// create the textarea and canvas elements
|
||||
var height = '10px';
|
||||
var width = '10px';
|
||||
this.element.append(jQuery('<div id="mergely-splash">'));
|
||||
this.element.append(jQuery('<div class="mergely-margin" style="height: ' + height + '"><canvas id="' + this.id + '-lhs-margin" width="8px" height="' + height + '"></canvas></div>'));
|
||||
this.element.append(jQuery('<div style="position:relative;width:' + width + '; height:' + height + '" id="' + this.id + '-editor-lhs" class="mergely-column"><textarea style="" id="' + this.id + '-lhs"></textarea></div>'));
|
||||
this.element.append(jQuery('<div class="mergely-canvas" style="height: ' + height + '"><canvas id="' + this.id + '-lhs-' + this.id + '-rhs-canvas" style="width:28px" width="28px" height="' + height + '"></canvas></div>'));
|
||||
var rmargin = jQuery('<div class="mergely-margin" style="height: ' + height + '"><canvas id="' + this.id + '-rhs-margin" width="8px" height="' + height + '"></canvas></div>');
|
||||
|
||||
var splash = jQuery('<div id="mergely-splash">');
|
||||
var canvasLhs = jQuery(`<div class="mergely-margin" style="height: '${height}'"><canvas id="lhs-margin" width="8px" height="'${height}'"></canvas></div>`);
|
||||
canvasLhs.find('#lhs-margin').attr('id', `${this.id}-lhs-margin`);
|
||||
var editorLhs = jQuery(`<div style="position:relative;width:'${width}'; height:'${height}'" id="editor-lhs" class="mergely-column"><textarea id="text-lhs"></textarea></div>`);
|
||||
editorLhs.eq(0).attr('id', `${this.id}-editor-lhs`);
|
||||
editorLhs.find('#text-lhs').attr('id', `${this.id}-lhs`);
|
||||
var canvasMid = jQuery(`<div class="mergely-canvas" style="height: '${height}'"><canvas id="lhs-rhs-canvas" style="width:28px" width="28px" height="'${height}'"></canvas></div>`);
|
||||
canvasMid.find('#mergely-canvas').attr('id', `${this.id}-mergely-canvas`);
|
||||
canvasMid.find('#lhs-rhs-canvas').attr('id', `${this.id}-lhs-${this.id}-rhs-canvas`);
|
||||
|
||||
this.element.append(splash);
|
||||
this.element.append(canvasLhs);
|
||||
this.element.append(editorLhs);
|
||||
this.element.append(canvasMid);
|
||||
var canvasRhs = jQuery(`<div class="mergely-margin" style="height: '${height}'"><canvas id="rhs-margin" width="8px" height="'${height}'"></canvas></div>`);
|
||||
canvasRhs.find('#rhs-margin').attr('id', `${this.id}-rhs-margin`);
|
||||
if (this.settings.rhs_margin == 'left') {
|
||||
this.element.append(canvasRhs);
|
||||
}
|
||||
var editorRhs = jQuery(`<div style="width:'${width}'; height:'${height}'" id="editor-rhs" class="mergely-column"><textarea id="text-rhs"></textarea></div>`);
|
||||
editorRhs.eq(0).attr('id', `${this.id}-editor-rhs`);
|
||||
editorRhs.find('#text-rhs').attr('id', `${this.id}-rhs`);
|
||||
this.element.append(editorRhs);
|
||||
if (this.settings.rhs_margin != 'left') {
|
||||
this.element.append(canvasRhs);
|
||||
}
|
||||
if (!this.settings.sidebar) {
|
||||
this.element.find('.mergely-margin').css({display: 'none'});
|
||||
}
|
||||
if (this.settings.rhs_margin == 'left') {
|
||||
this.element.append(rmargin);
|
||||
}
|
||||
this.element.append(jQuery('<div style="width:' + width + '; height:' + height + '" id="' + this.id + '-editor-rhs" class="mergely-column"><textarea style="" id="' + this.id + '-rhs"></textarea></div>'));
|
||||
if (this.settings.rhs_margin != 'left') {
|
||||
this.element.append(rmargin);
|
||||
}
|
||||
if (['lgpl-separate-notice', 'gpl-separate-notice', 'mpl-separate-notice', 'commercial'].indexOf(this.settings.license) < 0) {
|
||||
const _lic = {
|
||||
'lgpl': 'GNU LGPL v3.0',
|
||||
@@ -1046,21 +1061,13 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
}
|
||||
return changes;
|
||||
},
|
||||
_get_viewport: function(editor_name1, editor_name2) {
|
||||
var lhsvp = this.editor[editor_name1].getViewport();
|
||||
var rhsvp = this.editor[editor_name2].getViewport();
|
||||
return {from: Math.min(lhsvp.from, rhsvp.from), to: Math.max(lhsvp.to, rhsvp.to)};
|
||||
_get_viewport_side: function(editor_name) {
|
||||
return this.editor[editor_name].getViewport();
|
||||
},
|
||||
_is_change_in_view: function(vp, change) {
|
||||
if (!this.settings.viewport) return true;
|
||||
if ((change['lhs-line-from'] < vp.from && change['lhs-line-to'] < vp.to) ||
|
||||
(change['lhs-line-from'] > vp.from && change['lhs-line-to'] > vp.to) ||
|
||||
(change['rhs-line-from'] < vp.from && change['rhs-line-to'] < vp.to) ||
|
||||
(change['rhs-line-from'] > vp.from && change['rhs-line-to'] > vp.to)) {
|
||||
// if the change is outside the viewport, skip
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
_is_change_in_view: function(side, vp, change) {
|
||||
return (change[`${side}-line-from`] >= vp.from && change[`${side}-line-from`] <= vp.to) ||
|
||||
(change[`${side}-line-to`] >= vp.from && change[`${side}-line-to`] <= vp.to) ||
|
||||
(vp.from >= change[`${side}-line-from`] && vp.to <= change[`${side}-line-to`]);
|
||||
},
|
||||
_set_top_offset: function (editor_name1) {
|
||||
// save the current scroll position of the editor
|
||||
@@ -1105,12 +1112,15 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
}
|
||||
var lhschc = this.editor[editor_name1].charCoords({line: 0});
|
||||
var rhschc = this.editor[editor_name2].charCoords({line: 0});
|
||||
var vp = this._get_viewport(editor_name1, editor_name2);
|
||||
var lhsvp = this._get_viewport_side(editor_name1);
|
||||
var rhsvp = this._get_viewport_side(editor_name2);
|
||||
|
||||
for (var i = 0; i < changes.length; ++i) {
|
||||
var change = changes[i];
|
||||
|
||||
if (!this.settings.sidebar && !this._is_change_in_view(vp, change)) {
|
||||
if (this.settings.viewport &&
|
||||
!this._is_change_in_view(lhsvp, 'lhs', change) &&
|
||||
!this._is_change_in_view(lhsvp, 'rhs', change)) {
|
||||
// if the change is outside the viewport, skip
|
||||
delete change['lhs-y-start'];
|
||||
delete change['lhs-y-end'];
|
||||
@@ -1205,11 +1215,18 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
var led = this.editor[editor_name1];
|
||||
var red = this.editor[editor_name2];
|
||||
var current_diff = this._current_diff;
|
||||
var lhsvp = this._get_viewport_side(editor_name1);
|
||||
var rhsvp = this._get_viewport_side(editor_name2);
|
||||
|
||||
var timer = new Mgly.Timer();
|
||||
led.operation(function() {
|
||||
for (var i = 0; i < changes.length; ++i) {
|
||||
var change = changes[i];
|
||||
if (!this._is_change_in_view('lhs', lhsvp, change)) {
|
||||
// if the change is outside the viewport, skip
|
||||
continue;
|
||||
}
|
||||
|
||||
var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
|
||||
var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
|
||||
var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
|
||||
@@ -1252,24 +1269,22 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
led.setGutterMarker(llf, 'merge', rhs_button.get(0));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var vp = this._get_viewport(editor_name1, editor_name2);
|
||||
}.bind(this));
|
||||
|
||||
this.trace('change', 'markup lhs-editor time', timer.stop());
|
||||
red.operation(function() {
|
||||
for (var i = 0; i < changes.length; ++i) {
|
||||
var change = changes[i];
|
||||
if (!this._is_change_in_view('rhs', rhsvp, change)) {
|
||||
// if the change is outside the viewport, skip
|
||||
continue;
|
||||
}
|
||||
|
||||
var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
|
||||
var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
|
||||
var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
|
||||
var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
|
||||
|
||||
if (!self._is_change_in_view(vp, change)) {
|
||||
// if the change is outside the viewport, skip
|
||||
continue;
|
||||
}
|
||||
|
||||
var clazz = ['mergely', 'rhs', change['op'], 'cid-' + i];
|
||||
red.addLineClass(rlf, 'background', 'start');
|
||||
red.addLineClass(rlt, 'background', 'end');
|
||||
@@ -1307,7 +1322,7 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
red.setGutterMarker(rlf, 'merge', lhs_button.get(0));
|
||||
}
|
||||
}
|
||||
});
|
||||
}.bind(this));
|
||||
this.trace('change', 'markup rhs-editor time', timer.stop());
|
||||
|
||||
// mark text deleted, LCS changes
|
||||
@@ -1319,17 +1334,17 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
|
||||
var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
|
||||
|
||||
if (!this._is_change_in_view(vp, change)) {
|
||||
// if the change is outside the viewport, skip
|
||||
continue;
|
||||
}
|
||||
if (change['op'] == 'd') {
|
||||
// apply delete to cross-out (left-hand side only)
|
||||
var from = llf;
|
||||
var to = llt;
|
||||
var to_ln = led.lineInfo(to);
|
||||
if (to_ln) {
|
||||
marktext.push([led, {line:from, ch:0}, {line:to, ch:to_ln.text.length}, {className: 'mergely ch d lhs'}]);
|
||||
|
||||
if (this._is_change_in_view('lhs', lhsvp, change)) {
|
||||
// the change is within the viewport
|
||||
var to_ln = led.lineInfo(to);
|
||||
if (to_ln) {
|
||||
marktext.push([led, {line:from, ch:0}, {line:to, ch:to_ln.text.length}, {className: 'mergely ch d lhs'}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (change['op'] == 'c') {
|
||||
@@ -1338,13 +1353,13 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
((j >= 0) && (j <= llt)) || ((k >= 0) && (k <= rlt));
|
||||
++j, ++k) {
|
||||
var lhs_line, rhs_line;
|
||||
if (k + p > rlt) {
|
||||
if (k + p > rlt && this._is_change_in_view('lhs', lhsvp, change)) {
|
||||
// lhs continues past rhs, mark lhs as deleted
|
||||
lhs_line = led.getLine( j );
|
||||
marktext.push([led, {line:j, ch:0}, {line:j, ch:lhs_line.length}, {className: 'mergely ch d lhs'}]);
|
||||
continue;
|
||||
}
|
||||
if (j + p > llt) {
|
||||
if (j + p > llt && this._is_change_in_view('rhs', rhsvp, change)) {
|
||||
// rhs continues past lhs, mark rhs as added
|
||||
rhs_line = red.getLine( k );
|
||||
marktext.push([red, {line:k, ch:0}, {line:k, ch:rhs_line.length}, {className: 'mergely ch a rhs'}]);
|
||||
@@ -1355,10 +1370,14 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
var lcs = new Mgly.LCS(lhs_line, rhs_line);
|
||||
lcs.diff(
|
||||
function added (from, to) {
|
||||
marktext.push([red, {line:k, ch:from}, {line:k, ch:to}, {className: 'mergely ch a rhs'}]);
|
||||
if (self._is_change_in_view('rhs', rhsvp, change)) {
|
||||
marktext.push([red, {line:k, ch:from}, {line:k, ch:to}, {className: 'mergely ch a rhs'}]);
|
||||
}
|
||||
},
|
||||
function removed (from, to) {
|
||||
marktext.push([led, {line:j, ch:from}, {line:j, ch:to}, {className: 'mergely ch d lhs'}]);
|
||||
if (self._is_change_in_view('lhs', lhsvp, change)) {
|
||||
marktext.push([led, {line:j, ch:from}, {line:j, ch:to}, {className: 'mergely ch d lhs'}]);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1413,29 +1432,37 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
return false;
|
||||
});
|
||||
|
||||
// gutter markup
|
||||
var lhsLineNumbers = jQuery('#mergely-lhs ~ .CodeMirror').find('.CodeMirror-linenumber');
|
||||
var rhsLineNumbers = jQuery('#mergely-rhs ~ .CodeMirror').find('.CodeMirror-linenumber');
|
||||
// gutter markup that highlights all gutter line numbers for the current change.
|
||||
// cm doesn't give us the ability to style the line numbers directly.
|
||||
var lhsLineNumbers = jQuery('#mergely-lhs ~ .CodeMirror .CodeMirror-code .CodeMirror-linenumber.CodeMirror-gutter-elt');
|
||||
var rhsLineNumbers = jQuery('#mergely-rhs ~ .CodeMirror .CodeMirror-code .CodeMirror-linenumber.CodeMirror-gutter-elt');
|
||||
var jf, jt, i, j;
|
||||
rhsLineNumbers.removeClass('mergely current');
|
||||
lhsLineNumbers.removeClass('mergely current');
|
||||
for (var i = 0; i < changes.length; ++i) {
|
||||
if (current_diff == i && change.op !== 'd') {
|
||||
var change = changes[i];
|
||||
var j, jf = change['rhs-line-from'], jt = change['rhs-line-to'] + 1;
|
||||
for (j = jf; j < jt; j++) {
|
||||
var n = (j + 1).toString();
|
||||
rhsLineNumbers
|
||||
.filter(function(i, node) { return jQuery(node).text() === n; })
|
||||
.addClass('mergely current');
|
||||
var lhsvpFrom = parseInt(lhsLineNumbers.eq(0).text(), 10) - 1;
|
||||
var lhsvpTo = parseInt(lhsLineNumbers.eq(lhsLineNumbers.length - 1).text(), 10);
|
||||
var rhsvpFrom = parseInt(rhsLineNumbers.eq(0).text(), 10) - 1;
|
||||
var rhsvpTo = parseInt(rhsLineNumbers.eq(rhsLineNumbers.length - 1).text(), 10);
|
||||
|
||||
for (i = 0; i < changes.length; ++i) {
|
||||
change = changes[i];
|
||||
|
||||
if (current_diff == i && change.op !== 'a') {
|
||||
jf = change['lhs-line-from']
|
||||
jt = change['lhs-line-to'] + 1;
|
||||
for (j = jf; j < jt; ++j) {
|
||||
if (j >= lhsvpFrom && j <= lhsvpTo) {
|
||||
lhsLineNumbers.eq(j - lhsvpFrom).addClass('mergely current');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (current_diff == i && change.op !== 'a') {
|
||||
var change = changes[i];
|
||||
jf = change['lhs-line-from'], jt = change['lhs-line-to'] + 1;
|
||||
for (j = jf; j < jt; j++) {
|
||||
var n = (j + 1).toString();
|
||||
lhsLineNumbers.filter(function(i, node) { return jQuery(node).text() === n; })
|
||||
.addClass('mergely current');
|
||||
if (current_diff == i && change.op !== 'd') {
|
||||
jf = change['rhs-line-from']
|
||||
jt = change['rhs-line-to'] + 1;
|
||||
for (j = jf; j < jt; ++j) {
|
||||
if (j >= rhsvpFrom && j <= rhsvpTo) {
|
||||
rhsLineNumbers.eq(j - rhsvpFrom).addClass('mergely current');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1446,52 +1473,40 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
if (!change) return;
|
||||
var led = this.editor[this.id+'-lhs'];
|
||||
var red = this.editor[this.id+'-rhs'];
|
||||
var ed = {lhs:led, rhs:red};
|
||||
var i, from, to;
|
||||
var ed = { lhs:led, rhs:red };
|
||||
var from = change[side + '-line-from'];
|
||||
var to = change[side + '-line-to'];
|
||||
var ofrom = change[oside + '-line-from'];
|
||||
var oto = change[oside + '-line-to'];
|
||||
var doc = ed[side].getDoc();
|
||||
var odoc = ed[oside].getDoc();
|
||||
var fromlen = from >= 0 ? doc.getLine(from).length + 1 : 0;
|
||||
var tolen = to >= 0 ? doc.getLine(to).length + 1 : 0;
|
||||
var otolen = oto >= 0 ? odoc.getLine(oto).length + 1 : 0;
|
||||
var ofromlen = ofrom >= 0 ? odoc.getLine(ofrom).length + 1 : 0;
|
||||
var text;
|
||||
|
||||
var text = ed[side].getRange(
|
||||
CodeMirror.Pos(change[side + '-line-from'], 0),
|
||||
CodeMirror.Pos(change[side + '-line-to'] + 1, 0));
|
||||
|
||||
if (change['op'] == 'c') {
|
||||
ed[oside].replaceRange(text,
|
||||
CodeMirror.Pos(change[oside + '-line-from'], 0),
|
||||
CodeMirror.Pos(change[oside + '-line-to'] + 1, 0));
|
||||
if (change['op'] === 'c') {
|
||||
text = doc.getRange(CodeMirror.Pos(from, 0), CodeMirror.Pos(to, tolen));
|
||||
odoc.replaceRange(text, CodeMirror.Pos(ofrom, 0), CodeMirror.Pos(oto, otolen));
|
||||
} else if ((oside === 'lhs' && change['op'] === 'd') || (oside === 'rhs' && change['op'] === 'a')) {
|
||||
if (from > 0) {
|
||||
text = doc.getRange(CodeMirror.Pos(from - 1, fromlen), CodeMirror.Pos(to, tolen));
|
||||
} else {
|
||||
text = doc.getRange(CodeMirror.Pos(0, 0), CodeMirror.Pos(to + 1, 0));
|
||||
}
|
||||
odoc.replaceRange(text, CodeMirror.Pos(ofrom - 1, 0), CodeMirror.Pos(oto + 1, 0));
|
||||
} else if ((oside === 'rhs' && change['op'] === 'd') || (oside === 'lhs' && change['op'] === 'a')) {
|
||||
if (from > 0) {
|
||||
text = doc.getRange(CodeMirror.Pos(from - 1, fromlen), CodeMirror.Pos(to, tolen));
|
||||
} else {
|
||||
text = doc.getRange(CodeMirror.Pos(0, 0), CodeMirror.Pos(to + 1, 0));
|
||||
}
|
||||
if (ofrom < 0) {
|
||||
ofrom = 0;
|
||||
}
|
||||
odoc.replaceRange(text, CodeMirror.Pos(ofrom, ofromlen));
|
||||
}
|
||||
else if (side == 'rhs') {
|
||||
if (change['op'] == 'a') {
|
||||
ed[oside].replaceRange(text,
|
||||
CodeMirror.Pos(change[oside + '-line-from'] + 1, 0),
|
||||
CodeMirror.Pos(change[oside + '-line-to'] + 1, 0));
|
||||
}
|
||||
else {// 'd'
|
||||
from = parseInt(change[oside + '-line-from'], 10);
|
||||
to = parseInt(change[oside + '-line-to'], 10);
|
||||
for (i = to; i >= from; --i) {
|
||||
ed[oside].setCursor({line: i, ch: -1});
|
||||
ed[oside].execCommand('deleteLine');
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (side == 'lhs') {
|
||||
if (change['op'] == 'a') {
|
||||
from = parseInt(change[oside + '-line-from'], 10);
|
||||
to = parseInt(change[oside + '-line-to'], 10);
|
||||
for (i = to; i >= from; --i) {
|
||||
//ed[oside].removeLine(i);
|
||||
ed[oside].setCursor({line: i, ch: -1});
|
||||
ed[oside].execCommand('deleteLine');
|
||||
}
|
||||
}
|
||||
else {// 'd'
|
||||
ed[oside].replaceRange( text,
|
||||
CodeMirror.Pos(change[oside + '-line-from'] + 1, 0));
|
||||
}
|
||||
}
|
||||
//reset
|
||||
ed['lhs'].setValue(ed['lhs'].getValue());
|
||||
ed['rhs'].setValue(ed['rhs'].getValue());
|
||||
|
||||
this._scroll_to_change(change);
|
||||
},
|
||||
_draw_info: function(editor_name1, editor_name2) {
|
||||
@@ -1550,7 +1565,9 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
ctx_rhs.fillRect(0, 0, 6.5, ex.visible_page_height);
|
||||
ctx_rhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
|
||||
|
||||
var vp = this._get_viewport(editor_name1, editor_name2);
|
||||
var lhsvp = this._get_viewport_side(editor_name1);
|
||||
var rhsvp = this._get_viewport_side(editor_name2);
|
||||
|
||||
for (var i = 0; i < changes.length; ++i) {
|
||||
var change = changes[i];
|
||||
var fill = this.settings.fgcolor[change['op']];
|
||||
@@ -1580,7 +1597,9 @@ jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
|
||||
ctx_rhs.fillRect(1.5, rhs_y_start, 4.5, Math.max(rhs_y_end - rhs_y_start, 5));
|
||||
ctx_rhs.strokeRect(1.5, rhs_y_start, 4.5, Math.max(rhs_y_end - rhs_y_start, 5));
|
||||
|
||||
if (!this._is_change_in_view(vp, change)) {
|
||||
if (!this._is_change_in_view('lhs', lhsvp, change) &&
|
||||
!this._is_change_in_view('rhs', rhsvp, change)) {
|
||||
// if the change is outside the viewport, skip
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1711,6 +1730,7 @@ jQuery.pluginMaker = function(plugin) {
|
||||
after = args.slice(1);
|
||||
var rc;
|
||||
this.each(function() {
|
||||
var tthis = this;
|
||||
// see if we have an instance
|
||||
var instance = jQuery.data(this, plugin.prototype.name);
|
||||
if (instance) {
|
||||
@@ -1724,6 +1744,9 @@ jQuery.pluginMaker = function(plugin) {
|
||||
} else {
|
||||
// create the plugin
|
||||
var _plugin = new plugin(this, options);
|
||||
jQuery.fn[`${plugin.prototype.name}Unregister`] = function() {
|
||||
jQuery.data(tthis, plugin.prototype.name, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (rc != undefined) return rc;
|
||||
|
||||
3412
tests/data/macbeth.js
Normal file
3412
tests/data/macbeth.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
const jQuery = require('jquery');
|
||||
const CodeMirror = require('CodeMirror');
|
||||
const mergely = require('../src/mergely');
|
||||
const macbeth = require('./data/macbeth').join('\n');
|
||||
const $ = jQuery;
|
||||
|
||||
(function($) {
|
||||
@@ -18,6 +19,12 @@ describe('mergely', function () {
|
||||
return editor;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
$('#mergely').mergely('unbind');
|
||||
$('#mergely').mergelyUnregister();
|
||||
$('#mergely').remove();
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('initializes without arguments', function (done) {
|
||||
$(document).ready(() => {
|
||||
@@ -25,6 +32,7 @@ describe('mergely', function () {
|
||||
height: 100,
|
||||
license: 'lgpl-separate-notice'
|
||||
});
|
||||
editor.id = 1;
|
||||
expect(editor).to.exist;
|
||||
expect(editor.mergely).to.exist;
|
||||
const children = editor.children();
|
||||
@@ -35,23 +43,20 @@ describe('mergely', function () {
|
||||
expect($(children[3]).attr('class')).to.equal('mergely-canvas');
|
||||
expect($(children[4]).attr('class')).to.equal('mergely-column');
|
||||
expect($(children[5]).attr('class')).to.equal('mergely-margin');
|
||||
expect($('body').hasScrollBar()).to.equal(false);
|
||||
//expect($('body').hasScrollBar()).to.equal(false);
|
||||
expect($('.mergely-editor-lhs .CodeMirror-code').text()).to.equal('');
|
||||
expect($('.mergely-editor-rhs .CodeMirror-code').text()).to.equal('');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it.only('initializes with static arguments for lhs/rhs text', function (done) {
|
||||
it('initializes with static arguments for lhs/rhs text', function (done) {
|
||||
$(document).ready(() => {
|
||||
const editor = init({
|
||||
height: 100,
|
||||
license: 'lgpl-separate-notice',
|
||||
lhs: (setValue) => {
|
||||
console.log('here');
|
||||
setValue('left-hand side text')
|
||||
},
|
||||
rhs: (setValue) => setValue('right-hand side text'),
|
||||
lhs: (setValue) => setValue('left-hand side text'),
|
||||
rhs: (setValue) => setValue('right-hand side text')
|
||||
});
|
||||
expect(editor).to.exist;
|
||||
expect(editor.mergely).to.exist;
|
||||
@@ -63,12 +68,492 @@ describe('mergely', function () {
|
||||
expect($(children[3]).attr('class')).to.equal('mergely-canvas');
|
||||
expect($(children[4]).attr('class')).to.equal('mergely-column');
|
||||
expect($(children[5]).attr('class')).to.equal('mergely-margin');
|
||||
expect($('body').hasScrollBar()).to.equal(false);
|
||||
expect($('#mergely-editor-lhs .CodeMirror-code .CodeMirror-line').text()).to.equal('left-hand side text');
|
||||
expect($('#mergely-editor-rhs .CodeMirror-code .CodeMirror-line').text()).to.equal('right-hand side text');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes with no sidebar', function (done) {
|
||||
$(document).ready(() => {
|
||||
const editor = init({
|
||||
height: 100,
|
||||
license: 'lgpl-separate-notice',
|
||||
sidebar: false
|
||||
});
|
||||
expect(editor).to.exist;
|
||||
expect(editor.mergely).to.exist;
|
||||
const children = editor.children();
|
||||
expect(children.length).to.equal(6);
|
||||
expect($(children[0]).attr('id')).to.equal('mergely-splash');
|
||||
expect($(children[1]).attr('class')).to.equal('mergely-margin');
|
||||
expect($(children[1]).css('display')).to.equal('none');
|
||||
expect($(children[2]).attr('class')).to.equal('mergely-column');
|
||||
expect($(children[3]).attr('class')).to.equal('mergely-canvas');
|
||||
expect($(children[4]).attr('class')).to.equal('mergely-column');
|
||||
expect($(children[5]).attr('class')).to.equal('mergely-margin');
|
||||
expect($(children[5]).css('display')).to.equal('none');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('gets lhs and rhs text', function (done) {
|
||||
$(document).ready(() => {
|
||||
const editor = init({
|
||||
height: 100,
|
||||
license: 'lgpl-separate-notice',
|
||||
lhs: (setValue) => setValue('left-hand side text'),
|
||||
rhs: (setValue) => setValue('right-hand side text')
|
||||
});
|
||||
expect($('#mergely').mergely('get', 'lhs')).to.equal('left-hand side text');
|
||||
expect($('#mergely').mergely('get', 'rhs')).to.equal('right-hand side text');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeCurrentChange', () => {
|
||||
const opts = [{ // add 0-7 merge lhs
|
||||
lhs: '',
|
||||
rhs: '\na',
|
||||
dir: 'lhs',
|
||||
name: 'add is line 2 and insert into end of empty lhs'
|
||||
},
|
||||
{
|
||||
lhs: '',
|
||||
rhs: 'a\n',
|
||||
dir: 'lhs',
|
||||
name: 'add is line 1 and insert into end of empty lhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a',
|
||||
rhs: 'a\nb',
|
||||
dir: 'lhs',
|
||||
name: 'add is line 2 and insert into end to existing lhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a',
|
||||
rhs: 'a\nbcd',
|
||||
dir: 'lhs',
|
||||
name: 'add is line 2 of length 3 and insert into end of existing lhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a',
|
||||
rhs: 'a\nb\nc\nd',
|
||||
dir: 'lhs',
|
||||
name: 'add is lines 2-4 and insert into end of existing lhs'
|
||||
},
|
||||
{
|
||||
lhs: '',
|
||||
rhs: '\nabcd',
|
||||
dir: 'lhs',
|
||||
name: 'add is line 2 of length 4 and lhs is empty'
|
||||
},
|
||||
{
|
||||
lhs: 'abcd',
|
||||
rhs: '\nabcd',
|
||||
dir: 'lhs',
|
||||
name: 'add is line 1 cr and insert into front of lhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nd',
|
||||
rhs: 'a\nb\nc\nd',
|
||||
dir: 'lhs',
|
||||
name: 'add is lines 2-3 and insert into middle of lhs'
|
||||
},
|
||||
// add 8-15 merge rhs
|
||||
{
|
||||
lhs: '',
|
||||
rhs: '\na',
|
||||
dir: 'rhs',
|
||||
name: 'add is line 2 and empty lhs will replace non-empty rhs'
|
||||
},
|
||||
{
|
||||
lhs: '',
|
||||
rhs: 'a\n',
|
||||
dir: 'rhs',
|
||||
name: 'add is line 1 and empty lhs will replace line 1 of rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a',
|
||||
rhs: 'a\nb',
|
||||
dir: 'rhs',
|
||||
name: 'add is line 2 and empty end lhs will replace line 2 of rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a',
|
||||
rhs: 'a\nbcd',
|
||||
dir: 'rhs',
|
||||
name: 'add is line 2 of length 3 and empty end lhs will replace line 2 of rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a',
|
||||
rhs: 'a\nb\nc\nd',
|
||||
dir: 'rhs',
|
||||
name: 'add is lines 2-4 and empty end lhs will replace last 3 lines of rhs'
|
||||
},
|
||||
{
|
||||
lhs: '',
|
||||
rhs: '\nabcd',
|
||||
dir: 'rhs',
|
||||
name: 'add is line 2 of length 4 and empty lhs will replace last line of rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'abcd',
|
||||
rhs: '\nabcd',
|
||||
dir: 'rhs',
|
||||
name: 'add is line 1 cr and empty front will replace first line of rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nd',
|
||||
rhs: 'a\nb\nc\nd',
|
||||
dir: 'rhs',
|
||||
name: 'add is lines 2-3 and empty middle will replace middle two lines of rhs'
|
||||
},
|
||||
// change 16-21 merge lhs
|
||||
{
|
||||
lhs: 'a',
|
||||
rhs: '',
|
||||
dir: 'lhs',
|
||||
name: 'change is line 1 and will replace empty rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nb\nc',
|
||||
rhs: '',
|
||||
dir: 'lhs',
|
||||
name: 'change is lines 1-3 and will replace empty rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a',
|
||||
rhs: 'b',
|
||||
dir: 'lhs',
|
||||
name: 'change is line 1 and will replace opposite line 1'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nb',
|
||||
rhs: 'c\nd',
|
||||
dir: 'lhs',
|
||||
name: 'change is lines 2 and will replace opposite line 2'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nbc\nef',
|
||||
rhs: 'a\nbcd\nef',
|
||||
dir: 'lhs',
|
||||
name: 'change is middle line 2 and will replace opposite middle line 2'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nb\n\n',
|
||||
rhs: 'a1\nb1\nc1\nd',
|
||||
dir: 'lhs',
|
||||
name: 'change is all lines 1-3 and will replace opposite all lines 1-4'
|
||||
},
|
||||
// change 22-27 merge rhs
|
||||
{
|
||||
lhs: 'a',
|
||||
rhs: '',
|
||||
dir: 'rhs',
|
||||
name: 'change is line 1 and will empty the rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nb\nc',
|
||||
rhs: '',
|
||||
dir: 'rhs',
|
||||
name: 'change is lines 1-3 and will replace empty rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a',
|
||||
rhs: 'b',
|
||||
dir: 'rhs',
|
||||
name: 'change is line 1 and will replace opposite rhs line 1'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nb',
|
||||
rhs: 'c\nd',
|
||||
dir: 'rhs',
|
||||
name: 'change is lines 2 and will replace opposite rhs line 2'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nbc\nef',
|
||||
rhs: 'a\nbcd\nef',
|
||||
dir: 'rhs',
|
||||
name: 'change is middle line 2 and will replace opposite rhs middle line 2'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nb\n\n',
|
||||
rhs: 'a1\nb1\nc1\nd',
|
||||
dir: 'rhs',
|
||||
name: 'change is all lines 1-3 and will replace opposite rhs all lines 1-4'
|
||||
},
|
||||
// delete 28-35 merge lhs
|
||||
{
|
||||
lhs: '\na',
|
||||
rhs: '',
|
||||
dir: 'lhs',
|
||||
name: 'delete is line 2 and will merge empty rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a\n',
|
||||
rhs: '',
|
||||
dir: 'lhs',
|
||||
name: 'delete is line 1 and will merge empty rhs leaving cr on line 2'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nb',
|
||||
rhs: 'a',
|
||||
dir: 'lhs',
|
||||
name: 'delete is line 2 and will merge into end to existing rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nbcd',
|
||||
rhs: 'a',
|
||||
dir: 'lhs',
|
||||
name: 'delete is line 2 of length 3 and insert into end of existing rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nb\nc\nd',
|
||||
rhs: 'a',
|
||||
dir: 'lhs',
|
||||
name: 'delete is lines 2-4 and insert into end of existing rhs'
|
||||
},
|
||||
{
|
||||
lhs: '\nabcd',
|
||||
rhs: '',
|
||||
dir: 'lhs',
|
||||
name: 'delete is line 2 of length 4 and insert into end of empty rhs'
|
||||
},
|
||||
{
|
||||
lhs: '\nabcd',
|
||||
rhs: 'abcd',
|
||||
dir: 'lhs',
|
||||
name: 'delete is line 1 cr and insert into front of rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nb\nc\nd',
|
||||
rhs: 'a\nd',
|
||||
dir: 'lhs',
|
||||
name: 'delete is lines 2-3 and insert into middle of rhs'
|
||||
},
|
||||
// delete 36-43 merge rhs
|
||||
{
|
||||
lhs: '\na',
|
||||
rhs: '',
|
||||
dir: 'rhs',
|
||||
name: 'delete is line 2 and will merge empty rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a\n',
|
||||
rhs: '',
|
||||
dir: 'rhs',
|
||||
name: 'delete is line 1 and will merge empty rhs leaving cr on line 2'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nb',
|
||||
rhs: 'a',
|
||||
dir: 'rhs',
|
||||
name: 'delete is line 2 and will merge into end to existing rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nbcd',
|
||||
rhs: 'a',
|
||||
dir: 'rhs',
|
||||
name: 'delete is line 2 of length 3 and insert into end of existing rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nb\nc\nd',
|
||||
rhs: 'a',
|
||||
dir: 'rhs',
|
||||
name: 'delete is lines 2-4 and insert into end of existing rhs'
|
||||
},
|
||||
{
|
||||
lhs: '\nabcd',
|
||||
rhs: '',
|
||||
dir: 'rhs',
|
||||
name: 'delete is line 2 of length 4 and insert into end of empty rhs'
|
||||
},
|
||||
{
|
||||
lhs: '\nabcd',
|
||||
rhs: 'abcd',
|
||||
dir: 'rhs',
|
||||
name: 'delete is line 1 cr and insert into front of rhs'
|
||||
},
|
||||
{
|
||||
lhs: 'a\nb\nc\nd',
|
||||
rhs: 'a\nd',
|
||||
dir: 'rhs',
|
||||
name: 'delete is lines 2-3 and insert into middle of rhs'
|
||||
}];
|
||||
|
||||
opts.forEach((opt, i) => {
|
||||
it(`merge-case-${i} should merge change to ${opt.dir} where ${opt.name}`, function (done) {
|
||||
$(document).ready(() => {
|
||||
const editor = init({
|
||||
height: 100,
|
||||
license: 'lgpl-separate-notice',
|
||||
change_timeout: 0,
|
||||
lhs: (setValue) => setValue(opt.lhs),
|
||||
rhs: (setValue) => setValue(opt.rhs)
|
||||
});
|
||||
expect($('#mergely').mergely('get', 'lhs')).to.equal(opt.lhs);
|
||||
expect($('#mergely').mergely('get', 'rhs')).to.equal(opt.rhs);
|
||||
setTimeout(() => {
|
||||
for (let i = 0; i < opt.next; ++i) {
|
||||
$('#mergely').mergely('scrollToDiff', 'next');
|
||||
}
|
||||
$('#mergely').mergely('mergeCurrentChange', opt.dir);
|
||||
if (opt.dir === 'lhs') {
|
||||
expect($('#mergely').mergely('get', 'lhs')).to.equal(opt.expect || opt.rhs);
|
||||
expect($('#mergely').mergely('get', 'rhs')).to.equal(opt.rhs);
|
||||
} else {
|
||||
expect($('#mergely').mergely('get', 'lhs')).to.equal(opt.lhs);
|
||||
expect($('#mergely').mergely('get', 'rhs')).to.equal(opt.expect || opt.lhs);
|
||||
}
|
||||
done();
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_is_change_in_view', () => {
|
||||
it('should be false when change less-than viewport', function (done) {
|
||||
$(document).ready(() => {
|
||||
const editor = init({
|
||||
height: 100,
|
||||
viewport: true,
|
||||
license: 'lgpl-separate-notice',
|
||||
lhs: (setValue) => setValue(macbeth),
|
||||
rhs: (setValue) => setValue(macbeth)
|
||||
});
|
||||
const { mergely } = $('#mergely');
|
||||
expect($('#mergely').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, {
|
||||
'lhs-line-from': 0,
|
||||
'lhs-line-to': 9
|
||||
})).to.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be true when change less-than-equal viewport', function (done) {
|
||||
$(document).ready(() => {
|
||||
const editor = init({
|
||||
height: 100,
|
||||
viewport: true,
|
||||
license: 'lgpl-separate-notice',
|
||||
lhs: (setValue) => setValue(macbeth),
|
||||
rhs: (setValue) => setValue(macbeth)
|
||||
});
|
||||
const { mergely } = $('#mergely');
|
||||
expect($('#mergely').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, {
|
||||
'lhs-line-from': 0,
|
||||
'lhs-line-to': 10
|
||||
})).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be false when change greater-than viewport', function (done) {
|
||||
$(document).ready(() => {
|
||||
const editor = init({
|
||||
height: 100,
|
||||
viewport: true,
|
||||
license: 'lgpl-separate-notice',
|
||||
lhs: (setValue) => setValue(macbeth),
|
||||
rhs: (setValue) => setValue(macbeth)
|
||||
});
|
||||
const { mergely } = $('#mergely');
|
||||
expect($('#mergely').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, {
|
||||
'lhs-line-from': 21,
|
||||
'lhs-line-to': 22
|
||||
})).to.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be true when change straddles viewport from', function (done) {
|
||||
$(document).ready(() => {
|
||||
const editor = init({
|
||||
height: 100,
|
||||
viewport: true,
|
||||
license: 'lgpl-separate-notice',
|
||||
lhs: (setValue) => setValue(macbeth),
|
||||
rhs: (setValue) => setValue(macbeth)
|
||||
});
|
||||
const { mergely } = $('#mergely');
|
||||
expect($('#mergely').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, {
|
||||
'lhs-line-from': 5,
|
||||
'lhs-line-to': 11
|
||||
})).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be true when change straddles viewport to', function (done) {
|
||||
$(document).ready(() => {
|
||||
const editor = init({
|
||||
height: 100,
|
||||
viewport: true,
|
||||
license: 'lgpl-separate-notice',
|
||||
lhs: (setValue) => setValue(macbeth),
|
||||
rhs: (setValue) => setValue(macbeth)
|
||||
});
|
||||
const { mergely } = $('#mergely');
|
||||
expect($('#mergely').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, {
|
||||
'lhs-line-from': 15,
|
||||
'lhs-line-to': 21
|
||||
})).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be true when change encompasses viewport', function (done) {
|
||||
$(document).ready(() => {
|
||||
const editor = init({
|
||||
height: 100,
|
||||
viewport: true,
|
||||
license: 'lgpl-separate-notice',
|
||||
lhs: (setValue) => setValue(macbeth),
|
||||
rhs: (setValue) => setValue(macbeth)
|
||||
});
|
||||
const { mergely } = $('#mergely');
|
||||
expect($('#mergely').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, {
|
||||
'lhs-line-from': 0,
|
||||
'lhs-line-to': 25
|
||||
})).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it.only('should not be vulnerable to XSS', function (done) {
|
||||
function initXSS(options) {
|
||||
// $('body').css({'margin': '0px'}).append("<div id='mergely<script>alert(123)</script>' />");
|
||||
|
||||
$('body').get(0).innerHTML = "<div id='mergely\"<script id='injected'>alert(123)</script>'></div>";
|
||||
|
||||
const editor = $('#mergely');
|
||||
editor.mergely(options);
|
||||
return editor;
|
||||
};
|
||||
|
||||
$(document).ready(() => {
|
||||
const editor = initXSS({
|
||||
height: 100,
|
||||
viewport: true,
|
||||
license: 'lgpl-separate-notice',
|
||||
lhs: (setValue) => setValue(macbeth),
|
||||
rhs: (setValue) => setValue(macbeth)
|
||||
});
|
||||
const { mergely } = $('#mergely');
|
||||
// console.log('HERE', $('body').html());
|
||||
// const { mergely } = $('#mergely"<script>alert(123)</script>');
|
||||
// expect($('#mergely<script>alert(123)</script>').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, {
|
||||
// 'lhs-line-from': 0,
|
||||
// 'lhs-line-to': 25
|
||||
// })).to.be.true;
|
||||
expect($('body').find('#injected')).to.have.length(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,12 +27,12 @@ module.exports = {
|
||||
CodeMirror: 'CodeMirror'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
sourceMap: true,
|
||||
include: /\.js$/,
|
||||
// include: /\.min\.js$/,
|
||||
exclude: /node_modules/
|
||||
}),
|
||||
// new webpack.optimize.UglifyJsPlugin({
|
||||
// sourceMap: true,
|
||||
// include: /\.js$/,
|
||||
// // include: /\.min\.js$/,
|
||||
// exclude: /node_modules/
|
||||
// }),
|
||||
new ExtractTextPlugin('mergely.css')
|
||||
]
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user