`.
+ */
+const compareOutputToExpected = (options) => {
+ const inputFile = fileWithPath(`${options.testName}.enex`);
+ const outputFile = fileWithPath(`${options.testName}.html`);
+ const testTitle = `should convert from Enex to Html: ${options.testName}`;
+
+ it(testTitle, asyncTest(async () => {
+ const enexInput = await shim.fsDriver().readFile(inputFile);
+ const expectedOutput = await shim.fsDriver().readFile(outputFile);
+ const actualOutput = await enexXmlToHtml(enexInput, options.resources);
+
+ expect(actualOutput).toEqual(expectedOutput);
+ }));
+};
+
+describe('EnexToHtml', function() {
+ beforeEach(async (done) => {
+ await setupDatabaseAndSynchronizer(1);
+ await switchClient(1);
+ done();
+ });
+
+ compareOutputToExpected({
+ testName: 'checklist-list',
+ resources: [],
+ });
+
+ compareOutputToExpected({
+ testName: 'svg',
+ resources: [],
+ });
+
+ compareOutputToExpected({
+ testName: 'en-media--image',
+ resources: [{
+ filename: '',
+ id: '89ce7da62c6b2832929a6964237e98e9', // Mock id
+ mime: 'image/jpeg',
+ size: 50347,
+ title: '',
+ }],
+ });
+
+ compareOutputToExpected({
+ testName: 'en-media--audio',
+ resources: [audioResource],
+ });
+
+ compareOutputToExpected({
+ testName: 'attachment',
+ resources: [{
+ filename: 'attachment-1',
+ id: '21ca2b948f222a38802940ec7e2e5de3',
+ mime: 'application/pdf', // Any non-image/non-audio mime type will do
+ size: 1000,
+ }],
+ });
+
+ it('fails when not given a matching resource', asyncTest(async () => {
+ // To test the promise-unexpectedly-resolved case, add `audioResource` to the array.
+ const resources = [];
+ const inputFile = fileWithPath('en-media--image.enex');
+ const enexInput = await shim.fsDriver().readFile(inputFile);
+ const promisedOutput = enexXmlToHtml(enexInput, resources);
+
+ promisedOutput.then(() => {
+ // Promise should not be resolved
+ expect(false).toEqual(true);
+ }, (reason) => {
+ expect(reason)
+ .toBe('Hash with no associated resource: 89ce7da62c6b2832929a6964237e98e9');
+ });
+ }));
+
+});
diff --git a/CliClient/tests/enex_to_html/attachment.enex b/CliClient/tests/enex_to_html/attachment.enex
new file mode 100644
index 000000000..cf66a3f07
--- /dev/null
+++ b/CliClient/tests/enex_to_html/attachment.enex
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CliClient/tests/enex_to_html/attachment.html b/CliClient/tests/enex_to_html/attachment.html
new file mode 100644
index 000000000..c32fb1310
--- /dev/null
+++ b/CliClient/tests/enex_to_html/attachment.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CliClient/tests/enex_to_html/checklist-list.enex b/CliClient/tests/enex_to_html/checklist-list.enex
new file mode 100644
index 000000000..2552fe504
--- /dev/null
+++ b/CliClient/tests/enex_to_html/checklist-list.enex
@@ -0,0 +1,11 @@
+
+
+
For example, consider an exported Evernote list with todo checkboxes like this:
+
+
+
+
diff --git a/CliClient/tests/enex_to_html/checklist-list.html b/CliClient/tests/enex_to_html/checklist-list.html
new file mode 100644
index 000000000..5a6172e9d
--- /dev/null
+++ b/CliClient/tests/enex_to_html/checklist-list.html
@@ -0,0 +1,16 @@
+
+
+
For example, consider an exported Evernote list with todo checkboxes like this:
+
+
+
\ No newline at end of file
diff --git a/CliClient/tests/enex_to_html/en-media--audio.enex b/CliClient/tests/enex_to_html/en-media--audio.enex
new file mode 100644
index 000000000..bcd20bcb6
--- /dev/null
+++ b/CliClient/tests/enex_to_html/en-media--audio.enex
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CliClient/tests/enex_to_html/en-media--audio.html b/CliClient/tests/enex_to_html/en-media--audio.html
new file mode 100644
index 000000000..c71c6cf7a
--- /dev/null
+++ b/CliClient/tests/enex_to_html/en-media--audio.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Your browser does not support HTML5 audio.
+
+
audio test
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CliClient/tests/enex_to_html/en-media--image.enex b/CliClient/tests/enex_to_html/en-media--image.enex
new file mode 100644
index 000000000..92a477426
--- /dev/null
+++ b/CliClient/tests/enex_to_html/en-media--image.enex
@@ -0,0 +1 @@
+ This is a test
A test for bold
A test for italic
diff --git a/CliClient/tests/enex_to_html/en-media--image.html b/CliClient/tests/enex_to_html/en-media--image.html
new file mode 100644
index 000000000..f3c09555e
--- /dev/null
+++ b/CliClient/tests/enex_to_html/en-media--image.html
@@ -0,0 +1,14 @@
+
+ This is a test
+ A test for bold
+
+ A test for italic
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CliClient/tests/enex_to_html/svg.enex b/CliClient/tests/enex_to_html/svg.enex
new file mode 100644
index 000000000..33cfb89ec
--- /dev/null
+++ b/CliClient/tests/enex_to_html/svg.enex
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/CliClient/tests/enex_to_html/svg.html b/CliClient/tests/enex_to_html/svg.html
new file mode 100644
index 000000000..c4f57e7cd
--- /dev/null
+++ b/CliClient/tests/enex_to_html/svg.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/ElectronClient/app/app.js b/ElectronClient/app/app.js
index 1093c6027..88fb9796d 100644
--- a/ElectronClient/app/app.js
+++ b/ElectronClient/app/app.js
@@ -378,12 +378,15 @@ class Application extends BaseApplication {
message: _('Importing from "%s" as "%s" format. Please wait...', path, module.format),
});
- const importOptions = {};
- importOptions.path = path;
- importOptions.format = module.format;
- importOptions.destinationFolderId = !module.isNoteArchive && moduleSource === 'file' ? selectedFolderId : null;
- importOptions.onError = (error) => {
- console.warn(error);
+ const importOptions = {
+ path,
+ format: module.format,
+ modulePath: module.path,
+ onError: console.warn,
+ destinationFolderId:
+ !module.isNoteArchive && moduleSource === 'file'
+ ? selectedFolderId
+ : null,
};
const service = new InteropService();
diff --git a/ElectronClient/app/package-lock.json b/ElectronClient/app/package-lock.json
index ab4c25d5a..8fb9c3338 100644
--- a/ElectronClient/app/package-lock.json
+++ b/ElectronClient/app/package-lock.json
@@ -557,15 +557,13 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
- "dev": true,
- "optional": true
+ "dev": true
},
"is-glob": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
- "optional": true,
"requires": {
"is-extglob": "^1.0.0"
}
@@ -1390,6 +1388,22 @@
}
}
},
+ "clean-html": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/clean-html/-/clean-html-1.5.0.tgz",
+ "integrity": "sha512-eDu0vN44ZBvoEU0oRIKwWPIccGWXtdnUNmKJuTukZ1de00Uoqavb5pfIMKiC7/r+knQ5RbvAjGuVZiN3JwJL4Q==",
+ "requires": {
+ "htmlparser2": "^3.8.2",
+ "minimist": "^1.1.1"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+ }
+ }
+ },
"cli-boxes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
@@ -1839,6 +1853,32 @@
"@babel/runtime": "^7.1.2"
}
},
+ "dom-serializer": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz",
+ "integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==",
+ "requires": {
+ "domelementtype": "^2.0.1",
+ "entities": "^2.0.0"
+ },
+ "dependencies": {
+ "domelementtype": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
+ "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
+ },
+ "entities": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
+ "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
+ }
+ }
+ },
+ "domelementtype": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+ "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
+ },
"domexception": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
@@ -1847,6 +1887,23 @@
"webidl-conversions": "^4.0.2"
}
},
+ "domhandler": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
+ "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
+ "requires": {
+ "domelementtype": "1"
+ }
+ },
+ "domutils": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+ "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+ "requires": {
+ "dom-serializer": "0",
+ "domelementtype": "1"
+ }
+ },
"dot-prop": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
@@ -2746,8 +2803,7 @@
"version": "2.1.1",
"resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true,
- "optional": true
+ "dev": true
},
"aproba": {
"version": "1.2.0",
@@ -2771,15 +2827,13 @@
"version": "1.0.0",
"resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
- "dev": true,
- "optional": true
+ "dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": false,
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -2796,22 +2850,19 @@
"version": "1.1.0",
"resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
- "dev": true,
- "optional": true
+ "dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
- "dev": true,
- "optional": true
+ "dev": true
},
"console-control-strings": {
"version": "1.1.0",
"resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
- "dev": true,
- "optional": true
+ "dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -2932,8 +2983,7 @@
"version": "2.0.3",
"resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.5",
@@ -2947,7 +2997,6 @@
"resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -2964,7 +3013,6 @@
"resolved": false,
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -2973,15 +3021,13 @@
"version": "0.0.8",
"resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
- "dev": true,
- "optional": true
+ "dev": true
},
"minipass": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"dev": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -2991,8 +3037,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
- "dev": true,
- "optional": true
+ "dev": true
}
}
},
@@ -3011,7 +3056,6 @@
"resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -3100,8 +3144,7 @@
"version": "1.0.1",
"resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
- "dev": true,
- "optional": true
+ "dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -3115,7 +3158,6 @@
"resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -3211,8 +3253,7 @@
"version": "5.1.1",
"resolved": false,
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
- "dev": true,
- "optional": true
+ "dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -3254,7 +3295,6 @@
"resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -3276,7 +3316,6 @@
"resolved": false,
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -3334,15 +3373,13 @@
"version": "1.0.2",
"resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
- "dev": true,
- "optional": true
+ "dev": true
},
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
- "dev": true,
- "optional": true
+ "dev": true
}
}
},
@@ -3431,15 +3468,13 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
- "dev": true,
- "optional": true
+ "dev": true
},
"is-glob": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
- "optional": true,
"requires": {
"is-extglob": "^1.0.0"
}
@@ -3615,6 +3650,44 @@
}
}
},
+ "htmlparser2": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
+ "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
+ "requires": {
+ "domelementtype": "^1.3.1",
+ "domhandler": "^2.3.0",
+ "domutils": "^1.5.1",
+ "entities": "^1.1.1",
+ "inherits": "^2.0.1",
+ "readable-stream": "^3.1.1"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
+ "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
+ "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ }
+ }
+ },
"http-errors": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.1.tgz",
@@ -4824,7 +4897,6 @@
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
"dev": true,
- "optional": true,
"requires": {
"remove-trailing-separator": "^1.0.1"
}
@@ -5196,8 +5268,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
- "dev": true,
- "optional": true
+ "dev": true
},
"is-glob": {
"version": "2.0.1",
@@ -5837,15 +5908,13 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
- "dev": true,
- "optional": true
+ "dev": true
},
"repeat-element": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz",
"integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=",
- "dev": true,
- "optional": true
+ "dev": true
},
"repeat-string": {
"version": "1.6.1",
diff --git a/ElectronClient/app/package.json b/ElectronClient/app/package.json
index 50b307b30..03d7345bc 100644
--- a/ElectronClient/app/package.json
+++ b/ElectronClient/app/package.json
@@ -87,6 +87,7 @@
"async-mutex": "^0.1.3",
"base-64": "^0.1.0",
"chokidar": "^3.0.0",
+ "clean-html": "^1.5.0",
"compare-versions": "^3.2.1",
"diacritics": "^1.3.0",
"diff-match-patch": "^1.0.4",
diff --git a/ElectronClient/package-lock.json b/ElectronClient/package-lock.json
new file mode 100644
index 000000000..d66aa295b
--- /dev/null
+++ b/ElectronClient/package-lock.json
@@ -0,0 +1,11 @@
+{
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "html-format": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/html-format/-/html-format-1.0.1.tgz",
+ "integrity": "sha512-ePp+h+akaQiLeCGPefWQ4QJXVXhWx4sU4ZxJVFlaY0AeVgh/tnHGTL27ao09JrdEEelXYMAWi4ynKKheck4tdw=="
+ }
+ }
+}
diff --git a/ReactNativeClient/lib/import-enex-html-gen.js b/ReactNativeClient/lib/import-enex-html-gen.js
new file mode 100644
index 000000000..7da611b6b
--- /dev/null
+++ b/ReactNativeClient/lib/import-enex-html-gen.js
@@ -0,0 +1,170 @@
+const stringToStream = require('string-to-stream');
+const cleanHtml = require('clean-html');
+const resourceUtils = require('lib/resourceUtils.js');
+
+function addResourceTag(lines, resource, attributes) {
+ // Note: refactor to use Resource.markdownTag
+ if (!attributes.alt) attributes.alt = resource.title;
+ if (!attributes.alt) attributes.alt = resource.filename;
+ if (!attributes.alt) attributes.alt = '';
+
+ const src = `:/${resource.id}`;
+
+ if (resourceUtils.isImageMimeType(resource.mime)) {
+ lines.push(resourceUtils.imgElement({src, attributes}));
+ } else if (resource.mime === 'audio/x-m4a') {
+ /**
+ * TODO: once https://github.com/laurent22/joplin/issues/1794 is resolved,
+ * come back to this and make sure it works.
+ */
+ lines.push(resourceUtils.audioElement({
+ src,
+ alt: attributes.alt,
+ id: resource.id,
+ }));
+ } else {
+ // TODO: figure out what other mime types can be handled more gracefully
+ lines.push(resourceUtils.attachmentElement({
+ src,
+ attributes,
+ id: resource.id,
+ }));
+ }
+
+ return lines;
+}
+
+function attributeToLowerCase(node) {
+ if (!node.attributes) return {};
+ let output = {};
+ for (let n in node.attributes) {
+ if (!node.attributes.hasOwnProperty(n)) continue;
+ output[n.toLowerCase()] = node.attributes[n];
+ }
+ return output;
+}
+
+function enexXmlToHtml_(stream, resources) {
+ let remainingResources = resources.slice();
+
+ const removeRemainingResource = id => {
+ for (let i = 0; i < remainingResources.length; i++) {
+ const r = remainingResources[i];
+ if (r.id === id) {
+ remainingResources.splice(i, 1);
+ }
+ }
+ };
+
+ return new Promise((resolve, reject) => {
+ const options = {};
+ const strict = false;
+ var saxStream = require('sax').createStream(strict, options);
+
+ let section = {
+ type: 'text',
+ lines: [],
+ parent: null,
+ };
+
+ saxStream.on('error', function(e) {
+ console.warn(e);
+ // reject(e);
+ });
+
+
+ saxStream.on('text', function(text) {
+ section.lines.push(text);
+ });
+
+ saxStream.on('opentag', function(node) {
+ const tagName = node.name.toLowerCase();
+ const attributesStr = resourceUtils.attributesToStr(node.attributes);
+
+ if (tagName === 'en-media') {
+ const nodeAttributes = attributeToLowerCase(node);
+ const hash = nodeAttributes.hash;
+
+ let resource = null;
+ for (let i = 0; i < resources.length; i++) {
+ let r = resources[i];
+ if (r.id == hash) {
+ resource = r;
+ removeRemainingResource(r.id);
+ break;
+ }
+ }
+
+ if (!resource) {
+ // TODO: Extract this duplicate of code in ./import-enex-md-gen.js
+ let found = false;
+ for (let i = 0; i < remainingResources.length; i++) {
+ let r = remainingResources[i];
+ if (!r.id) {
+ resource = Object.assign({}, r);
+ resource.id = hash;
+ remainingResources.splice(i, 1);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ reject('Hash with no associated resource: ' + hash);
+ }
+ }
+
+ // If the resource does not appear among the note's resources, it
+ // means it's an attachement. It will be appended along with the
+ // other remaining resources at the bottom of the markdown text.
+ if (resource && !!resource.id) {
+ section.lines = addResourceTag(section.lines, resource, nodeAttributes);
+ }
+ } else if (tagName == 'en-todo') {
+ section.lines.push(' ');
+ } else if (node.isSelfClosing) {
+ section.lines.push(`<${tagName}${attributesStr}>`);
+ } else {
+ section.lines.push(`<${tagName}${attributesStr} />`);
+ }
+ });
+
+ saxStream.on('closetag', function(n) {
+ const tagName = n ? n.toLowerCase() : n;
+ section.lines.push(`${tagName}>`);
+ });
+
+ saxStream.on('attribute', function(attr) {});
+
+ saxStream.on('end', function() {
+ resolve({
+ content: section,
+ resources: remainingResources,
+ });
+ });
+
+ stream.pipe(saxStream);
+ });
+}
+
+async function enexXmlToHtml(xmlString, resources, options = {}) {
+ const stream = stringToStream(xmlString);
+ let result = await enexXmlToHtml_(stream, resources, options);
+
+ try {
+ const preCleaning = result.content.lines.join(''); // xmlString
+ const final = await beautifyHtml(preCleaning);
+ return final.join('');
+ } catch (error) {
+ console.warn(error);
+ }
+}
+
+const beautifyHtml = (html) => {
+ return new Promise((resolve, _reject) => {
+ const options = {wrap: 0};
+ cleanHtml.clean(html, options, (...cleanedHtml) => resolve(cleanedHtml));
+ });
+};
+
+module.exports = {enexXmlToHtml};
diff --git a/ReactNativeClient/lib/import-enex-md-gen.js b/ReactNativeClient/lib/import-enex-md-gen.js
index fe96c7bdb..9f4213847 100644
--- a/ReactNativeClient/lib/import-enex-md-gen.js
+++ b/ReactNativeClient/lib/import-enex-md-gen.js
@@ -1,5 +1,6 @@
const stringPadding = require('string-padding');
const stringToStream = require('string-to-stream');
+const resourceUtils = require('lib/resourceUtils.js');
const BLOCK_OPEN = '[[BLOCK_OPEN]]';
const BLOCK_CLOSE = '[[BLOCK_CLOSE]]';
@@ -295,12 +296,6 @@ function collapseWhiteSpaceAndAppend(lines, state, text) {
return lines;
}
-const imageMimeTypes = ['image/cgm', 'image/fits', 'image/g3fax', 'image/gif', 'image/ief', 'image/jp2', 'image/jpeg', 'image/jpm', 'image/jpx', 'image/naplps', 'image/png', 'image/prs.btif', 'image/prs.pti', 'image/t38', 'image/tiff', 'image/tiff-fx', 'image/vnd.adobe.photoshop', 'image/vnd.cns.inf2', 'image/vnd.djvu', 'image/vnd.dwg', 'image/vnd.dxf', 'image/vnd.fastbidsheet', 'image/vnd.fpx', 'image/vnd.fst', 'image/vnd.fujixerox.edmics-mmr', 'image/vnd.fujixerox.edmics-rlc', 'image/vnd.globalgraphics.pgb', 'image/vnd.microsoft.icon', 'image/vnd.mix', 'image/vnd.ms-modi', 'image/vnd.net-fpx', 'image/vnd.sealed.png', 'image/vnd.sealedmedia.softseal.gif', 'image/vnd.sealedmedia.softseal.jpg', 'image/vnd.svf', 'image/vnd.wap.wbmp', 'image/vnd.xiff'];
-
-function isImageMimeType(m) {
- return imageMimeTypes.indexOf(m) >= 0;
-}
-
function tagAttributeToMdText(attr) {
// HTML attributes may contain newlines so remove them.
// https://github.com/laurent22/joplin/issues/1583
@@ -318,7 +313,7 @@ function addResourceTag(lines, resource, alt = '') {
if (!alt) alt = '';
alt = tagAttributeToMdText(alt);
- if (isImageMimeType(resource.mime)) {
+ if (resourceUtils.isImageMimeType(resource.mime)) {
lines.push('![');
lines.push(alt);
lines.push('](:/' + resource.id + ')');
diff --git a/ReactNativeClient/lib/import-enex.js b/ReactNativeClient/lib/import-enex.js
index b2fafe854..ee0cd6045 100644
--- a/ReactNativeClient/lib/import-enex.js
+++ b/ReactNativeClient/lib/import-enex.js
@@ -5,6 +5,7 @@ const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const Resource = require('lib/models/Resource.js');
const { enexXmlToMd } = require('./import-enex-md-gen.js');
+const { enexXmlToHtml } = require('./import-enex-html-gen.js');
const { time } = require('lib/time-utils.js');
const Levenshtein = require('levenshtein');
const md5 = require('md5');
@@ -164,6 +165,7 @@ async function saveNoteToStorage(note, fuzzyMatching = false) {
function importEnex(parentFolderId, filePath, importOptions = null) {
if (!importOptions) importOptions = {};
+ // console.info(JSON.stringify({importOptions}, null, 2));
if (!('fuzzyMatching' in importOptions)) importOptions.fuzzyMatching = false;
if (!('onProgress' in importOptions)) importOptions.onProgress = function() {};
if (!('onError' in importOptions)) importOptions.onError = function() {};
@@ -216,9 +218,15 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
while (notes.length) {
let note = notes.shift();
- const body = await enexXmlToMd(note.bodyXml, note.resources);
+ const body = importOptions.outputFormat === 'html' ?
+ await enexXmlToHtml(note.bodyXml, note.resources) :
+ await enexXmlToMd(note.bodyXml, note.resources);
delete note.bodyXml;
+ note.markup_language = importOptions.outputFormat === 'html' ?
+ Note.MARKUP_LANGUAGE_HTML :
+ Note.MARKUP_LANGUAGE_MARKDOWN;
+
// console.info('*************************************************************************');
// console.info(body);
// console.info('*************************************************************************');
diff --git a/ReactNativeClient/lib/resourceUtils.js b/ReactNativeClient/lib/resourceUtils.js
new file mode 100644
index 000000000..a6c585354
--- /dev/null
+++ b/ReactNativeClient/lib/resourceUtils.js
@@ -0,0 +1,84 @@
+const imageMimeTypes = [
+ 'image/cgm',
+ 'image/fits',
+ 'image/g3fax',
+ 'image/gif',
+ 'image/ief',
+ 'image/jp2',
+ 'image/jpeg',
+ 'image/jpm',
+ 'image/jpx',
+ 'image/naplps',
+ 'image/png',
+ 'image/prs.btif',
+ 'image/prs.pti',
+ 'image/t38',
+ 'image/tiff',
+ 'image/tiff-fx',
+ 'image/vnd.adobe.photoshop',
+ 'image/vnd.cns.inf2',
+ 'image/vnd.djvu',
+ 'image/vnd.dwg',
+ 'image/vnd.dxf',
+ 'image/vnd.fastbidsheet',
+ 'image/vnd.fpx',
+ 'image/vnd.fst',
+ 'image/vnd.fujixerox.edmics-mmr',
+ 'image/vnd.fujixerox.edmics-rlc',
+ 'image/vnd.globalgraphics.pgb',
+ 'image/vnd.microsoft.icon',
+ 'image/vnd.mix',
+ 'image/vnd.ms-modi',
+ 'image/vnd.net-fpx',
+ 'image/vnd.sealed.png',
+ 'image/vnd.sealedmedia.softseal.gif',
+ 'image/vnd.sealedmedia.softseal.jpg',
+ 'image/vnd.svf',
+ 'image/vnd.wap.wbmp',
+ 'image/vnd.xiff',
+];
+
+const escapeQuotes = (str) => str.replace(/"/g, '"');
+
+const attributesToStr = (attributes) =>
+ Object.entries(attributes)
+ .map(([key, value]) => ` ${key}="${escapeQuotes(value)}"`)
+ .join('');
+
+const ipcProxySendToHost = (id) =>
+ `onclick="ipcProxySendToHost('joplin://${id}'); return false;"`;
+
+const attachmentElement = ({src, attributes, id}) =>
+ [
+ ``,
+ ` ${attributes.alt || src}`,
+ ' ',
+ ].join('');
+
+const imgElement = ({src, attributes}) =>
+ ` `;
+
+const audioElement = ({src, alt, id}) =>
+ [
+ '',
+ ` `,
+ ' ',
+ ' Your browser does not support HTML5 audio.',
+ '
',
+ ' ',
+ '',
+ ` `,
+ ` ${alt || src || id || 'Download audio'}`,
+ ' ',
+ '
',
+ ].join('');
+
+const resourceUtils = {
+ imgElement,
+ audioElement,
+ attachmentElement,
+ attributesToStr,
+ isImageMimeType: (m) => imageMimeTypes.indexOf(m) >= 0,
+};
+
+module.exports = resourceUtils;
diff --git a/ReactNativeClient/lib/services/InteropService.js b/ReactNativeClient/lib/services/InteropService.js
index d0693c4b3..63babd1b1 100644
--- a/ReactNativeClient/lib/services/InteropService.js
+++ b/ReactNativeClient/lib/services/InteropService.js
@@ -13,8 +13,7 @@ const { toTitleCase } = require('lib/string-utils');
class InteropService {
constructor() {
- this.modules_ = null;
- }
+ this.modules_ = null; }
modules() {
if (this.modules_) return this.modules_;
@@ -42,7 +41,16 @@ class InteropService {
format: 'enex',
fileExtensions: ['enex'],
sources: ['file'],
- description: _('Evernote Export File'),
+ description: _('Evernote Export File (as Markdown)'),
+ importerClass: 'InteropService_Importer_EnexToMd',
+ },
+ {
+ format: 'enex',
+ fileExtensions: ['enex'],
+ sources: ['file'],
+ description: _('Evernote Export File (as HTML)'),
+ // TODO: Consider doing this the same way as the multiple `md` importers are handled
+ importerClass: 'InteropService_Importer_EnexToHtml',
},
];
@@ -71,7 +79,7 @@ class InteropService {
];
importModules = importModules.map(a => {
- const className = 'InteropService_Importer_' + toTitleCase(a.format);
+ const className = a.importerClass || 'InteropService_Importer_' + toTitleCase(a.format);
const output = Object.assign(
{},
{
@@ -112,8 +120,9 @@ class InteropService {
return this.modules_;
}
- moduleByFormat_(type, format) {
+ findModuleByFormat_(type, format) {
const modules = this.modules();
+ // console.log(JSON.stringify({modules}, null, 2))
for (let i = 0; i < modules.length; i++) {
const m = modules[i];
if (m.format === format && m.type === type) return modules[i];
@@ -121,8 +130,8 @@ class InteropService {
return null;
}
- newModule_(type, format) {
- const module = this.moduleByFormat_(type, format);
+ newModuleByFormat_(type, format) {
+ const module = this.findModuleByFormat_(type, format);
if (!module) throw new Error(_('Cannot load "%s" module for format "%s"', type, format));
const ModuleClass = require(module.path);
const output = new ModuleClass();
@@ -130,6 +139,26 @@ class InteropService {
return output;
}
+ /**
+ * The existing `newModuleByFormat_` fn would load by the input format. This
+ * was fine when there was a 1-1 mapping of input formats to output formats,
+ * but now that we have 2 possible outputs for an `enex` input, we need to be
+ * explicit with which importer we want to use.
+ *
+ * In the long run, it might make sense to simply move all the existing
+ * formatters to the `newModuleFromPath_` approach, so that there's only one
+ * way to do this mapping.
+ *
+ * https://github.com/laurent22/joplin/pull/1795#pullrequestreview-281574417
+ */
+ newModuleFromPath_(options) {
+ if (!options || !options.modulePath) throw new Error('Cannot load module without a defined path to load from.');
+ const ModuleClass = require(options.modulePath);
+ const output = new ModuleClass();
+ output.setMetadata(options); // TODO: Check that this metadata is equivalent to module above
+ return output;
+ }
+
moduleByFileExtension_(type, ext) {
ext = ext.toLowerCase();
@@ -173,7 +202,10 @@ class InteropService {
let result = { warnings: [] };
- const importer = this.newModule_('importer', options.format);
+ // console.log('options passed to InteropService:');
+ // console.log(JSON.stringify({options}, null, 2));
+
+ const importer = this.newModuleFromPath_(options);
await importer.init(options.path, options);
result = await importer.exec(result);
@@ -248,7 +280,7 @@ class InteropService {
await queueExportItem(BaseModel.TYPE_TAG, exportedTagIds[i]);
}
- const exporter = this.newModule_('exporter', exportFormat);
+ const exporter = this.newModuleByFormat_('exporter', exportFormat);
await exporter.init(exportPath);
const typeOrder = [BaseModel.TYPE_FOLDER, BaseModel.TYPE_RESOURCE, BaseModel.TYPE_NOTE, BaseModel.TYPE_TAG, BaseModel.TYPE_NOTE_TAG];
diff --git a/ReactNativeClient/lib/services/InteropService_Importer_EnexToHtml.js b/ReactNativeClient/lib/services/InteropService_Importer_EnexToHtml.js
new file mode 100644
index 000000000..a58dd002c
--- /dev/null
+++ b/ReactNativeClient/lib/services/InteropService_Importer_EnexToHtml.js
@@ -0,0 +1,22 @@
+const InteropService_Importer_Base = require('lib/services/InteropService_Importer_Base');
+const Folder = require('lib/models/Folder.js');
+const { filename } = require('lib/path-utils.js');
+
+class InteropService_Importer_EnexToHtml extends InteropService_Importer_Base {
+ async exec(result) {
+ const { importEnex } = require('lib/import-enex');
+
+ let folder = this.options_.destinationFolder;
+
+ if (!folder) {
+ const folderTitle = await Folder.findUniqueItemTitle(filename(this.sourcePath_));
+ folder = await Folder.save({ title: folderTitle });
+ }
+
+ await importEnex(folder.id, this.sourcePath_, {...this.options_, outputFormat: 'html'});
+
+ return result;
+ }
+}
+
+module.exports = InteropService_Importer_EnexToHtml;
diff --git a/ReactNativeClient/lib/services/InteropService_Importer_Enex.js b/ReactNativeClient/lib/services/InteropService_Importer_EnexToMd.js
similarity index 81%
rename from ReactNativeClient/lib/services/InteropService_Importer_Enex.js
rename to ReactNativeClient/lib/services/InteropService_Importer_EnexToMd.js
index ad97fa67c..c3ebb8694 100644
--- a/ReactNativeClient/lib/services/InteropService_Importer_Enex.js
+++ b/ReactNativeClient/lib/services/InteropService_Importer_EnexToMd.js
@@ -2,7 +2,7 @@ const InteropService_Importer_Base = require('lib/services/InteropService_Import
const Folder = require('lib/models/Folder.js');
const { filename } = require('lib/path-utils.js');
-class InteropService_Importer_Enex extends InteropService_Importer_Base {
+class InteropService_Importer_EnexToMd extends InteropService_Importer_Base {
async exec(result) {
const { importEnex } = require('lib/import-enex');
@@ -19,4 +19,4 @@ class InteropService_Importer_Enex extends InteropService_Importer_Base {
}
}
-module.exports = InteropService_Importer_Enex;
+module.exports = InteropService_Importer_EnexToMd;