1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-01-14 00:29:38 +02:00

Compare commits

..

7 Commits

Author SHA1 Message Date
Laurent Cozic
8411e1270e fix 2021-08-22 11:25:51 +01:00
Laurent Cozic
e9d4a777fd refactor 2021-08-22 11:23:07 +01:00
Laurent Cozic
85984f1f39 comment 2021-08-21 18:17:17 +01:00
Laurent Cozic
3c0524c6e9 update 2021-08-21 17:50:40 +01:00
Laurent Cozic
769d47a768 flags 2021-08-21 16:59:50 +01:00
Laurent Cozic
87fe0e4dcf update 2021-08-21 15:14:54 +01:00
Laurent Cozic
2513e0aaab update 2021-08-21 15:06:41 +01:00
207 changed files with 2764 additions and 3517 deletions

View File

@@ -84,6 +84,9 @@ packages/app-cli/tests/HtmlToMd.js.map
packages/app-cli/tests/MdToHtml.d.ts
packages/app-cli/tests/MdToHtml.js
packages/app-cli/tests/MdToHtml.js.map
packages/app-cli/tests/MdToMd.d.ts
packages/app-cli/tests/MdToMd.js
packages/app-cli/tests/MdToMd.js.map
packages/app-cli/tests/services/keychain/KeychainService.d.ts
packages/app-cli/tests/services/keychain/KeychainService.js
packages/app-cli/tests/services/keychain/KeychainService.js.map
@@ -1092,6 +1095,12 @@ packages/lib/services/CommandService.test.js.map
packages/lib/services/DecryptionWorker.d.ts
packages/lib/services/DecryptionWorker.js
packages/lib/services/DecryptionWorker.js.map
packages/lib/services/EncryptionService.d.ts
packages/lib/services/EncryptionService.js
packages/lib/services/EncryptionService.js.map
packages/lib/services/EncryptionService.test.d.ts
packages/lib/services/EncryptionService.test.js
packages/lib/services/EncryptionService.test.js.map
packages/lib/services/ExternalEditWatcher.d.ts
packages/lib/services/ExternalEditWatcher.js
packages/lib/services/ExternalEditWatcher.js.map
@@ -1185,12 +1194,6 @@ packages/lib/services/database/types.js.map
packages/lib/services/debug/populateDatabase.d.ts
packages/lib/services/debug/populateDatabase.js
packages/lib/services/debug/populateDatabase.js.map
packages/lib/services/e2ee/EncryptionService.d.ts
packages/lib/services/e2ee/EncryptionService.js
packages/lib/services/e2ee/EncryptionService.js.map
packages/lib/services/e2ee/EncryptionService.test.d.ts
packages/lib/services/e2ee/EncryptionService.test.js
packages/lib/services/e2ee/EncryptionService.test.js.map
packages/lib/services/e2ee/types.d.ts
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/types.js.map
@@ -1245,9 +1248,6 @@ packages/lib/services/interop/InteropService_Importer_Jex.js.map
packages/lib/services/interop/InteropService_Importer_Md.d.ts
packages/lib/services/interop/InteropService_Importer_Md.js
packages/lib/services/interop/InteropService_Importer_Md.js.map
packages/lib/services/interop/InteropService_Importer_Md.test.d.ts
packages/lib/services/interop/InteropService_Importer_Md.test.js
packages/lib/services/interop/InteropService_Importer_Md.test.js.map
packages/lib/services/interop/InteropService_Importer_Raw.d.ts
packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/InteropService_Importer_Raw.js.map
@@ -1740,9 +1740,6 @@ packages/tools/generate-database-types.js.map
packages/tools/generate-images.d.ts
packages/tools/generate-images.js
packages/tools/generate-images.js.map
packages/tools/git-changelog.d.ts
packages/tools/git-changelog.js
packages/tools/git-changelog.js.map
packages/tools/lerna-add.d.ts
packages/tools/lerna-add.js
packages/tools/lerna-add.js.map

21
.gitignore vendored
View File

@@ -69,6 +69,9 @@ packages/app-cli/tests/HtmlToMd.js.map
packages/app-cli/tests/MdToHtml.d.ts
packages/app-cli/tests/MdToHtml.js
packages/app-cli/tests/MdToHtml.js.map
packages/app-cli/tests/MdToMd.d.ts
packages/app-cli/tests/MdToMd.js
packages/app-cli/tests/MdToMd.js.map
packages/app-cli/tests/services/keychain/KeychainService.d.ts
packages/app-cli/tests/services/keychain/KeychainService.js
packages/app-cli/tests/services/keychain/KeychainService.js.map
@@ -1077,6 +1080,12 @@ packages/lib/services/CommandService.test.js.map
packages/lib/services/DecryptionWorker.d.ts
packages/lib/services/DecryptionWorker.js
packages/lib/services/DecryptionWorker.js.map
packages/lib/services/EncryptionService.d.ts
packages/lib/services/EncryptionService.js
packages/lib/services/EncryptionService.js.map
packages/lib/services/EncryptionService.test.d.ts
packages/lib/services/EncryptionService.test.js
packages/lib/services/EncryptionService.test.js.map
packages/lib/services/ExternalEditWatcher.d.ts
packages/lib/services/ExternalEditWatcher.js
packages/lib/services/ExternalEditWatcher.js.map
@@ -1170,12 +1179,6 @@ packages/lib/services/database/types.js.map
packages/lib/services/debug/populateDatabase.d.ts
packages/lib/services/debug/populateDatabase.js
packages/lib/services/debug/populateDatabase.js.map
packages/lib/services/e2ee/EncryptionService.d.ts
packages/lib/services/e2ee/EncryptionService.js
packages/lib/services/e2ee/EncryptionService.js.map
packages/lib/services/e2ee/EncryptionService.test.d.ts
packages/lib/services/e2ee/EncryptionService.test.js
packages/lib/services/e2ee/EncryptionService.test.js.map
packages/lib/services/e2ee/types.d.ts
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/types.js.map
@@ -1230,9 +1233,6 @@ packages/lib/services/interop/InteropService_Importer_Jex.js.map
packages/lib/services/interop/InteropService_Importer_Md.d.ts
packages/lib/services/interop/InteropService_Importer_Md.js
packages/lib/services/interop/InteropService_Importer_Md.js.map
packages/lib/services/interop/InteropService_Importer_Md.test.d.ts
packages/lib/services/interop/InteropService_Importer_Md.test.js
packages/lib/services/interop/InteropService_Importer_Md.test.js.map
packages/lib/services/interop/InteropService_Importer_Raw.d.ts
packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/InteropService_Importer_Raw.js.map
@@ -1725,9 +1725,6 @@ packages/tools/generate-database-types.js.map
packages/tools/generate-images.d.ts
packages/tools/generate-images.js
packages/tools/generate-images.js.map
packages/tools/git-changelog.d.ts
packages/tools/git-changelog.js
packages/tools/git-changelog.js.map
packages/tools/lerna-add.d.ts
packages/tools/lerna-add.js
packages/tools/lerna-add.js.map

View File

