mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Desktop: Fix import interop service (#1887)
* Revert "Revert "Desktop: Add ENEX to HTML export (#1795)"" This reverts commit50b66cceca
. * Revert "Revert "Desktop, Cli: Fixed interop service so that it still allow auto-detecting importer based on format (required for Cli and for test units)"" This reverts commitc7c57ab2a5
. * Fix the .md importer * Add comment re future refactor * Rm importerClass for .md importer * Fix EnexToMd module name
This commit is contained in:
parent
8a8ecaade3
commit
172d925f0f
1
.gitignore
vendored
1
.gitignore
vendored
@ -43,3 +43,4 @@ ReactNativeClient/lib/rnInjectedJs/
|
||||
ElectronClient/app/gui/note-viewer/fonts/
|
||||
ElectronClient/app/gui/note-viewer/lib.js
|
||||
Tools/commit_hook.txt
|
||||
.vscode/*
|
||||
|
106
CliClient/package-lock.json
generated
106
CliClient/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
108
CliClient/tests/EnexToHtml.js
Normal file
108
CliClient/tests/EnexToHtml.js
Normal file
@ -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
|
||||
* `<note>...</note>` node in an `.enex` file already extracted from
|
||||
* `<content><![CDATA[...]]</content>`.
|
||||
*/
|
||||
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');
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
8
CliClient/tests/enex_to_html/attachment.enex
Normal file
8
CliClient/tests/enex_to_html/attachment.enex
Normal file
@ -0,0 +1,8 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<en-media hash="21ca2b948f222a38802940ec7e2e5de3" type="application/pdf" style="cursor:pointer;" />
|
||||
</div>
|
||||
<div>
|
||||
<br />
|
||||
</div>
|
||||
</en-note>
|
7
CliClient/tests/enex_to_html/attachment.html
Normal file
7
CliClient/tests/enex_to_html/attachment.html
Normal file
@ -0,0 +1,7 @@
|
||||
<en-note>
|
||||
<div><a href="#" hash="21ca2b948f222a38802940ec7e2e5de3" type="application/pdf" style="cursor:pointer;" alt="attachment-1" onclick="ipcProxySendToHost('joplin://21ca2b948f222a38802940ec7e2e5de3'); return false;">attachment-1</a></div>
|
||||
<div>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
</en-note>
|
11
CliClient/tests/enex_to_html/checklist-list.enex
Normal file
11
CliClient/tests/enex_to_html/checklist-list.enex
Normal file
@ -0,0 +1,11 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<p>For example, consider an exported Evernote list with todo checkboxes like this:</p>
|
||||
|
||||
<ul>
|
||||
<li><div><en-todo checked="true"/>Foo</div></li>
|
||||
<li><div><en-todo checked="false"/><b>Bar</b></div></li>
|
||||
<li><div><en-todo checked="false"/><i>Baz</i></div></li>
|
||||
</ul>
|
||||
</div>
|
||||
</en-note>
|
16
CliClient/tests/enex_to_html/checklist-list.html
Normal file
16
CliClient/tests/enex_to_html/checklist-list.html
Normal file
@ -0,0 +1,16 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<p>For example, consider an exported Evernote list with todo checkboxes like this:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<div><input type="checkbox" onclick="return false;">Foo</div>
|
||||
</li>
|
||||
<li>
|
||||
<div><input type="checkbox" onclick="return false;"><b>Bar</b></div>
|
||||
</li>
|
||||
<li>
|
||||
<div><input type="checkbox" onclick="return false;"><i>Baz</i></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</en-note>
|
3
CliClient/tests/enex_to_html/en-media--audio.enex
Normal file
3
CliClient/tests/enex_to_html/en-media--audio.enex
Normal file
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
|
||||
<en-note><div><en-media hash="9168ee833d03c5ea7c730ac6673978c1" type="audio/x-m4a" title="Attachment"/></div><div><br/></div></en-note>
|
13
CliClient/tests/enex_to_html/en-media--audio.html
Normal file
13
CliClient/tests/enex_to_html/en-media--audio.html
Normal file
@ -0,0 +1,13 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<audio controls="" preload="none" style="width:480px;">
|
||||
<source src=":/9168ee833d03c5ea7c730ac6673978c1" type="audio/mp4">
|
||||
<p>Your browser does not support HTML5 audio.</p>
|
||||
</audio>
|
||||
<p><a href=":/9168ee833d03c5ea7c730ac6673978c1" onclick="ipcProxySendToHost('joplin://9168ee833d03c5ea7c730ac6673978c1'); return false;">audio test</a></p>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
</en-note>
|
1
CliClient/tests/enex_to_html/en-media--image.enex
Normal file
1
CliClient/tests/enex_to_html/en-media--image.enex
Normal file
@ -0,0 +1 @@
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div><en-todo checked="false" />This is a test</div><div><en-todo checked="false" />A test for <span style="font-weight: bold;">bold</span></div><div><en-todo checked="false" />A test for <i>italic</i><br /></div><div><br /></div><div><i><en-media hash="89ce7da62c6b2832929a6964237e98e9" type="image/jpeg" /></i></div></en-note>
|
14
CliClient/tests/enex_to_html/en-media--image.html
Normal file
14
CliClient/tests/enex_to_html/en-media--image.html
Normal file
@ -0,0 +1,14 @@
|
||||
<en-note>
|
||||
<div><input type="checkbox" onclick="return false;">This is a test</div>
|
||||
<div><input type="checkbox" onclick="return false;">A test for <span style="font-weight: bold;">bold</span></div>
|
||||
<div>
|
||||
<input type="checkbox" onclick="return false;">A test for <i>italic</i>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
<div><i><img src=":/89ce7da62c6b2832929a6964237e98e9" hash="89ce7da62c6b2832929a6964237e98e9" type="image/jpeg" alt=""></i></div>
|
||||
</en-note>
|
5
CliClient/tests/enex_to_html/svg.enex
Normal file
5
CliClient/tests/enex_to_html/svg.enex
Normal file
@ -0,0 +1,5 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<img style="margin:0px;padding:0px;outline:0px;width:74px;height:36px;position:absolute;bottom:-5px;left:0px;transform:translate(0px, 100%);stroke-dasharray:90;transition:stroke-dashoffset 0.5s cubic-bezier(0.97, 0.16, 0.62, 0.76) 0s;stroke-dashoffset:0;" src="data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' data-evernote-id='97' class='js-evernote-checked'%3e%3cuse xlink:href='https://wordminds.com/wp-content/themes/wordminds/assets/img/hint_left.svg%23hint_left' data-evernote-id='98' class='js-evernote-checked'%3e%3c/use%3e%3c/svg%3e"/>
|
||||
</div>
|
||||
</en-note>
|
3
CliClient/tests/enex_to_html/svg.html
Normal file
3
CliClient/tests/enex_to_html/svg.html
Normal file
@ -0,0 +1,3 @@
|
||||
<en-note>
|
||||
<div><img style="margin:0px;padding:0px;outline:0px;width:74px;height:36px;position:absolute;bottom:-5px;left:0px;transform:translate(0px, 100%);stroke-dasharray:90;transition:stroke-dashoffset 0.5s cubic-bezier(0.97, 0.16, 0.62, 0.76) 0s;stroke-dashoffset:0;" src="data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' data-evernote-id='97' class='js-evernote-checked'%3e%3cuse xlink:href='https://wordminds.com/wp-content/themes/wordminds/assets/img/hint_left.svg%23hint_left' data-evernote-id='98' class='js-evernote-checked'%3e%3c/use%3e%3c/svg%3e"></div>
|
||||
</en-note>
|
@ -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();
|
||||
|
1281
ElectronClient/app/package-lock.json
generated
1281
ElectronClient/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
170
ReactNativeClient/lib/import-enex-html-gen.js
Normal file
170
ReactNativeClient/lib/import-enex-html-gen.js
Normal file
@ -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('<input type="checkbox" onclick="return false;" />');
|
||||
} 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() {});
|
||||
|
||||
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) => {
|
||||
const options = {wrap: 0};
|
||||
cleanHtml.clean(html, options, (...cleanedHtml) => resolve(cleanedHtml));
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {enexXmlToHtml};
|
@ -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})`);
|
||||
|
@ -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('*************************************************************************');
|
||||
|
84
ReactNativeClient/lib/resourceUtils.js
Normal file
84
ReactNativeClient/lib/resourceUtils.js
Normal file
@ -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}) =>
|
||||
[
|
||||
`<a href='#' ${attributesToStr(attributes)} ${ipcProxySendToHost(id)}>`,
|
||||
` ${attributes.alt || src}`,
|
||||
'</a>',
|
||||
].join('');
|
||||
|
||||
const imgElement = ({src, attributes}) =>
|
||||
`<img src="${src}" ${attributesToStr(attributes)} />`;
|
||||
|
||||
const audioElement = ({src, alt, id}) =>
|
||||
[
|
||||
'<audio controls preload="none" style="width:480px;">',
|
||||
` <source src="${src}" type="audio/mp4" />`,
|
||||
' <p>',
|
||||
' Your browser does not support HTML5 audio.',
|
||||
' </p>',
|
||||
'</audio>',
|
||||
'<p>',
|
||||
` <a href="${src}" ${ipcProxySendToHost(id)}>`,
|
||||
` ${alt || src || id || 'Download audio'}`,
|
||||
' </a>',
|
||||
'</p>',
|
||||
].join('');
|
||||
|
||||
const resourceUtils = {
|
||||
imgElement,
|
||||
audioElement,
|
||||
attachmentElement,
|
||||
attributesToStr,
|
||||
isImageMimeType: (m) => imageMimeTypes.indexOf(m) >= 0,
|
||||
};
|
||||
|
||||
module.exports = resourceUtils;
|
@ -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,17 @@ class InteropService {
|
||||
format: 'enex',
|
||||
fileExtensions: ['enex'],
|
||||
sources: ['file'],
|
||||
description: _('Evernote Export File'),
|
||||
description: _('Evernote Export File (as Markdown)'),
|
||||
importerClass: 'InteropService_Importer_EnexToMd',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
@ -112,21 +121,57 @@ class InteropService {
|
||||
return this.modules_;
|
||||
}
|
||||
|
||||
moduleByFormat_(type, format) {
|
||||
// Find the module that matches the given type ("importer" or "exporter")
|
||||
// and the given format. Some formats can have multiple assocated importers
|
||||
// or exporters, such as ENEX. In this case, the one marked as "isDefault"
|
||||
// is returned. This is useful to auto-detect the module based on the format.
|
||||
// For more precise matching, newModuleFromPath_ should be used.
|
||||
findModuleByFormat_(type, format) {
|
||||
const modules = this.modules();
|
||||
const matches = [];
|
||||
for (let i = 0; i < modules.length; i++) {
|
||||
const m = modules[i];
|
||||
if (m.format === format && m.type === type) return modules[i];
|
||||
if (m.format === format && m.type === type) matches.push(modules[i]);
|
||||
}
|
||||
return null;
|
||||
|
||||
const output = matches.find(m => !!m.isDefault);
|
||||
if (output) return output;
|
||||
|
||||
return matches.length ? matches[0] : null;
|
||||
}
|
||||
|
||||
newModule_(type, format) {
|
||||
const module = this.moduleByFormat_(type, format);
|
||||
if (!module) throw new Error(_('Cannot load "%s" module for format "%s"', type, format));
|
||||
const ModuleClass = require(module.path);
|
||||
/**
|
||||
* NOTE TO FUTURE SELF: 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. This isn't a priority right now (per the convo in:
|
||||
* https://github.com/laurent22/joplin/pull/1795#discussion_r322379121) but
|
||||
* we can do it if it ever becomes necessary.
|
||||
*/
|
||||
newModuleByFormat_(type, format) {
|
||||
const moduleMetadata = this.findModuleByFormat_(type, format);
|
||||
if (!moduleMetadata) throw new Error(_('Cannot load "%s" module for format "%s"', type, format));
|
||||
const ModuleClass = require(moduleMetadata.path);
|
||||
const output = new ModuleClass();
|
||||
output.setMetadata(module);
|
||||
output.setMetadata(moduleMetadata);
|
||||
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.
|
||||
*
|
||||
* https://github.com/laurent22/joplin/pull/1795#pullrequestreview-281574417
|
||||
*/
|
||||
newModuleFromPath_(type, 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();
|
||||
const moduleMetadata = this.findModuleByFormat_(type, options.format);
|
||||
output.setMetadata({options, ...moduleMetadata}); // TODO: Check that this metadata is equivalent to module above
|
||||
return output;
|
||||
}
|
||||
|
||||
@ -173,7 +218,17 @@ 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));
|
||||
|
||||
let importer = null;
|
||||
|
||||
if (options.modulePath) {
|
||||
importer = this.newModuleFromPath_('importer', options);
|
||||
} else {
|
||||
importer = this.newModuleByFormat_('importer', options.format);
|
||||
}
|
||||
|
||||
await importer.init(options.path, options);
|
||||
result = await importer.exec(result);
|
||||
|
||||
@ -248,7 +303,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];
|
||||
|
@ -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;
|
@ -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;
|
Loading…
Reference in New Issue
Block a user