diff --git a/apps/mobile/android/app/src/main/assets/init.js b/apps/mobile/android/app/src/main/assets/init.js index e1831d37b..da948d5e5 100644 --- a/apps/mobile/android/app/src/main/assets/init.js +++ b/apps/mobile/android/app/src/main/assets/init.js @@ -32,6 +32,14 @@ function init_tiny(size) { }, statusbar: false, contextmenu: false, + content_style: ` + span.diff-del { + background-color: #FDB0C0; + } + span.diff-ins { + background-color: #CAFFFB; + } +`, browser_spellcheck: true, autoresize_bottom_margin: 50, imagetools_toolbar: 'rotateleft rotateright | flipv fliph', diff --git a/apps/mobile/html/Web.bundle/site/init.js b/apps/mobile/html/Web.bundle/site/init.js index e1831d37b..34cd5e1db 100644 --- a/apps/mobile/html/Web.bundle/site/init.js +++ b/apps/mobile/html/Web.bundle/site/init.js @@ -30,6 +30,14 @@ function init_tiny(size) { images_upload_handler: function (blobInfo, success, failure) { success('data:' + blobInfo.blob().type + ';base64,' + blobInfo.base64()); }, + content_style: ` + span.diff-del { + background-color: #FDB0C0; + } + span.diff-ins { + background-color: #CAFFFB; + } +`, statusbar: false, contextmenu: false, browser_spellcheck: true, @@ -48,13 +56,12 @@ function init_tiny(size) { setTheme(); reactNativeEventHandler('status', true); - editor.on('SelectionChange', function (e) { selectchange(); reactNativeEventHandler('history', { - undo: editor.undoManager.hasUndo(), - redo: editor.undoManager.hasRedo(), - }); + undo: editor.undoManager.hasUndo(), + redo: editor.undoManager.hasRedo(), + }); }); editor.on('focus', () => { @@ -80,7 +87,7 @@ function init_tiny(size) { } const onChange = (event) => { - console.log("called") + console.log('called'); if (isLoading) { isLoading = false; return; diff --git a/apps/mobile/src/components/MergeEditor/index.js b/apps/mobile/src/components/MergeEditor/index.js index afda3bd44..07fc79127 100644 --- a/apps/mobile/src/components/MergeEditor/index.js +++ b/apps/mobile/src/components/MergeEditor/index.js @@ -31,6 +31,7 @@ import Paragraph from '../Typography/Paragraph'; import KeepAwake from '@sayem314/react-native-keep-awake'; import {timeConverter} from '../../utils/TimeUtils'; import tiny from '../../views/Editor/tiny/tiny'; +import diff from '../../utils/differ'; const {Value, timing} = Animated; @@ -103,14 +104,26 @@ const MergeEditor = () => { const insets = useSafeAreaInsets(); const onPrimaryWebViewLoad = () => { - tiny.call(primaryWebView, tiny.html(primaryData.data), true); + let htmlDiff = { + before: primaryData.data, + }; + if (secondaryData.data) { + htmlDiff = diff.diff_dual_pane(primaryData.data, secondaryData.data); + } + tiny.call(primaryWebView, tiny.html(htmlDiff.before), true); let theme = {...colors}; theme.factor = normalize(1); tiny.call(primaryWebView, tiny.updateTheme(JSON.stringify(theme)), true); }; const onSecondaryWebViewLoad = () => { - tiny.call(secondaryWebView, tiny.html(secondaryData.data), true); + let htmlDiff = { + before: primaryData.data, + }; + if (secondaryData.data) { + htmlDiff = diff.diff_dual_pane(primaryData.data, secondaryData.data); + } + tiny.call(secondaryWebView, tiny.html(htmlDiff.after), true); let theme = {...colors}; theme.factor = normalize(1); tiny.call(secondaryWebView, tiny.updateTheme(JSON.stringify(theme)), true); @@ -154,7 +167,9 @@ const MergeEditor = () => { if (keepContentFrom === 'primary') { await db.notes.add({ content: { - data: primaryData.data, + data: primaryData.data + ? diff.clean(primaryData.data) + : primaryData.data, resolved: true, type: primaryData.type, }, @@ -164,7 +179,9 @@ const MergeEditor = () => { } else if (keepContentFrom === 'secondary') { await db.notes.add({ content: { - data: secondaryData.data, + data: secondaryData.data + ? diff.clean(secondaryData.data) + : secondaryData.data, type: secondaryData.type, resolved: true, }, @@ -176,7 +193,9 @@ const MergeEditor = () => { if (copyToSave === 'primary') { await db.notes.add({ content: { - data: primaryData.data, + data: primaryData.data + ? diff.clean(primaryData.data) + : primaryData.data, type: primaryData.type, }, id: null, @@ -184,7 +203,9 @@ const MergeEditor = () => { } else if (copyToSave === 'secondary') { await db.notes.add({ content: { - data: secondaryData.data, + data: secondaryData.data + ? diff.clean(secondaryData.data) + : secondaryData.data, type: secondaryData.type, }, id: null, diff --git a/apps/mobile/src/utils/differ.js b/apps/mobile/src/utils/differ.js new file mode 100644 index 000000000..b30ec7866 --- /dev/null +++ b/apps/mobile/src/utils/differ.js @@ -0,0 +1,894 @@ +(function () { + function is_end_of_tag(char) { + return char === ">"; + } + + function is_start_of_tag(char) { + return char === "<"; + } + + function is_close_tag(tag) { + return /^\s*<\s*\/[^>]+>\s*$/.test(tag); + } + + function is_whitespace(char) { + return /^\s+$/.test(char); + } + + function is_tag(token) { + return /^\s*<[^>]+>\s*$/.test(token); + } + + function isnt_tag(token) { + return !is_tag(token); + } + + /* + * Checks if the current word is the beginning of an atomic tag. An atomic tag is one whose + * child nodes should not be compared - the entire tag should be treated as one token. This + * is useful for tags where it does not make sense to insert and tags. + * + * @param {string} word The characters of the current token read so far. + * + * @return {string|null} The name of the atomic tag if the word will be an atomic tag, + * null otherwise + */ + function is_start_of_atomic_tag(word) { + var result = /^<(iframe|object|math|svg|script)/.exec(word); + if (result) { + result = result[1]; + } + return result; + } + + /* + * Checks if the current word is the end of an atomic tag (i.e. it has all the characters, + * except for the end bracket of the closing tag, such as '