From 2f14832c348c4b8c8c5a5d83375d45444e6d52a0 Mon Sep 17 00:00:00 2001 From: Devon Zuegel Date: Tue, 17 Sep 2019 13:19:32 -0700 Subject: [PATCH] Desktop: Add ENEX to HTML export (#1795) * Add `escape` to go back from Dropbox Login screen * Add .vscode/ to .gitignore * Remove call to enexXmlToMd * The 2 enex importers have distinct functionality! * Add tmp #deleteAllNotebooks * checkbox state still not persisting * images now fixed, but checkboxes still broken * Figured out that #ipcProxySendToHost is important for handling checkbox * cleanup closing br and en-todo tags + add notes * Handle en-media, add NOTEs & TODOs, & format html * Clean up some of the logging * cleanHtml is a nice beautifier, but callback hell ensues... * Rm #htmlFormat * Recreating the xml actually seems to work * Add test (not functional rn) * Add test for checkboxes * Add test for image en-media * Separate tests into 2 function calls * Clean up test * Add `en-media-audio` test * Add bad resource test * Misc cleanup * Rm SlateEditor files * Misc cleanup * Remove #deleteAllNotebooks button * Add names to tests * Extract resourceUtils * Rm DropboxLoginScreen esc behavior, part of another PR * Misc cleanup * Improve audioElement, add attachment import support * Misc cleanup * Add svg test for enex_to_html * Clean up test * Set markup_language to MARKUP_LANGUAGE_HTML to tell renderer that the content is only HTML * Rename to newModuleByFormat_ for clarity * Add comment to clarify newModuleFromPath_ --- .gitignore | 3 +- CliClient/package-lock.json | 106 +++++++++-- CliClient/package.json | 1 + CliClient/tests/EnexToHtml.js | 108 +++++++++++ CliClient/tests/enex_to_html/attachment.enex | 8 + CliClient/tests/enex_to_html/attachment.html | 7 + .../tests/enex_to_html/checklist-list.enex | 11 ++ .../tests/enex_to_html/checklist-list.html | 16 ++ .../tests/enex_to_html/en-media--audio.enex | 3 + .../tests/enex_to_html/en-media--audio.html | 13 ++ .../tests/enex_to_html/en-media--image.enex | 1 + .../tests/enex_to_html/en-media--image.html | 14 ++ CliClient/tests/enex_to_html/svg.enex | 5 + CliClient/tests/enex_to_html/svg.html | 3 + ElectronClient/app/app.js | 15 +- ElectronClient/app/package-lock.json | 159 +++++++++++----- ElectronClient/app/package.json | 1 + ElectronClient/package-lock.json | 11 ++ ReactNativeClient/lib/import-enex-html-gen.js | 170 ++++++++++++++++++ ReactNativeClient/lib/import-enex-md-gen.js | 9 +- ReactNativeClient/lib/import-enex.js | 10 +- ReactNativeClient/lib/resourceUtils.js | 84 +++++++++ .../lib/services/InteropService.js | 50 +++++- .../InteropService_Importer_EnexToHtml.js | 22 +++ ...js => InteropService_Importer_EnexToMd.js} | 4 +- 25 files changed, 749 insertions(+), 85 deletions(-) create mode 100644 CliClient/tests/EnexToHtml.js create mode 100644 CliClient/tests/enex_to_html/attachment.enex create mode 100644 CliClient/tests/enex_to_html/attachment.html create mode 100644 CliClient/tests/enex_to_html/checklist-list.enex create mode 100644 CliClient/tests/enex_to_html/checklist-list.html create mode 100644 CliClient/tests/enex_to_html/en-media--audio.enex create mode 100644 CliClient/tests/enex_to_html/en-media--audio.html create mode 100644 CliClient/tests/enex_to_html/en-media--image.enex create mode 100644 CliClient/tests/enex_to_html/en-media--image.html create mode 100644 CliClient/tests/enex_to_html/svg.enex create mode 100644 CliClient/tests/enex_to_html/svg.html create mode 100644 ElectronClient/package-lock.json create mode 100644 ReactNativeClient/lib/import-enex-html-gen.js create mode 100644 ReactNativeClient/lib/resourceUtils.js create mode 100644 ReactNativeClient/lib/services/InteropService_Importer_EnexToHtml.js rename ReactNativeClient/lib/services/{InteropService_Importer_Enex.js => InteropService_Importer_EnexToMd.js} (81%) diff --git a/.gitignore b/.gitignore index 0f503c65a..14940ee97 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ ReactNativeClient/lib/csstojs/ ReactNativeClient/lib/rnInjectedJs/ ElectronClient/app/gui/note-viewer/fonts/ ElectronClient/app/gui/note-viewer/lib.js -Tools/commit_hook.txt \ No newline at end of file +Tools/commit_hook.txt +.vscode/* diff --git a/CliClient/package-lock.json b/CliClient/package-lock.json index 745d2fabe..088d24f6a 100644 --- a/CliClient/package-lock.json +++ b/CliClient/package-lock.json @@ -312,6 +312,22 @@ "source-map": "0.5.x" } }, + "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=" + } + } + }, "cliss": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/cliss/-/cliss-0.0.2.tgz", @@ -550,6 +566,32 @@ "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz", "integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg==" }, + "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", @@ -558,6 +600,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" + } + }, "ecc-jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", @@ -1015,22 +1074,41 @@ "uglify-js": "3.3.x" } }, - "http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "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": { - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "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" + } } } }, @@ -3130,7 +3208,7 @@ "requires": { "chalk": "^2.1.0", "emphasize": "^1.5.0", - "node-emoji": "git+https://github.com/laurent22/node-emoji.git", + "node-emoji": "git+https://github.com/laurent22/node-emoji.git#9fa01eac463e94dde1316ef8c53089eeef4973b5", "slice-ansi": "^1.0.0", "string-width": "^2.1.1", "terminal-kit": "^1.13.11", diff --git a/CliClient/package.json b/CliClient/package.json index bf9f01d31..cb7228a1c 100644 --- a/CliClient/package.json +++ b/CliClient/package.json @@ -31,6 +31,7 @@ "app-module-path": "^2.2.0", "async-mutex": "^0.1.3", "base-64": "^0.1.0", + "clean-html": "^1.5.0", "compare-version": "^0.1.2", "diacritics": "^1.3.0", "diff-match-patch": "^1.0.4", diff --git a/CliClient/tests/EnexToHtml.js b/CliClient/tests/EnexToHtml.js new file mode 100644 index 000000000..513676d83 --- /dev/null +++ b/CliClient/tests/EnexToHtml.js @@ -0,0 +1,108 @@ +require('app-module-path').addPath(__dirname); + +const { asyncTest, setupDatabaseAndSynchronizer, switchClient } = require('test-utils.js'); +const { shim } = require('lib/shim'); +const { enexXmlToHtml } = require('lib/import-enex-html-gen.js'); + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 60 * 1000; // Can run for a while since everything is in the same test unit + +process.on('unhandledRejection', (reason, p) => { + console.warn('Unhandled Rejection at: Promise', p, 'reason:', reason); +}); + +const fileWithPath = (filename) => + `${__dirname}/enex_to_html/${filename}`; + +const audioResource = { + filename: 'audio test', + id: '9168ee833d03c5ea7c730ac6673978c1', + mime: 'audio/x-m4a', + size: 82011, + title: 'audio test', +}; + +/** + * Tests the importer for a single note, checking that the result of + * processing the given `.enex` input file matches the contents of the given + * `.html` file. + * + * Note that this does not test the importing of an entire exported `.enex` + * archive, but rather a single node of such a file. Thus, the test data files + * (e.g. `./enex_to_html/code1.enex`) correspond to the contents of a single + * `...` node in an `.enex` file already extracted from + * ``. + */ +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 @@ + +
attachment-1
+
+
+
+
+
\ 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:

+ +
    +
  • Foo
  • +
  • Bar
  • +
  • Baz
  • +
+
+
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:

+
    +
  • +
    Foo
    +
  • +
  • +
    Bar
    +
  • +
  • +
    Baz
    +
  • +
+
+
\ 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 @@ + +
+ +

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(``); + }); + + 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}) => + [ + '', + '

', + ` `, + ` ${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;