1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

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_
This commit is contained in:
Devon Zuegel 2019-09-17 13:19:32 -07:00 committed by Laurent Cozic
parent 52ace55db0
commit 2f14832c34
25 changed files with 749 additions and 85 deletions

1
.gitignore vendored
View File

@ -43,3 +43,4 @@ ReactNativeClient/lib/rnInjectedJs/
ElectronClient/app/gui/note-viewer/fonts/ ElectronClient/app/gui/note-viewer/fonts/
ElectronClient/app/gui/note-viewer/lib.js ElectronClient/app/gui/note-viewer/lib.js
Tools/commit_hook.txt Tools/commit_hook.txt
.vscode/*

View File

@ -312,6 +312,22 @@
"source-map": "0.5.x" "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": { "cliss": {
"version": "0.0.2", "version": "0.0.2",
"resolved": "https://registry.npmjs.org/cliss/-/cliss-0.0.2.tgz", "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", "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz",
"integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg==" "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": { "domexception": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
@ -558,6 +600,23 @@
"webidl-conversions": "^4.0.2" "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": { "ecc-jsbn": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
@ -1015,22 +1074,41 @@
"uglify-js": "3.3.x" "uglify-js": "3.3.x"
} }
}, },
"http-errors": { "htmlparser2": {
"version": "1.7.3", "version": "3.10.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
"requires": { "requires": {
"depd": "~1.1.2", "domelementtype": "^1.3.1",
"inherits": "2.0.4", "domhandler": "^2.3.0",
"setprototypeof": "1.1.1", "domutils": "^1.5.1",
"statuses": ">= 1.5.0 < 2", "entities": "^1.1.1",
"toidentifier": "1.0.0" "inherits": "^2.0.1",
"readable-stream": "^3.1.1"
}, },
"dependencies": { "dependencies": {
"inherits": { "readable-stream": {
"version": "2.0.4", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "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": { "requires": {
"chalk": "^2.1.0", "chalk": "^2.1.0",
"emphasize": "^1.5.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", "slice-ansi": "^1.0.0",
"string-width": "^2.1.1", "string-width": "^2.1.1",
"terminal-kit": "^1.13.11", "terminal-kit": "^1.13.11",

View File

@ -31,6 +31,7 @@
"app-module-path": "^2.2.0", "app-module-path": "^2.2.0",
"async-mutex": "^0.1.3", "async-mutex": "^0.1.3",
"base-64": "^0.1.0", "base-64": "^0.1.0",
"clean-html": "^1.5.0",
"compare-version": "^0.1.2", "compare-version": "^0.1.2",
"diacritics": "^1.3.0", "diacritics": "^1.3.0",
"diff-match-patch": "^1.0.4", "diff-match-patch": "^1.0.4",

View 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');
});
}));
});

View File

@ -0,0 +1,8 @@
<en-note>
<div>
<en-media hash="21ca2b948f222a38802940ec7e2e5de3" type="application/pdf" style="cursor:pointer;" />
</div>
<div>
<br />
</div>
</en-note>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -378,12 +378,15 @@ class Application extends BaseApplication {
message: _('Importing from "%s" as "%s" format. Please wait...', path, module.format), message: _('Importing from "%s" as "%s" format. Please wait...', path, module.format),
}); });
const importOptions = {}; const importOptions = {
importOptions.path = path; path,
importOptions.format = module.format; format: module.format,
importOptions.destinationFolderId = !module.isNoteArchive && moduleSource === 'file' ? selectedFolderId : null; modulePath: module.path,
importOptions.onError = (error) => { onError: console.warn,
console.warn(error); destinationFolderId:
!module.isNoteArchive && moduleSource === 'file'
? selectedFolderId
: null,
}; };
const service = new InteropService(); const service = new InteropService();

View File

@ -557,15 +557,13 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true, "dev": true
"optional": true
}, },
"is-glob": { "is-glob": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"is-extglob": "^1.0.0" "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": { "cli-boxes": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
@ -1839,6 +1853,32 @@
"@babel/runtime": "^7.1.2" "@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": { "domexception": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
@ -1847,6 +1887,23 @@
"webidl-conversions": "^4.0.2" "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": { "dot-prop": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
@ -2746,8 +2803,7 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": false, "resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true, "dev": true
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -2771,15 +2827,13 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": false, "resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true, "dev": true
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": false, "resolved": false,
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -2796,22 +2850,19 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": false, "resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true, "dev": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": false, "resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true, "dev": true
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"resolved": false, "resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"dev": true, "dev": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -2932,8 +2983,7 @@
"version": "2.0.3", "version": "2.0.3",
"resolved": false, "resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true, "dev": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -2947,7 +2997,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -2964,7 +3013,6 @@
"resolved": false, "resolved": false,
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -2973,15 +3021,13 @@
"version": "0.0.8", "version": "0.0.8",
"resolved": false, "resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true, "dev": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -2991,8 +3037,7 @@
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
"dev": true, "dev": true
"optional": true
} }
} }
}, },
@ -3011,7 +3056,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -3100,8 +3144,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": false, "resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true, "dev": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -3115,7 +3158,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -3211,8 +3253,7 @@
"version": "5.1.1", "version": "5.1.1",
"resolved": false, "resolved": false,
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
"dev": true, "dev": true
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -3254,7 +3295,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -3276,7 +3316,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -3334,15 +3373,13 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": false, "resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true, "dev": true
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"dev": true, "dev": true
"optional": true
} }
} }
}, },
@ -3431,15 +3468,13 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true, "dev": true
"optional": true
}, },
"is-glob": { "is-glob": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"is-extglob": "^1.0.0" "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": { "http-errors": {
"version": "1.7.1", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.1.tgz", "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", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"remove-trailing-separator": "^1.0.1" "remove-trailing-separator": "^1.0.1"
} }
@ -5196,8 +5268,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true, "dev": true
"optional": true
}, },
"is-glob": { "is-glob": {
"version": "2.0.1", "version": "2.0.1",
@ -5837,15 +5908,13 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
"dev": true, "dev": true
"optional": true
}, },
"repeat-element": { "repeat-element": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz",
"integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=",
"dev": true, "dev": true
"optional": true
}, },
"repeat-string": { "repeat-string": {
"version": "1.6.1", "version": "1.6.1",

View File

@ -87,6 +87,7 @@
"async-mutex": "^0.1.3", "async-mutex": "^0.1.3",
"base-64": "^0.1.0", "base-64": "^0.1.0",
"chokidar": "^3.0.0", "chokidar": "^3.0.0",
"clean-html": "^1.5.0",
"compare-versions": "^3.2.1", "compare-versions": "^3.2.1",
"diacritics": "^1.3.0", "diacritics": "^1.3.0",
"diff-match-patch": "^1.0.4", "diff-match-patch": "^1.0.4",

11
ElectronClient/package-lock.json generated Normal file
View File

@ -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=="
}
}
}

View 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(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};

View File

@ -1,5 +1,6 @@
const stringPadding = require('string-padding'); const stringPadding = require('string-padding');
const stringToStream = require('string-to-stream'); const stringToStream = require('string-to-stream');
const resourceUtils = require('lib/resourceUtils.js');
const BLOCK_OPEN = '[[BLOCK_OPEN]]'; const BLOCK_OPEN = '[[BLOCK_OPEN]]';
const BLOCK_CLOSE = '[[BLOCK_CLOSE]]'; const BLOCK_CLOSE = '[[BLOCK_CLOSE]]';
@ -295,12 +296,6 @@ function collapseWhiteSpaceAndAppend(lines, state, text) {
return lines; 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) { function tagAttributeToMdText(attr) {
// HTML attributes may contain newlines so remove them. // HTML attributes may contain newlines so remove them.
// https://github.com/laurent22/joplin/issues/1583 // https://github.com/laurent22/joplin/issues/1583
@ -318,7 +313,7 @@ function addResourceTag(lines, resource, alt = '') {
if (!alt) alt = ''; if (!alt) alt = '';
alt = tagAttributeToMdText(alt); alt = tagAttributeToMdText(alt);
if (isImageMimeType(resource.mime)) { if (resourceUtils.isImageMimeType(resource.mime)) {
lines.push('!['); lines.push('![');
lines.push(alt); lines.push(alt);
lines.push('](:/' + resource.id + ')'); lines.push('](:/' + resource.id + ')');

View File

@ -5,6 +5,7 @@ const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js'); const Tag = require('lib/models/Tag.js');
const Resource = require('lib/models/Resource.js'); const Resource = require('lib/models/Resource.js');
const { enexXmlToMd } = require('./import-enex-md-gen.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 { time } = require('lib/time-utils.js');
const Levenshtein = require('levenshtein'); const Levenshtein = require('levenshtein');
const md5 = require('md5'); const md5 = require('md5');
@ -164,6 +165,7 @@ async function saveNoteToStorage(note, fuzzyMatching = false) {
function importEnex(parentFolderId, filePath, importOptions = null) { function importEnex(parentFolderId, filePath, importOptions = null) {
if (!importOptions) importOptions = {}; if (!importOptions) importOptions = {};
// console.info(JSON.stringify({importOptions}, null, 2));
if (!('fuzzyMatching' in importOptions)) importOptions.fuzzyMatching = false; if (!('fuzzyMatching' in importOptions)) importOptions.fuzzyMatching = false;
if (!('onProgress' in importOptions)) importOptions.onProgress = function() {}; if (!('onProgress' in importOptions)) importOptions.onProgress = function() {};
if (!('onError' in importOptions)) importOptions.onError = function() {}; if (!('onError' in importOptions)) importOptions.onError = function() {};
@ -216,9 +218,15 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
while (notes.length) { while (notes.length) {
let note = notes.shift(); 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; delete note.bodyXml;
note.markup_language = importOptions.outputFormat === 'html' ?
Note.MARKUP_LANGUAGE_HTML :
Note.MARKUP_LANGUAGE_MARKDOWN;
// console.info('*************************************************************************'); // console.info('*************************************************************************');
// console.info(body); // console.info(body);
// console.info('*************************************************************************'); // console.info('*************************************************************************');

View 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;

View File

@ -13,8 +13,7 @@ const { toTitleCase } = require('lib/string-utils');
class InteropService { class InteropService {
constructor() { constructor() {
this.modules_ = null; this.modules_ = null; }
}
modules() { modules() {
if (this.modules_) return this.modules_; if (this.modules_) return this.modules_;
@ -42,7 +41,16 @@ class InteropService {
format: 'enex', format: 'enex',
fileExtensions: ['enex'], fileExtensions: ['enex'],
sources: ['file'], 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 => { importModules = importModules.map(a => {
const className = 'InteropService_Importer_' + toTitleCase(a.format); const className = a.importerClass || 'InteropService_Importer_' + toTitleCase(a.format);
const output = Object.assign( const output = Object.assign(
{}, {},
{ {
@ -112,8 +120,9 @@ class InteropService {
return this.modules_; return this.modules_;
} }
moduleByFormat_(type, format) { findModuleByFormat_(type, format) {
const modules = this.modules(); const modules = this.modules();
// console.log(JSON.stringify({modules}, null, 2))
for (let i = 0; i < modules.length; i++) { for (let i = 0; i < modules.length; i++) {
const m = modules[i]; const m = modules[i];
if (m.format === format && m.type === type) return modules[i]; if (m.format === format && m.type === type) return modules[i];
@ -121,8 +130,8 @@ class InteropService {
return null; return null;
} }
newModule_(type, format) { newModuleByFormat_(type, format) {
const module = this.moduleByFormat_(type, format); const module = this.findModuleByFormat_(type, format);
if (!module) throw new Error(_('Cannot load "%s" module for format "%s"', type, format)); if (!module) throw new Error(_('Cannot load "%s" module for format "%s"', type, format));
const ModuleClass = require(module.path); const ModuleClass = require(module.path);
const output = new ModuleClass(); const output = new ModuleClass();
@ -130,6 +139,26 @@ class InteropService {
return output; 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) { moduleByFileExtension_(type, ext) {
ext = ext.toLowerCase(); ext = ext.toLowerCase();
@ -173,7 +202,10 @@ class InteropService {
let result = { warnings: [] }; 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); await importer.init(options.path, options);
result = await importer.exec(result); result = await importer.exec(result);
@ -248,7 +280,7 @@ class InteropService {
await queueExportItem(BaseModel.TYPE_TAG, exportedTagIds[i]); await queueExportItem(BaseModel.TYPE_TAG, exportedTagIds[i]);
} }
const exporter = this.newModule_('exporter', exportFormat); const exporter = this.newModuleByFormat_('exporter', exportFormat);
await exporter.init(exportPath); await exporter.init(exportPath);
const typeOrder = [BaseModel.TYPE_FOLDER, BaseModel.TYPE_RESOURCE, BaseModel.TYPE_NOTE, BaseModel.TYPE_TAG, BaseModel.TYPE_NOTE_TAG]; const typeOrder = [BaseModel.TYPE_FOLDER, BaseModel.TYPE_RESOURCE, BaseModel.TYPE_NOTE, BaseModel.TYPE_TAG, BaseModel.TYPE_NOTE_TAG];

View File

@ -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;

View File

@ -2,7 +2,7 @@ const InteropService_Importer_Base = require('lib/services/InteropService_Import
const Folder = require('lib/models/Folder.js'); const Folder = require('lib/models/Folder.js');
const { filename } = require('lib/path-utils.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) { async exec(result) {
const { importEnex } = require('lib/import-enex'); 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;