@@ -31,14 +31,12 @@ Joplin is available in multiple languages thanks to the help of its users. You c
If you want to start contributing to the project's code, please follow these guidelines before creating a pull request:
- Explain WHY you want to add this change. Explain it inside the pull request and you may link to an issue for additional information, but the PR should gives a clear overview of why you want to add this.
- Bug fixes are always welcome. Start by reviewing the [list of bugs](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
- A good way to easily start contributing is to pick and work on a [good first issue](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). We try to make these issues as clear as possible and provide basic info on how the code should be changed, and if something is unclear feel free to ask for more information on the issue.
- Before adding a new feature, ask about it in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue) or the [Joplin Forum](https://discourse.joplinapp.org/), or check if existing discussions exist to make sure the new functionality is desired.
- **Changes that will consist in more than 50 lines of code should be discussed the [Joplin Forum](https://discourse.joplinapp.org/)**, so that you don't spend too much time implementing something that might not be accepted.
- All the applications share the same backend (database, synchronisation, settings, models, business logic, etc.) so if you change something in the backend in one app, makes sure it still work in the other apps. Usually it does, but keep this in mind.
- Pull requests that make many changes using an automated tool, like for spell fixing, styling, etc. will not be accepted. An exception would be if the changes have been discussed in the forum and someone has agreed to review **and test** the pull request.
- Pull requests that make address multiple issues will most likely stall and eventually be closed. This is because we might be fine with one of the changes but not with others and untangling that kind of pull request is too much hassle both for maintainers and the person who submitted it. So most of the time someone gives up and the PR gets closed. So please keep the pull request focused on one issue.
Building the apps is relatively easy - please [see the build instructions](https://github.com/laurent22/joplin/blob/dev/BUILD.md) for more details.

View File

@@ -1,11 +1,6 @@
# https://versatile.nl/blog/deploying-lerna-web-apps-with-docker
FROM node:16-bullseye
RUN apt-get update \
&& apt-get install -y \
python \
&& rm -rf /var/lib/apt/lists/*
FROM node:16
RUN echo "Node: $(node --version)"
RUN echo "Npm: $(npm --version)"

View File

@@ -511,7 +511,7 @@ Current translations:
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | Mustafa Al-Dailemi (dailemi@hotmail.com)Language-Team: | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | Mustafa Al-Dailemi (dailemi@hotmail.com)Language-Team: | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Atalanttore](mailto:atalanttore@googlemail.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 54%
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
@@ -527,7 +527,7 @@ Current translations:
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 87%
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | Alexander Dawson | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 67%
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 68%
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Nicolas Suzuki](mailto:nicolas.suzuki@pm.me) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 90%
@@ -536,13 +536,13 @@ Current translations:
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 42%
<img src="https://joplinapp.org/images/flags/country-4x3/vi.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 81%
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [南宫小骏](mailto:jackytsu@vip.qq.com) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Po-Chiang Chao](mailto:BobChao%29%20%28bobchao@gmail.com) | 94%
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [南宫小骏](mailto:jackytsu@vip.qq.com) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Po-Chiang Chao](mailto:BobChao%29%20%28bobchao@gmail.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 95%
<!-- LOCALE-TABLE-AUTO-GENERATED -->

View File

@@ -1,6 +1,6 @@
const { BaseCommand } = require('./base-command.js');
import { _ } from '@joplin/lib/locale';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import BaseItem from '@joplin/lib/models/BaseItem';
import Setting from '@joplin/lib/models/Setting';

View File

@@ -27,7 +27,7 @@ const { shimInit } = require('@joplin/lib/shim-init-node.js');
const shim = require('@joplin/lib/shim').default;
const { _ } = require('@joplin/lib/locale');
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default;
const EncryptionService = require('@joplin/lib/services/EncryptionService').default;
const envFromArgs = require('@joplin/lib/envFromArgs');
const env = envFromArgs(process.argv);

View File

@@ -32,7 +32,7 @@
],
"owner": "Laurent Cozic"
},
"version": "2.4.0",
"version": "2.3.2",
"bin": {
"joplin": "./main.js"
},
@@ -40,8 +40,8 @@
"node": ">=10.0.0"
},
"dependencies": {
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"@joplin/lib": "~2.3",
"@joplin/renderer": "~2.3",
"aws-sdk": "^2.588.0",
"chalk": "^4.1.0",
"compare-version": "^0.1.2",
@@ -65,7 +65,7 @@
"yargs-parser": "^7.0.0"
},
"devDependencies": {
"@joplin/tools": "~2.4",
"@joplin/tools": "~2.3",
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",

View File

@@ -0,0 +1,50 @@
const mdImporterService = require('@joplin/lib/services/interop/InteropService_Importer_Md').default;
const Note = require('@joplin/lib/models/Note').default;
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
const importer = new mdImporterService();
describe('InteropService_Importer_Md: importLocalImages', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should import linked files and modify tags appropriately', async function() {
const tagNonExistentFile = '![does not exist](does_not_exist.png)';
const note = await importer.importFile(`${__dirname}/md_to_md/sample.md`, 'notebook');
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(2);
const inexistentLinkUnchanged = note.body.includes(tagNonExistentFile);
expect(inexistentLinkUnchanged).toBe(true);
});
it('should only create 1 resource for duplicate links, all tags should be updated', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-duplicate-links.md`, 'notebook');
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
const reg = new RegExp(items[0].id, 'g');
const matched = note.body.match(reg);
expect(matched.length).toBe(2);
});
it('should import linked files and modify tags appropriately when link is also in alt text', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-link-in-alt-text.md`, 'notebook');
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
});
it('should passthrough unchanged if no links present', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-no-links.md`, 'notebook');
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(0);
expect(note.body).toContain('Unidentified vessel travelling at sub warp speed, bearing 235.7. Fluctuations in energy readings from it, Captain. All transporters off.');
});
it('should import linked image with special characters in name', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-special-chars.md`, 'notebook');
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
});
it('should import resources for files', async function() {
const note = await importer.importFile(`${__dirname}/md_to_md/sample-files.md`, 'notebook');
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(4);
});
});

View File

@@ -0,0 +1,2 @@
![link 1](../support/photo.jpg)
![link 2](../support/photo.jpg)

View File

@@ -0,0 +1,9 @@
# Markdown file test
![../support/photo.jpg](../support/photo.jpg)
[welcome.pdf](../support/welcome.pdf)
[sample.md](sample.md)
[sample2.md](./sample.md)

View File

@@ -0,0 +1,3 @@
# Markdown
![../support/photo.jpg](../support/photo.jpg) should put resource link inside () not []
![../support/photo.jpg]( ../support/photo.jpg ) this case (spaces before/after link but within parens) is not currently covered

View File

@@ -0,0 +1 @@
![link special chars](../support/photo-åäö.jpg)

View File

@@ -0,0 +1,13 @@
# Markdown
lorem ipsum ![alt text here](../support/photo.jpg)
- [ ] check!
- [ ] boxes!
![alt text here](../support/photo-two.jpg)ipsum lorem
**strong text**
![does not exist](does_not_exist.png) lorem ipsum
**some directory**
![a directory](../support) lorem ipsum

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,3 +0,0 @@
# Test Spaces
I hope this get's imported correctly!

View File

@@ -1 +0,0 @@
[Section 1](./sample-no-links.md#markdown)

View File

@@ -1,3 +0,0 @@
# Markdown file test
[sample.md](sample-cycles-b.md)

View File

@@ -1,4 +0,0 @@
# Markdown file test
[sample.md](./sample-cycles-a.md)

View File

@@ -1,2 +0,0 @@
![link 1](../../photo.jpg)
![link 2](../../photo.jpg)

View File

@@ -1 +0,0 @@
![sample](file://../../photo.jpg)

View File

@@ -1,9 +0,0 @@
# Markdown file test
![../../photo.jpg](../../photo.jpg)
[welcome.pdf](../../welcome.pdf)
[sample.md](sample.md)
[sample2.md](./sample.md)

View File

@@ -1,3 +0,0 @@
# Markdown
![../../photo.jpg](../../photo.jpg) should put resource link inside () not []
![../../photo.jpg]( ../../photo.jpg ) this case (spaces before/after link but within parens) is not currently covered

View File

@@ -1,3 +0,0 @@
![Alt text](../../photo.jpg "Scott Joplin")
![Worst Case](<../../photo sample.jpg> "title")
[Worst Case](<./sample spaces.md> "title")

View File

@@ -1 +0,0 @@
I am here, but am I alive?

View File

@@ -1,3 +0,0 @@
# Some Title
[link](./sample-md)

View File

@@ -1,4 +0,0 @@
![link special chars](../../photo-åäö.jpg)
[sample photo](../../photo%20sample.jpg)
[sample.md](./sample%20spaces.md)
[sample special syntax](<../../photo sample.jpg>)

View File

@@ -1,4 +0,0 @@
<img src="../../photo.jpg">
<img src='../../photo-two.jpg'>
<img src='does-not-exist' alt="../../photo.jpg">
<a href="./sample-no-links.md">

View File

@@ -1,13 +0,0 @@
# Markdown
lorem ipsum ![alt text here](../../photo.jpg)
- [ ] check!
- [ ] boxes!
![alt text here](../../photo-two.jpg)ipsum lorem
**strong text**
![does not exist](does_not_exist.png) lorem ipsum
**some directory**
![a directory](../..) lorem ipsum

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Joplin Web Clipper [DEV]",
"version": "2.4.0",
"version": "2.3.0",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": "script-src 'self'; object-src 'self'",

View File

@@ -7,7 +7,7 @@ import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import ClipperServer from '@joplin/lib/ClipperServer';
import Setting from '@joplin/lib/models/Setting';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import { AppState } from '../app';
class ClipperConfigScreenComponent extends React.Component {

View File

@@ -299,7 +299,7 @@ export default function(props: Props) {
function renderRepoApiError() {
if (!repoApiError) return null;
return <RepoApiErrorMessage maxWidth={maxWidth} type="error">{_('Could not connect to plugin repository.')}<br/><br/>- <StyledLink href="#" onClick={() => { setFetchManifestTime(Date.now()); }}>{_('Try again')}</StyledLink><br/><br/>- <StyledLink href="#" onClick={onBrowsePlugins}>{_('Browse all plugins')}</StyledLink></RepoApiErrorMessage>;
return <RepoApiErrorMessage maxWidth={maxWidth} type="error">{_('Could not connect to plugin repository')} - <StyledLink href="#" onClick={() => { setFetchManifestTime(Date.now()); }}>{_('Try again')}</StyledLink></RepoApiErrorMessage>;
}
function renderBottomArea() {

View File

@@ -1,7 +1,7 @@
const React = require('react');
const { connect } = require('react-redux');
import Setting from '@joplin/lib/models/Setting';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';

View File

@@ -29,7 +29,7 @@ import { themeStyle } from '@joplin/lib/theme';
import validateLayout from '../ResizableLayout/utils/validateLayout';
import iterateItems from '../ResizableLayout/utils/iterateItems';
import removeItem from '../ResizableLayout/utils/removeItem';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import ShareFolderDialog from '../ShareFolderDialog/ShareFolderDialog';
import { ShareInvitation } from '@joplin/lib/services/share/reducer';
import ShareService from '@joplin/lib/services/share/ShareService';

View File

@@ -34,7 +34,7 @@ export const runtime = (comp: any): CommandRuntime => {
value: '',
autocomplete: startFolders,
onClose: async (answer: any) => {
if (answer) {
if (answer != null) {
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], answer.value);
}

View File

@@ -381,9 +381,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
`.CodeMirror-selected {
background: #6b6b6b !important;
}` : '';
// Vim mode draws a fat cursor in the background, we don't want to add background colors
// to the inline code in this case (it would hide the cursor)
const codeBackgroundColor = Setting.value('editor.keyboardMode') !== 'vim' ? theme.codeBackgroundColor : 'inherit';
const monospaceFonts = [];
if (Setting.value('style.editor.monospaceFontFamily')) monospaceFonts.push(`"${Setting.value('style.editor.monospaceFontFamily')}"`);
monospaceFonts.push('monospace');
@@ -479,9 +476,9 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
}
/* Negative margins are needed to componsate for the border */
div.CodeMirror span.cm-comment.cm-jn-inline-code:not(.cm-search-marker):not(.cm-fat-cursor-mark):not(.cm-search-marker-selected):not(.CodeMirror-selectedtext) {
div.CodeMirror span.cm-comment.cm-jn-inline-code {
border: 1px solid ${theme.codeBorderColor};
background-color: ${codeBackgroundColor};
background-color: ${theme.codeBackgroundColor};
margin-left: -1px;
margin-right: -1px;
border-radius: .25em;

View File

@@ -1,11 +1,6 @@
import { useState, useCallback } from 'react';
import Logger from '@joplin/lib/Logger';
import { SearchMarkers } from './useSearchMarkers';
const logger = Logger.create('useNoteSearchBar');
const queryMaxLength = 1000;
interface LocalSearch {
query: string;
selectedIndex: number;
@@ -29,14 +24,6 @@ export default function useNoteSearchBar() {
const [localSearch, setLocalSearch] = useState<LocalSearch>(defaultLocalSearch());
const onChange = useCallback((query: string) => {
// A query that's too long would make CodeMirror throw an exception
// which would crash the app.
// https://github.com/laurent22/joplin/issues/5380
if (query.length > queryMaxLength) {
logger.warn(`Query is longer than ${queryMaxLength} characters - it is going to be trimmed`);
query = query.substr(0, queryMaxLength);
}
setLocalSearch((prev: LocalSearch) => {
return {
query: query,

View File

@@ -24,7 +24,7 @@ const Logger = require('@joplin/lib/Logger').default;
const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
const shim = require('@joplin/lib/shim').default;
const { shimInit } = require('@joplin/lib/shim-init-node.js');
const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default;
const EncryptionService = require('@joplin/lib/services/EncryptionService').default;
const bridge = require('electron').remote.require('./bridge').default;
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
const React = require('react');

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.3",
"version": "2.3.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/app-desktop",
"version": "2.4.3",
"version": "2.3.5",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.3",
"version": "2.3.5",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -93,7 +93,7 @@
},
"homepage": "https://github.com/laurent22/joplin#readme",
"devDependencies": {
"@joplin/tools": "~2.4",
"@joplin/tools": "~2.3",
"@testing-library/react-hooks": "^3.4.2",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
@@ -122,8 +122,8 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"@joplin/lib": "~2.3",
"@joplin/renderer": "~2.3",
"async-mutex": "^0.1.3",
"codemirror": "^5.56.0",
"color": "^3.1.2",

View File

@@ -142,7 +142,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097648
versionName "2.4.0"
versionName "2.3.4"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -6,7 +6,7 @@ const { BaseScreenComponent } = require('../base-screen.js');
const { themeStyle } = require('../global-style.js');
const DialogBox = require('react-native-dialogbox').default;
const { dialogs } = require('../../utils/dialogs.js');
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import shared from '@joplin/lib/components/shared/encryption-config-shared';

View File

@@ -492,7 +492,7 @@
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.4.0;
MARKETING_VERSION = 12.3.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -519,7 +519,7 @@
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.4.0;
MARKETING_VERSION = 12.3.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -666,7 +666,7 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.4.0;
MARKETING_VERSION = 12.3.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
@@ -697,7 +697,7 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.4.0;
MARKETING_VERSION = 12.3.1;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -488,7 +488,7 @@ SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b
FBReactNativeSpec: 6da2c8ff1ebe6b6cf4510fcca58c24c4d02b16fc
FBReactNativeSpec: d2f54de51f69366bd1f5c1fb9270698dce678f8d
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
JoplinCommonShareExtension: 270b4f8eb4e22828eeda433a04ed689fc1fd09b5
JoplinRNShareExtension: 7137e9787374e1b0797ecbef9103d1588d90e403

View File

@@ -97,7 +97,7 @@ SyncTargetRegistry.addClass(SyncTargetJoplinCloud);
import FsDriverRN from './utils/fs-driver-rn';
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import MigrationService from '@joplin/lib/services/MigrationService';
import { clearSharedFilesCache } from './utils/ShareUtils';
import setIgnoreTlsErrors from './utils/TlsUtils';

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"id": "<%= pluginId %>",
"app_min_version": "2.4",
"app_min_version": "2.3",
"version": "1.0.0",
"name": "<%= pluginName %>",
"description": "<%= pluginDescription %>",

View File

@@ -1,6 +1,6 @@
{
"name": "generator-joplin",
"version": "2.4.0",
"version": "2.3.0",
"description": "Scaffolds out a new Joplin plugin",
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
"author": {

View File

@@ -36,7 +36,7 @@ const SyncTargetNextcloud = require('./SyncTargetNextcloud.js');
const SyncTargetWebDAV = require('./SyncTargetWebDAV.js');
const SyncTargetDropbox = require('./SyncTargetDropbox.js');
const SyncTargetAmazonS3 = require('./SyncTargetAmazonS3.js');
import EncryptionService from './services/e2ee/EncryptionService';
import EncryptionService from './services/EncryptionService';
import ResourceFetcher from './services/ResourceFetcher';
import SearchEngineUtils from './services/searchengine/SearchEngineUtils';
import SearchEngine from './services/searchengine/SearchEngine';

View File

@@ -1,6 +1,6 @@
import Logger from './Logger';
import Synchronizer from './Synchronizer';
import EncryptionService from './services/e2ee/EncryptionService';
import EncryptionService from './services/EncryptionService';
import shim from './shim';
import ResourceService from './services/ResourceService';
import ShareService from './services/share/ShareService';

View File

@@ -15,7 +15,7 @@ import MasterKey from './models/MasterKey';
import BaseModel, { ModelType } from './BaseModel';
import time from './time';
import ResourceService from './services/ResourceService';
import EncryptionService from './services/e2ee/EncryptionService';
import EncryptionService from './services/EncryptionService';
import JoplinError from './JoplinError';
import ShareService from './services/share/ShareService';
import TaskQueue from './TaskQueue';

View File

@@ -1,4 +1,4 @@
import EncryptionService from '../../services/e2ee/EncryptionService';
import EncryptionService from '../../services/EncryptionService';
import { _ } from '../../locale';
import BaseItem from '../../models/BaseItem';
import Setting from '../../models/Setting';

View File

@@ -56,7 +56,7 @@ export default class FsDriverBase {
return output;
}
public async findUniqueFilename(name: string, reservedNames: string[] = null, markdownSafe: boolean = false): Promise<string> {
public async findUniqueFilename(name: string, reservedNames: string[] = null): Promise<string> {
if (reservedNames === null) {
reservedNames = [];
}
@@ -70,11 +70,7 @@ export default class FsDriverBase {
// Check if the filename does not exist in the filesystem and is not reserved
const exists = await this.exists(nameToTry) || reservedNames.includes(nameToTry);
if (!exists) return nameToTry;
if (!markdownSafe) {
nameToTry = `${nameNoExt} (${counter})${extension}`;
} else {
nameToTry = `${nameNoExt}-${counter}${extension}`;
}
nameToTry = `${nameNoExt} (${counter})${extension}`;
counter++;
if (counter >= 1000) {
nameToTry = `${nameNoExt} (${new Date().getTime()})${extension}`;

View File

@@ -45,39 +45,18 @@ class HtmlUtils {
}
// Returns the **encoded** URLs, so to be useful they should be decoded again before use.
private extractUrls(regex: RegExp, html: string) {
public extractImageUrls(html: string) {
if (!html) return [];
const output = [];
let matches;
while ((matches = regex.exec(html))) {
while ((matches = imageRegex.exec(html))) {
output.push(matches[2]);
}
return output.filter(url => !!url);
}
// Returns the **encoded** URLs, so to be useful they should be decoded again before use.
public extractImageUrls(html: string) {
return this.extractUrls(imageRegex, html);
}
// Returns the **encoded** URLs, so to be useful they should be decoded again before use.
public extractAnchorUrls(html: string) {
return this.extractUrls(anchorRegex, html);
}
// Returns the **encoded** URLs, so to be useful they should be decoded again before use.
public extractFileUrls(html: string) {
return this.extractImageUrls(html).concat(this.extractAnchorUrls(html));
}
public replaceResourceUrl(html: string, urlToReplace: string, id: string) {
const htmlLinkRegex = `(?<=(?:src|href)=["'])${urlToReplace}(?=["'])`;
const htmlReg = new RegExp(htmlLinkRegex, 'g');
return html.replace(htmlReg, `:/${id}`);
}
public replaceImageUrls(html: string, callback: Function) {
return this.processImageTags(html, (data: any) => {
const newSrc = callback(data.src);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -48,7 +48,7 @@ stats['bg_BG'] = {"percentDone":56};
stats['ca'] = {"percentDone":95};
stats['hr_HR'] = {"percentDone":96};
stats['cs_CZ'] = {"percentDone":95};
stats['da_DK'] = {"percentDone":99};
stats['da_DK'] = {"percentDone":96};
stats['de_DE'] = {"percentDone":95};
stats['et_EE'] = {"percentDone":54};
stats['en_GB'] = {"percentDone":100};
@@ -64,7 +64,7 @@ stats['hu_HU'] = {"percentDone":84};
stats['nl_BE'] = {"percentDone":87};
stats['nl_NL'] = {"percentDone":90};
stats['nb_NO'] = {"percentDone":96};
stats['fa'] = {"percentDone":67};
stats['fa'] = {"percentDone":68};
stats['pl_PL'] = {"percentDone":90};
stats['pt_BR'] = {"percentDone":96};
stats['pt_PT'] = {"percentDone":90};
@@ -73,13 +73,13 @@ stats['sl_SI'] = {"percentDone":91};
stats['sv'] = {"percentDone":96};
stats['th_TH'] = {"percentDone":42};
stats['vi'] = {"percentDone":96};
stats['tr_TR'] = {"percentDone":99};
stats['tr_TR'] = {"percentDone":95};
stats['uk_UA'] = {"percentDone":89};
stats['el_GR'] = {"percentDone":92};
stats['ru_RU'] = {"percentDone":89};
stats['ru_RU'] = {"percentDone":90};
stats['sr_RS'] = {"percentDone":81};
stats['zh_CN'] = {"percentDone":99};
stats['zh_TW'] = {"percentDone":94};
stats['zh_CN'] = {"percentDone":96};
stats['zh_TW'] = {"percentDone":95};
stats['ja_JP'] = {"percentDone":95};
stats['ko'] = {"percentDone":95};
module.exports = { locales: locales, stats: stats };

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -100,12 +100,6 @@ const markdownUtils = {
return output;
},
replaceResourceUrl(md: string, urlToReplace: string, id: string) {
const linkRegex = `(?<=\\]\\()\\<?${urlToReplace}\\>?(?=.*\\))`;
const reg = new RegExp(linkRegex, 'g');
return md.replace(reg, `:/${id}`);
},
extractImageUrls(md: string) {
return markdownUtils.extractFileUrls(md,true);
},

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/lib",
"version": "2.4.0",
"version": "2.3.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/lib",
"version": "2.4.0",
"version": "2.3.1",
"license": "ISC",
"dependencies": {
"async-mutex": "^0.1.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/lib",
"version": "2.4.0",
"version": "2.3.1",
"description": "Joplin Core library",
"author": "Laurent Cozic",
"homepage": "",
@@ -27,7 +27,7 @@
"dependencies": {
"@joplin/fork-htmlparser2": "^4.1.33",
"@joplin/fork-sax": "^1.2.37",
"@joplin/renderer": "~2.4",
"@joplin/renderer": "^2.3.1",
"@joplin/turndown": "^4.0.55",
"@joplin/turndown-plugin-gfm": "^1.0.37",
"async-mutex": "^0.1.3",

View File

@@ -1,11 +1,11 @@
import { fileContentEqual, setupDatabaseAndSynchronizer, supportDir, switchClient, objectsEqual, checkThrowAsync, msleep } from '../../testing/test-utils';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import Setting from '../../models/Setting';
import BaseItem from '../../models/BaseItem';
import MasterKey from '../../models/MasterKey';
import EncryptionService from './EncryptionService';
import { setEncryptionEnabled } from '../synchronizer/syncInfoUtils';
import { fileContentEqual, setupDatabaseAndSynchronizer, supportDir, switchClient, objectsEqual, checkThrowAsync, msleep } from '../testing/test-utils';
import Folder from '../models/Folder';
import Note from '../models/Note';
import Setting from '../models/Setting';
import BaseItem from '../models/BaseItem';
import MasterKey from '../models/MasterKey';
import EncryptionService from '../services/EncryptionService';
import { setEncryptionEnabled } from '../services/synchronizer/syncInfoUtils';
let service: EncryptionService = null;

View File

@@ -1,12 +1,12 @@
import { MasterKeyEntity } from './types';
import Logger from '../../Logger';
import shim from '../../shim';
import Setting from '../../models/Setting';
import MasterKey from '../../models/MasterKey';
import BaseItem from '../../models/BaseItem';
import JoplinError from '../../JoplinError';
import { getActiveMasterKeyId, setActiveMasterKeyId } from '../synchronizer/syncInfoUtils';
const { padLeft } = require('../../string-utils.js');
import { MasterKeyEntity } from './e2ee/types';
import Logger from '../Logger';
import shim from '../shim';
import Setting from '../models/Setting';
import MasterKey from '../models/MasterKey';
import BaseItem from '../models/BaseItem';
import JoplinError from '../JoplinError';
import { getActiveMasterKeyId, setActiveMasterKeyId } from './synchronizer/syncInfoUtils';
const { padLeft } = require('../string-utils.js');
function hexPad(s: string, length: number) {
return padLeft(s, length, '0');
@@ -189,9 +189,7 @@ export default class EncryptionService {
}
masterKeysThatNeedUpgrading(masterKeys: MasterKeyEntity[]) {
const output = MasterKey.allWithoutEncryptionMethod(masterKeys, this.defaultMasterKeyEncryptionMethod_);
// Anything below 5 is a new encryption method and doesn't need an upgrade
return output.filter(mk => mk.encryption_method <= 5);
return MasterKey.allWithoutEncryptionMethod(masterKeys, this.defaultMasterKeyEncryptionMethod_);
}
async upgradeMasterKey(model: MasterKeyEntity, decryptionPassword: string) {

View File

@@ -1,4 +1,4 @@
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow } from '../../testing/test-utils';
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService } from '../../testing/test-utils';
import MasterKey from '../../models/MasterKey';
import { showMissingMasterKeyMessage } from './utils';
import { localSyncInfo, setMasterKeyEnabled } from '../synchronizer/syncInfoUtils';
@@ -34,8 +34,6 @@ describe('e2ee/utils', function() {
setMasterKeyEnabled(mk2.id, true);
expect(showMissingMasterKeyMessage(localSyncInfo(), [mk1.id, mk2.id])).toBe(true);
await expectNotThrow(async () => showMissingMasterKeyMessage(localSyncInfo(), ['not_downloaded_yet']));
const syncInfo = localSyncInfo();
syncInfo.masterKeys = [];
expect(showMissingMasterKeyMessage(syncInfo, [mk1.id, mk2.id])).toBe(false);

View File

@@ -3,7 +3,7 @@ import BaseItem from '../../models/BaseItem';
import MasterKey from '../../models/MasterKey';
import Setting from '../../models/Setting';
import { MasterKeyEntity } from './types';
import EncryptionService from './EncryptionService';
import EncryptionService from '../EncryptionService';
import { getActiveMasterKeyId, masterKeyEnabled, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
const logger = Logger.create('e2ee/utils');
@@ -98,14 +98,6 @@ export function showMissingMasterKeyMessage(syncInfo: SyncInfo, notLoadedMasterK
for (let i = notLoadedMasterKeys.length - 1; i >= 0; i--) {
const mk = syncInfo.masterKeys.find(mk => mk.id === notLoadedMasterKeys[i]);
// A "notLoadedMasterKey" is a key that either doesn't exist or for
// which a password hasn't been set yet. For the purpose of this
// function, we only want to notify the user about unset passwords.
// Master keys that haven't been downloaded yet should normally be
// downloaded at some point.
// https://github.com/laurent22/joplin/issues/5391
if (!mk) continue;
if (!masterKeyEnabled(mk)) notLoadedMasterKeys.pop();
}

View File

@@ -442,10 +442,10 @@ describe('services_InteropService', function() {
await service.export({ path: outDir, format: 'md' });
expect(await shim.fsDriver().exists(`${outDir}/folder1/生活.md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/生活-1.md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/生活-2.md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/生活 (1).md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/生活 (2).md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/Untitled.md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/Untitled-1.md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/Untitled (1).md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/folder1/salut, ça roule _.md`)).toBe(true);
expect(await shim.fsDriver().exists(`${outDir}/ジョプリン/ジョプリン.md`)).toBe(true);
}));

View File

@@ -53,7 +53,7 @@ export default class InteropService {
{
...defaultImportExportModule(ModuleType.Importer),
format: 'md',
fileExtensions: ['md', 'markdown', 'txt', 'html'],
fileExtensions: ['md', 'markdown', 'txt'],
sources: [FileSystemItem.File, FileSystemItem.Directory],
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
description: _('Markdown'),
@@ -401,16 +401,10 @@ export default class InteropService {
resourcePaths: {},
};
// Prepare to process each type before starting any
// This will allow exporters to operate on the full context
for (let typeOrderIndex = 0; typeOrderIndex < typeOrder.length; typeOrderIndex++) {
const type = typeOrder[typeOrderIndex];
await exporter.prepareForProcessingItemType(type, itemsToExport);
}
for (let typeOrderIndex = 0; typeOrderIndex < typeOrder.length; typeOrderIndex++) {
const type = typeOrder[typeOrderIndex];
for (let i = 0; i < itemsToExport.length; i++) {
const itemType = itemsToExport[i].type;

View File

@@ -9,7 +9,6 @@ const Folder = require('../../models/Folder').default;
const Resource = require('../../models/Resource').default;
const Note = require('../../models/Note').default;
const shim = require('../../shim').default;
const { MarkupToHtml } = require('@joplin/renderer');
describe('interop/InteropService_Exporter_Md', function() {
@@ -52,7 +51,7 @@ describe('interop/InteropService_Exporter_Md', function() {
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note1.body))[0]);
const folder2 = await Folder.save({ title: 'folder2' });
let note3 = await Note.save({ title: 'note3', parent_id: folder2.id, markup_language: MarkupToHtml.MARKUP_LANGUAGE_HTML });
let note3 = await Note.save({ title: 'note3', parent_id: folder2.id });
await shim.attachFileToNote(note3, `${supportDir}/photo.jpg`);
note3 = await Note.load(note3.id);
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
@@ -68,53 +67,7 @@ describe('interop/InteropService_Exporter_Md', function() {
expect(Object.keys(exporter.context().notePaths).length).toBe(3, 'There should be 3 note paths in the context.');
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
expect(exporter.context().notePaths[note2.id]).toBe('folder1/note2.md');
expect(exporter.context().notePaths[note3.id]).toBe('folder2/note3.html');
}));
it('should create resource paths and add them to context', (async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir());
const itemsToExport = [];
const queueExportItem = (itemType, itemOrId) => {
itemsToExport.push({
type: itemType,
itemOrId: itemOrId,
});
};
const folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
const note2 = await Note.save({ title: 'note2', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${supportDir}/photo.jpg`);
note1 = await Note.load(note1.id);
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
queueExportItem(BaseModel.TYPE_NOTE, note1);
queueExportItem(BaseModel.TYPE_NOTE, note2);
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note1.body))[0]);
const resource1 = await Resource.load(itemsToExport[3].itemOrId);
const folder2 = await Folder.save({ title: 'folder2' });
let note3 = await Note.save({ title: 'note3', parent_id: folder2.id });
await shim.attachFileToNote(note3, `${supportDir}/photo.jpg`);
note3 = await Note.load(note3.id);
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
queueExportItem(BaseModel.TYPE_NOTE, note3);
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note3.body))[0]);
const resource2 = await Resource.load(itemsToExport[6].itemOrId);
await exporter.processItem(Folder.modelType(), folder1);
await exporter.processItem(Folder.modelType(), folder2);
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
await exporter.processResource(resource1, Resource.fullPath(resource1));
await exporter.processResource(resource2, Resource.fullPath(resource2));
expect(!exporter.context() && !(exporter.context().destResourcePaths || Object.keys(exporter.context().destResourcePaths).length)).toBe(false, 'Context should be empty before processing.');
expect(Object.keys(exporter.context().destResourcePaths).length).toBe(2, 'There should be 2 resource paths in the context.');
expect(exporter.context().destResourcePaths[resource1.id]).toBe(`${exportDir()}/_resources/photo.jpg`);
expect(exporter.context().destResourcePaths[resource2.id]).toBe(`${exportDir()}/_resources/photo-1.jpg`);
expect(exporter.context().notePaths[note3.id]).toBe('folder2/note3.md');
}));
it('should handle duplicate note names', (async () => {
@@ -141,7 +94,7 @@ describe('interop/InteropService_Exporter_Md', function() {
expect(Object.keys(exporter.context().notePaths).length).toBe(2, 'There should be 2 note paths in the context.');
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
expect(exporter.context().notePaths[note1_2.id]).toBe('folder1/note1-1.md');
expect(exporter.context().notePaths[note1_2.id]).toBe('folder1/note1 (1).md');
}));
it('should not override existing files', (async () => {
@@ -168,7 +121,7 @@ describe('interop/InteropService_Exporter_Md', function() {
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
expect(Object.keys(exporter.context().notePaths).length).toBe(1, 'There should be 1 note paths in the context.');
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1-1.md');
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1 (1).md');
}));
it('should save resource files in _resource directory', (async () => {
@@ -204,8 +157,8 @@ describe('interop/InteropService_Exporter_Md', function() {
await exporter.processResource(resource1, Resource.fullPath(resource1));
await exporter.processResource(resource2, Resource.fullPath(resource2));
expect(await shim.fsDriver().exists(`${exportDir()}/_resources/photo.jpg`)).toBe(true, 'Resource file should be copied to _resources directory.');
expect(await shim.fsDriver().exists(`${exportDir()}/_resources/photo-1.jpg`)).toBe(true, 'Resource file should be copied to _resources directory.');
expect(await shim.fsDriver().exists(`${exportDir()}/_resources/${Resource.filename(resource1)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
expect(await shim.fsDriver().exists(`${exportDir()}/_resources/${Resource.filename(resource2)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
}));
it('should create folders in fs', (async () => {
@@ -302,51 +255,23 @@ describe('interop/InteropService_Exporter_Md', function() {
queueExportItem(BaseModel.TYPE_NOTE, note2);
const resource2 = await Resource.load((await Note.linkedResourceIds(note2.body))[0]);
let note3 = await Note.save({ title: 'note3', parent_id: folder2.id });
await shim.attachFileToNote(note3, `${supportDir}/photo.jpg`);
note3 = await Note.load(note3.id);
queueExportItem(BaseModel.TYPE_NOTE, note3);
const resource3 = await Resource.load((await Note.linkedResourceIds(note3.body))[0]);
note3 = await Note.save({ ...note3, body: `<img src=":/${resource3.id}" alt="alt">` });
note3 = await Note.load(note3.id);
let note4 = await Note.save({ title: 'note4', parent_id: folder2.id });
await shim.attachFileToNote(note4, `${supportDir}/photo.jpg`);
note4 = await Note.load(note4.id);
queueExportItem(BaseModel.TYPE_NOTE, note4);
const resource4 = await Resource.load((await Note.linkedResourceIds(note4.body))[0]);
note4 = await Note.save({ ...note4, body: `![](:/${resource4.id} "title")` });
note4 = await Note.load(note4.id);
await exporter.processItem(Folder.modelType(), folder1);
await exporter.processItem(Folder.modelType(), folder2);
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
await exporter.processResource(resource1, Resource.fullPath(resource1));
await exporter.processResource(resource2, Resource.fullPath(resource2));
await exporter.processResource(resource3, Resource.fullPath(resource3));
await exporter.processResource(resource4, Resource.fullPath(resource3));
const context = {
resourcePaths: {},
};
context.resourcePaths[resource1.id] = 'resource1.jpg';
context.resourcePaths[resource2.id] = 'resource2.jpg';
context.resourcePaths[resource3.id] = 'resource3.jpg';
context.resourcePaths[resource4.id] = 'resource3.jpg';
exporter.updateContext(context);
await exporter.processItem(Note.modelType(), note1);
await exporter.processItem(Note.modelType(), note2);
await exporter.processItem(Note.modelType(), note3);
await exporter.processItem(Note.modelType(), note4);
const note1_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note1.id]}`);
const note2_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note2.id]}`);
const note3_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note3.id]}`);
const note4_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note4.id]}`);
expect(note1_body).toContain('](../_resources/photo.jpg)', 'Resource id should be replaced with a relative path.');
expect(note2_body).toContain('](../../_resources/photo-1.jpg)', 'Resource id should be replaced with a relative path.');
expect(note3_body).toContain('<img src="../../_resources/photo-2.jpg" alt="alt">', 'Resource id should be replaced with a relative path.');
expect(note4_body).toContain('](../../_resources/photo-3.jpg "title")', 'Resource id should be replaced with a relative path.');
expect(note1_body).toContain('](../_resources/resource1.jpg)', 'Resource id should be replaced with a relative path.');
expect(note2_body).toContain('](../../_resources/resource2.jpg)', 'Resource id should be replaced with a relative path.');
}));
it('should replace note ids with relative paths', (async () => {

View File

@@ -4,9 +4,7 @@ import shim from '../../shim';
import markdownUtils from '../../markdownUtils';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import { NoteEntity, ResourceEntity } from '../database/types';
import { basename, dirname, friendlySafeFilename } from '../../path-utils';
import { MarkupToHtml } from '@joplin/renderer';
export default class InteropService_Exporter_Md extends InteropService_Exporter_Base {
@@ -31,7 +29,7 @@ export default class InteropService_Exporter_Md extends InteropService_Exporter_
output = `${pathPart}/${output}`;
} else {
output = `${friendlySafeFilename(item.title, null)}/${output}`;
if (findUniqueFilename) output = await shim.fsDriver().findUniqueFilename(output, null, true);
if (findUniqueFilename) output = await shim.fsDriver().findUniqueFilename(output);
}
}
if (!item.parent_id) return output;
@@ -48,7 +46,7 @@ export default class InteropService_Exporter_Md extends InteropService_Exporter_
async replaceResourceIdsByRelativePaths_(noteBody: string, relativePathToRoot: string) {
const linkedResourceIds = await Note.linkedResourceIds(noteBody);
const resourcePaths = this.context() && this.context().destResourcePaths ? this.context().destResourcePaths : {};
const resourcePaths = this.context() && this.context().resourcePaths ? this.context().resourcePaths : {};
const createRelativePath = function(resourcePath: string) {
return `${relativePathToRoot}_resources/${basename(resourcePath)}`;
@@ -85,18 +83,17 @@ export default class InteropService_Exporter_Md extends InteropService_Exporter_
notePaths: {},
};
for (let i = 0; i < itemsToExport.length; i++) {
const it = itemsToExport[i].type;
const itemType = itemsToExport[i].type;
if (it !== itemType) continue;
if (itemType !== itemType) continue;
const itemOrId = itemsToExport[i].itemOrId;
const note = typeof itemOrId === 'object' ? itemOrId : await Note.load(itemOrId);
if (!note) continue;
const ext = note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML ? 'html' : 'md';
let notePath = `${await this.makeDirPath_(note, null, false)}${friendlySafeFilename(note.title, null)}.${ext}`;
notePath = await shim.fsDriver().findUniqueFilename(`${this.destDir_}/${notePath}`, Object.values(context.notePaths), true);
let notePath = `${await this.makeDirPath_(note, null, false)}${friendlySafeFilename(note.title, null)}.md`;
notePath = await shim.fsDriver().findUniqueFilename(`${this.destDir_}/${notePath}`, Object.values(context.notePaths));
context.notePaths[note.id] = notePath;
}
@@ -110,10 +107,6 @@ export default class InteropService_Exporter_Md extends InteropService_Exporter_
}
}
private async getNoteExportContent_(modNote: NoteEntity) {
return await Note.replaceResourceInternalToExternalLinks(await Note.serialize(modNote, ['body']));
}
async processItem(_itemType: number, item: any) {
if ([BaseModel.TYPE_NOTE, BaseModel.TYPE_FOLDER].indexOf(item.type_) < 0) return;
@@ -131,36 +124,15 @@ export default class InteropService_Exporter_Md extends InteropService_Exporter_
const noteBody = await this.relaceLinkedItemIdsByRelativePaths_(item);
const modNote = Object.assign({}, item, { body: noteBody });
const noteContent = await this.getNoteExportContent_(modNote);
const noteContent = await Note.serializeForEdit(modNote);
await shim.fsDriver().mkdir(dirname(noteFilePath));
await shim.fsDriver().writeFile(noteFilePath, noteContent, 'utf-8');
}
}
private async findReasonableFilename(resource: ResourceEntity, filePath: string) {
let fileName = basename(filePath);
if (resource.filename) {
fileName = resource.filename;
} else if (resource.title) {
fileName = friendlySafeFilename(resource.title);
}
// Fall back on the resource filename saved in the users resource folder
return fileName;
}
async processResource(resource: ResourceEntity, filePath: string) {
const context = this.context();
if (!context.destResourcePaths) context.destResourcePaths = {};
const fileName = await this.findReasonableFilename(resource, filePath);
let destResourcePath = `${this.resourceDir_}/${fileName}`;
destResourcePath = await shim.fsDriver().findUniqueFilename(destResourcePath, Object.values(context.destResourcePaths), true);
async processResource(_resource: any, filePath: string) {
const destResourcePath = `${this.resourceDir_}/${basename(filePath)}`;
await shim.fsDriver().copy(filePath, destResourcePath);
context.destResourcePaths[resource.id] = destResourcePath;
this.updateContext(context);
}
async close() {}

View File

@@ -1,120 +0,0 @@
import InteropService_Importer_Md from '../../services/interop/InteropService_Importer_Md';
import Note from '../../models/Note';
import { setupDatabaseAndSynchronizer, supportDir, switchClient } from '../../testing/test-utils';
import { MarkupToHtml } from '@joplin/renderer';
describe('InteropService_Importer_Md: importLocalImages', function() {
async function importNote(path: string) {
const importer = new InteropService_Importer_Md();
importer.setMetadata({ fileExtensions: ['md', 'html'] });
return await importer.importFile(path, 'notebook');
}
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should import linked files and modify tags appropriately', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample.md`);
const tagNonExistentFile = '![does not exist](does_not_exist.png)';
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(2);
const inexistentLinkUnchanged = note.body.includes(tagNonExistentFile);
expect(inexistentLinkUnchanged).toBe(true);
});
it('should only create 1 resource for duplicate links, all tags should be updated', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-duplicate-links.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
const reg = new RegExp(items[0].id, 'g');
const matched = note.body.match(reg);
expect(matched.length).toBe(2);
});
it('should import linked files and modify tags appropriately when link is also in alt text', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-link-in-alt-text.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
});
it('should passthrough unchanged if no links present', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-no-links.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(0);
expect(note.body).toContain('Unidentified vessel travelling at sub warp speed, bearing 235.7. Fluctuations in energy readings from it, Captain. All transporters off.');
});
it('should import linked image with special characters in name', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-special-chars.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(3);
const noteIds = await Note.linkedNoteIds(note.body);
expect(noteIds.length).toBe(1);
const spaceSyntaxLeft = note.body.includes('<../../photo sample.jpg>');
expect(spaceSyntaxLeft).toBe(false);
});
it('should import resources and notes for files', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-files.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(3);
const noteIds = await Note.linkedNoteIds(note.body);
expect(noteIds.length).toBe(1);
});
it('should gracefully handle reference cycles in notes', async function() {
const importer = new InteropService_Importer_Md();
importer.setMetadata({ fileExtensions: ['md'] });
const noteA = await importer.importFile(`${supportDir}/test_notes/md/sample-cycles-a.md`, 'notebook');
const noteB = await importer.importFile(`${supportDir}/test_notes/md/sample-cycles-b.md`, 'notebook');
const noteAIds = await Note.linkedNoteIds(noteA.body);
expect(noteAIds.length).toBe(1);
const noteBIds = await Note.linkedNoteIds(noteB.body);
expect(noteBIds.length).toBe(1);
expect(noteAIds[0]).toEqual(noteB.id);
expect(noteBIds[0]).toEqual(noteA.id);
});
it('should not import resources from file:// links', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-file-links.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(0);
expect(note.body).toContain('![sample](file://../../photo.jpg)');
});
it('should attach resources that are missing the file extension', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-no-extension.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(1);
});
it('should attach resources that include anchor links', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-anchor-link.md`);
const itemIds = await Note.linkedItemIds(note.body);
expect(itemIds.length).toBe(1);
expect(note.body).toContain(`[Section 1](:/${itemIds[0]}#markdown)`);
});
it('should attach resources that include a title', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample-link-title.md`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(3);
const noteIds = await Note.linkedNoteIds(note.body);
expect(noteIds.length).toBe(1);
});
it('should import notes with html file extension as html', async function() {
const note = await importNote(`${supportDir}/test_notes/md/sample.html`);
const items = await Note.linkedItems(note.body);
expect(items.length).toBe(3);
const noteIds = await Note.linkedNoteIds(note.body);
expect(noteIds.length).toBe(1);
expect(note.markup_language).toBe(MarkupToHtml.MARKUP_LANGUAGE_HTML);
const preservedAlt = note.body.includes('alt="../../photo.jpg"');
expect(preservedAlt).toBe(true);
});
});

View File

@@ -4,18 +4,14 @@ import { _ } from '../../locale';
import InteropService_Importer_Base from './InteropService_Importer_Base';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import { NoteEntity } from '../database/types';
import { basename, filename, rtrimSlashes, fileExtension, dirname } from '../../path-utils';
const { basename, filename, rtrimSlashes, fileExtension, dirname } = require('../../path-utils');
import shim from '../../shim';
import markdownUtils from '../../markdownUtils';
import htmlUtils from '../../htmlUtils';
const { unique } = require('../../ArrayUtils');
const { pregQuote } = require('../../string-utils-common');
import { MarkupToHtml } from '@joplin/renderer';
const { MarkupToHtml } = require('@joplin/renderer');
export default class InteropService_Importer_Md extends InteropService_Importer_Base {
private importedNotes: Record<string, NoteEntity> = {};
async exec(result: ImportExportResult) {
let parentFolderId = null;
@@ -63,100 +59,52 @@ export default class InteropService_Importer_Md extends InteropService_Importer_
}
}
private trimAnchorLink(link: string) {
if (link.indexOf('#') <= 0) return link;
const splitted = link.split('#');
splitted.pop();
return splitted.join('#');
}
/**
* Parse text for links, attempt to find local file, if found create Joplin resource
* and update link accordingly.
*/
async importLocalFiles(filePath: string, md: string, parentFolderId: string) {
async importLocalFiles(filePath: string, md: string) {
let updated = md;
const markdownLinks = markdownUtils.extractFileUrls(md);
const htmlLinks = htmlUtils.extractFileUrls(md);
const fileLinks = unique(markdownLinks.concat(htmlLinks));
const fileLinks = unique(markdownUtils.extractFileUrls(md));
await Promise.all(fileLinks.map(async (encodedLink: string) => {
const link = decodeURI(encodedLink);
// Handle anchor links appropriately
const trimmedLink = this.trimAnchorLink(link);
const attachmentPath = filename(`${dirname(filePath)}/${trimmedLink}`, true);
const pathWithExtension = `${attachmentPath}.${fileExtension(trimmedLink)}`;
const attachmentPath = filename(`${dirname(filePath)}/${link}`, true);
const pathWithExtension = `${attachmentPath}.${fileExtension(link)}`;
const stat = await shim.fsDriver().stat(pathWithExtension);
const isDir = stat ? stat.isDirectory() : false;
if (stat && !isDir) {
const supportedFileExtension = this.metadata().fileExtensions;
const resolvedPath = shim.fsDriver().resolve(pathWithExtension);
let id: string = '';
// If the link looks like a note, then import it
if (supportedFileExtension.indexOf(fileExtension(trimmedLink).toLowerCase()) >= 0) {
// If the note hasn't been imported yet, do so now
if (!this.importedNotes[resolvedPath]) {
await this.importFile(resolvedPath, parentFolderId);
}
id = this.importedNotes[resolvedPath].id;
} else {
const resource = await shim.createResourceFromPath(pathWithExtension);
id = resource.id;
}
// The first is a normal link, the second is supports the <link> and [](<link with spaces>) syntax
// Only opening patterns are consider in order to cover all occurances
// We need to use the encoded link as well because some links (link's with spaces)
// will appear encoded in the source. Other links (unicode chars) will not
const linksToReplace = [this.trimAnchorLink(link), this.trimAnchorLink(encodedLink)];
for (let j = 0; j < linksToReplace.length; j++) {
const linkToReplace = pregQuote(linksToReplace[j]);
// Markdown links
updated = markdownUtils.replaceResourceUrl(updated, linkToReplace, id);
// HTML links
updated = htmlUtils.replaceResourceUrl(updated, linkToReplace, id);
}
const resource = await shim.createResourceFromPath(pathWithExtension);
// NOTE: use ](link) in case the link also appears elsewhere, such as in alt text
const linkPatternEscaped = pregQuote(`](${link})`);
const reg = new RegExp(linkPatternEscaped, 'g');
updated = updated.replace(reg, `](:/${resource.id})`);
}
}));
return updated;
}
async importFile(filePath: string, parentFolderId: string) {
const resolvedPath = shim.fsDriver().resolve(filePath);
if (this.importedNotes[resolvedPath]) return this.importedNotes[resolvedPath];
const stat = await shim.fsDriver().stat(resolvedPath);
if (!stat) throw new Error(`Cannot read ${resolvedPath}`);
const ext = fileExtension(resolvedPath);
const title = filename(resolvedPath);
const body = await shim.fsDriver().readFile(resolvedPath);
const stat = await shim.fsDriver().stat(filePath);
if (!stat) throw new Error(`Cannot read ${filePath}`);
const title = filename(filePath);
const body = await shim.fsDriver().readFile(filePath);
let updatedBody;
try {
updatedBody = await this.importLocalFiles(filePath, body);
} catch (error) {
// console.error(`Problem importing links for file ${filePath}, error:\n ${error}`);
}
const note = {
parent_id: parentFolderId,
title: title,
body: body,
body: updatedBody || body,
updated_time: stat.mtime.getTime(),
created_time: stat.birthtime.getTime(),
user_updated_time: stat.mtime.getTime(),
user_created_time: stat.birthtime.getTime(),
markup_language: ext === 'html' ? MarkupToHtml.MARKUP_LANGUAGE_HTML : MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN,
markup_language: MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN,
};
this.importedNotes[resolvedPath] = await Note.save(note, { autoTimestamp: false });
try {
const updatedBody = await this.importLocalFiles(resolvedPath, body, parentFolderId);
const updatedNote = {
...this.importedNotes[resolvedPath],
body: updatedBody || body,
};
this.importedNotes[resolvedPath] = await Note.save(updatedNote, { isNew: false });
} catch (error) {
// console.error(`Problem importing links for file ${resolvedPath}, error:\n ${error}`);
}
return this.importedNotes[resolvedPath];
return Note.save(note, { autoTimestamp: false });
}
}

View File

@@ -3,62 +3,6 @@ import Setting, { SettingItem as InternalSettingItem, SettingSectionSource } fro
import Plugin from '../Plugin';
import { SettingItem, SettingSection } from './types';
// That's all the plugin as of 27/08/21 - any new plugin after that will not be
// able to use the registerSetting API. Fixes in particular all the ambrt
// plugins. Some of them don't need this hack but it's easier that way.
const registerSettingAllowedPluginIds: string[] = [
'b53da1f6-868c-468d-b60c-2897a27166ac',
'com.andrejilderda.macOSTheme',
'com.export-to-ssg.aman-d-1-n-only',
'com.github.BeatLink.joplin-plugin-untagged',
'com.github.joplin.kanban',
'com.github.marc0l92.joplin-plugin-jira-issue',
'com.github.uphy.PlantUmlPlugin',
'com.gitlab.BeatLink.joplin-plugin-repeating-todos',
'com.joplin_plugin.nlr',
'com.lki.homenote',
'com.plugin.randomNotePlugin',
'com.shantanugoel.JoplinCMLineNumbersPlugin',
'com.whatever.inline-tags',
'com.whatever.quick-links',
'com.xUser5000.bibtex',
'cx.evermeet.tessus.menu-shortcut-toolbar',
'fd117a99-b165-4824-893c-5825439a842d',
'io.github.jackgruber.backup',
'io.github.jackgruber.combine-notes',
'io.github.jackgruber.copytags',
'io.github.jackgruber.hotfolder',
'io.github.jackgruber.note-overview',
'io.treymo.LinkGraph',
'joplin-insert-date',
'joplin-plugin-conflict-resolution',
'joplin.plugin.ambrt.backlinksToNote',
'joplin.plugin.ambrt.convertToNewNote',
'joplin.plugin.ambrt.copyNoteLink',
'joplin.plugin.ambrt.embedSearch',
'joplin.plugin.ambrt.fold-cm',
'joplin.plugin.ambrt.goToItem',
'joplin.plugin.anki-sync',
'joplin.plugin.benji.favorites',
'joplin.plugin.benji.persistentLayout',
'joplin.plugin.benji.quick-move',
'joplin.plugin.forcewake.tags-generator',
'joplin.plugin.note.tabs',
'joplin.plugin.quick.html.tags',
'joplin.plugin.spoiler.cards',
'joplin.plugin.templates',
'net.rmusin.joplin-table-formatter',
'net.rmusin.resource-search',
'org.joplinapp.plugins.AbcSheetMusic',
'org.joplinapp.plugins.admonition',
'org.joplinapp.plugins.ToggleSidebars',
'osw.joplin.markdowncalc',
'outline',
'plugin.azamahJunior.note-statistics',
'plugin.calebjohn.MathMode',
'plugin.calebjohn.rich-markdown',
];
export interface ChangeEvent {
/**
* Setting keys that have been changed
@@ -133,13 +77,7 @@ export default class JoplinSettings {
* Registers a new setting.
*/
public async registerSetting(key: string, settingItem: SettingItem) {
// It's a warning for older plugins and an error for new ones.
this.plugin_.deprecationNotice(
'1.8',
'joplin.settings.registerSetting() is deprecated in favour of joplin.settings.registerSettings()',
!registerSettingAllowedPluginIds.includes(this.plugin_.id)
);
this.plugin_.deprecationNotice('1.8', 'joplin.settings.registerSetting() is deprecated in favour of joplin.settings.registerSettings()', true);
await this.registerSettings({ [key]: settingItem });
}

View File

@@ -42,7 +42,7 @@ const SyncTargetNextcloud = require('../SyncTargetNextcloud.js');
const SyncTargetDropbox = require('../SyncTargetDropbox.js');
const SyncTargetAmazonS3 = require('../SyncTargetAmazonS3.js');
import SyncTargetJoplinServer from '../SyncTargetJoplinServer';
import EncryptionService from '../services/e2ee/EncryptionService';
import EncryptionService from '../services/EncryptionService';
import DecryptionWorker from '../services/DecryptionWorker';
import RevisionService from '../services/RevisionService';
import ResourceFetcher from '../services/ResourceFetcher';

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/plugin-repo-cli",
"version": "2.4.0",
"version": "2.3.1",
"description": "",
"main": "index.js",
"bin": {
@@ -18,8 +18,8 @@
"author": "",
"license": "MIT",
"dependencies": {
"@joplin/lib": "~2.4",
"@joplin/tools": "~2.4",
"@joplin/lib": "^2.3.1",
"@joplin/tools": "^2.3.1",
"fs-extra": "^9.0.1",
"gh-release-assets": "^2.0.0",
"node-fetch": "^2.6.1",

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.3.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.3.1",
"license": "MIT",
"dependencies": {
"font-awesome-filetypes": "^2.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.3.1",
"description": "The Joplin note renderer, used the mobile and desktop application",
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/renderer",
"main": "index.js",

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/server",
"version": "2.4.2",
"version": "2.3.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/server",
"version": "2.4.2",
"version": "2.3.7",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@koa/cors": "^3.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "2.4.2",
"version": "2.3.7",
"private": true,
"scripts": {
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
@@ -19,8 +19,8 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"@joplin/lib": "~2.3",
"@joplin/renderer": "~2.3",
"@koa/cors": "^3.1.0",
"bcryptjs": "^2.4.3",
"bulma": "^0.9.1",
@@ -51,7 +51,7 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@joplin/tools": "~2.4",
"@joplin/tools": "~2.3",
"@rmp135/sql-ts": "^1.7.0",
"@types/fs-extra": "^8.0.0",
"@types/jest": "^26.0.15",

Binary file not shown.

View File

@@ -5,7 +5,7 @@ it('should pass', async function() {
// import { afterAllTests, beforeAllDb, beforeEachDb, db } from "./utils/testing/testUtils";
// import sqlts from '@rmp135/sql-ts';
// import config from "./config";
// import { connectDb, DbConnection, disconnectDb, migrateDown, migrateList, migrateUp, nextMigration } from "./services/database/types";
// import { connectDb, DbConnection, disconnectDb, migrateDown, migrateList, migrateUp, nextMigration } from "./db";
// async function dbSchemaSnapshot(db:DbConnection):Promise<any> {
// return sqlts.toObject({

View File

@@ -3,7 +3,6 @@ import { DatabaseConfig } from './utils/types';
import * as pathUtils from 'path';
import time from '@joplin/lib/time';
import Logger from '@joplin/lib/Logger';
import { databaseSchema } from './services/database/types';
// Make sure bigInteger values are numbers and not strings
//
@@ -306,3 +305,401 @@ export async function connectionCheck(db: DbConnection): Promise<ConnectionCheck
};
}
}
export type Uuid = string;
export enum ItemAddressingType {
Id = 1,
Path,
}
export enum NotificationLevel {
Important = 10,
Normal = 20,
}
export enum ItemType {
Item = 1,
UserItem = 2,
User,
}
export enum EmailSender {
NoReply = 1,
Support = 2,
}
export enum ChangeType {
Create = 1,
Update = 2,
Delete = 3,
}
export enum UserFlagType {
FailedPaymentWarning = 1,
FailedPaymentFinal = 2,
AccountOverLimit = 3,
AccountWithoutSubscription = 4,
SubscriptionCancelled = 5,
ManuallyDisabled = 6,
}
export enum FileContentType {
Any = 1,
JoplinItem = 2,
}
export function changeTypeToString(t: ChangeType): string {
if (t === ChangeType.Create) return 'create';
if (t === ChangeType.Update) return 'update';
if (t === ChangeType.Delete) return 'delete';
throw new Error(`Unkown type: ${t}`);
}
export enum ShareType {
Note = 1, // When a note is shared via a public link
Folder = 3, // When a complete folder is shared with another Joplin Server user
}
export enum ShareUserStatus {
Waiting = 0,
Accepted = 1,
Rejected = 2,
}
export interface WithDates {
updated_time?: number;
created_time?: number;
}
export interface WithUuid {
id?: Uuid;
}
interface DatabaseTableColumn {
type: string;
}
interface DatabaseTable {
[key: string]: DatabaseTableColumn;
}
interface DatabaseTables {
[key: string]: DatabaseTable;
}
// AUTO-GENERATED-TYPES
// Auto-generated using `npm run generate-types`
export interface Session extends WithDates, WithUuid {
user_id?: Uuid;
auth_code?: string;
}
export interface File {
id?: Uuid;
owner_id?: Uuid;
name?: string;
content?: any;
mime_type?: string;
size?: number;
is_directory?: number;
is_root?: number;
parent_id?: Uuid;
updated_time?: string;
created_time?: string;
source_file_id?: Uuid;
content_type?: number;
content_id?: Uuid;
}
export interface ApiClient extends WithDates, WithUuid {
name?: string;
secret?: string;
}
export interface Notification extends WithDates, WithUuid {
owner_id?: Uuid;
level?: NotificationLevel;
key?: string;
message?: string;
read?: number;
canBeDismissed?: number;
}
export interface ShareUser extends WithDates, WithUuid {
share_id?: Uuid;
user_id?: Uuid;
status?: ShareUserStatus;
}
export interface Item extends WithDates, WithUuid {
name?: string;
mime_type?: string;
content?: Buffer;
content_size?: number;
jop_id?: Uuid;
jop_parent_id?: Uuid;
jop_share_id?: Uuid;
jop_type?: number;
jop_encryption_applied?: number;
jop_updated_time?: number;
}
export interface UserItem extends WithDates {
id?: number;
user_id?: Uuid;
item_id?: Uuid;
}
export interface ItemResource {
id?: number;
item_id?: Uuid;
resource_id?: Uuid;
}
export interface KeyValue {
id?: number;
key?: string;
type?: number;
value?: string;
}
export interface Share extends WithDates, WithUuid {
owner_id?: Uuid;
item_id?: Uuid;
type?: ShareType;
folder_id?: Uuid;
note_id?: Uuid;
}
export interface Change extends WithDates, WithUuid {
counter?: number;
item_type?: ItemType;
item_id?: Uuid;
item_name?: string;
type?: ChangeType;
previous_item?: string;
user_id?: Uuid;
}
export interface Email extends WithDates {
id?: number;
recipient_name?: string;
recipient_email?: string;
recipient_id?: Uuid;
sender_id?: EmailSender;
subject?: string;
body?: string;
sent_time?: number;
sent_success?: number;
error?: string;
key?: string;
}
export interface Token extends WithDates {
id?: number;
value?: string;
user_id?: Uuid;
}
export interface Subscription {
id?: number;
user_id?: Uuid;
stripe_user_id?: Uuid;
stripe_subscription_id?: Uuid;
last_payment_time?: number;
last_payment_failed_time?: number;
updated_time?: string;
created_time?: string;
is_deleted?: number;
}
export interface User extends WithDates, WithUuid {
email?: string;
password?: string;
full_name?: string;
is_admin?: number;
email_confirmed?: number;
must_set_password?: number;
account_type?: number;
can_upload?: number;
max_item_size?: number | null;
can_share_folder?: number | null;
can_share_note?: number | null;
max_total_item_size?: number | null;
total_item_size?: number;
enabled?: number;
}
export interface UserFlag extends WithDates {
id?: number;
user_id?: Uuid;
type?: UserFlagType;
}
export const databaseSchema: DatabaseTables = {
sessions: {
id: { type: 'string' },
user_id: { type: 'string' },
auth_code: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
files: {
id: { type: 'string' },
owner_id: { type: 'string' },
name: { type: 'string' },
content: { type: 'any' },
mime_type: { type: 'string' },
size: { type: 'number' },
is_directory: { type: 'number' },
is_root: { type: 'number' },
parent_id: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
source_file_id: { type: 'string' },
content_type: { type: 'number' },
content_id: { type: 'string' },
},
api_clients: {
id: { type: 'string' },
name: { type: 'string' },
secret: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
notifications: {
id: { type: 'string' },
owner_id: { type: 'string' },
level: { type: 'number' },
key: { type: 'string' },
message: { type: 'string' },
read: { type: 'number' },
canBeDismissed: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
share_users: {
id: { type: 'string' },
share_id: { type: 'string' },
user_id: { type: 'string' },
status: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
items: {
id: { type: 'string' },
name: { type: 'string' },
mime_type: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
content: { type: 'any' },
content_size: { type: 'number' },
jop_id: { type: 'string' },
jop_parent_id: { type: 'string' },
jop_share_id: { type: 'string' },
jop_type: { type: 'number' },
jop_encryption_applied: { type: 'number' },
jop_updated_time: { type: 'string' },
},
user_items: {
id: { type: 'number' },
user_id: { type: 'string' },
item_id: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
item_resources: {
id: { type: 'number' },
item_id: { type: 'string' },
resource_id: { type: 'string' },
},
key_values: {
id: { type: 'number' },
key: { type: 'string' },
type: { type: 'number' },
value: { type: 'string' },
},
shares: {
id: { type: 'string' },
owner_id: { type: 'string' },
item_id: { type: 'string' },
type: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
folder_id: { type: 'string' },
note_id: { type: 'string' },
},
changes: {
counter: { type: 'number' },
id: { type: 'string' },
item_type: { type: 'number' },
item_id: { type: 'string' },
item_name: { type: 'string' },
type: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
previous_item: { type: 'string' },
user_id: { type: 'string' },
},
emails: {
id: { type: 'number' },
recipient_name: { type: 'string' },
recipient_email: { type: 'string' },
recipient_id: { type: 'string' },
sender_id: { type: 'number' },
subject: { type: 'string' },
body: { type: 'string' },
sent_time: { type: 'string' },
sent_success: { type: 'number' },
error: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
key: { type: 'string' },
},
tokens: {
id: { type: 'number' },
value: { type: 'string' },
user_id: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
subscriptions: {
id: { type: 'number' },
user_id: { type: 'string' },
stripe_user_id: { type: 'string' },
stripe_subscription_id: { type: 'string' },
last_payment_time: { type: 'string' },
last_payment_failed_time: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
is_deleted: { type: 'number' },
},
users: {
id: { type: 'string' },
email: { type: 'string' },
password: { type: 'string' },
full_name: { type: 'string' },
is_admin: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
email_confirmed: { type: 'number' },
must_set_password: { type: 'number' },
account_type: { type: 'number' },
can_upload: { type: 'number' },
max_item_size: { type: 'number' },
can_share_folder: { type: 'number' },
can_share_note: { type: 'number' },
max_total_item_size: { type: 'string' },
total_item_size: { type: 'string' },
enabled: { type: 'number' },
},
user_flags: {
id: { type: 'number' },
user_id: { type: 'string' },
type: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
};
// AUTO-GENERATED-TYPES

View File

@@ -1,6 +1,5 @@
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, koaAppContext, koaNext } from '../utils/testing/testUtils';
import { Notification } from '../services/database/types';
import { defaultAdminEmail, defaultAdminPassword } from '../db';
import { defaultAdminEmail, defaultAdminPassword, Notification } from '../db';
import notificationHandler from './notificationHandler';
describe('notificationHandler', function() {

View File

@@ -1,7 +1,6 @@
import { AppContext, KoaNext, NotificationView } from '../utils/types';
import { isApiRequest } from '../utils/requestUtils';
import { NotificationLevel } from '../services/database/types';
import { defaultAdminEmail, defaultAdminPassword } from '../db';
import { defaultAdminEmail, defaultAdminPassword, NotificationLevel } from '../db';
import { _ } from '@joplin/lib/locale';
import Logger from '@joplin/lib/Logger';
import * as MarkdownIt from 'markdown-it';

View File

@@ -1,5 +1,5 @@
import BaseModel from './BaseModel';
import { ApiClient } from '../services/database/types';
import { ApiClient } from '../db';
export default class ApiClientModel extends BaseModel<ApiClient> {

View File

@@ -1,5 +1,4 @@
import { WithDates, WithUuid, databaseSchema, ItemType, Uuid, User } from '../services/database/types';
import { DbConnection } from '../db';
import { WithDates, WithUuid, databaseSchema, DbConnection, ItemType, Uuid, User } from '../db';
import TransactionHandler from '../utils/TransactionHandler';
import uuidgen from '../utils/uuidgen';
import { ErrorUnprocessableEntity, ErrorBadRequest } from '../utils/errors';

View File

@@ -1,8 +1,7 @@
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, expectThrow, createFolder, createItemTree3, expectNotThrow } from '../utils/testing/testUtils';
import { ChangeType, Item, Uuid } from '../services/database/types';
import { ChangeType, Item, SqliteMaxVariableNum, Uuid } from '../db';
import { msleep } from '../utils/time';
import { ChangePagination } from './ChangeModel';
import { SqliteMaxVariableNum } from '../db';
async function makeTestItem(userId: Uuid, num: number): Promise<Item> {
return models().item().saveForUser(userId, {

View File

@@ -1,6 +1,5 @@
import { Knex } from 'knex';
import { SqliteMaxVariableNum } from '../db';
import { Change, ChangeType, Item, Uuid } from '../services/database/types';
import { Change, ChangeType, Item, SqliteMaxVariableNum, Uuid } from '../db';
import { md5 } from '../utils/crypto';
import { ErrorResyncRequired } from '../utils/errors';
import BaseModel, { SaveOptions } from './BaseModel';

View File

@@ -1,4 +1,4 @@
import { EmailSender } from '../services/database/types';
import { EmailSender } from '../db';
import { beforeAllDb, afterAllTests, beforeEachDb, models, createUserAndSession } from '../utils/testing/testUtils';
import paymentFailedTemplate from '../views/emails/paymentFailedTemplate';

Some files were not shown because too many files have changed in this diff Show More