1
0
mirror of synced 2025-12-27 09:58:14 +08:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Jamie Peabody
5f3034fa7f feat: Supports unicode diacritical marks when rendering line diff (fixes #169) 2024-06-16 20:19:11 +01:00
semantic-release-bot
65c71be17e chore(release): 5.2.0 [skip ci]
# [5.2.0](https://github.com/wickedest/Mergely/compare/v5.1.4...v5.2.0) (2024-06-09)

### Features

* Allows height to be not explicit height, e.g. 'inherit' or '100%' ([#196](https://github.com/wickedest/Mergely/issues/196)) ([b9e3641](b9e3641c85))
2024-06-09 21:25:57 +00:00
Jamie Peabody
b9e3641c85 feat: Allows height to be not explicit height, e.g. 'inherit' or '100%' (#196) 2024-06-09 22:25:04 +01:00
10 changed files with 156 additions and 17 deletions

View File

@@ -1,3 +1,10 @@
# [5.2.0](https://github.com/wickedest/Mergely/compare/v5.1.4...v5.2.0) (2024-06-09)
### Features
* Allows height to be not explicit height, e.g. 'inherit' or '100%' ([#196](https://github.com/wickedest/Mergely/issues/196)) ([b9e3641](https://github.com/wickedest/Mergely/commit/b9e3641c852a8926db5efdf33e65a607d5f2df5e))
## [5.1.4](https://github.com/wickedest/Mergely/compare/v5.1.3...v5.1.4) (2024-05-17) ## [5.1.4](https://github.com/wickedest/Mergely/compare/v5.1.3...v5.1.4) (2024-05-17)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "mergely", "name": "mergely",
"version": "5.1.4", "version": "5.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mergely", "name": "mergely",
"version": "5.1.4", "version": "5.2.0",
"license": "(GPL-3.0 OR LGPL-3.0 OR MPL-1.1 OR SEE LICENSE IN LICENSE)", "license": "(GPL-3.0 OR LGPL-3.0 OR MPL-1.1 OR SEE LICENSE IN LICENSE)",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.1.6", "@babel/core": "^7.1.6",

View File

@@ -1,6 +1,6 @@
{ {
"name": "mergely", "name": "mergely",
"version": "5.1.4", "version": "5.2.0",
"description": "A javascript UI for diff/merge", "description": "A javascript UI for diff/merge",
"license": "(GPL-3.0 OR LGPL-3.0 OR MPL-1.1 OR SEE LICENSE IN LICENSE)", "license": "(GPL-3.0 OR LGPL-3.0 OR MPL-1.1 OR SEE LICENSE IN LICENSE)",
"author": { "author": {

View File

@@ -65,7 +65,6 @@ CodeMirrorDiffView.prototype.unbind = function() {
this.el.removeChild(this.el.lastChild); this.el.removeChild(this.el.lastChild);
} }
if (this._origEl) { if (this._origEl) {
this.el.style = this._origEl.style;
this.el.className = this._origEl.className; this.el.className = this._origEl.className;
} }
this._unbound = true; this._unbound = true;
@@ -257,16 +256,17 @@ CodeMirrorDiffView.prototype.resize = function() {
CodeMirrorDiffView.prototype.bind = function(container) { CodeMirrorDiffView.prototype.bind = function(container) {
this.trace('api#bind', container); this.trace('api#bind', container);
this._origEl = {
style: container.style,
className: container.className
};
const el = dom.getMergelyContainer({ clazz: container.className }); const el = dom.getMergelyContainer({ clazz: container.className });
const computedStyle = window.getComputedStyle(container); const computedStyle = window.getComputedStyle(container);
if (!computedStyle.height || computedStyle.height === '0px') { if (!el.style.height
&& (!computedStyle.height || computedStyle.height === '0px')
) {
throw new Error( throw new Error(
`The element "${container.id}" requires an explicit height`); `The element "${container.id}" requires an explicit height`);
} }
this._origEl = {
className: container.className
};
this.id = `${container.id}`; this.id = `${container.id}`;
this.lhsId = `${container.id}-lhs`; this.lhsId = `${container.id}-lhs`;
this.rhsId = `${container.id}-rhs`; this.rhsId = `${container.id}-rhs`;
@@ -753,6 +753,9 @@ CodeMirrorDiffView.prototype._set_top_offset = function (side) {
// this is the distance from the top of the screen to the top of the // this is the distance from the top of the screen to the top of the
// content of the first codemirror editor // content of the first codemirror editor
const topnode = this._queryElement('.CodeMirror-measure'); const topnode = this._queryElement('.CodeMirror-measure');
if (!topnode.offsetParent) {
return false;
}
const top_offset = topnode.offsetParent.offsetTop + 4; const top_offset = topnode.offsetParent.offsetTop + 4;
// restore editor's scroll position // restore editor's scroll position

View File

@@ -244,7 +244,9 @@ function CodeifyText(lhs, rhs, options) {
if (typeof lhs === 'string') { if (typeof lhs === 'string') {
if (this.options.split === 'chars') { if (this.options.split === 'chars') {
this.lhs = lhs.split(''); // split characters and include their diacritical marks
this.lhs = lhs.match(/\p{Letter}\p{Mark}*|\p{White_Space}/gu) || [];
// this.lhs = [...lhs];
} else if (this.options.split === 'words') { } else if (this.options.split === 'words') {
this.lhs = lhs.split(/\s/); this.lhs = lhs.split(/\s/);
} else if (this.options.split === 'lines') { } else if (this.options.split === 'lines') {
@@ -255,7 +257,9 @@ function CodeifyText(lhs, rhs, options) {
} }
if (typeof rhs === 'string') { if (typeof rhs === 'string') {
if (this.options.split === 'chars') { if (this.options.split === 'chars') {
this.rhs = rhs.split(''); // split characters and include their diacritical marks
this.rhs = rhs.match(/\p{Letter}\p{Mark}*|\p{White_Space}/gu) || [];
// this.rhs = [...rhs];
} else if (this.options.split === 'words') { } else if (this.options.split === 'words') {
this.rhs = rhs.split(/\s/); this.rhs = rhs.split(/\s/);
} else if (this.options.split === 'lines') { } else if (this.options.split === 'lines') {

View File

@@ -46,7 +46,9 @@ class Mergely {
} }
const computedStyle = window.getComputedStyle(element); const computedStyle = window.getComputedStyle(element);
if (!computedStyle.height || computedStyle.height === '0px') { if (!element.style.height
&& (!computedStyle.height || computedStyle.height === '0px')
) {
throw new Error( throw new Error(
`The element "${selector}" requires an explicit height`); `The element "${selector}" requires an explicit height`);
} }

View File

@@ -2,6 +2,8 @@ const diff = require('./diff');
const trace = console.log; const trace = console.log;
const expLetters = new RegExp(/\p{Letter}\p{Mark}*|\p{White_Space}/gu);
class VDoc { class VDoc {
constructor(options) { constructor(options) {
this.options = options; this.options = options;
@@ -275,15 +277,18 @@ class VLine {
editor.setGutterMarker(this.id, name, item); editor.setGutterMarker(this.id, name, item);
} }
if (this.markup.length) { if (this.markup.length) {
// while Mergely diffs unicode chars (letters+mark), CM is by character,
// so diffs need to be mapped.
const mapped = mapLettersToChars(editor.getValue());
for (const markup of this.markup) { for (const markup of this.markup) {
const [ charFrom, charTo, className ] = markup; const [ charFrom, charTo, className ] = markup;
const fromPos = { line: this.id }; const fromPos = { line: this.id };
const toPos = { line: this.id }; const toPos = { line: this.id };
if (charFrom >= 0) { if (charFrom >= 0) {
fromPos.ch = charFrom; fromPos.ch = mapped[charFrom];
} }
if (charTo >= 0) { if (charTo >= 0) {
toPos.ch = charTo; toPos.ch = mapped[charTo];
} }
this._clearMarkup.push( this._clearMarkup.push(
editor.markText(fromPos, toPos, { className })); editor.markText(fromPos, toPos, { className }));
@@ -334,4 +339,15 @@ function getExtents(side, change) {
}; };
} }
function mapLettersToChars(text) {
let match;
let mapped = {};
let index = 0;
expLetters.lastIndex = 0;
while ((match = expLetters.exec(text)) !== null) {
mapped[index++] = match.index;
}
return mapped;
}
module.exports = VDoc; module.exports = VDoc;

View File

@@ -233,7 +233,109 @@ describe('markup', () => {
expect(rhs_spans[1].innerText).to.equal('h'); expect(rhs_spans[1].innerText).to.equal('h');
expect(rhs_spans[2].innerText).to.equal('ir'); expect(rhs_spans[2].innerText).to.equal('ir');
} }
} },
{
name: 'single word single diacritic non-spacing marks',
lhs: 'كلمة',
rhs: 'كَلمة',
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(1);
expect(lhs_spans[0].innerText).to.equal('ك');
const rhs_spans = editor.querySelectorAll(RHS_INLINE_TEXT + '.cid-0');
expect(rhs_spans).to.have.length(1);
expect(rhs_spans[0].innerText).to.equal('كَ');
}
},
{
name: 'single word multiple diacritic non-spacing marks',
lhs: ['\u006E', '\u0061', '\u0314', '\u0065'].join(''), // na̔e
rhs: ['\u006E', '\u0061', '\u0314', '\u034A', '\u0065'].join(''), // na̔͊e
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(1);
expect(lhs_spans[0].innerText).to.equal(['\u0061', '\u0314'].join(''));
const rhs_spans = editor.querySelectorAll(RHS_INLINE_TEXT + '.cid-0');
expect(rhs_spans).to.have.length(1);
expect(rhs_spans[0].innerText).to.equal('a̔͊');
}
},
{
name: 'multiple words diacritic non-spacing marks',
lhs: 'كلمة اخرى',
rhs: 'كْلمة اخرى',
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(1);
expect(lhs_spans[0].innerText).to.equal('ك');
const rhs_spans = editor.querySelectorAll(RHS_INLINE_TEXT + '.cid-0');
expect(rhs_spans).to.have.length(1);
expect(rhs_spans[0].innerText).to.equal('كْ');
}
},
{
name: 'nonnormalizable diacritic non-spacing marks',
lhs: 'naeg',
// there are 2 marks on 'e', tilde (0303) and x (0353)
rhs: ['\u006E', '\u0061', '\u0353', '\u0065', '\u0353', '\u0303', '\u0067'].join(''),
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(1);
expect(lhs_spans[0].innerText).to.equal('ae');
const rhs_spans = editor.querySelectorAll(RHS_INLINE_TEXT + '.cid-0');
expect(rhs_spans).to.have.length(1);
expect(rhs_spans[0].innerText).to.equal(
['\u0061', '\u0353', '\u0065', '\u0353', '\u0303'].join('')
);
}
},
{
name: 'nonnormalizable diacritic non-spacing marks',
lhs: [
'\u0065', '\u0353', '\u0303',
'\u0065', '\u0353', '\u0303',
'\u0065', '\u0353', '\u0303',
'x',
'\u0065', '\u0353', '\u0303',
].join(''),
// there are 2 marks on 'e', tilde (0303) and x (0353)
rhs: [
'\u0065', '\u0353', '\u0303',
'\u0065', '\u0353', '\u0303',
'\u0065', '\u0353', '\u0303',
'y',
'\u0065', '\u0353', '\u0303',
].join(''),
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(1);
expect(lhs_spans[0].innerText).to.equal('x');
const rhs_spans = editor.querySelectorAll(RHS_INLINE_TEXT + '.cid-0');
expect(rhs_spans).to.have.length(1);
expect(rhs_spans[0].innerText).to.equal('y');
}
},
]; ];
// to debug, add `only: true` to the test `opts` above, and run `npm run debug` // to debug, add `only: true` to the test `opts` above, and run `npm run debug`

View File

@@ -1,4 +1,5 @@
const path = require('path') const path = require('path')
const chalk = require('chalk');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = { module.exports = {
@@ -50,8 +51,8 @@ module.exports = {
compiler.hooks.entryOption.tap('MyPlugin', (context, entry) => { compiler.hooks.entryOption.tap('MyPlugin', (context, entry) => {
console.log('-'.repeat(78)); console.log('-'.repeat(78));
console.log('Applications:'); console.log('Applications:');
console.log('http://localhost:8080/app.html'); console.log(chalk.bold(chalk.underline(chalk.cyan('http://localhost:8080/app.html'))));
console.log('http://localhost:8080/app-styles.html'); console.log(chalk.bold(chalk.underline(chalk.cyan('http://localhost:8080/app-styles.html'))));
console.log('-'.repeat(78)); console.log('-'.repeat(78));
}); });
} }

View File

@@ -15,6 +15,10 @@ module.exports = (mode) => {
...webpackDevConfig.output, ...webpackDevConfig.output,
path: path.join(__dirname, 'lib'), path: path.join(__dirname, 'lib'),
filename: './[name].js', filename: './[name].js',
library: {
name: 'mergely',
type: 'umd',
}
}, },
optimization: { optimization: {
minimize: true, minimize: true,